2020-09-03 19:06:17 +05:30
|
|
|
/*
|
|
|
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2020-09-10 15:39:17 +05:30
|
|
|
import {MEGOLM_ALGORITHM, DecryptionSource} from "./common.js";
|
2021-03-02 19:59:35 +05:30
|
|
|
import {groupEventsBySession} from "./megolm/decryption/utils.js";
|
2020-09-10 15:39:17 +05:30
|
|
|
import {mergeMap} from "../../utils/mergeMap.js";
|
2021-03-02 23:43:52 +05:30
|
|
|
import {groupBy} from "../../utils/groupBy.js";
|
2020-09-03 19:06:17 +05:30
|
|
|
import {makeTxnId} from "../common.js";
|
|
|
|
|
2020-09-03 21:20:28 +05:30
|
|
|
const ENCRYPTED_TYPE = "m.room.encrypted";
|
2020-11-09 21:19:16 +05:30
|
|
|
// how often ensureMessageKeyIsShared can check if it needs to
|
|
|
|
// create a new outbound session
|
|
|
|
// note that encrypt could still create a new session
|
|
|
|
const MIN_PRESHARE_INTERVAL = 60 * 1000; // 1min
|
2020-09-03 19:06:17 +05:30
|
|
|
|
2021-03-08 20:49:46 +05:30
|
|
|
// TODO: this class is a good candidate for splitting up into encryption and decryption, there doesn't seem to be much overlap
|
2020-09-03 19:06:17 +05:30
|
|
|
export class RoomEncryption {
|
2020-09-18 16:40:41 +05:30
|
|
|
constructor({room, deviceTracker, olmEncryption, megolmEncryption, megolmDecryption, encryptionParams, storage, sessionBackup, notifyMissingMegolmSession, clock}) {
|
2020-09-03 19:06:17 +05:30
|
|
|
this._room = room;
|
|
|
|
this._deviceTracker = deviceTracker;
|
|
|
|
this._olmEncryption = olmEncryption;
|
2020-09-03 21:20:28 +05:30
|
|
|
this._megolmEncryption = megolmEncryption;
|
2020-09-04 19:57:14 +05:30
|
|
|
this._megolmDecryption = megolmDecryption;
|
2020-09-03 19:06:17 +05:30
|
|
|
// content of the m.room.encryption event
|
2020-09-03 21:20:28 +05:30
|
|
|
this._encryptionParams = encryptionParams;
|
2020-09-04 15:39:19 +05:30
|
|
|
this._megolmBackfillCache = this._megolmDecryption.createSessionCache();
|
2020-10-14 16:13:35 +05:30
|
|
|
this._megolmSyncCache = this._megolmDecryption.createSessionCache(1);
|
2021-03-02 19:59:35 +05:30
|
|
|
// caches devices to verify events
|
2020-09-08 14:20:39 +05:30
|
|
|
this._senderDeviceCache = new Map();
|
2020-09-08 17:54:48 +05:30
|
|
|
this._storage = storage;
|
2020-09-17 19:28:46 +05:30
|
|
|
this._sessionBackup = sessionBackup;
|
2020-09-17 21:30:00 +05:30
|
|
|
this._notifyMissingMegolmSession = notifyMissingMegolmSession;
|
2020-09-18 16:40:41 +05:30
|
|
|
this._clock = clock;
|
2020-09-25 14:15:00 +05:30
|
|
|
this._isFlushingRoomKeyShares = false;
|
2020-11-09 21:19:16 +05:30
|
|
|
this._lastKeyPreShareTime = null;
|
2021-04-09 14:07:43 +05:30
|
|
|
this._keySharePromise = null;
|
2021-03-02 19:59:35 +05:30
|
|
|
this._disposed = false;
|
2020-09-17 17:50:15 +05:30
|
|
|
}
|
|
|
|
|
2021-03-02 19:59:35 +05:30
|
|
|
enableSessionBackup(sessionBackup) {
|
2020-09-17 19:28:46 +05:30
|
|
|
if (this._sessionBackup) {
|
|
|
|
return;
|
|
|
|
}
|
2020-09-17 17:50:15 +05:30
|
|
|
this._sessionBackup = sessionBackup;
|
2021-03-02 19:59:35 +05:30
|
|
|
}
|
|
|
|
|
2021-03-15 17:26:40 +05:30
|
|
|
async restoreMissingSessionsFromBackup(entries, log) {
|
2021-03-03 17:22:43 +05:30
|
|
|
const events = entries.filter(e => e.isEncrypted && !e.isDecrypted && e.event).map(e => e.event);
|
2021-03-02 19:59:35 +05:30
|
|
|
const eventsBySession = groupEventsBySession(events);
|
|
|
|
const groups = Array.from(eventsBySession.values());
|
2021-03-05 00:17:02 +05:30
|
|
|
const txn = await this._storage.readTxn([this._storage.storeNames.inboundGroupSessions]);
|
2021-03-02 19:59:35 +05:30
|
|
|
const hasSessions = await Promise.all(groups.map(async group => {
|
|
|
|
return this._megolmDecryption.hasSession(this._room.id, group.senderKey, group.sessionId, txn);
|
|
|
|
}));
|
|
|
|
const missingSessions = groups.filter((_, i) => !hasSessions[i]);
|
|
|
|
if (missingSessions.length) {
|
|
|
|
// start with last sessions which should be for the last items in the timeline
|
|
|
|
for (var i = missingSessions.length - 1; i >= 0; i--) {
|
|
|
|
const session = missingSessions[i];
|
2021-03-15 17:26:40 +05:30
|
|
|
await log.wrap("session", log => this._requestMissingSessionFromBackup(session.senderKey, session.sessionId, log));
|
2021-03-02 19:59:35 +05:30
|
|
|
}
|
2020-09-17 21:30:00 +05:30
|
|
|
}
|
2020-09-04 15:39:19 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
notifyTimelineClosed() {
|
|
|
|
// empty the backfill cache when closing the timeline
|
|
|
|
this._megolmBackfillCache.dispose();
|
|
|
|
this._megolmBackfillCache = this._megolmDecryption.createSessionCache();
|
2020-09-08 14:20:39 +05:30
|
|
|
this._senderDeviceCache = new Map(); // purge the sender device cache
|
2020-09-03 19:06:17 +05:30
|
|
|
}
|
|
|
|
|
2021-03-03 16:21:50 +05:30
|
|
|
async writeMemberChanges(memberChanges, txn, log) {
|
2021-05-06 18:53:33 +05:30
|
|
|
let shouldFlush = false;
|
2020-09-11 18:11:12 +05:30
|
|
|
const memberChangesArray = Array.from(memberChanges.values());
|
2021-05-06 18:53:58 +05:30
|
|
|
// this also clears our session if we leave the room ourselves
|
2020-09-11 18:11:12 +05:30
|
|
|
if (memberChangesArray.some(m => m.hasLeft)) {
|
2021-03-03 16:21:50 +05:30
|
|
|
log.log({
|
|
|
|
l: "discardOutboundSession",
|
|
|
|
leftUsers: memberChangesArray.filter(m => m.hasLeft).map(m => m.userId),
|
|
|
|
});
|
2020-09-11 18:11:12 +05:30
|
|
|
this._megolmEncryption.discardOutboundSession(this._room.id, txn);
|
2020-09-08 14:39:09 +05:30
|
|
|
}
|
2020-09-11 18:11:12 +05:30
|
|
|
if (memberChangesArray.some(m => m.hasJoined)) {
|
2021-03-03 16:21:50 +05:30
|
|
|
shouldFlush = await this._addShareRoomKeyOperationForNewMembers(memberChangesArray, txn, log);
|
2020-09-11 18:11:12 +05:30
|
|
|
}
|
|
|
|
await this._deviceTracker.writeMemberChanges(this._room, memberChanges, txn);
|
2021-03-02 23:44:29 +05:30
|
|
|
return shouldFlush;
|
2020-09-03 19:06:17 +05:30
|
|
|
}
|
|
|
|
|
2021-03-02 03:44:14 +05:30
|
|
|
async prepareDecryptAll(events, newKeys, source, txn) {
|
2020-10-05 20:36:29 +05:30
|
|
|
const errors = new Map();
|
2020-09-10 15:39:17 +05:30
|
|
|
const validEvents = [];
|
|
|
|
for (const event of events) {
|
|
|
|
if (event.redacted_because || event.unsigned?.redacted_because) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (event.content?.algorithm !== MEGOLM_ALGORITHM) {
|
|
|
|
errors.set(event.event_id, new Error("Unsupported algorithm: " + event.content?.algorithm));
|
|
|
|
}
|
|
|
|
validEvents.push(event);
|
2020-09-08 20:46:01 +05:30
|
|
|
}
|
2020-09-10 15:39:17 +05:30
|
|
|
let customCache;
|
|
|
|
let sessionCache;
|
2021-03-01 19:34:45 +05:30
|
|
|
// we have different caches so we can keep them small but still
|
|
|
|
// have backfill and sync not invalidate each other
|
2020-09-10 15:39:17 +05:30
|
|
|
if (source === DecryptionSource.Sync) {
|
|
|
|
sessionCache = this._megolmSyncCache;
|
|
|
|
} else if (source === DecryptionSource.Timeline) {
|
|
|
|
sessionCache = this._megolmBackfillCache;
|
|
|
|
} else if (source === DecryptionSource.Retry) {
|
|
|
|
// when retrying, we could have mixed events from at the bottom of the timeline (sync)
|
|
|
|
// and somewhere else, so create a custom cache we use just for this operation.
|
2020-09-18 16:40:41 +05:30
|
|
|
customCache = this._megolmDecryption.createSessionCache();
|
2020-09-10 15:39:17 +05:30
|
|
|
sessionCache = customCache;
|
|
|
|
} else {
|
|
|
|
throw new Error("Unknown source: " + source);
|
2020-09-04 15:40:12 +05:30
|
|
|
}
|
2020-09-10 15:39:17 +05:30
|
|
|
const preparation = await this._megolmDecryption.prepareDecryptAll(
|
2021-03-01 19:34:45 +05:30
|
|
|
this._room.id, validEvents, newKeys, sessionCache, txn);
|
2020-09-10 15:39:17 +05:30
|
|
|
if (customCache) {
|
|
|
|
customCache.dispose();
|
2020-09-04 15:40:12 +05:30
|
|
|
}
|
2021-03-02 03:44:14 +05:30
|
|
|
return new DecryptionPreparation(preparation, errors, source, this, events);
|
2020-09-10 15:39:17 +05:30
|
|
|
}
|
|
|
|
|
2021-03-15 17:26:40 +05:30
|
|
|
async _processDecryptionResults(events, results, errors, source, txn, log) {
|
2021-03-02 19:59:35 +05:30
|
|
|
const missingSessionEvents = events.filter(event => {
|
2020-09-23 21:04:25 +05:30
|
|
|
const error = errors.get(event.event_id);
|
2021-03-02 19:59:35 +05:30
|
|
|
return error?.code === "MEGOLM_NO_SESSION";
|
|
|
|
});
|
|
|
|
if (!missingSessionEvents.length) {
|
|
|
|
return;
|
|
|
|
}
|
2021-03-15 21:24:43 +05:30
|
|
|
// store missing event ids if received from sync
|
2021-03-15 17:26:40 +05:30
|
|
|
const missingEventsBySession = groupEventsBySession(missingSessionEvents);
|
2021-03-02 19:59:35 +05:30
|
|
|
if (source === DecryptionSource.Sync) {
|
2021-03-15 17:26:40 +05:30
|
|
|
await Promise.all(Array.from(missingEventsBySession.values()).map(async group => {
|
2021-03-02 19:59:35 +05:30
|
|
|
const eventIds = group.events.map(e => e.event_id);
|
|
|
|
return this._megolmDecryption.addMissingKeyEventIds(
|
|
|
|
this._room.id, group.senderKey, group.sessionId, eventIds, txn);
|
|
|
|
}));
|
2020-09-10 15:39:17 +05:30
|
|
|
}
|
2021-03-15 21:24:43 +05:30
|
|
|
|
|
|
|
if (!this._sessionBackup) {
|
|
|
|
return;
|
|
|
|
}
|
2021-03-02 19:59:35 +05:30
|
|
|
|
2021-03-15 19:53:35 +05:30
|
|
|
log.wrapDetached("check key backup", async log => {
|
2021-03-02 19:59:35 +05:30
|
|
|
// if the message came from sync, wait 10s to see if the room key arrives late,
|
|
|
|
// and only after that proceed to request from backup
|
2021-03-15 17:26:40 +05:30
|
|
|
log.set("source", source);
|
|
|
|
log.set("events", missingSessionEvents.length);
|
|
|
|
log.set("sessions", missingEventsBySession.size);
|
2021-03-02 19:59:35 +05:30
|
|
|
if (source === DecryptionSource.Sync) {
|
|
|
|
await this._clock.createTimeout(10000).elapsed();
|
|
|
|
if (this._disposed) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// now check which sessions have been received already
|
2021-03-05 00:17:02 +05:30
|
|
|
const txn = await this._storage.readTxn([this._storage.storeNames.inboundGroupSessions]);
|
2021-03-15 17:26:40 +05:30
|
|
|
await Promise.all(Array.from(missingEventsBySession).map(async ([key, group]) => {
|
2021-03-02 19:59:35 +05:30
|
|
|
if (await this._megolmDecryption.hasSession(this._room.id, group.senderKey, group.sessionId, txn)) {
|
2021-03-15 17:26:40 +05:30
|
|
|
missingEventsBySession.delete(key);
|
2021-03-02 19:59:35 +05:30
|
|
|
}
|
|
|
|
}));
|
|
|
|
}
|
2021-03-15 17:26:40 +05:30
|
|
|
await Promise.all(Array.from(missingEventsBySession.values()).map(group => {
|
|
|
|
return log.wrap("session", log => this._requestMissingSessionFromBackup(group.senderKey, group.sessionId, log));
|
2021-03-02 19:59:35 +05:30
|
|
|
}));
|
|
|
|
});
|
2020-09-04 15:39:19 +05:30
|
|
|
}
|
|
|
|
|
2020-09-08 14:20:39 +05:30
|
|
|
async _verifyDecryptionResult(result, txn) {
|
|
|
|
let device = this._senderDeviceCache.get(result.senderCurve25519Key);
|
|
|
|
if (!device) {
|
|
|
|
device = await this._deviceTracker.getDeviceByCurve25519Key(result.senderCurve25519Key, txn);
|
|
|
|
this._senderDeviceCache.set(result.senderCurve25519Key, device);
|
|
|
|
}
|
|
|
|
if (device) {
|
|
|
|
result.setDevice(device);
|
|
|
|
} else if (!this._room.isTrackingMembers) {
|
|
|
|
result.setRoomNotTrackedYet();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-15 17:26:40 +05:30
|
|
|
async _requestMissingSessionFromBackup(senderKey, sessionId, log) {
|
2020-09-22 13:00:25 +05:30
|
|
|
// show prompt to enable secret storage
|
|
|
|
if (!this._sessionBackup) {
|
2021-03-15 17:26:40 +05:30
|
|
|
log.set("enabled", false);
|
2020-09-22 13:00:25 +05:30
|
|
|
this._notifyMissingMegolmSession();
|
|
|
|
return;
|
|
|
|
}
|
2021-03-15 17:26:40 +05:30
|
|
|
log.set("id", sessionId);
|
|
|
|
log.set("senderKey", senderKey);
|
2020-09-17 21:27:52 +05:30
|
|
|
try {
|
2021-03-15 17:26:40 +05:30
|
|
|
const session = await this._sessionBackup.getSession(this._room.id, sessionId, log);
|
2020-09-17 21:27:52 +05:30
|
|
|
if (session?.algorithm === MEGOLM_ALGORITHM) {
|
|
|
|
if (session["sender_key"] !== senderKey) {
|
2021-03-15 17:26:40 +05:30
|
|
|
log.set("wrong_sender_key", session["sender_key"]);
|
|
|
|
log.logLevel = log.level.Warn;
|
2020-09-17 21:27:52 +05:30
|
|
|
return;
|
|
|
|
}
|
2021-03-01 19:34:45 +05:30
|
|
|
let roomKey = this._megolmDecryption.roomKeyFromBackup(this._room.id, sessionId, session);
|
2020-09-17 21:27:52 +05:30
|
|
|
if (roomKey) {
|
2021-03-15 19:03:14 +05:30
|
|
|
let keyIsBestOne = false;
|
2021-03-15 18:08:27 +05:30
|
|
|
let retryEventIds;
|
2021-03-01 19:34:45 +05:30
|
|
|
try {
|
2021-03-05 00:17:02 +05:30
|
|
|
const txn = await this._storage.readWriteTxn([this._storage.storeNames.inboundGroupSessions]);
|
2021-03-01 19:34:45 +05:30
|
|
|
try {
|
2021-03-15 19:03:14 +05:30
|
|
|
keyIsBestOne = await this._megolmDecryption.writeRoomKey(roomKey, txn);
|
2021-03-15 17:26:40 +05:30
|
|
|
log.set("isBetter", keyIsBestOne);
|
2021-03-15 18:08:27 +05:30
|
|
|
if (keyIsBestOne) {
|
|
|
|
retryEventIds = roomKey.eventIds;
|
|
|
|
}
|
2021-03-01 19:34:45 +05:30
|
|
|
} catch (err) {
|
|
|
|
txn.abort();
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
await txn.complete();
|
|
|
|
} finally {
|
|
|
|
// can still access properties on it afterwards
|
|
|
|
// this is just clearing the internal sessionInfo
|
|
|
|
roomKey.dispose();
|
|
|
|
}
|
2021-03-15 19:03:14 +05:30
|
|
|
if (keyIsBestOne) {
|
2021-03-15 17:26:40 +05:30
|
|
|
await log.wrap("retryDecryption", log => this._room.notifyRoomKey(roomKey, retryEventIds || [], log));
|
2021-03-01 19:34:45 +05:30
|
|
|
}
|
2020-09-17 21:27:52 +05:30
|
|
|
}
|
|
|
|
} else if (session?.algorithm) {
|
2021-03-15 17:26:40 +05:30
|
|
|
log.set("unknown algorithm", session.algorithm);
|
2020-09-17 17:50:15 +05:30
|
|
|
}
|
2020-09-17 21:27:52 +05:30
|
|
|
} catch (err) {
|
2021-03-02 19:59:35 +05:30
|
|
|
if (!(err.name === "HomeServerError" && err.errcode === "M_NOT_FOUND")) {
|
2021-03-15 17:26:40 +05:30
|
|
|
log.set("not_found", true);
|
|
|
|
} else {
|
|
|
|
log.error = err;
|
|
|
|
log.logLevel = log.level.Error;
|
2021-03-02 19:59:35 +05:30
|
|
|
}
|
2020-09-17 17:50:15 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-03-01 19:34:45 +05:30
|
|
|
* @param {RoomKey} roomKeys
|
2021-03-02 19:59:35 +05:30
|
|
|
* @param {Transaction} txn
|
|
|
|
* @return {Promise<Array<string>>} the event ids that should be retried to decrypt
|
2020-09-23 21:29:42 +05:30
|
|
|
*/
|
2021-03-02 19:59:35 +05:30
|
|
|
getEventIdsForMissingKey(roomKey, txn) {
|
|
|
|
return this._megolmDecryption.getEventIdsForMissingKey(this._room.id, roomKey.senderKey, roomKey.sessionId, txn);
|
2020-11-06 15:02:37 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
/** shares the encryption key for the next message if needed */
|
2021-02-23 23:52:59 +05:30
|
|
|
async ensureMessageKeyIsShared(hsApi, log) {
|
2020-11-10 18:32:27 +05:30
|
|
|
if (this._lastKeyPreShareTime?.measure() < MIN_PRESHARE_INTERVAL) {
|
2020-11-09 21:19:16 +05:30
|
|
|
return;
|
|
|
|
}
|
|
|
|
this._lastKeyPreShareTime = this._clock.createMeasure();
|
2021-04-09 14:07:43 +05:30
|
|
|
try {
|
|
|
|
this._keySharePromise = (async () => {
|
|
|
|
const roomKeyMessage = await this._megolmEncryption.ensureOutboundSession(this._room.id, this._encryptionParams);
|
|
|
|
if (roomKeyMessage) {
|
|
|
|
await log.wrap("share key", log => this._shareNewRoomKey(roomKeyMessage, hsApi, log));
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
await this._keySharePromise;
|
|
|
|
} finally {
|
|
|
|
this._keySharePromise = null;
|
2020-11-06 15:02:37 +05:30
|
|
|
}
|
2020-09-04 15:40:12 +05:30
|
|
|
}
|
|
|
|
|
2021-02-23 23:52:59 +05:30
|
|
|
async encrypt(type, content, hsApi, log) {
|
2021-04-09 14:07:43 +05:30
|
|
|
// ensureMessageKeyIsShared is still running,
|
|
|
|
// wait for it to create and share a key if needed
|
|
|
|
if (this._keySharePromise) {
|
|
|
|
log.set("waitForRunningKeyShare", true);
|
|
|
|
await this._keySharePromise;
|
|
|
|
}
|
2021-02-23 23:52:59 +05:30
|
|
|
const megolmResult = await log.wrap("megolm encrypt", () => this._megolmEncryption.encrypt(this._room.id, type, content, this._encryptionParams));
|
2020-09-03 21:20:28 +05:30
|
|
|
if (megolmResult.roomKeyMessage) {
|
2021-04-09 14:07:55 +05:30
|
|
|
await log.wrap("share key", log => this._shareNewRoomKey(megolmResult.roomKeyMessage, hsApi, log));
|
2020-09-03 21:20:28 +05:30
|
|
|
}
|
|
|
|
return {
|
|
|
|
type: ENCRYPTED_TYPE,
|
|
|
|
content: megolmResult.content
|
|
|
|
};
|
2020-09-03 19:06:17 +05:30
|
|
|
}
|
|
|
|
|
2020-09-08 18:08:27 +05:30
|
|
|
needsToShareKeys(memberChanges) {
|
|
|
|
for (const m of memberChanges.values()) {
|
2020-09-11 18:11:12 +05:30
|
|
|
if (m.hasJoined) {
|
2020-09-08 18:08:27 +05:30
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-02-23 23:52:59 +05:30
|
|
|
async _shareNewRoomKey(roomKeyMessage, hsApi, log) {
|
2021-03-05 00:17:02 +05:30
|
|
|
let writeOpTxn = await this._storage.readWriteTxn([this._storage.storeNames.operations]);
|
2021-03-03 23:26:16 +05:30
|
|
|
let operation;
|
2020-09-11 18:11:12 +05:30
|
|
|
try {
|
2021-03-03 23:26:16 +05:30
|
|
|
operation = this._writeRoomKeyShareOperation(roomKeyMessage, null, writeOpTxn);
|
2020-09-11 18:11:12 +05:30
|
|
|
} catch (err) {
|
|
|
|
writeOpTxn.abort();
|
|
|
|
throw err;
|
2020-09-08 17:54:48 +05:30
|
|
|
}
|
2020-09-11 18:11:12 +05:30
|
|
|
// TODO: at this point we have the room key stored, and the rest is sort of optional
|
|
|
|
// it would be nice if we could signal SendQueue that any error from here on is non-fatal and
|
|
|
|
// return the encrypted payload.
|
2021-03-03 23:26:16 +05:30
|
|
|
await this._processShareRoomKeyOperation(operation, hsApi, log);
|
2020-09-11 18:11:12 +05:30
|
|
|
}
|
|
|
|
|
2021-03-03 16:21:50 +05:30
|
|
|
async _addShareRoomKeyOperationForNewMembers(memberChangesArray, txn, log) {
|
2020-09-11 18:11:12 +05:30
|
|
|
const userIds = memberChangesArray.filter(m => m.hasJoined).map(m => m.userId);
|
|
|
|
const roomKeyMessage = await this._megolmEncryption.createRoomKeyMessage(
|
|
|
|
this._room.id, txn);
|
2020-09-08 17:54:48 +05:30
|
|
|
if (roomKeyMessage) {
|
2021-03-03 16:21:50 +05:30
|
|
|
log.log({
|
|
|
|
l: "share key for new members", userIds,
|
|
|
|
id: roomKeyMessage.session_id,
|
|
|
|
chain_index: roomKeyMessage.chain_index
|
|
|
|
});
|
2020-09-11 18:11:12 +05:30
|
|
|
this._writeRoomKeyShareOperation(roomKeyMessage, userIds, txn);
|
2021-03-02 23:44:29 +05:30
|
|
|
return true;
|
2020-09-08 17:54:48 +05:30
|
|
|
}
|
2021-03-02 23:44:29 +05:30
|
|
|
return false;
|
2020-09-08 17:54:48 +05:30
|
|
|
}
|
|
|
|
|
2021-02-17 23:15:04 +05:30
|
|
|
async flushPendingRoomKeyShares(hsApi, operations, log) {
|
2020-09-25 14:15:00 +05:30
|
|
|
// this has to be reentrant as it can be called from Room.start while still running
|
|
|
|
if (this._isFlushingRoomKeyShares) {
|
|
|
|
return;
|
2020-09-11 18:11:12 +05:30
|
|
|
}
|
2020-09-25 14:15:00 +05:30
|
|
|
this._isFlushingRoomKeyShares = true;
|
|
|
|
try {
|
|
|
|
if (!operations) {
|
2021-03-05 00:17:02 +05:30
|
|
|
const txn = await this._storage.readTxn([this._storage.storeNames.operations]);
|
2020-09-25 14:15:00 +05:30
|
|
|
operations = await txn.operations.getAllByTypeAndScope("share_room_key", this._room.id);
|
2020-09-11 18:11:12 +05:30
|
|
|
}
|
2020-09-25 14:15:00 +05:30
|
|
|
for (const operation of operations) {
|
|
|
|
// just to be sure
|
|
|
|
if (operation.type !== "share_room_key") {
|
|
|
|
continue;
|
|
|
|
}
|
2021-03-03 23:26:16 +05:30
|
|
|
await log.wrap("operation", log => this._processShareRoomKeyOperation(operation, hsApi, log));
|
2020-09-11 18:11:12 +05:30
|
|
|
}
|
2020-09-25 14:15:00 +05:30
|
|
|
} finally {
|
|
|
|
this._isFlushingRoomKeyShares = false;
|
2020-09-08 17:54:48 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-03 23:26:16 +05:30
|
|
|
_writeRoomKeyShareOperation(roomKeyMessage, userIds, txn) {
|
|
|
|
const id = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString();
|
|
|
|
const operation = {
|
|
|
|
id,
|
|
|
|
type: "share_room_key",
|
|
|
|
scope: this._room.id,
|
|
|
|
userIds,
|
|
|
|
roomKeyMessage,
|
|
|
|
};
|
|
|
|
txn.operations.add(operation);
|
|
|
|
return operation;
|
|
|
|
}
|
|
|
|
|
|
|
|
async _processShareRoomKeyOperation(operation, hsApi, log) {
|
|
|
|
log.set("id", operation.id);
|
2021-03-08 20:49:46 +05:30
|
|
|
|
2021-03-03 23:26:16 +05:30
|
|
|
await this._deviceTracker.trackRoom(this._room, log);
|
|
|
|
let devices;
|
|
|
|
if (operation.userIds === null) {
|
|
|
|
devices = await this._deviceTracker.devicesForTrackedRoom(this._room.id, hsApi, log);
|
|
|
|
const userIds = Array.from(devices.reduce((set, device) => set.add(device.userId), new Set()));
|
|
|
|
operation.userIds = userIds;
|
2021-03-08 20:49:46 +05:30
|
|
|
await this._updateOperationsStore(operations => operations.update(operation));
|
2021-03-03 23:26:16 +05:30
|
|
|
} else {
|
|
|
|
devices = await this._deviceTracker.devicesForRoomMembers(this._room.id, operation.userIds, hsApi, log);
|
|
|
|
}
|
|
|
|
|
2021-02-23 23:52:59 +05:30
|
|
|
const messages = await log.wrap("olm encrypt", log => this._olmEncryption.encrypt(
|
2021-03-03 23:26:16 +05:30
|
|
|
"m.room_key", operation.roomKeyMessage, devices, hsApi, log));
|
2021-03-08 20:49:46 +05:30
|
|
|
const missingDevices = devices.filter(d => !messages.some(m => m.device === d));
|
2021-02-23 23:52:59 +05:30
|
|
|
await log.wrap("send", log => this._sendMessagesToDevices(ENCRYPTED_TYPE, messages, hsApi, log));
|
2021-03-08 20:49:46 +05:30
|
|
|
if (missingDevices.length) {
|
|
|
|
await log.wrap("missingDevices", async log => {
|
|
|
|
log.set("devices", missingDevices.map(d => d.deviceId));
|
|
|
|
const unsentUserIds = operation.userIds.filter(userId => missingDevices.some(d => d.userId === userId));
|
|
|
|
log.set("unsentUserIds", unsentUserIds);
|
|
|
|
operation.userIds = unsentUserIds;
|
|
|
|
// first remove the users that we've sent the keys already from the operation,
|
|
|
|
// so if anything fails, we don't send them again
|
|
|
|
await this._updateOperationsStore(operations => operations.update(operation));
|
|
|
|
// now, let the devices we could not claim their key
|
|
|
|
const withheldMessage = this._megolmEncryption.createWithheldMessage(operation.roomKeyMessage, "m.no_olm", "OTKs exhausted");
|
|
|
|
await this._sendSharedMessageToDevices("org.matrix.room_key.withheld", withheldMessage, missingDevices, hsApi, log);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
await this._updateOperationsStore(operations => operations.remove(operation.id));
|
|
|
|
}
|
2021-03-03 23:26:16 +05:30
|
|
|
|
2021-03-08 20:49:46 +05:30
|
|
|
async _updateOperationsStore(callback) {
|
|
|
|
const writeTxn = await this._storage.readWriteTxn([this._storage.storeNames.operations]);
|
2021-03-03 23:26:16 +05:30
|
|
|
try {
|
2021-03-08 20:49:46 +05:30
|
|
|
callback(writeTxn.operations);
|
2021-03-03 23:26:16 +05:30
|
|
|
} catch (err) {
|
2021-03-08 20:49:46 +05:30
|
|
|
writeTxn.abort();
|
2021-03-03 23:26:16 +05:30
|
|
|
throw err;
|
|
|
|
}
|
2021-03-08 20:49:46 +05:30
|
|
|
await writeTxn.complete();
|
|
|
|
}
|
|
|
|
|
|
|
|
async _sendSharedMessageToDevices(type, message, devices, hsApi, log) {
|
|
|
|
const devicesByUser = groupBy(devices, device => device.userId);
|
|
|
|
const payload = {
|
|
|
|
messages: Array.from(devicesByUser.entries()).reduce((userMap, [userId, devices]) => {
|
|
|
|
userMap[userId] = devices.reduce((deviceMap, device) => {
|
|
|
|
deviceMap[device.deviceId] = message;
|
|
|
|
return deviceMap;
|
|
|
|
}, {});
|
|
|
|
return userMap;
|
|
|
|
}, {})
|
|
|
|
};
|
|
|
|
const txnId = makeTxnId();
|
|
|
|
await hsApi.sendToDevice(type, payload, txnId, {log}).response();
|
2020-09-08 17:54:48 +05:30
|
|
|
}
|
|
|
|
|
2021-02-23 23:52:59 +05:30
|
|
|
async _sendMessagesToDevices(type, messages, hsApi, log) {
|
|
|
|
log.set("messages", messages.length);
|
2020-09-03 19:06:17 +05:30
|
|
|
const messagesByUser = groupBy(messages, message => message.device.userId);
|
|
|
|
const payload = {
|
|
|
|
messages: Array.from(messagesByUser.entries()).reduce((userMap, [userId, messages]) => {
|
|
|
|
userMap[userId] = messages.reduce((deviceMap, message) => {
|
|
|
|
deviceMap[message.device.deviceId] = message.content;
|
|
|
|
return deviceMap;
|
|
|
|
}, {});
|
|
|
|
return userMap;
|
|
|
|
}, {})
|
|
|
|
};
|
|
|
|
const txnId = makeTxnId();
|
2021-02-23 23:52:59 +05:30
|
|
|
await hsApi.sendToDevice(type, payload, txnId, {log}).response();
|
2020-09-03 19:06:17 +05:30
|
|
|
}
|
2020-09-18 16:38:18 +05:30
|
|
|
|
2021-03-03 15:57:55 +05:30
|
|
|
filterUndecryptedEventEntriesForKeys(entries, keys) {
|
2021-03-02 03:00:33 +05:30
|
|
|
return entries.filter(entry => {
|
2021-03-03 15:57:55 +05:30
|
|
|
if (entry.isEncrypted && !entry.isDecrypted) {
|
|
|
|
const {event} = entry;
|
|
|
|
if (event) {
|
|
|
|
const senderKey = event.content?.["sender_key"];
|
|
|
|
const sessionId = event.content?.["session_id"];
|
|
|
|
return keys.some(key => senderKey === key.senderKey && sessionId === key.sessionId);
|
|
|
|
}
|
2021-03-02 03:00:33 +05:30
|
|
|
}
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-09-18 16:38:18 +05:30
|
|
|
dispose() {
|
|
|
|
this._disposed = true;
|
2020-10-14 14:55:49 +05:30
|
|
|
this._megolmBackfillCache.dispose();
|
|
|
|
this._megolmSyncCache.dispose();
|
2020-09-18 16:38:18 +05:30
|
|
|
}
|
2020-09-03 19:06:17 +05:30
|
|
|
}
|
2020-09-10 15:39:17 +05:30
|
|
|
|
|
|
|
/**
|
|
|
|
* wrappers around megolm decryption classes to be able to post-process
|
|
|
|
* the decryption results before turning them
|
|
|
|
*/
|
|
|
|
class DecryptionPreparation {
|
2021-03-02 03:44:14 +05:30
|
|
|
constructor(megolmDecryptionPreparation, extraErrors, source, roomEncryption, events) {
|
2020-09-10 15:39:17 +05:30
|
|
|
this._megolmDecryptionPreparation = megolmDecryptionPreparation;
|
|
|
|
this._extraErrors = extraErrors;
|
2021-03-02 03:44:14 +05:30
|
|
|
this._source = source;
|
2020-09-10 15:39:17 +05:30
|
|
|
this._roomEncryption = roomEncryption;
|
2020-09-23 21:04:25 +05:30
|
|
|
this._events = events;
|
2020-09-10 15:39:17 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
async decrypt() {
|
|
|
|
return new DecryptionChanges(
|
|
|
|
await this._megolmDecryptionPreparation.decrypt(),
|
|
|
|
this._extraErrors,
|
2021-03-02 03:44:14 +05:30
|
|
|
this._source,
|
2020-09-23 21:04:25 +05:30
|
|
|
this._roomEncryption,
|
|
|
|
this._events);
|
2020-09-10 15:39:17 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
dispose() {
|
2020-09-10 20:10:30 +05:30
|
|
|
this._megolmDecryptionPreparation.dispose();
|
2020-09-10 15:39:17 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class DecryptionChanges {
|
2021-03-02 03:44:14 +05:30
|
|
|
constructor(megolmDecryptionChanges, extraErrors, source, roomEncryption, events) {
|
2020-09-10 15:39:17 +05:30
|
|
|
this._megolmDecryptionChanges = megolmDecryptionChanges;
|
|
|
|
this._extraErrors = extraErrors;
|
2021-03-02 03:44:14 +05:30
|
|
|
this._source = source;
|
2020-09-10 15:39:17 +05:30
|
|
|
this._roomEncryption = roomEncryption;
|
2020-09-23 21:04:25 +05:30
|
|
|
this._events = events;
|
2020-09-10 15:39:17 +05:30
|
|
|
}
|
|
|
|
|
2021-03-15 17:26:40 +05:30
|
|
|
async write(txn, log) {
|
2020-09-10 15:39:17 +05:30
|
|
|
const {results, errors} = await this._megolmDecryptionChanges.write(txn);
|
|
|
|
mergeMap(this._extraErrors, errors);
|
2021-03-15 17:26:40 +05:30
|
|
|
await this._roomEncryption._processDecryptionResults(this._events, results, errors, this._source, txn, log);
|
2021-03-02 03:44:14 +05:30
|
|
|
return new BatchDecryptionResult(results, errors, this._roomEncryption);
|
2020-09-10 15:39:17 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class BatchDecryptionResult {
|
2021-03-02 03:44:14 +05:30
|
|
|
constructor(results, errors, roomEncryption) {
|
2020-09-10 15:39:17 +05:30
|
|
|
this.results = results;
|
|
|
|
this.errors = errors;
|
2021-03-02 03:44:14 +05:30
|
|
|
this._roomEncryption = roomEncryption;
|
2020-09-10 15:39:17 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
applyToEntries(entries) {
|
|
|
|
for (const entry of entries) {
|
|
|
|
const result = this.results.get(entry.id);
|
|
|
|
if (result) {
|
|
|
|
entry.setDecryptionResult(result);
|
|
|
|
} else {
|
|
|
|
const error = this.errors.get(entry.id);
|
|
|
|
if (error) {
|
|
|
|
entry.setDecryptionError(error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-03-02 03:44:14 +05:30
|
|
|
|
|
|
|
verifySenders(txn) {
|
|
|
|
return Promise.all(Array.from(this.results.values()).map(result => {
|
|
|
|
return this._roomEncryption._verifyDecryptionResult(result, txn);
|
|
|
|
}));
|
|
|
|
}
|
2020-09-10 15:39:17 +05:30
|
|
|
}
|