From a4d924acd1864be651397cd7ff3ba000304ce435 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 20 Jan 2022 11:15:48 +0100 Subject: [PATCH 01/52] make KeyLoader use proper olm types --- .../e2ee/megolm/decryption/KeyLoader.ts | 45 +++++++------------ src/matrix/e2ee/megolm/decryption/RoomKey.ts | 10 +++-- .../megolm/decryption/SessionDecryption.ts | 4 +- 3 files changed, 25 insertions(+), 34 deletions(-) diff --git a/src/matrix/e2ee/megolm/decryption/KeyLoader.ts b/src/matrix/e2ee/megolm/decryption/KeyLoader.ts index 3aca957d..c1925e84 100644 --- a/src/matrix/e2ee/megolm/decryption/KeyLoader.ts +++ b/src/matrix/e2ee/megolm/decryption/KeyLoader.ts @@ -17,25 +17,14 @@ limitations under the License. import {isBetterThan, IncomingRoomKey} from "./RoomKey"; import {BaseLRUCache} from "../../../../utils/LRUCache"; import type {RoomKey} from "./RoomKey"; +import type * as OlmNamespace from "@matrix-org/olm"; +type Olm = typeof OlmNamespace; export declare class OlmDecryptionResult { readonly plaintext: string; readonly message_index: number; } -export declare class OlmInboundGroupSession { - constructor(); - free(): void; - pickle(key: string | Uint8Array): string; - unpickle(key: string | Uint8Array, pickle: string); - create(session_key: string): string; - import_session(session_key: string): string; - decrypt(message: string): OlmDecryptionResult; - session_id(): string; - first_known_index(): number; - export_session(message_index: number): string; -} - /* Because Olm only has very limited memory available when compiled to wasm, we limit the amount of sessions held in memory. @@ -43,11 +32,11 @@ we limit the amount of sessions held in memory. export class KeyLoader extends BaseLRUCache { private pickleKey: string; - private olm: any; + private olm: Olm; private resolveUnusedOperation?: () => void; private operationBecomesUnusedPromise?: Promise; - constructor(olm: any, pickleKey: string, limit: number) { + constructor(olm: Olm, pickleKey: string, limit: number) { super(limit); this.pickleKey = pickleKey; this.olm = olm; @@ -60,7 +49,7 @@ export class KeyLoader extends BaseLRUCache { } } - async useKey(key: RoomKey, callback: (session: OlmInboundGroupSession, pickleKey: string) => Promise | T): Promise { + async useKey(key: RoomKey, callback: (session: Olm.InboundGroupSession, pickleKey: string) => Promise | T): Promise { const keyOp = await this.allocateOperation(key); try { return await callback(keyOp.session, this.pickleKey); @@ -186,11 +175,11 @@ export class KeyLoader extends BaseLRUCache { } class KeyOperation { - session: OlmInboundGroupSession; + session: Olm.InboundGroupSession; key: RoomKey; refCount: number; - constructor(key: RoomKey, session: OlmInboundGroupSession) { + constructor(key: RoomKey, session: Olm.InboundGroupSession) { this.key = key; this.session = session; this.refCount = 1; @@ -248,7 +237,7 @@ export function tests() { get serializationKey(): string { return `key-${this.sessionId}-${this._firstKnownIndex}`; } get serializationType(): string { return "type"; } get eventIds(): string[] | undefined { return undefined; } - loadInto(session: OlmInboundGroupSession) { + loadInto(session: Olm.InboundGroupSession) { const mockSession = session as MockInboundSession; mockSession.sessionId = this.sessionId; mockSession.firstKnownIndex = this._firstKnownIndex; @@ -284,7 +273,7 @@ export function tests() { return { "load key gives correct session": async assert => { - const loader = new KeyLoader(olm, PICKLE_KEY, 2); + const loader = new KeyLoader(olm as any as Olm, PICKLE_KEY, 2); let callback1Called = false; let callback2Called = false; const p1 = loader.useKey(new MockRoomKey(roomId, aliceSenderKey, sessionId1, 1), async session => { @@ -305,7 +294,7 @@ export function tests() { assert(callback2Called); }, "keys with different first index are kept separate": async assert => { - const loader = new KeyLoader(olm, PICKLE_KEY, 2); + const loader = new KeyLoader(olm as any as Olm, PICKLE_KEY, 2); let callback1Called = false; let callback2Called = false; const p1 = loader.useKey(new MockRoomKey(roomId, aliceSenderKey, sessionId1, 1), async session => { @@ -326,7 +315,7 @@ export function tests() { assert(callback2Called); }, "useKey blocks as long as no free sessions are available": async assert => { - const loader = new KeyLoader(olm, PICKLE_KEY, 1); + const loader = new KeyLoader(olm as any as Olm, PICKLE_KEY, 1); let resolve; let callbackCalled = false; loader.useKey(new MockRoomKey(roomId, aliceSenderKey, sessionId1, 1), async session => { @@ -343,7 +332,7 @@ export function tests() { assert.equal(callbackCalled, true); }, "cache hit while key in use, then replace (check refCount works properly)": async assert => { - const loader = new KeyLoader(olm, PICKLE_KEY, 1); + const loader = new KeyLoader(olm as any as Olm, PICKLE_KEY, 1); let resolve1, resolve2; const key1 = new MockRoomKey(roomId, aliceSenderKey, sessionId1, 1); const p1 = loader.useKey(key1, async session => { @@ -371,7 +360,7 @@ export function tests() { assert.equal(callbackCalled, true); }, "cache hit while key not in use": async assert => { - const loader = new KeyLoader(olm, PICKLE_KEY, 2); + const loader = new KeyLoader(olm as any as Olm, PICKLE_KEY, 2); let resolve1, resolve2, invocations = 0; const key1 = new MockRoomKey(roomId, aliceSenderKey, sessionId1, 1); await loader.useKey(key1, async session => { invocations += 1; }); @@ -385,7 +374,7 @@ export function tests() { }, "dispose calls free on all sessions": async assert => { instances = 0; - const loader = new KeyLoader(olm, PICKLE_KEY, 2); + const loader = new KeyLoader(olm as any as Olm, PICKLE_KEY, 2); await loader.useKey(new MockRoomKey(roomId, aliceSenderKey, sessionId1, 1), async session => {}); await loader.useKey(new MockRoomKey(roomId, aliceSenderKey, sessionId2, 1), async session => {}); assert.equal(instances, 2); @@ -395,7 +384,7 @@ export function tests() { assert.strictEqual(loader.size, 0, "loader.size"); }, "checkBetterThanKeyInStorage false with cache": async assert => { - const loader = new KeyLoader(olm, PICKLE_KEY, 2); + const loader = new KeyLoader(olm as any as Olm, PICKLE_KEY, 2); const key1 = new MockRoomKey(roomId, aliceSenderKey, sessionId1, 2); await loader.useKey(key1, async session => {}); // fake we've checked with storage that this is the best key, @@ -409,7 +398,7 @@ export function tests() { assert.strictEqual(key2.isBetter, false); }, "checkBetterThanKeyInStorage true with cache": async assert => { - const loader = new KeyLoader(olm, PICKLE_KEY, 2); + const loader = new KeyLoader(olm as any as Olm, PICKLE_KEY, 2); const key1 = new MockRoomKey(roomId, aliceSenderKey, sessionId1, 2); key1.isBetter = true; // fake we've check with storage so far (not including key2) this is the best key await loader.useKey(key1, async session => {}); @@ -420,7 +409,7 @@ export function tests() { assert.strictEqual(key2.isBetter, true); }, "prefer to remove worst key for a session from cache": async assert => { - const loader = new KeyLoader(olm, PICKLE_KEY, 2); + const loader = new KeyLoader(olm as any as Olm, PICKLE_KEY, 2); const key1 = new MockRoomKey(roomId, aliceSenderKey, sessionId1, 2); await loader.useKey(key1, async session => {}); key1.isBetter = true; // set to true just so it gets returned from getCachedKey diff --git a/src/matrix/e2ee/megolm/decryption/RoomKey.ts b/src/matrix/e2ee/megolm/decryption/RoomKey.ts index 81f1a9be..2cb65b33 100644 --- a/src/matrix/e2ee/megolm/decryption/RoomKey.ts +++ b/src/matrix/e2ee/megolm/decryption/RoomKey.ts @@ -17,7 +17,9 @@ limitations under the License. import type {InboundGroupSessionEntry} from "../../../storage/idb/stores/InboundGroupSessionStore"; import type {Transaction} from "../../../storage/idb/Transaction"; import type {DecryptionResult} from "../../DecryptionResult"; -import type {KeyLoader, OlmInboundGroupSession} from "./KeyLoader"; +import type {KeyLoader} from "./KeyLoader"; +import type * as OlmNamespace from "@matrix-org/olm"; +type Olm = typeof OlmNamespace; export abstract class RoomKey { private _isBetter: boolean | undefined; @@ -33,7 +35,7 @@ export abstract class RoomKey { abstract get serializationKey(): string; abstract get serializationType(): string; abstract get eventIds(): string[] | undefined; - abstract loadInto(session: OlmInboundGroupSession, pickleKey: string): void; + abstract loadInto(session: Olm.InboundGroupSession, pickleKey: string): void; /* Whether the key has been checked against storage (or is from storage) * to be the better key for a given session. Given that all keys are checked to be better * as part of writing, we can trust that when this returns true, it really is the best key @@ -44,7 +46,7 @@ export abstract class RoomKey { set isBetter(value: boolean | undefined) { this._isBetter = value; } } -export function isBetterThan(newSession: OlmInboundGroupSession, existingSession: OlmInboundGroupSession) { +export function isBetterThan(newSession: Olm.InboundGroupSession, existingSession: Olm.InboundGroupSession) { return newSession.first_known_index() < existingSession.first_known_index(); } @@ -87,7 +89,7 @@ export abstract class IncomingRoomKey extends RoomKey { get eventIds() { return this._eventIds; } - private async _checkBetterThanKeyInStorage(loader: KeyLoader, callback: (((session: OlmInboundGroupSession, pickleKey: string) => void) | undefined), txn: Transaction): Promise { + private async _checkBetterThanKeyInStorage(loader: KeyLoader, callback: (((session: Olm.InboundGroupSession, pickleKey: string) => void) | undefined), txn: Transaction): Promise { if (this.isBetter !== undefined) { return this.isBetter; } diff --git a/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts b/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts index 7e466806..f56feb47 100644 --- a/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts +++ b/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts @@ -17,7 +17,7 @@ limitations under the License. import {DecryptionResult} from "../../DecryptionResult.js"; import {DecryptionError} from "../../common.js"; import {ReplayDetectionEntry} from "./ReplayDetectionEntry"; -import type {RoomKey} from "./RoomKey.js"; +import type {RoomKey} from "./RoomKey"; import type {KeyLoader, OlmDecryptionResult} from "./KeyLoader"; import type {OlmWorker} from "../../OlmWorker"; import type {TimelineEvent} from "../../../storage/types"; @@ -61,7 +61,7 @@ export class SessionDecryption { this.decryptionRequests!.push(request); decryptionResult = await request.response(); } else { - decryptionResult = session.decrypt(ciphertext); + decryptionResult = session.decrypt(ciphertext) as OlmDecryptionResult; } const {plaintext} = decryptionResult!; let payload; From 993a86ddb269f884ff1dcaf6492731a3d318cebb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 20 Jan 2022 11:16:08 +0100 Subject: [PATCH 02/52] convert SessionBackup to typescript and pass in keyloader --- src/matrix/Session.js | 17 ++-- src/matrix/e2ee/megolm/SessionBackup.js | 62 -------------- src/matrix/e2ee/megolm/SessionBackup.ts | 107 ++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 69 deletions(-) delete mode 100644 src/matrix/e2ee/megolm/SessionBackup.js create mode 100644 src/matrix/e2ee/megolm/SessionBackup.ts diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 8ccc71cc..0d7d057d 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -70,6 +70,7 @@ export class Session { this._e2eeAccount = null; this._deviceTracker = null; this._olmEncryption = null; + this._keyLoader = null; this._megolmEncryption = null; this._megolmDecryption = null; this._getSyncToken = () => this.syncToken; @@ -141,8 +142,8 @@ export class Session { now: this._platform.clock.now, ownDeviceId: this._sessionInfo.deviceId, }); - const keyLoader = new MegOlmKeyLoader(this._olm, PICKLE_KEY, 20); - this._megolmDecryption = new MegOlmDecryption(keyLoader, this._olmWorker); + this._keyLoader = new MegOlmKeyLoader(this._olm, PICKLE_KEY, 20); + this._megolmDecryption = new MegOlmDecryption(this._keyLoader, this._olmWorker); this._deviceMessageHandler.enableEncryption({olmDecryption, megolmDecryption: this._megolmDecryption}); } @@ -246,12 +247,14 @@ export class Session { async _createSessionBackup(ssssKey, txn) { const secretStorage = new SecretStorage({key: ssssKey, platform: this._platform}); - this._sessionBackup = await SessionBackup.fromSecretStorage({ - platform: this._platform, - olm: this._olm, secretStorage, - hsApi: this._hsApi, + this._sessionBackup = await SessionBackup.fromSecretStorage( + this._platform, + this._olm, + secretStorage, + this._hsApi, + this._keyLoader, txn - }); + ); if (this._sessionBackup) { for (const room of this._rooms.values()) { if (room.isEncrypted) { diff --git a/src/matrix/e2ee/megolm/SessionBackup.js b/src/matrix/e2ee/megolm/SessionBackup.js deleted file mode 100644 index daba9961..00000000 --- a/src/matrix/e2ee/megolm/SessionBackup.js +++ /dev/null @@ -1,62 +0,0 @@ -/* -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. -*/ - -export class SessionBackup { - constructor({backupInfo, decryption, hsApi}) { - this._backupInfo = backupInfo; - this._decryption = decryption; - this._hsApi = hsApi; - } - - async getSession(roomId, sessionId, log) { - const sessionResponse = await this._hsApi.roomKeyForRoomAndSession(this._backupInfo.version, roomId, sessionId, {log}).response(); - const sessionInfo = this._decryption.decrypt( - sessionResponse.session_data.ephemeral, - sessionResponse.session_data.mac, - sessionResponse.session_data.ciphertext, - ); - return JSON.parse(sessionInfo); - } - - get version() { - return this._backupInfo.version; - } - - dispose() { - this._decryption.free(); - } - - static async fromSecretStorage({platform, olm, secretStorage, hsApi, txn}) { - const base64PrivateKey = await secretStorage.readSecret("m.megolm_backup.v1", txn); - if (base64PrivateKey) { - const privateKey = new Uint8Array(platform.encoding.base64.decode(base64PrivateKey)); - const backupInfo = await hsApi.roomKeysVersion().response(); - const expectedPubKey = backupInfo.auth_data.public_key; - const decryption = new olm.PkDecryption(); - try { - const pubKey = decryption.init_with_private_key(privateKey); - if (pubKey !== expectedPubKey) { - throw new Error(`Bad backup key, public key does not match. Calculated ${pubKey} but expected ${expectedPubKey}`); - } - } catch(err) { - decryption.free(); - throw err; - } - return new SessionBackup({backupInfo, decryption, hsApi}); - } - } -} - diff --git a/src/matrix/e2ee/megolm/SessionBackup.ts b/src/matrix/e2ee/megolm/SessionBackup.ts new file mode 100644 index 00000000..9e875185 --- /dev/null +++ b/src/matrix/e2ee/megolm/SessionBackup.ts @@ -0,0 +1,107 @@ +/* +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. +*/ + +import type {HomeServerApi} from "../../net/HomeServerApi"; +import type {RoomKey} from "./decryption/RoomKey"; +import type {KeyLoader} from "./decryption/KeyLoader"; +import type {SecretStorage} from "../../ssss/SecretStorage"; +import type {ILogItem} from "../../../logging/types"; +import type {Platform} from "../../../platform/web/Platform"; +import type {Transaction} from "../../storage/idb/Transaction"; +import type * as OlmNamespace from "@matrix-org/olm"; +type Olm = typeof OlmNamespace; + +type SignatureMap = { + [userId: string]: {[deviceIdAndAlgorithm: string]: string} +} + +interface BaseBackupInfo { + version: string, + etag: string, + count: number, +} + +const Curve25519Algorithm = "m.megolm_backup.v1.curve25519-aes-sha2"; + +interface Curve25519BackupInfo extends BaseBackupInfo { + algorithm: typeof Curve25519Algorithm, + auth_data: Curve25519AuthData, +} + +interface OtherBackupInfo extends BaseBackupInfo { + algorithm: "other" +}; + +type BackupInfo = Curve25519BackupInfo | OtherBackupInfo; + +interface Curve25519AuthData { + public_key: string, + signatures: SignatureMap +} + +type AuthData = Curve25519AuthData; + +export class SessionBackup { + constructor( + private readonly backupInfo: BackupInfo, + private readonly decryption: Olm.PkDecryption, + private readonly hsApi: HomeServerApi, + private readonly keyLoader: KeyLoader + ) {} + + async getSession(roomId: string, sessionId: string, log: ILogItem) { + const sessionResponse = await this.hsApi.roomKeyForRoomAndSession(this.backupInfo.version, roomId, sessionId, {log}).response(); + const sessionInfo = this.decryption.decrypt( + sessionResponse.session_data.ephemeral, + sessionResponse.session_data.mac, + sessionResponse.session_data.ciphertext, + ); + return JSON.parse(sessionInfo); + } + + get version() { + return this.backupInfo.version; + } + + dispose() { + this.decryption.free(); + } + + static async fromSecretStorage(platform: Platform, olm: Olm, secretStorage: SecretStorage, hsApi: HomeServerApi, keyLoader: KeyLoader, txn: Transaction) { + const base64PrivateKey = await secretStorage.readSecret("m.megolm_backup.v1", txn); + if (base64PrivateKey) { + const privateKey = new Uint8Array(platform.encoding.base64.decode(base64PrivateKey)); + const backupInfo = await hsApi.roomKeysVersion().response() as BackupInfo; + if (backupInfo.algorithm === Curve25519Algorithm) { + const expectedPubKey = backupInfo.auth_data.public_key; + const decryption = new olm.PkDecryption(); + try { + const pubKey = decryption.init_with_private_key(privateKey); + if (pubKey !== expectedPubKey) { + throw new Error(`Bad backup key, public key does not match. Calculated ${pubKey} but expected ${expectedPubKey}`); + } + } catch(err) { + decryption.free(); + throw err; + } + return new SessionBackup(backupInfo, decryption, hsApi, keyLoader); + } else { + throw new Error(`Unknown backup algorithm: ${backupInfo.algorithm}`); + } + } + } +} + From 5d87d8bde3710c6d05c5f9530f335559f3de51e9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 25 Jan 2022 18:43:44 +0100 Subject: [PATCH 03/52] change store.get return type when no value is found to undefined IDBRequest.result is undefined according to the official TS type decls. --- src/matrix/storage/idb/QueryTarget.ts | 4 ++-- src/matrix/storage/idb/Store.ts | 2 +- src/matrix/storage/idb/stores/AccountDataStore.ts | 2 +- src/matrix/storage/idb/stores/DeviceIdentityStore.ts | 6 +++--- .../storage/idb/stores/GroupSessionDecryptionStore.ts | 2 +- src/matrix/storage/idb/stores/InboundGroupSessionStore.ts | 2 +- src/matrix/storage/idb/stores/OlmSessionStore.ts | 2 +- src/matrix/storage/idb/stores/OutboundGroupSessionStore.ts | 2 +- src/matrix/storage/idb/stores/RoomMemberStore.ts | 2 +- src/matrix/storage/idb/stores/RoomStateStore.ts | 2 +- src/matrix/storage/idb/stores/TimelineEventStore.ts | 4 ++-- src/matrix/storage/idb/stores/TimelineFragmentStore.ts | 2 +- src/matrix/storage/idb/stores/UserIdentityStore.ts | 2 +- 13 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/matrix/storage/idb/QueryTarget.ts b/src/matrix/storage/idb/QueryTarget.ts index 26821e0f..97b85569 100644 --- a/src/matrix/storage/idb/QueryTarget.ts +++ b/src/matrix/storage/idb/QueryTarget.ts @@ -37,7 +37,7 @@ interface QueryTargetInterface { openKeyCursor(range?: IDBQuery, direction?: IDBCursorDirection | undefined): IDBRequest; supports(method: string): boolean; keyPath: string | string[]; - get(key: IDBValidKey | IDBKeyRange): IDBRequest; + get(key: IDBValidKey | IDBKeyRange): IDBRequest; getKey(key: IDBValidKey | IDBKeyRange): IDBRequest; } @@ -78,7 +78,7 @@ export class QueryTarget { return this._target.supports(methodName); } - get(key: IDBValidKey | IDBKeyRange): Promise { + get(key: IDBValidKey | IDBKeyRange): Promise { return reqAsPromise(this._target.get(key)); } diff --git a/src/matrix/storage/idb/Store.ts b/src/matrix/storage/idb/Store.ts index 07cc90b0..f45b3f3e 100644 --- a/src/matrix/storage/idb/Store.ts +++ b/src/matrix/storage/idb/Store.ts @@ -91,7 +91,7 @@ export class QueryTargetWrapper { } } - get(key: IDBValidKey | IDBKeyRange): IDBRequest { + get(key: IDBValidKey | IDBKeyRange): IDBRequest { try { LOG_REQUESTS && logRequest("get", [key], this._qt); return this._qt.get(key); diff --git a/src/matrix/storage/idb/stores/AccountDataStore.ts b/src/matrix/storage/idb/stores/AccountDataStore.ts index 32aec513..2081ad8f 100644 --- a/src/matrix/storage/idb/stores/AccountDataStore.ts +++ b/src/matrix/storage/idb/stores/AccountDataStore.ts @@ -28,7 +28,7 @@ export class AccountDataStore { this._store = store; } - async get(type: string): Promise { + async get(type: string): Promise { return await this._store.get(type); } diff --git a/src/matrix/storage/idb/stores/DeviceIdentityStore.ts b/src/matrix/storage/idb/stores/DeviceIdentityStore.ts index 6b9f5332..2936f079 100644 --- a/src/matrix/storage/idb/stores/DeviceIdentityStore.ts +++ b/src/matrix/storage/idb/stores/DeviceIdentityStore.ts @@ -17,7 +17,7 @@ limitations under the License. import {MAX_UNICODE, MIN_UNICODE} from "./common"; import {Store} from "../Store"; -interface DeviceIdentity { +export interface DeviceIdentity { userId: string; deviceId: string; ed25519Key: string; @@ -65,7 +65,7 @@ export class DeviceIdentityStore { return deviceIds; } - get(userId: string, deviceId: string): Promise { + get(userId: string, deviceId: string): Promise { return this._store.get(encodeKey(userId, deviceId)); } @@ -74,7 +74,7 @@ export class DeviceIdentityStore { this._store.put(deviceIdentity); } - getByCurve25519Key(curve25519Key: string): Promise { + getByCurve25519Key(curve25519Key: string): Promise { return this._store.index("byCurve25519Key").get(curve25519Key); } diff --git a/src/matrix/storage/idb/stores/GroupSessionDecryptionStore.ts b/src/matrix/storage/idb/stores/GroupSessionDecryptionStore.ts index 1627e44a..a73c882d 100644 --- a/src/matrix/storage/idb/stores/GroupSessionDecryptionStore.ts +++ b/src/matrix/storage/idb/stores/GroupSessionDecryptionStore.ts @@ -35,7 +35,7 @@ export class GroupSessionDecryptionStore { this._store = store; } - get(roomId: string, sessionId: string, messageIndex: number): Promise { + get(roomId: string, sessionId: string, messageIndex: number): Promise { return this._store.get(encodeKey(roomId, sessionId, messageIndex)); } diff --git a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts index 22093884..c670302c 100644 --- a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts +++ b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts @@ -46,7 +46,7 @@ export class InboundGroupSessionStore { return key === fetchedKey; } - get(roomId: string, senderKey: string, sessionId: string): Promise { + get(roomId: string, senderKey: string, sessionId: string): Promise { return this._store.get(encodeKey(roomId, senderKey, sessionId)); } diff --git a/src/matrix/storage/idb/stores/OlmSessionStore.ts b/src/matrix/storage/idb/stores/OlmSessionStore.ts index 3310215e..d5a79de2 100644 --- a/src/matrix/storage/idb/stores/OlmSessionStore.ts +++ b/src/matrix/storage/idb/stores/OlmSessionStore.ts @@ -62,7 +62,7 @@ export class OlmSessionStore { }); } - get(senderKey: string, sessionId: string): Promise { + get(senderKey: string, sessionId: string): Promise { return this._store.get(encodeKey(senderKey, sessionId)); } diff --git a/src/matrix/storage/idb/stores/OutboundGroupSessionStore.ts b/src/matrix/storage/idb/stores/OutboundGroupSessionStore.ts index 9712c717..28ef23b0 100644 --- a/src/matrix/storage/idb/stores/OutboundGroupSessionStore.ts +++ b/src/matrix/storage/idb/stores/OutboundGroupSessionStore.ts @@ -32,7 +32,7 @@ export class OutboundGroupSessionStore { this._store.delete(roomId); } - get(roomId: string): Promise { + get(roomId: string): Promise { return this._store.get(roomId); } diff --git a/src/matrix/storage/idb/stores/RoomMemberStore.ts b/src/matrix/storage/idb/stores/RoomMemberStore.ts index 00cf607f..78024e7d 100644 --- a/src/matrix/storage/idb/stores/RoomMemberStore.ts +++ b/src/matrix/storage/idb/stores/RoomMemberStore.ts @@ -46,7 +46,7 @@ export class RoomMemberStore { this._roomMembersStore = roomMembersStore; } - get(roomId: string, userId: string): Promise { + get(roomId: string, userId: string): Promise { return this._roomMembersStore.get(encodeKey(roomId, userId)); } diff --git a/src/matrix/storage/idb/stores/RoomStateStore.ts b/src/matrix/storage/idb/stores/RoomStateStore.ts index b7ece0f7..d2bf811d 100644 --- a/src/matrix/storage/idb/stores/RoomStateStore.ts +++ b/src/matrix/storage/idb/stores/RoomStateStore.ts @@ -36,7 +36,7 @@ export class RoomStateStore { this._roomStateStore = idbStore; } - get(roomId: string, type: string, stateKey: string): Promise { + get(roomId: string, type: string, stateKey: string): Promise { const key = encodeKey(roomId, type, stateKey); return this._roomStateStore.get(key); } diff --git a/src/matrix/storage/idb/stores/TimelineEventStore.ts b/src/matrix/storage/idb/stores/TimelineEventStore.ts index bb6f652f..62a82fc0 100644 --- a/src/matrix/storage/idb/stores/TimelineEventStore.ts +++ b/src/matrix/storage/idb/stores/TimelineEventStore.ts @@ -301,11 +301,11 @@ export class TimelineEventStore { this._timelineStore.put(entry as TimelineEventStorageEntry); } - get(roomId: string, eventKey: EventKey): Promise { + get(roomId: string, eventKey: EventKey): Promise { return this._timelineStore.get(encodeKey(roomId, eventKey.fragmentId, eventKey.eventIndex)); } - getByEventId(roomId: string, eventId: string): Promise { + getByEventId(roomId: string, eventId: string): Promise { return this._timelineStore.index("byEventId").get(encodeEventIdKey(roomId, eventId)); } diff --git a/src/matrix/storage/idb/stores/TimelineFragmentStore.ts b/src/matrix/storage/idb/stores/TimelineFragmentStore.ts index 5753a93e..4e15589f 100644 --- a/src/matrix/storage/idb/stores/TimelineFragmentStore.ts +++ b/src/matrix/storage/idb/stores/TimelineFragmentStore.ts @@ -83,7 +83,7 @@ export class TimelineFragmentStore { this._store.put(fragment); } - get(roomId: string, fragmentId: number): Promise { + get(roomId: string, fragmentId: number): Promise { return this._store.get(encodeKey(roomId, fragmentId)); } diff --git a/src/matrix/storage/idb/stores/UserIdentityStore.ts b/src/matrix/storage/idb/stores/UserIdentityStore.ts index 692d8384..1c55baf0 100644 --- a/src/matrix/storage/idb/stores/UserIdentityStore.ts +++ b/src/matrix/storage/idb/stores/UserIdentityStore.ts @@ -28,7 +28,7 @@ export class UserIdentityStore { this._store = store; } - get(userId: string): Promise { + get(userId: string): Promise { return this._store.get(userId); } From a3e294bb6011ecad16f0466a5ba167893a0a1223 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 25 Jan 2022 18:45:39 +0100 Subject: [PATCH 04/52] small cleanup --- src/matrix/e2ee/megolm/decryption/RoomKey.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/matrix/e2ee/megolm/decryption/RoomKey.ts b/src/matrix/e2ee/megolm/decryption/RoomKey.ts index 2cb65b33..299b0a81 100644 --- a/src/matrix/e2ee/megolm/decryption/RoomKey.ts +++ b/src/matrix/e2ee/megolm/decryption/RoomKey.ts @@ -148,15 +148,8 @@ class DeviceMessageRoomKey extends IncomingRoomKey { } class BackupRoomKey extends IncomingRoomKey { - private _roomId: string; - private _sessionId: string; - private _backupInfo: string; - - constructor(roomId, sessionId, backupInfo) { + constructor(private _roomId: string, private _sessionId: string, private _backupInfo: object) { super(); - this._roomId = roomId; - this._sessionId = sessionId; - this._backupInfo = backupInfo; } get roomId() { return this._roomId; } From 290aaad63a9dbb7d3acd845f08edc9c2b5d55143 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 25 Jan 2022 18:46:51 +0100 Subject: [PATCH 05/52] add sessionsNeedingBackup store --- src/matrix/storage/common.ts | 1 + src/matrix/storage/idb/Transaction.ts | 5 ++ src/matrix/storage/idb/schema.ts | 8 ++- .../idb/stores/SessionNeedingBackupStore.ts | 56 +++++++++++++++++++ 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts diff --git a/src/matrix/storage/common.ts b/src/matrix/storage/common.ts index 23bb0d31..3929c061 100644 --- a/src/matrix/storage/common.ts +++ b/src/matrix/storage/common.ts @@ -33,6 +33,7 @@ export enum StoreNames { groupSessionDecryptions = "groupSessionDecryptions", operations = "operations", accountData = "accountData", + sessionsNeedingBackup = "sessionsNeedingBackup", } export const STORE_NAMES: Readonly = Object.values(StoreNames); diff --git a/src/matrix/storage/idb/Transaction.ts b/src/matrix/storage/idb/Transaction.ts index 80894105..9b765310 100644 --- a/src/matrix/storage/idb/Transaction.ts +++ b/src/matrix/storage/idb/Transaction.ts @@ -36,6 +36,7 @@ import {OutboundGroupSessionStore} from "./stores/OutboundGroupSessionStore"; import {GroupSessionDecryptionStore} from "./stores/GroupSessionDecryptionStore"; import {OperationStore} from "./stores/OperationStore"; import {AccountDataStore} from "./stores/AccountDataStore"; +import {SessionNeedingBackupStore} from "./stores/SessionNeedingBackupStore"; import type {ILogger, ILogItem} from "../../../logging/types"; export type IDBKey = IDBValidKey | IDBKeyRange; @@ -151,6 +152,10 @@ export class Transaction { get inboundGroupSessions(): InboundGroupSessionStore { return this._store(StoreNames.inboundGroupSessions, idbStore => new InboundGroupSessionStore(idbStore)); } + + get sessionsNeedingBackup(): SessionNeedingBackupStore { + return this._store(StoreNames.sessionsNeedingBackup, idbStore => new SessionNeedingBackupStore(idbStore)); + } get outboundGroupSessions(): OutboundGroupSessionStore { return this._store(StoreNames.outboundGroupSessions, idbStore => new OutboundGroupSessionStore(idbStore)); diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index ad3e5896..44a04bba 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -31,7 +31,8 @@ export const schema: MigrationFunc[] = [ fixMissingRoomsInUserIdentities, changeSSSSKeyPrefix, backupAndRestoreE2EEAccountToLocalStorage, - clearAllStores + clearAllStores, + createSessionsNeedingBackup ]; // TODO: how to deal with git merge conflicts of this array? @@ -270,3 +271,8 @@ async function clearAllStores(db: IDBDatabase, txn: IDBTransaction) { } } } + +// v15 adds the sessionsNeedingBackup store, for session backup +function createSessionsNeedingBackup(db: IDBDatabase): void { + db.createObjectStore("sessionsNeedingBackup", {keyPath: "key"}); +} diff --git a/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts b/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts new file mode 100644 index 00000000..27104191 --- /dev/null +++ b/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts @@ -0,0 +1,56 @@ +/* +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. +*/ + +import type {Store} from "../Store"; + +export type BackupEntry = { + roomId: string; + senderKey: string; + sessionId: string; +}; + +type StorageEntry = { + key: string +}; + +function encodeKey(roomId: string, senderKey: string, sessionId: string): string { + return `${roomId}|${senderKey}|${sessionId}`; +} + +function decodeKey(key: string): BackupEntry { + const [roomId, senderKey, sessionId] = key.split("|"); + return {roomId, senderKey, sessionId}; +} + +export class SessionNeedingBackupStore { + constructor(private store: Store) {} + + async getFirstEntries(amount: number): Promise { + const storageEntries = await this.store.selectLimit(undefined, amount); + return storageEntries.map(s => decodeKey(s.key)); + } + + set(roomId: string, senderKey: string, sessionId: string): void { + const storageEntry : StorageEntry = { + key: encodeKey(roomId, senderKey, sessionId), + }; + this.store.put(storageEntry); + } + + remove(roomId: string, senderKey: string, sessionId: string): void { + this.store.delete(encodeKey(roomId, senderKey, sessionId)); + } +} From 8f4e3c62cea231141ab8c6fe36277c221c622326 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 25 Jan 2022 18:47:42 +0100 Subject: [PATCH 06/52] add hs endpoint for backup keys upload --- src/matrix/net/HomeServerApi.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts index 3a6517cd..a23321f9 100644 --- a/src/matrix/net/HomeServerApi.ts +++ b/src/matrix/net/HomeServerApi.ts @@ -227,6 +227,10 @@ export class HomeServerApi { return this._get(`/room_keys/keys/${encodeURIComponent(roomId)}/${encodeURIComponent(sessionId)}`, {version}, undefined, options); } + uploadRoomKeysToBackup(version: string, payload: Record, options?: IRequestOptions): IHomeServerRequest { + return this._put(`/room_keys/keys`, {version}, payload, options); + } + uploadAttachment(blob: Blob, filename: string, options?: IRequestOptions): IHomeServerRequest { return this._authedRequest("POST", `${this._homeserver}/_matrix/media/r0/upload`, {filename}, blob, options); } From ffece4f3574bdea17f5b27acc9e5f95875daef01 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 25 Jan 2022 18:48:03 +0100 Subject: [PATCH 07/52] move some validation of into session backup --- src/matrix/e2ee/RoomEncryption.js | 51 ++++++++++++++----------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index d9151a85..5a511d6e 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -182,35 +182,30 @@ export class RoomEncryption { log.set("id", sessionId); log.set("senderKey", senderKey); try { - const session = await this._sessionBackup.getSession(this._room.id, sessionId, log); - if (session?.algorithm === MEGOLM_ALGORITHM) { - let roomKey = this._megolmDecryption.roomKeyFromBackup(this._room.id, sessionId, session); - if (roomKey) { - if (roomKey.senderKey !== senderKey) { - log.set("wrong_sender_key", roomKey.senderKey); - log.logLevel = log.level.Warn; - return; - } - let keyIsBestOne = false; - let retryEventIds; - const txn = await this._storage.readWriteTxn([this._storage.storeNames.inboundGroupSessions]); - try { - keyIsBestOne = await this._megolmDecryption.writeRoomKey(roomKey, txn); - log.set("isBetter", keyIsBestOne); - if (keyIsBestOne) { - retryEventIds = roomKey.eventIds; - } - } catch (err) { - txn.abort(); - throw err; - } - await txn.complete(); - if (keyIsBestOne) { - await log.wrap("retryDecryption", log => this._room.notifyRoomKey(roomKey, retryEventIds || [], log)); - } + const roomKey = await this._sessionBackup.getRoomKey(this._room.id, sessionId, log); + if (roomKey) { + if (roomKey.senderKey !== senderKey) { + log.set("wrong_sender_key", roomKey.senderKey); + log.logLevel = log.level.Warn; + return; + } + let keyIsBestOne = false; + let retryEventIds; + const txn = await this._storage.readWriteTxn([this._storage.storeNames.inboundGroupSessions]); + try { + keyIsBestOne = await this._megolmDecryption.writeRoomKey(roomKey, txn); + log.set("isBetter", keyIsBestOne); + if (keyIsBestOne) { + retryEventIds = roomKey.eventIds; + } + } catch (err) { + txn.abort(); + throw err; + } + await txn.complete(); + if (keyIsBestOne) { + await log.wrap("retryDecryption", log => this._room.notifyRoomKey(roomKey, retryEventIds || [], log)); } - } else if (session?.algorithm) { - log.set("unknown algorithm", session.algorithm); } } catch (err) { if (!(err.name === "HomeServerError" && err.errcode === "M_NOT_FOUND")) { From 933a1b4636c0c05b3b2e38d9ca1a109eedb7dfd0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 25 Jan 2022 18:48:19 +0100 Subject: [PATCH 08/52] draft of session backup writing + some refactoring --- src/matrix/Session.js | 9 ++ src/matrix/e2ee/megolm/SessionBackup.ts | 195 +++++++++++++++++++++--- 2 files changed, 179 insertions(+), 25 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 0d7d057d..94b68106 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -253,6 +253,7 @@ export class Session { secretStorage, this._hsApi, this._keyLoader, + this._storage, txn ); if (this._sessionBackup) { @@ -580,6 +581,11 @@ export class Session { if (preparation) { await log.wrap("deviceMsgs", log => this._deviceMessageHandler.writeSync(preparation, txn, log)); + // this should come after the deviceMessageHandler, so the room keys are already written and their + // isBetter property has been checked + if (this._sessionBackup) { + this._sessionBackup.writeKeys(preparation.newRoomKeys, txn, log); + } } // store account data @@ -617,6 +623,9 @@ export class Session { await log.wrap("uploadKeys", log => this._e2eeAccount.uploadKeys(this._storage, false, log)); } } + if (this._sessionBackup) { + this._sessionBackup.flush(); + } } applyRoomCollectionChangesAfterSync(inviteStates, roomStates, archivedRoomStates) { diff --git a/src/matrix/e2ee/megolm/SessionBackup.ts b/src/matrix/e2ee/megolm/SessionBackup.ts index 9e875185..b76f3fca 100644 --- a/src/matrix/e2ee/megolm/SessionBackup.ts +++ b/src/matrix/e2ee/megolm/SessionBackup.ts @@ -14,10 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ +import {StoreNames} from "../../storage/common"; +import {LRUCache} from "../../../utils/LRUCache"; +import {keyFromStorage, keyFromBackup} from "./decryption/RoomKey"; +import {MEGOLM_ALGORITHM} from "../common"; + import type {HomeServerApi} from "../../net/HomeServerApi"; -import type {RoomKey} from "./decryption/RoomKey"; +import type {IncomingRoomKey, RoomKey} from "./decryption/RoomKey"; import type {KeyLoader} from "./decryption/KeyLoader"; import type {SecretStorage} from "../../ssss/SecretStorage"; +import type {Storage} from "../../storage/idb/Storage"; +import type {DeviceIdentity} from "../../storage/idb/stores/DeviceIdentityStore"; import type {ILogItem} from "../../../logging/types"; import type {Platform} from "../../../platform/web/Platform"; import type {Transaction} from "../../storage/idb/Transaction"; @@ -47,6 +54,7 @@ interface OtherBackupInfo extends BaseBackupInfo { type BackupInfo = Curve25519BackupInfo | OtherBackupInfo; + interface Curve25519AuthData { public_key: string, signatures: SignatureMap @@ -54,50 +62,139 @@ interface Curve25519AuthData { type AuthData = Curve25519AuthData; +type SessionInfo = { + first_message_index: number, + forwarded_count: number, + is_verified: boolean, + session_data: Curve29915SessionData | any +} + +type Curve29915SessionData = { + ciphertext: string, + mac: string, + ephemeral: string, +} + +type MegOlmSessionKeyInfo = { + algorithm: MEGOLM_ALGORITHM, + sender_key: string, + sender_claimed_keys: {[algorithm: string]: string}, + forwarding_curve25519_key_chain: string[], + session_key: string +} + +type SessionKeyInfo = MegOlmSessionKeyInfo | {algorithm: string}; + export class SessionBackup { constructor( private readonly backupInfo: BackupInfo, - private readonly decryption: Olm.PkDecryption, + private readonly algorithm: Curve25519, private readonly hsApi: HomeServerApi, - private readonly keyLoader: KeyLoader + private readonly keyLoader: KeyLoader, + private readonly storage: Storage, + private readonly platform: Platform, ) {} - async getSession(roomId: string, sessionId: string, log: ILogItem) { + async getRoomKey(roomId: string, sessionId: string, log: ILogItem): Promise { const sessionResponse = await this.hsApi.roomKeyForRoomAndSession(this.backupInfo.version, roomId, sessionId, {log}).response(); - const sessionInfo = this.decryption.decrypt( - sessionResponse.session_data.ephemeral, - sessionResponse.session_data.mac, - sessionResponse.session_data.ciphertext, - ); - return JSON.parse(sessionInfo); + if (!sessionResponse.session_data) { + return; + } + const sessionKeyInfo = this.algorithm.decryptRoomKey(sessionResponse.session_data); + if (sessionKeyInfo?.algorithm === MEGOLM_ALGORITHM) { + return keyFromBackup(roomId, sessionId, sessionKeyInfo); + } else if (sessionKeyInfo?.algorithm) { + log.set("unknown algorithm", sessionKeyInfo.algorithm); + } } - get version() { + writeKeys(roomKeys: IncomingRoomKey[], txn: Transaction): boolean { + let hasBetter = false; + for (const key of roomKeys) { + if (key.isBetter) { + txn.sessionsNeedingBackup.set(key.roomId, key.senderKey, key.sessionId); + hasBetter = true; + } + } + return hasBetter; + } + + async flush() { + while (true) { + await this.platform.clock.createTimeout(this.platform.random() * 10000).elapsed(); + const txn = await this.storage.readTxn([ + StoreNames.sessionsNeedingBackup, + StoreNames.inboundGroupSessions, + ]); + const keysNeedingBackup = await txn.sessionsNeedingBackup.getFirstEntries(20); + if (keysNeedingBackup.length === 0) { + return; + } + const roomKeys = await Promise.all(keysNeedingBackup.map(k => keyFromStorage(k.roomId, k.senderKey, k.sessionId, txn))); + const payload: { + rooms: { + [roomId: string]: { + sessions: {[sessionId: string]: SessionInfo} + } + } + } = { rooms: {} }; + const payloadRooms = payload.rooms; + for (const key of roomKeys) { + if (key) { + let roomPayload = payloadRooms[key.roomId]; + if (!roomPayload) { + roomPayload = payloadRooms[key.roomId] = { sessions: {} }; + } + roomPayload.sessions[key.sessionId] = await this.encodeRoomKey(key); + } + } + await this.hsApi.uploadRoomKeysToBackup(this.backupInfo.version, payload).response(); + { + const txn = await this.storage.readWriteTxn([ + StoreNames.sessionsNeedingBackup, + ]); + try { + for (const key of keysNeedingBackup) { + txn.sessionsNeedingBackup.remove(key.roomId, key.senderKey, key.sessionId); + } + } catch (err) { + txn.abort(); + throw err; + } + await txn.complete(); + } + } + } + + private async encodeRoomKey(roomKey: RoomKey): Promise { + return await this.keyLoader.useKey(roomKey, session => { + const firstMessageIndex = session.first_known_index(); + const sessionKey = session.export_session(firstMessageIndex); + return { + first_message_index: firstMessageIndex, + forwarded_count: 0, + is_verified: false, + session_data: this.algorithm.encryptRoomKey(roomKey, sessionKey) + }; + }); + } + + get version(): string { return this.backupInfo.version; } dispose() { - this.decryption.free(); + this.algorithm.dispose(); } - static async fromSecretStorage(platform: Platform, olm: Olm, secretStorage: SecretStorage, hsApi: HomeServerApi, keyLoader: KeyLoader, txn: Transaction) { + static async fromSecretStorage(platform: Platform, olm: Olm, secretStorage: SecretStorage, hsApi: HomeServerApi, keyLoader: KeyLoader, storage: Storage, txn: Transaction) { const base64PrivateKey = await secretStorage.readSecret("m.megolm_backup.v1", txn); if (base64PrivateKey) { const privateKey = new Uint8Array(platform.encoding.base64.decode(base64PrivateKey)); const backupInfo = await hsApi.roomKeysVersion().response() as BackupInfo; if (backupInfo.algorithm === Curve25519Algorithm) { - const expectedPubKey = backupInfo.auth_data.public_key; - const decryption = new olm.PkDecryption(); - try { - const pubKey = decryption.init_with_private_key(privateKey); - if (pubKey !== expectedPubKey) { - throw new Error(`Bad backup key, public key does not match. Calculated ${pubKey} but expected ${expectedPubKey}`); - } - } catch(err) { - decryption.free(); - throw err; - } - return new SessionBackup(backupInfo, decryption, hsApi, keyLoader); + const algorithm = Curve25519.fromAuthData(backupInfo.auth_data, privateKey, olm); + return new SessionBackup(backupInfo, algorithm, hsApi, keyLoader, storage, platform); } else { throw new Error(`Unknown backup algorithm: ${backupInfo.algorithm}`); } @@ -105,3 +202,51 @@ export class SessionBackup { } } +class Curve25519 { + constructor( + private readonly encryption: Olm.PkEncryption, + private readonly decryption: Olm.PkDecryption + ) {} + + static fromAuthData(authData: Curve25519AuthData, privateKey: Uint8Array, olm: Olm): Curve25519 { + const expectedPubKey = authData.public_key; + const decryption = new olm.PkDecryption(); + const encryption = new olm.PkEncryption(); + try { + const pubKey = decryption.init_with_private_key(privateKey); + if (pubKey !== expectedPubKey) { + throw new Error(`Bad backup key, public key does not match. Calculated ${pubKey} but expected ${expectedPubKey}`); + } + encryption.set_recipient_key(pubKey); + } catch(err) { + decryption.free(); + throw err; + } + return new Curve25519(encryption, decryption); + } + + decryptRoomKey(sessionData: Curve29915SessionData): SessionKeyInfo { + const sessionInfo = this.decryption.decrypt( + sessionData.ephemeral, + sessionData.mac, + sessionData.ciphertext, + ); + return JSON.parse(sessionInfo) as SessionKeyInfo; + } + + encryptRoomKey(key: RoomKey, sessionKey: string): Curve29915SessionData { + const sessionInfo: SessionKeyInfo = { + algorithm: MEGOLM_ALGORITHM, + sender_key: key.senderKey, + sender_claimed_keys: {ed25519: key.claimedEd25519Key}, + forwarding_curve25519_key_chain: [], + session_key: sessionKey + }; + return this.encryption.encrypt(JSON.stringify(sessionInfo)) as Curve29915SessionData; + } + + dispose() { + this.decryption.free(); + this.encryption.free(); + } +} From 86caa5f9b1896489ad0a2bd6d5e09556f5c237b0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jan 2022 09:51:48 +0100 Subject: [PATCH 09/52] rename session backup to key backup to be consistent with RoomKey --- src/domain/AccountSetupViewModel.js | 4 +- src/domain/session/SessionStatusViewModel.js | 14 ++--- ...ckupViewModel.js => KeyBackupViewModel.js} | 8 +-- .../session/settings/SettingsViewModel.js | 8 +-- src/matrix/Session.js | 58 +++++++++---------- src/matrix/e2ee/RoomEncryption.js | 16 ++--- .../KeyBackup.ts} | 32 +++++----- src/matrix/room/BaseRoom.js | 8 +-- src/matrix/storage/idb/schema.ts | 2 +- src/platform/web/ui/login/AccountSetupView.js | 4 +- .../web/ui/session/SessionStatusView.js | 2 +- ...ttingsView.js => KeyBackupSettingsView.js} | 2 +- .../web/ui/session/settings/SettingsView.js | 6 +- 13 files changed, 82 insertions(+), 82 deletions(-) rename src/domain/session/settings/{SessionBackupViewModel.js => KeyBackupViewModel.js} (94%) rename src/matrix/e2ee/megolm/{SessionBackup.ts => keybackup/KeyBackup.ts} (89%) rename src/platform/web/ui/session/settings/{SessionBackupSettingsView.js => KeyBackupSettingsView.js} (98%) diff --git a/src/domain/AccountSetupViewModel.js b/src/domain/AccountSetupViewModel.js index b2ce808a..4ad0d8d5 100644 --- a/src/domain/AccountSetupViewModel.js +++ b/src/domain/AccountSetupViewModel.js @@ -16,7 +16,7 @@ limitations under the License. import {ViewModel} from "./ViewModel.js"; import {KeyType} from "../matrix/ssss/index"; -import {Status} from "./session/settings/SessionBackupViewModel.js"; +import {Status} from "./session/settings/KeyBackupViewModel.js"; export class AccountSetupViewModel extends ViewModel { constructor(accountSetup) { @@ -50,7 +50,7 @@ export class AccountSetupViewModel extends ViewModel { } } -// this vm adopts the same shape as SessionBackupViewModel so the same view can be reused. +// this vm adopts the same shape as KeyBackupViewModel so the same view can be reused. class DecryptDehydratedDeviceViewModel extends ViewModel { constructor(accountSetupViewModel, decryptedCallback) { super(); diff --git a/src/domain/session/SessionStatusViewModel.js b/src/domain/session/SessionStatusViewModel.js index fcedb371..3f2263ac 100644 --- a/src/domain/session/SessionStatusViewModel.js +++ b/src/domain/session/SessionStatusViewModel.js @@ -36,7 +36,7 @@ export class SessionStatusViewModel extends ViewModel { this._reconnector = reconnector; this._status = this._calculateState(reconnector.connectionStatus.get(), sync.status.get()); this._session = session; - this._setupSessionBackupUrl = this.urlCreator.urlForSegment("settings"); + this._setupKeyBackupUrl = this.urlCreator.urlForSegment("settings"); this._dismissSecretStorage = false; } @@ -44,17 +44,17 @@ export class SessionStatusViewModel extends ViewModel { const update = () => this._updateStatus(); this.track(this._sync.status.subscribe(update)); this.track(this._reconnector.connectionStatus.subscribe(update)); - this.track(this._session.needsSessionBackup.subscribe(() => { + this.track(this._session.needsKeyBackup.subscribe(() => { this.emitChange(); })); } - get setupSessionBackupUrl () { - return this._setupSessionBackupUrl; + get setupKeyBackupUrl () { + return this._setupKeyBackupUrl; } get isShown() { - return (this._session.needsSessionBackup.get() && !this._dismissSecretStorage) || this._status !== SessionStatus.Syncing; + return (this._session.needsKeyBackup.get() && !this._dismissSecretStorage) || this._status !== SessionStatus.Syncing; } get statusLabel() { @@ -70,7 +70,7 @@ export class SessionStatusViewModel extends ViewModel { case SessionStatus.SyncError: return this.i18n`Sync failed because of ${this._sync.error}`; } - if (this._session.needsSessionBackup.get()) { + if (this._session.needsKeyBackup.get()) { return this.i18n`Set up session backup to decrypt older messages.`; } return ""; @@ -135,7 +135,7 @@ export class SessionStatusViewModel extends ViewModel { get isSecretStorageShown() { // TODO: we need a model here where we can have multiple messages queued up and their buttons don't bleed into each other. - return this._status === SessionStatus.Syncing && this._session.needsSessionBackup.get() && !this._dismissSecretStorage; + return this._status === SessionStatus.Syncing && this._session.needsKeyBackup.get() && !this._dismissSecretStorage; } get canDismiss() { diff --git a/src/domain/session/settings/SessionBackupViewModel.js b/src/domain/session/settings/KeyBackupViewModel.js similarity index 94% rename from src/domain/session/settings/SessionBackupViewModel.js rename to src/domain/session/settings/KeyBackupViewModel.js index 5a127904..38d37c18 100644 --- a/src/domain/session/settings/SessionBackupViewModel.js +++ b/src/domain/session/settings/KeyBackupViewModel.js @@ -20,7 +20,7 @@ import {createEnum} from "../../../utils/enum"; export const Status = createEnum("Enabled", "SetupKey", "SetupPhrase", "Pending"); -export class SessionBackupViewModel extends ViewModel { +export class KeyBackupViewModel extends ViewModel { constructor(options) { super(options); this._session = options.session; @@ -43,7 +43,7 @@ export class SessionBackupViewModel extends ViewModel { let status; const hasSecretStorageKey = this._session.hasSecretStorageKey.get(); if (hasSecretStorageKey === true) { - status = this._session.sessionBackup ? Status.Enabled : Status.SetupKey; + status = this._session.keyBackup ? Status.Enabled : Status.SetupKey; } else if (hasSecretStorageKey === false) { status = Status.SetupKey; } else { @@ -59,7 +59,7 @@ export class SessionBackupViewModel extends ViewModel { } get purpose() { - return this.i18n`set up session backup`; + return this.i18n`set up key backup`; } offerDehydratedDeviceSetup() { @@ -75,7 +75,7 @@ export class SessionBackupViewModel extends ViewModel { } get backupVersion() { - return this._session.sessionBackup?.version; + return this._session.keyBackup?.version; } get status() { diff --git a/src/domain/session/settings/SettingsViewModel.js b/src/domain/session/settings/SettingsViewModel.js index 70e507b8..0b68f168 100644 --- a/src/domain/session/settings/SettingsViewModel.js +++ b/src/domain/session/settings/SettingsViewModel.js @@ -15,7 +15,7 @@ limitations under the License. */ import {ViewModel} from "../../ViewModel.js"; -import {SessionBackupViewModel} from "./SessionBackupViewModel.js"; +import {KeyBackupViewModel} from "./KeyBackupViewModel.js"; class PushNotificationStatus { constructor() { @@ -43,7 +43,7 @@ export class SettingsViewModel extends ViewModel { this._updateService = options.updateService; const {client} = options; this._client = client; - this._sessionBackupViewModel = this.track(new SessionBackupViewModel(this.childOptions({session: this._session}))); + this._keyBackupViewModel = this.track(new KeyBackupViewModel(this.childOptions({session: this._session}))); this._closeUrl = this.urlCreator.urlUntilSegment("session"); this._estimate = null; this.sentImageSizeLimit = null; @@ -115,8 +115,8 @@ export class SettingsViewModel extends ViewModel { return !!this.platform.updateService; } - get sessionBackupViewModel() { - return this._sessionBackupViewModel; + get keyBackupViewModel() { + return this._keyBackupViewModel; } get storageQuota() { diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 94b68106..9510cbfd 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -29,7 +29,7 @@ import {Decryption as OlmDecryption} from "./e2ee/olm/Decryption.js"; import {Encryption as OlmEncryption} from "./e2ee/olm/Encryption.js"; import {Decryption as MegOlmDecryption} from "./e2ee/megolm/Decryption"; import {KeyLoader as MegOlmKeyLoader} from "./e2ee/megolm/decryption/KeyLoader"; -import {SessionBackup} from "./e2ee/megolm/SessionBackup.js"; +import {KeyBackup} from "./e2ee/megolm/keybackup/KeyBackup"; import {Encryption as MegOlmEncryption} from "./e2ee/megolm/Encryption.js"; import {MEGOLM_ALGORITHM} from "./e2ee/common.js"; import {RoomEncryption} from "./e2ee/RoomEncryption.js"; @@ -75,7 +75,7 @@ export class Session { this._megolmDecryption = null; this._getSyncToken = () => this.syncToken; this._olmWorker = olmWorker; - this._sessionBackup = null; + this._keyBackup = null; this._hasSecretStorageKey = new ObservableValue(null); this._observedRoomStatus = new Map(); @@ -91,7 +91,7 @@ export class Session { } this._createRoomEncryption = this._createRoomEncryption.bind(this); this._forgetArchivedRoom = this._forgetArchivedRoom.bind(this); - this.needsSessionBackup = new ObservableValue(false); + this.needsKeyBackup = new ObservableValue(false); } get fingerprintKey() { @@ -170,11 +170,11 @@ export class Session { megolmEncryption: this._megolmEncryption, megolmDecryption: this._megolmDecryption, storage: this._storage, - sessionBackup: this._sessionBackup, + keyBackup: this._keyBackup, encryptionParams, notifyMissingMegolmSession: () => { - if (!this._sessionBackup) { - this.needsSessionBackup.set(true) + if (!this._keyBackup) { + this.needsKeyBackup.set(true) } }, clock: this._platform.clock @@ -183,7 +183,7 @@ export class Session { /** * Enable secret storage by providing the secret storage credential. - * This will also see if there is a megolm session backup and try to enable that if so. + * This will also see if there is a megolm key backup and try to enable that if so. * * @param {string} type either "passphrase" or "recoverykey" * @param {string} credential either the passphrase or the recovery key, depending on the type @@ -193,15 +193,15 @@ export class Session { if (!this._olm) { throw new Error("olm required"); } - if (this._sessionBackup) { + if (this._keyBackup) { return false; } const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm); - // and create session backup, which needs to read from accountData + // and create key backup, which needs to read from accountData const readTxn = await this._storage.readTxn([ this._storage.storeNames.accountData, ]); - await this._createSessionBackup(key, readTxn); + await this._createKeyBackup(key, readTxn); await this._writeSSSSKey(key); this._hasSecretStorageKey.set(true); return key; @@ -233,21 +233,21 @@ export class Session { throw err; } await writeTxn.complete(); - if (this._sessionBackup) { + if (this._keyBackup) { for (const room of this._rooms.values()) { if (room.isEncrypted) { - room.enableSessionBackup(undefined); + room.enableKeyBackup(undefined); } } - this._sessionBackup?.dispose(); - this._sessionBackup = undefined; + this._keyBackup?.dispose(); + this._keyBackup = undefined; } this._hasSecretStorageKey.set(false); } - async _createSessionBackup(ssssKey, txn) { + async _createKeyBackup(ssssKey, txn) { const secretStorage = new SecretStorage({key: ssssKey, platform: this._platform}); - this._sessionBackup = await SessionBackup.fromSecretStorage( + this._keyBackup = await KeyBackup.fromSecretStorage( this._platform, this._olm, secretStorage, @@ -256,18 +256,18 @@ export class Session { this._storage, txn ); - if (this._sessionBackup) { + if (this._keyBackup) { for (const room of this._rooms.values()) { if (room.isEncrypted) { - room.enableSessionBackup(this._sessionBackup); + room.enableKeyBackup(this._keyBackup); } } } - this.needsSessionBackup.set(false); + this.needsKeyBackup.set(false); } - get sessionBackup() { - return this._sessionBackup; + get keyBackup() { + return this._keyBackup; } get hasIdentity() { @@ -405,8 +405,8 @@ export class Session { dispose() { this._olmWorker?.dispose(); this._olmWorker = undefined; - this._sessionBackup?.dispose(); - this._sessionBackup = undefined; + this._keyBackup?.dispose(); + this._keyBackup = undefined; this._megolmDecryption?.dispose(); this._megolmDecryption = undefined; this._e2eeAccount?.dispose(); @@ -434,7 +434,7 @@ export class Session { await txn.complete(); } // enable session backup, this requests the latest backup version - if (!this._sessionBackup) { + if (!this._keyBackup) { if (dehydratedDevice) { await log.wrap("SSSSKeyFromDehydratedDeviceKey", async log => { const ssssKey = await createSSSSKeyFromDehydratedDeviceKey(dehydratedDevice.key, this._storage, this._platform); @@ -452,7 +452,7 @@ export class Session { const ssssKey = await ssssReadKey(txn); if (ssssKey) { // txn will end here as this does a network request - await this._createSessionBackup(ssssKey, txn); + await this._createKeyBackup(ssssKey, txn); } this._hasSecretStorageKey.set(!!ssssKey); } @@ -583,8 +583,8 @@ export class Session { await log.wrap("deviceMsgs", log => this._deviceMessageHandler.writeSync(preparation, txn, log)); // this should come after the deviceMessageHandler, so the room keys are already written and their // isBetter property has been checked - if (this._sessionBackup) { - this._sessionBackup.writeKeys(preparation.newRoomKeys, txn, log); + if (this._keyBackup) { + this._keyBackup.writeKeys(preparation.newRoomKeys, txn, log); } } @@ -623,8 +623,8 @@ export class Session { await log.wrap("uploadKeys", log => this._e2eeAccount.uploadKeys(this._storage, false, log)); } } - if (this._sessionBackup) { - this._sessionBackup.flush(); + if (this._keyBackup) { + this._keyBackup.flush(); } } diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index 5a511d6e..2e6ee254 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -28,7 +28,7 @@ const MIN_PRESHARE_INTERVAL = 60 * 1000; // 1min // TODO: this class is a good candidate for splitting up into encryption and decryption, there doesn't seem to be much overlap export class RoomEncryption { - constructor({room, deviceTracker, olmEncryption, megolmEncryption, megolmDecryption, encryptionParams, storage, sessionBackup, notifyMissingMegolmSession, clock}) { + constructor({room, deviceTracker, olmEncryption, megolmEncryption, megolmDecryption, encryptionParams, storage, keyBackup, notifyMissingMegolmSession, clock}) { this._room = room; this._deviceTracker = deviceTracker; this._olmEncryption = olmEncryption; @@ -39,7 +39,7 @@ export class RoomEncryption { // caches devices to verify events this._senderDeviceCache = new Map(); this._storage = storage; - this._sessionBackup = sessionBackup; + this._keyBackup = keyBackup; this._notifyMissingMegolmSession = notifyMissingMegolmSession; this._clock = clock; this._isFlushingRoomKeyShares = false; @@ -48,11 +48,11 @@ export class RoomEncryption { this._disposed = false; } - enableSessionBackup(sessionBackup) { - if (this._sessionBackup && !!sessionBackup) { + enableKeyBackup(keyBackup) { + if (this._keyBackup && !!keyBackup) { return; } - this._sessionBackup = sessionBackup; + this._keyBackup = keyBackup; } async restoreMissingSessionsFromBackup(entries, log) { @@ -130,7 +130,7 @@ export class RoomEncryption { })); } - if (!this._sessionBackup) { + if (!this._keyBackup) { return; } @@ -174,7 +174,7 @@ export class RoomEncryption { async _requestMissingSessionFromBackup(senderKey, sessionId, log) { // show prompt to enable secret storage - if (!this._sessionBackup) { + if (!this._keyBackup) { log.set("enabled", false); this._notifyMissingMegolmSession(); return; @@ -182,7 +182,7 @@ export class RoomEncryption { log.set("id", sessionId); log.set("senderKey", senderKey); try { - const roomKey = await this._sessionBackup.getRoomKey(this._room.id, sessionId, log); + const roomKey = await this._keyBackup.getRoomKey(this._room.id, sessionId, log); if (roomKey) { if (roomKey.senderKey !== senderKey) { log.set("wrong_sender_key", roomKey.senderKey); diff --git a/src/matrix/e2ee/megolm/SessionBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts similarity index 89% rename from src/matrix/e2ee/megolm/SessionBackup.ts rename to src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index b76f3fca..98456973 100644 --- a/src/matrix/e2ee/megolm/SessionBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -14,20 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {StoreNames} from "../../storage/common"; -import {LRUCache} from "../../../utils/LRUCache"; -import {keyFromStorage, keyFromBackup} from "./decryption/RoomKey"; -import {MEGOLM_ALGORITHM} from "../common"; +import {StoreNames} from "../../../storage/common"; +import {LRUCache} from "../../../../utils/LRUCache"; +import {keyFromStorage, keyFromBackup} from "../decryption/RoomKey"; +import {MEGOLM_ALGORITHM} from "../../common"; -import type {HomeServerApi} from "../../net/HomeServerApi"; -import type {IncomingRoomKey, RoomKey} from "./decryption/RoomKey"; -import type {KeyLoader} from "./decryption/KeyLoader"; -import type {SecretStorage} from "../../ssss/SecretStorage"; -import type {Storage} from "../../storage/idb/Storage"; -import type {DeviceIdentity} from "../../storage/idb/stores/DeviceIdentityStore"; -import type {ILogItem} from "../../../logging/types"; -import type {Platform} from "../../../platform/web/Platform"; -import type {Transaction} from "../../storage/idb/Transaction"; +import type {HomeServerApi} from "../../../net/HomeServerApi"; +import type {IncomingRoomKey, RoomKey} from "../decryption/RoomKey"; +import type {KeyLoader} from "../decryption/KeyLoader"; +import type {SecretStorage} from "../../../ssss/SecretStorage"; +import type {Storage} from "../../../storage/idb/Storage"; +import type {DeviceIdentity} from "../../../storage/idb/stores/DeviceIdentityStore"; +import type {ILogItem} from "../../../../logging/types"; +import type {Platform} from "../../../../platform/web/Platform"; +import type {Transaction} from "../../../storage/idb/Transaction"; import type * as OlmNamespace from "@matrix-org/olm"; type Olm = typeof OlmNamespace; @@ -85,7 +85,7 @@ type MegOlmSessionKeyInfo = { type SessionKeyInfo = MegOlmSessionKeyInfo | {algorithm: string}; -export class SessionBackup { +export class KeyBackup { constructor( private readonly backupInfo: BackupInfo, private readonly algorithm: Curve25519, @@ -187,14 +187,14 @@ export class SessionBackup { this.algorithm.dispose(); } - static async fromSecretStorage(platform: Platform, olm: Olm, secretStorage: SecretStorage, hsApi: HomeServerApi, keyLoader: KeyLoader, storage: Storage, txn: Transaction) { + static async fromSecretStorage(platform: Platform, olm: Olm, secretStorage: SecretStorage, hsApi: HomeServerApi, keyLoader: KeyLoader, storage: Storage, txn: Transaction): Promise { const base64PrivateKey = await secretStorage.readSecret("m.megolm_backup.v1", txn); if (base64PrivateKey) { const privateKey = new Uint8Array(platform.encoding.base64.decode(base64PrivateKey)); const backupInfo = await hsApi.roomKeysVersion().response() as BackupInfo; if (backupInfo.algorithm === Curve25519Algorithm) { const algorithm = Curve25519.fromAuthData(backupInfo.auth_data, privateKey, olm); - return new SessionBackup(backupInfo, algorithm, hsApi, keyLoader, storage, platform); + return new KeyBackup(backupInfo, algorithm, hsApi, keyLoader, storage, platform); } else { throw new Error(`Unknown backup algorithm: ${backupInfo.algorithm}`); } diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index 39232ae5..ecd2d860 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -461,11 +461,11 @@ export class BaseRoom extends EventEmitter { return observable; } - enableSessionBackup(sessionBackup) { - this._roomEncryption?.enableSessionBackup(sessionBackup); + enableKeyBackup(keyBackup) { + this._roomEncryption?.enableKeyBackup(keyBackup); // TODO: do we really want to do this every time you open the app? - if (this._timeline && sessionBackup) { - this._platform.logger.run("enableSessionBackup", log => { + if (this._timeline && keyBackup) { + this._platform.logger.run("enableKeyBackup", log => { return this._roomEncryption.restoreMissingSessionsFromBackup(this._timeline.remoteEntries, log); }); } diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index 44a04bba..2aca60a2 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -272,7 +272,7 @@ async function clearAllStores(db: IDBDatabase, txn: IDBTransaction) { } } -// v15 adds the sessionsNeedingBackup store, for session backup +// v15 adds the sessionsNeedingBackup store, for key backup function createSessionsNeedingBackup(db: IDBDatabase): void { db.createObjectStore("sessionsNeedingBackup", {keyPath: "key"}); } diff --git a/src/platform/web/ui/login/AccountSetupView.js b/src/platform/web/ui/login/AccountSetupView.js index 32a4afb5..e0d41693 100644 --- a/src/platform/web/ui/login/AccountSetupView.js +++ b/src/platform/web/ui/login/AccountSetupView.js @@ -15,13 +15,13 @@ limitations under the License. */ import {TemplateView} from "../general/TemplateView"; -import {SessionBackupSettingsView} from "../session/settings/SessionBackupSettingsView.js"; +import {KeyBackupSettingsView} from "../session/settings/KeyBackupSettingsView.js"; export class AccountSetupView extends TemplateView { render(t, vm) { return t.div({className: "Settings" /* hack for now to get the layout right*/}, [ t.h3(vm.i18n`Restore your encrypted history?`), - t.ifView(vm => vm.decryptDehydratedDeviceViewModel, vm => new SessionBackupSettingsView(vm.decryptDehydratedDeviceViewModel)), + t.ifView(vm => vm.decryptDehydratedDeviceViewModel, vm => new KeyBackupSettingsView(vm.decryptDehydratedDeviceViewModel)), t.map(vm => vm.deviceDecrypted, (decrypted, t) => { if (decrypted) { return t.p(vm.i18n`That worked out, you're good to go!`); diff --git a/src/platform/web/ui/session/SessionStatusView.js b/src/platform/web/ui/session/SessionStatusView.js index bd8c6dbb..a629b2a1 100644 --- a/src/platform/web/ui/session/SessionStatusView.js +++ b/src/platform/web/ui/session/SessionStatusView.js @@ -26,7 +26,7 @@ export class SessionStatusView extends TemplateView { spinner(t, {hidden: vm => !vm.isWaiting}), t.p(vm => vm.statusLabel), t.if(vm => vm.isConnectNowShown, t => t.button({className: "link", onClick: () => vm.connectNow()}, "Retry now")), - t.if(vm => vm.isSecretStorageShown, t => t.a({href: vm.setupSessionBackupUrl}, "Go to settings")), + t.if(vm => vm.isSecretStorageShown, t => t.a({href: vm.setupKeyBackupUrl}, "Go to settings")), t.if(vm => vm.canDismiss, t => t.div({className: "end"}, t.button({className: "dismiss", onClick: () => vm.dismiss()}))), ]); } diff --git a/src/platform/web/ui/session/settings/SessionBackupSettingsView.js b/src/platform/web/ui/session/settings/KeyBackupSettingsView.js similarity index 98% rename from src/platform/web/ui/session/settings/SessionBackupSettingsView.js rename to src/platform/web/ui/session/settings/KeyBackupSettingsView.js index b8206c55..bf3fd296 100644 --- a/src/platform/web/ui/session/settings/SessionBackupSettingsView.js +++ b/src/platform/web/ui/session/settings/KeyBackupSettingsView.js @@ -17,7 +17,7 @@ limitations under the License. import {TemplateView, InlineTemplateView} from "../../general/TemplateView"; import {StaticView} from "../../general/StaticView.js"; -export class SessionBackupSettingsView extends TemplateView { +export class KeyBackupSettingsView extends TemplateView { render(t, vm) { return t.mapView(vm => vm.status, status => { switch (status) { diff --git a/src/platform/web/ui/session/settings/SettingsView.js b/src/platform/web/ui/session/settings/SettingsView.js index 78ca2007..93e44307 100644 --- a/src/platform/web/ui/session/settings/SettingsView.js +++ b/src/platform/web/ui/session/settings/SettingsView.js @@ -15,7 +15,7 @@ limitations under the License. */ import {TemplateView} from "../../general/TemplateView"; -import {SessionBackupSettingsView} from "./SessionBackupSettingsView.js" +import {KeyBackupSettingsView} from "./KeyBackupSettingsView.js" export class SettingsView extends TemplateView { render(t, vm) { @@ -47,8 +47,8 @@ export class SettingsView extends TemplateView { }, vm.i18n`Log out`)), ); settingNodes.push( - t.h3("Session Backup"), - t.view(new SessionBackupSettingsView(vm.sessionBackupViewModel)) + t.h3("Key backup"), + t.view(new KeyBackupSettingsView(vm.keyBackupViewModel)) ); settingNodes.push( From cfb94206f90ea66f71e9ddaa52c805b67cc9cbc1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jan 2022 10:13:01 +0100 Subject: [PATCH 10/52] move curve25519 code to separate file --- .../e2ee/megolm/keybackup/Curve25519.ts | 89 ++++++++++++++ src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 115 ++++-------------- 2 files changed, 113 insertions(+), 91 deletions(-) create mode 100644 src/matrix/e2ee/megolm/keybackup/Curve25519.ts diff --git a/src/matrix/e2ee/megolm/keybackup/Curve25519.ts b/src/matrix/e2ee/megolm/keybackup/Curve25519.ts new file mode 100644 index 00000000..9a453b5a --- /dev/null +++ b/src/matrix/e2ee/megolm/keybackup/Curve25519.ts @@ -0,0 +1,89 @@ +/* +Copyright 2022 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. +*/ + +import {MEGOLM_ALGORITHM} from "../../common"; +import type {RoomKey} from "../decryption/RoomKey"; + +import type {BaseBackupInfo, SignatureMap, SessionKeyInfo} from "./KeyBackup"; +import type * as OlmNamespace from "@matrix-org/olm"; +type Olm = typeof OlmNamespace; + +export const Algorithm = "m.megolm_backup.v1.curve25519-aes-sha2"; + +export type BackupInfo = BaseBackupInfo & { + algorithm: typeof Algorithm, + auth_data: AuthData, +} + +export type AuthData = { + public_key: string, + signatures: SignatureMap +} + +export type SessionData = { + ciphertext: string, + mac: string, + ephemeral: string, +} + +export class BackupEncryption { + constructor( + private readonly encryption: Olm.PkEncryption, + private readonly decryption: Olm.PkDecryption + ) {} + + static fromAuthData(authData: AuthData, privateKey: Uint8Array, olm: Olm): BackupEncryption { + const expectedPubKey = authData.public_key; + const decryption = new olm.PkDecryption(); + const encryption = new olm.PkEncryption(); + try { + const pubKey = decryption.init_with_private_key(privateKey); + if (pubKey !== expectedPubKey) { + throw new Error(`Bad backup key, public key does not match. Calculated ${pubKey} but expected ${expectedPubKey}`); + } + encryption.set_recipient_key(pubKey); + } catch(err) { + decryption.free(); + throw err; + } + return new BackupEncryption(encryption, decryption); + } + + decryptRoomKey(sessionData: SessionData): SessionKeyInfo { + const sessionInfo = this.decryption.decrypt( + sessionData.ephemeral, + sessionData.mac, + sessionData.ciphertext, + ); + return JSON.parse(sessionInfo) as SessionKeyInfo; + } + + encryptRoomKey(key: RoomKey, sessionKey: string): SessionData { + const sessionInfo: SessionKeyInfo = { + algorithm: MEGOLM_ALGORITHM, + sender_key: key.senderKey, + sender_claimed_keys: {ed25519: key.claimedEd25519Key}, + forwarding_curve25519_key_chain: [], + session_key: sessionKey + }; + return this.encryption.encrypt(JSON.stringify(sessionInfo)) as SessionData; + } + + dispose() { + this.decryption.free(); + this.encryption.free(); + } +} diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index 98456973..dc541cf2 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -15,9 +15,9 @@ limitations under the License. */ import {StoreNames} from "../../../storage/common"; -import {LRUCache} from "../../../../utils/LRUCache"; import {keyFromStorage, keyFromBackup} from "../decryption/RoomKey"; import {MEGOLM_ALGORITHM} from "../../common"; +import * as Curve25519 from "./Curve25519"; import type {HomeServerApi} from "../../../net/HomeServerApi"; import type {IncomingRoomKey, RoomKey} from "../decryption/RoomKey"; @@ -31,48 +31,28 @@ import type {Transaction} from "../../../storage/idb/Transaction"; import type * as OlmNamespace from "@matrix-org/olm"; type Olm = typeof OlmNamespace; -type SignatureMap = { +export type SignatureMap = { [userId: string]: {[deviceIdAndAlgorithm: string]: string} } -interface BaseBackupInfo { +export type BaseBackupInfo = { version: string, etag: string, count: number, } -const Curve25519Algorithm = "m.megolm_backup.v1.curve25519-aes-sha2"; - -interface Curve25519BackupInfo extends BaseBackupInfo { - algorithm: typeof Curve25519Algorithm, - auth_data: Curve25519AuthData, -} - -interface OtherBackupInfo extends BaseBackupInfo { +type OtherBackupInfo = BaseBackupInfo & { algorithm: "other" }; -type BackupInfo = Curve25519BackupInfo | OtherBackupInfo; - - -interface Curve25519AuthData { - public_key: string, - signatures: SignatureMap -} - -type AuthData = Curve25519AuthData; +type BackupInfo = Curve25519.BackupInfo | OtherBackupInfo; +type AuthData = Curve25519.AuthData; type SessionInfo = { first_message_index: number, forwarded_count: number, is_verified: boolean, - session_data: Curve29915SessionData | any -} - -type Curve29915SessionData = { - ciphertext: string, - mac: string, - ephemeral: string, + session_data: Curve25519.SessionData | any } type MegOlmSessionKeyInfo = { @@ -83,12 +63,20 @@ type MegOlmSessionKeyInfo = { session_key: string } -type SessionKeyInfo = MegOlmSessionKeyInfo | {algorithm: string}; +export type SessionKeyInfo = MegOlmSessionKeyInfo | {algorithm: string}; + +type KeyBackupPayload = { + rooms: { + [roomId: string]: { + sessions: {[sessionId: string]: SessionInfo} + } + } +} export class KeyBackup { constructor( private readonly backupInfo: BackupInfo, - private readonly algorithm: Curve25519, + private readonly crypto: Curve25519.BackupEncryption, private readonly hsApi: HomeServerApi, private readonly keyLoader: KeyLoader, private readonly storage: Storage, @@ -100,7 +88,7 @@ export class KeyBackup { if (!sessionResponse.session_data) { return; } - const sessionKeyInfo = this.algorithm.decryptRoomKey(sessionResponse.session_data); + const sessionKeyInfo = this.crypto.decryptRoomKey(sessionResponse.session_data as Curve25519.SessionData); if (sessionKeyInfo?.algorithm === MEGOLM_ALGORITHM) { return keyFromBackup(roomId, sessionId, sessionKeyInfo); } else if (sessionKeyInfo?.algorithm) { @@ -131,13 +119,7 @@ export class KeyBackup { return; } const roomKeys = await Promise.all(keysNeedingBackup.map(k => keyFromStorage(k.roomId, k.senderKey, k.sessionId, txn))); - const payload: { - rooms: { - [roomId: string]: { - sessions: {[sessionId: string]: SessionInfo} - } - } - } = { rooms: {} }; + const payload: KeyBackupPayload = { rooms: {} }; const payloadRooms = payload.rooms; for (const key of roomKeys) { if (key) { @@ -174,7 +156,7 @@ export class KeyBackup { first_message_index: firstMessageIndex, forwarded_count: 0, is_verified: false, - session_data: this.algorithm.encryptRoomKey(roomKey, sessionKey) + session_data: this.crypto.encryptRoomKey(roomKey, sessionKey) }; }); } @@ -184,7 +166,7 @@ export class KeyBackup { } dispose() { - this.algorithm.dispose(); + this.crypto.dispose(); } static async fromSecretStorage(platform: Platform, olm: Olm, secretStorage: SecretStorage, hsApi: HomeServerApi, keyLoader: KeyLoader, storage: Storage, txn: Transaction): Promise { @@ -192,61 +174,12 @@ export class KeyBackup { if (base64PrivateKey) { const privateKey = new Uint8Array(platform.encoding.base64.decode(base64PrivateKey)); const backupInfo = await hsApi.roomKeysVersion().response() as BackupInfo; - if (backupInfo.algorithm === Curve25519Algorithm) { - const algorithm = Curve25519.fromAuthData(backupInfo.auth_data, privateKey, olm); - return new KeyBackup(backupInfo, algorithm, hsApi, keyLoader, storage, platform); + if (backupInfo.algorithm === Curve25519.Algorithm) { + const crypto = Curve25519.BackupEncryption.fromAuthData(backupInfo.auth_data, privateKey, olm); + return new KeyBackup(backupInfo, crypto, hsApi, keyLoader, storage, platform); } else { throw new Error(`Unknown backup algorithm: ${backupInfo.algorithm}`); } } } } - -class Curve25519 { - constructor( - private readonly encryption: Olm.PkEncryption, - private readonly decryption: Olm.PkDecryption - ) {} - - static fromAuthData(authData: Curve25519AuthData, privateKey: Uint8Array, olm: Olm): Curve25519 { - const expectedPubKey = authData.public_key; - const decryption = new olm.PkDecryption(); - const encryption = new olm.PkEncryption(); - try { - const pubKey = decryption.init_with_private_key(privateKey); - if (pubKey !== expectedPubKey) { - throw new Error(`Bad backup key, public key does not match. Calculated ${pubKey} but expected ${expectedPubKey}`); - } - encryption.set_recipient_key(pubKey); - } catch(err) { - decryption.free(); - throw err; - } - return new Curve25519(encryption, decryption); - } - - decryptRoomKey(sessionData: Curve29915SessionData): SessionKeyInfo { - const sessionInfo = this.decryption.decrypt( - sessionData.ephemeral, - sessionData.mac, - sessionData.ciphertext, - ); - return JSON.parse(sessionInfo) as SessionKeyInfo; - } - - encryptRoomKey(key: RoomKey, sessionKey: string): Curve29915SessionData { - const sessionInfo: SessionKeyInfo = { - algorithm: MEGOLM_ALGORITHM, - sender_key: key.senderKey, - sender_claimed_keys: {ed25519: key.claimedEd25519Key}, - forwarding_curve25519_key_chain: [], - session_key: sessionKey - }; - return this.encryption.encrypt(JSON.stringify(sessionInfo)) as Curve29915SessionData; - } - - dispose() { - this.decryption.free(); - this.encryption.free(); - } -} From 85155a43bbadde96f4239181401f3b394debd75a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jan 2022 10:17:31 +0100 Subject: [PATCH 11/52] cleanup types --- src/matrix/e2ee/megolm/keybackup/Curve25519.ts | 2 +- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/matrix/e2ee/megolm/keybackup/Curve25519.ts b/src/matrix/e2ee/megolm/keybackup/Curve25519.ts index 9a453b5a..f1345bc6 100644 --- a/src/matrix/e2ee/megolm/keybackup/Curve25519.ts +++ b/src/matrix/e2ee/megolm/keybackup/Curve25519.ts @@ -28,7 +28,7 @@ export type BackupInfo = BaseBackupInfo & { auth_data: AuthData, } -export type AuthData = { +type AuthData = { public_key: string, signatures: SignatureMap } diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index dc541cf2..f4c7b28b 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -24,7 +24,6 @@ import type {IncomingRoomKey, RoomKey} from "../decryption/RoomKey"; import type {KeyLoader} from "../decryption/KeyLoader"; import type {SecretStorage} from "../../../ssss/SecretStorage"; import type {Storage} from "../../../storage/idb/Storage"; -import type {DeviceIdentity} from "../../../storage/idb/stores/DeviceIdentityStore"; import type {ILogItem} from "../../../../logging/types"; import type {Platform} from "../../../../platform/web/Platform"; import type {Transaction} from "../../../storage/idb/Transaction"; @@ -46,13 +45,13 @@ type OtherBackupInfo = BaseBackupInfo & { }; type BackupInfo = Curve25519.BackupInfo | OtherBackupInfo; -type AuthData = Curve25519.AuthData; +type SessionData = Curve25519.SessionData; type SessionInfo = { first_message_index: number, forwarded_count: number, is_verified: boolean, - session_data: Curve25519.SessionData | any + session_data: SessionData } type MegOlmSessionKeyInfo = { @@ -63,6 +62,7 @@ type MegOlmSessionKeyInfo = { session_key: string } +// the type that session_data decrypts from / encrypts to export type SessionKeyInfo = MegOlmSessionKeyInfo | {algorithm: string}; type KeyBackupPayload = { From a791641b3418420f025066eea1293167f18a9027 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jan 2022 12:10:20 +0100 Subject: [PATCH 12/52] move types to separate file --- .../e2ee/megolm/keybackup/Curve25519.ts | 2 +- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 43 ------------- src/matrix/e2ee/megolm/keybackup/types.ts | 61 +++++++++++++++++++ 3 files changed, 62 insertions(+), 44 deletions(-) create mode 100644 src/matrix/e2ee/megolm/keybackup/types.ts diff --git a/src/matrix/e2ee/megolm/keybackup/Curve25519.ts b/src/matrix/e2ee/megolm/keybackup/Curve25519.ts index f1345bc6..8007d422 100644 --- a/src/matrix/e2ee/megolm/keybackup/Curve25519.ts +++ b/src/matrix/e2ee/megolm/keybackup/Curve25519.ts @@ -17,7 +17,7 @@ limitations under the License. import {MEGOLM_ALGORITHM} from "../../common"; import type {RoomKey} from "../decryption/RoomKey"; -import type {BaseBackupInfo, SignatureMap, SessionKeyInfo} from "./KeyBackup"; +import type {BaseBackupInfo, SignatureMap, SessionKeyInfo} from "./types"; import type * as OlmNamespace from "@matrix-org/olm"; type Olm = typeof OlmNamespace; diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index f4c7b28b..38faa5e2 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -30,49 +30,6 @@ import type {Transaction} from "../../../storage/idb/Transaction"; import type * as OlmNamespace from "@matrix-org/olm"; type Olm = typeof OlmNamespace; -export type SignatureMap = { - [userId: string]: {[deviceIdAndAlgorithm: string]: string} -} - -export type BaseBackupInfo = { - version: string, - etag: string, - count: number, -} - -type OtherBackupInfo = BaseBackupInfo & { - algorithm: "other" -}; - -type BackupInfo = Curve25519.BackupInfo | OtherBackupInfo; -type SessionData = Curve25519.SessionData; - -type SessionInfo = { - first_message_index: number, - forwarded_count: number, - is_verified: boolean, - session_data: SessionData -} - -type MegOlmSessionKeyInfo = { - algorithm: MEGOLM_ALGORITHM, - sender_key: string, - sender_claimed_keys: {[algorithm: string]: string}, - forwarding_curve25519_key_chain: string[], - session_key: string -} - -// the type that session_data decrypts from / encrypts to -export type SessionKeyInfo = MegOlmSessionKeyInfo | {algorithm: string}; - -type KeyBackupPayload = { - rooms: { - [roomId: string]: { - sessions: {[sessionId: string]: SessionInfo} - } - } -} - export class KeyBackup { constructor( private readonly backupInfo: BackupInfo, diff --git a/src/matrix/e2ee/megolm/keybackup/types.ts b/src/matrix/e2ee/megolm/keybackup/types.ts new file mode 100644 index 00000000..ce56cca7 --- /dev/null +++ b/src/matrix/e2ee/megolm/keybackup/types.ts @@ -0,0 +1,61 @@ +/* +Copyright 2022 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. +*/ + +import type * as Curve25519 from "./Curve25519"; +import type {MEGOLM_ALGORITHM} from "../../common"; + +export type SignatureMap = { + [userId: string]: {[deviceIdAndAlgorithm: string]: string} +} + +export type BaseBackupInfo = { + version: string, + etag: string, + count: number, +} + +export type OtherBackupInfo = BaseBackupInfo & { + algorithm: "other" +}; + +export type BackupInfo = Curve25519.BackupInfo | OtherBackupInfo; +export type SessionData = Curve25519.SessionData; + +export type SessionInfo = { + first_message_index: number, + forwarded_count: number, + is_verified: boolean, + session_data: SessionData +} + +export type MegOlmSessionKeyInfo = { + algorithm: MEGOLM_ALGORITHM, + sender_key: string, + sender_claimed_keys: {[algorithm: string]: string}, + forwarding_curve25519_key_chain: string[], + session_key: string +} + +// the type that session_data decrypts from / encrypts to +export type SessionKeyInfo = MegOlmSessionKeyInfo | {algorithm: string}; + +export type KeyBackupPayload = { + rooms: { + [roomId: string]: { + sessions: {[sessionId: string]: SessionInfo} + } + } +} From 524090e27d33b5700fd639cb46f87c8b22a4384c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jan 2022 15:12:11 +0100 Subject: [PATCH 13/52] support idb store/index.count --- src/matrix/storage/idb/QueryTarget.ts | 5 +++++ src/matrix/storage/idb/Store.ts | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/src/matrix/storage/idb/QueryTarget.ts b/src/matrix/storage/idb/QueryTarget.ts index 97b85569..f27123d5 100644 --- a/src/matrix/storage/idb/QueryTarget.ts +++ b/src/matrix/storage/idb/QueryTarget.ts @@ -37,6 +37,7 @@ interface QueryTargetInterface { openKeyCursor(range?: IDBQuery, direction?: IDBCursorDirection | undefined): IDBRequest; supports(method: string): boolean; keyPath: string | string[]; + count(keyRange?: IDBKeyRange): IDBRequest; get(key: IDBValidKey | IDBKeyRange): IDBRequest; getKey(key: IDBValidKey | IDBKeyRange): IDBRequest; } @@ -78,6 +79,10 @@ export class QueryTarget { return this._target.supports(methodName); } + count(keyRange?: IDBKeyRange): Promise { + return reqAsPromise(this._target.count(keyRange)); + } + get(key: IDBValidKey | IDBKeyRange): Promise { return reqAsPromise(this._target.get(key)); } diff --git a/src/matrix/storage/idb/Store.ts b/src/matrix/storage/idb/Store.ts index f45b3f3e..20e6f79a 100644 --- a/src/matrix/storage/idb/Store.ts +++ b/src/matrix/storage/idb/Store.ts @@ -118,6 +118,14 @@ export class QueryTargetWrapper { } } + count(keyRange?: IDBKeyRange): IDBRequest { + try { + return this._qtStore.count(keyRange); + } catch(err) { + throw new IDBRequestAttemptError("count", this._qt, err, [keyRange]); + } + } + index(name: string): IDBIndex { try { return this._qtStore.index(name); From 554aa45d482f3b00471291234efdadf2237b983f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jan 2022 15:18:23 +0100 Subject: [PATCH 14/52] add support for progress notifications in abortable operation --- src/utils/AbortableOperation.ts | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/utils/AbortableOperation.ts b/src/utils/AbortableOperation.ts index d03f820a..fba71a8c 100644 --- a/src/utils/AbortableOperation.ts +++ b/src/utils/AbortableOperation.ts @@ -14,27 +14,40 @@ See the License for the specific language governing permissions and limitations under the License. */ +import {BaseObservableValue, ObservableValue} from "../observable/ObservableValue"; + export interface IAbortable { abort(); } -type RunFn = (setAbortable: (a: IAbortable) => typeof a) => T; +export type SetAbortableFn = (a: IAbortable) => typeof a; +export type SetProgressFn

= (progress: P) => void; +type RunFn = (setAbortable: SetAbortableFn, setProgress: SetProgressFn

) => T; -export class AbortableOperation { +export class AbortableOperation implements IAbortable { public readonly result: T; - private _abortable: IAbortable | null; + private _abortable?: IAbortable; + private _progress: ObservableValue

; - constructor(run: RunFn) { - this._abortable = null; - const setAbortable = abortable => { + constructor(run: RunFn) { + this._abortable = undefined; + const setAbortable: SetAbortableFn = abortable => { this._abortable = abortable; return abortable; }; - this.result = run(setAbortable); + this._progress = new ObservableValue

(undefined); + const setProgress: SetProgressFn

= (progress: P) => { + this._progress.set(progress); + }; + this.result = run(setAbortable, setProgress); + } + + get progress(): BaseObservableValue

{ + return this._progress; } abort() { this._abortable?.abort(); - this._abortable = null; + this._abortable = undefined; } } From 60ed276b8a506b4a8467083d98b206fcd0ae442e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jan 2022 15:19:31 +0100 Subject: [PATCH 15/52] add progress notification and cancellation to key backup flush --- src/matrix/Session.js | 23 +++- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 105 ++++++++++++------ .../idb/stores/SessionNeedingBackupStore.ts | 4 + 3 files changed, 93 insertions(+), 39 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 9510cbfd..d8595d12 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -76,6 +76,7 @@ export class Session { this._getSyncToken = () => this.syncToken; this._olmWorker = olmWorker; this._keyBackup = null; + this._keyBackupOperation = new ObservableValue(null); this._hasSecretStorageKey = new ObservableValue(null); this._observedRoomStatus = new Map(); @@ -270,6 +271,10 @@ export class Session { return this._keyBackup; } + get keyBackupOperation() { + return this._keyBackupOperation; + } + get hasIdentity() { return !!this._e2eeAccount; } @@ -559,7 +564,7 @@ export class Session { async writeSync(syncResponse, syncFilterId, preparation, txn, log) { const changes = { syncInfo: null, - e2eeAccountChanges: null, + e2eeAccountChanges: null }; const syncToken = syncResponse.next_batch; if (syncToken !== this.syncToken) { @@ -584,7 +589,7 @@ export class Session { // this should come after the deviceMessageHandler, so the room keys are already written and their // isBetter property has been checked if (this._keyBackup) { - this._keyBackup.writeKeys(preparation.newRoomKeys, txn, log); + changes.shouldFlushKeyBackup = this._keyBackup.writeKeys(preparation.newRoomKeys, txn, log); } } @@ -623,8 +628,18 @@ export class Session { await log.wrap("uploadKeys", log => this._e2eeAccount.uploadKeys(this._storage, false, log)); } } - if (this._keyBackup) { - this._keyBackup.flush(); + // should flush and not already flushing + if (changes.shouldFlushKeyBackup && this._keyBackup && !this._keyBackupOperation.get()) { + log.wrapDetached("flush key backup", async log => { + const operation = this._keyBackup.flush(log); + this._keyBackupOperation.set(operation); + try { + await operation.result; + } catch (err) { + log.catch(err); + } + this._keyBackupOperation.set(null); + }); } } diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index 38faa5e2..cd09423c 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -18,7 +18,10 @@ import {StoreNames} from "../../../storage/common"; import {keyFromStorage, keyFromBackup} from "../decryption/RoomKey"; import {MEGOLM_ALGORITHM} from "../../common"; import * as Curve25519 from "./Curve25519"; +import {AbortableOperation} from "../../../../utils/AbortableOperation"; +import {SetAbortableFn} from "../../../../utils/AbortableOperation"; +import type {BackupInfo, SessionData, SessionKeyInfo, SessionInfo, KeyBackupPayload} from "./types"; import type {HomeServerApi} from "../../../net/HomeServerApi"; import type {IncomingRoomKey, RoomKey} from "../decryption/RoomKey"; import type {KeyLoader} from "../decryption/KeyLoader"; @@ -27,6 +30,7 @@ import type {Storage} from "../../../storage/idb/Storage"; import type {ILogItem} from "../../../../logging/types"; import type {Platform} from "../../../../platform/web/Platform"; import type {Transaction} from "../../../storage/idb/Transaction"; +import type {BackupEntry} from "../../../storage/idb/stores/SessionNeedingBackupStore"; import type * as OlmNamespace from "@matrix-org/olm"; type Olm = typeof OlmNamespace; @@ -45,7 +49,7 @@ export class KeyBackup { if (!sessionResponse.session_data) { return; } - const sessionKeyInfo = this.crypto.decryptRoomKey(sessionResponse.session_data as Curve25519.SessionData); + const sessionKeyInfo = this.crypto.decryptRoomKey(sessionResponse.session_data as SessionData); if (sessionKeyInfo?.algorithm === MEGOLM_ALGORITHM) { return keyFromBackup(roomId, sessionId, sessionKeyInfo); } else if (sessionKeyInfo?.algorithm) { @@ -64,45 +68,69 @@ export class KeyBackup { return hasBetter; } - async flush() { - while (true) { - await this.platform.clock.createTimeout(this.platform.random() * 10000).elapsed(); - const txn = await this.storage.readTxn([ - StoreNames.sessionsNeedingBackup, - StoreNames.inboundGroupSessions, - ]); - const keysNeedingBackup = await txn.sessionsNeedingBackup.getFirstEntries(20); - if (keysNeedingBackup.length === 0) { - return; - } - const roomKeys = await Promise.all(keysNeedingBackup.map(k => keyFromStorage(k.roomId, k.senderKey, k.sessionId, txn))); - const payload: KeyBackupPayload = { rooms: {} }; - const payloadRooms = payload.rooms; - for (const key of roomKeys) { - if (key) { - let roomPayload = payloadRooms[key.roomId]; - if (!roomPayload) { - roomPayload = payloadRooms[key.roomId] = { sessions: {} }; - } - roomPayload.sessions[key.sessionId] = await this.encodeRoomKey(key); - } - } - await this.hsApi.uploadRoomKeysToBackup(this.backupInfo.version, payload).response(); - { - const txn = await this.storage.readWriteTxn([ + // TODO: protect against having multiple concurrent flushes + flush(log: ILogItem): AbortableOperation, Progress> { + return new AbortableOperation(async (setAbortable, setProgress) => { + let total = 0; + let amountFinished = 0; + while (true) { + const timeout = this.platform.clock.createTimeout(this.platform.random() * 10000); + setAbortable(timeout); + await timeout.elapsed(); + const txn = await this.storage.readTxn([ StoreNames.sessionsNeedingBackup, + StoreNames.inboundGroupSessions, ]); - try { - for (const key of keysNeedingBackup) { - txn.sessionsNeedingBackup.remove(key.roomId, key.senderKey, key.sessionId); - } - } catch (err) { - txn.abort(); - throw err; + setAbortable(txn); + // fetch total again on each iteration as while we are flushing, sync might be adding keys + total = await txn.sessionsNeedingBackup.count(); + setProgress(new Progress(total, amountFinished)); + const keysNeedingBackup = await txn.sessionsNeedingBackup.getFirstEntries(20); + if (keysNeedingBackup.length === 0) { + return; } - await txn.complete(); + const roomKeysOrNotFound = await Promise.all(keysNeedingBackup.map(k => keyFromStorage(k.roomId, k.senderKey, k.sessionId, txn))); + const roomKeys = roomKeysOrNotFound.filter(k => !!k) as RoomKey[]; + if (roomKeys.length) { + const payload = await this.encodeKeysForBackup(roomKeys); + const uploadRequest = this.hsApi.uploadRoomKeysToBackup(this.backupInfo.version, payload); + setAbortable(uploadRequest); + await uploadRequest.response(); + } + this.removeBackedUpKeys(keysNeedingBackup, setAbortable); + amountFinished += keysNeedingBackup.length; + setProgress(new Progress(total, amountFinished)); } + }); + } + + private async encodeKeysForBackup(roomKeys: RoomKey[]): Promise { + const payload: KeyBackupPayload = { rooms: {} }; + const payloadRooms = payload.rooms; + for (const key of roomKeys) { + let roomPayload = payloadRooms[key.roomId]; + if (!roomPayload) { + roomPayload = payloadRooms[key.roomId] = { sessions: {} }; + } + roomPayload.sessions[key.sessionId] = await this.encodeRoomKey(key); } + return payload; + } + + private async removeBackedUpKeys(keysNeedingBackup: BackupEntry[], setAbortable: SetAbortableFn) { + const txn = await this.storage.readWriteTxn([ + StoreNames.sessionsNeedingBackup, + ]); + setAbortable(txn); + try { + for (const key of keysNeedingBackup) { + txn.sessionsNeedingBackup.remove(key.roomId, key.senderKey, key.sessionId); + } + } catch (err) { + txn.abort(); + throw err; + } + await txn.complete(); } private async encodeRoomKey(roomKey: RoomKey): Promise { @@ -140,3 +168,10 @@ export class KeyBackup { } } } + +export class Progress { + constructor( + public readonly total: number, + public readonly finished: number + ) {} +} diff --git a/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts b/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts index 27104191..71a383bb 100644 --- a/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts +++ b/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts @@ -53,4 +53,8 @@ export class SessionNeedingBackupStore { remove(roomId: string, senderKey: string, sessionId: string): void { this.store.delete(encodeKey(roomId, senderKey, sessionId)); } + + count(): Promise { + return this.store.count(); + } } From e80acd4d575c17ee3df2c2c7b5dc692a1d341450 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jan 2022 16:30:40 +0100 Subject: [PATCH 16/52] add migration when backup is enabled --- src/matrix/storage/idb/schema.ts | 52 ++++++++++++++----- .../idb/stores/SessionNeedingBackupStore.ts | 2 +- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index 2aca60a2..2da4d65c 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -6,6 +6,8 @@ import {addRoomToIdentity} from "../../e2ee/DeviceTracker.js"; import {SESSION_E2EE_KEY_PREFIX} from "../../e2ee/common.js"; import {SummaryData} from "../../room/RoomSummary"; import {RoomMemberStore, MemberData} from "./stores/RoomMemberStore"; +import {encodeKey as encodeBackupKey} from "./stores/SessionNeedingBackupStore"; +import {InboundGroupSessionStore, InboundGroupSessionEntry} from "./stores/InboundGroupSessionStore"; import {RoomStateEntry} from "./stores/RoomStateStore"; import {SessionStore} from "./stores/SessionStore"; import {Store} from "./Store"; @@ -39,6 +41,21 @@ export const schema: MigrationFunc[] = [ // TypeScript note: for now, do not bother introducing interfaces / alias // for old schemas. Just take them as `any`. +function createDatabaseNameHelper(db: IDBDatabase): ITransaction { + // the Store object gets passed in several things through the Transaction class (a wrapper around IDBTransaction), + // the only thing we should need here is the databaseName though, so we mock it out. + // ideally we should have an easier way to go from the idb primitive layer to the specific store classes where + // we implement logic, but for now we need this. + const databaseNameHelper: ITransaction = { + databaseName: db.name, + get idbFactory(): IDBFactory { throw new Error("unused");}, + get IDBKeyRange(): typeof IDBKeyRange { throw new Error("unused");}, + addWriteError() {}, + }; + return databaseNameHelper; +} + + // how do we deal with schema updates vs existing data migration in a way that //v1 function createInitialStores(db: IDBDatabase): void { @@ -223,17 +240,7 @@ async function changeSSSSKeyPrefix(db: IDBDatabase, txn: IDBTransaction) { // v13 async function backupAndRestoreE2EEAccountToLocalStorage(db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: ILogItem) { const session = txn.objectStore("session"); - // the Store object gets passed in several things through the Transaction class (a wrapper around IDBTransaction), - // the only thing we should need here is the databaseName though, so we mock it out. - // ideally we should have an easier way to go from the idb primitive layer to the specific store classes where - // we implement logic, but for now we need this. - const databaseNameHelper: ITransaction = { - databaseName: db.name, - get idbFactory(): IDBFactory { throw new Error("unused");}, - get IDBKeyRange(): typeof IDBKeyRange { throw new Error("unused");}, - addWriteError() {}, - }; - const sessionStore = new SessionStore(new Store(session, databaseNameHelper), localStorage); + const sessionStore = new SessionStore(new Store(session, createDatabaseNameHelper(db)), localStorage); // if we already have an e2ee identity, write a backup to local storage. // further updates to e2ee keys in the session store will also write to local storage from 0.2.15 on, // but here we make sure a backup is immediately created after installing the update and we don't wait until @@ -273,6 +280,25 @@ async function clearAllStores(db: IDBDatabase, txn: IDBTransaction) { } // v15 adds the sessionsNeedingBackup store, for key backup -function createSessionsNeedingBackup(db: IDBDatabase): void { - db.createObjectStore("sessionsNeedingBackup", {keyPath: "key"}); +async function createSessionsNeedingBackup(db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: ILogItem): Promise { + const backupStore = db.createObjectStore("sessionsNeedingBackup", {keyPath: "key"}); + const session = txn.objectStore("session"); + const ssssKey = await reqAsPromise(session.get(`${SESSION_E2EE_KEY_PREFIX}ssssKey`) as IDBRequest); + const keyBackupEnabled = !!ssssKey; + log.set("key backup", keyBackupEnabled); + if (keyBackupEnabled) { + let count = 0; + try { + const inboundGroupSessions = txn.objectStore("inboundGroupSessions"); + await iterateCursor(inboundGroupSessions.openCursor(), session => { + backupStore.add({key: encodeBackupKey(session.roomId, session.senderKey, session.sessionId)}); + count += 1; + return NOT_DONE; + }); + } catch (err) { + txn.abort(); + log.log("could not migrate operations").catch(err); + } + log.set("count", count); + } } diff --git a/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts b/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts index 71a383bb..327fce9a 100644 --- a/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts +++ b/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts @@ -26,7 +26,7 @@ type StorageEntry = { key: string }; -function encodeKey(roomId: string, senderKey: string, sessionId: string): string { +export function encodeKey(roomId: string, senderKey: string, sessionId: string): string { return `${roomId}|${senderKey}|${sessionId}`; } From bf08c0d85066b6967b927b680489f8652f599065 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 27 Jan 2022 14:19:37 +0100 Subject: [PATCH 17/52] deal with errors when enabling key backup fixes #449 --- src/matrix/Session.js | 82 +++++++++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 35 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index d8595d12..5e68829b 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -190,22 +190,25 @@ export class Session { * @param {string} credential either the passphrase or the recovery key, depending on the type * @return {Promise} resolves or rejects after having tried to enable secret storage */ - async enableSecretStorage(type, credential) { - if (!this._olm) { - throw new Error("olm required"); - } - if (this._keyBackup) { - return false; - } - const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm); - // and create key backup, which needs to read from accountData - const readTxn = await this._storage.readTxn([ - this._storage.storeNames.accountData, - ]); - await this._createKeyBackup(key, readTxn); - await this._writeSSSSKey(key); - this._hasSecretStorageKey.set(true); - return key; + enableSecretStorage(type, credential, log = undefined) { + return this._platform.logger.wrapOrRun(log, "enable secret storage", async log => { + if (!this._olm) { + throw new Error("olm required"); + } + if (this._keyBackup) { + return false; + } + const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm); + // and create key backup, which needs to read from accountData + const readTxn = await this._storage.readTxn([ + this._storage.storeNames.accountData, + ]); + if (await this._createKeyBackup(key, readTxn, log)) { + await this._writeSSSSKey(key); + this._hasSecretStorageKey.set(true); + return key; + } + }); } async _writeSSSSKey(key) { @@ -246,25 +249,33 @@ export class Session { this._hasSecretStorageKey.set(false); } - async _createKeyBackup(ssssKey, txn) { - const secretStorage = new SecretStorage({key: ssssKey, platform: this._platform}); - this._keyBackup = await KeyBackup.fromSecretStorage( - this._platform, - this._olm, - secretStorage, - this._hsApi, - this._keyLoader, - this._storage, - txn - ); - if (this._keyBackup) { - for (const room of this._rooms.values()) { - if (room.isEncrypted) { - room.enableKeyBackup(this._keyBackup); + _createKeyBackup(ssssKey, txn, log) { + return log.wrap("enable key backup", async log => { + try { + const secretStorage = new SecretStorage({key: ssssKey, platform: this._platform}); + this._keyBackup = await KeyBackup.fromSecretStorage( + this._platform, + this._olm, + secretStorage, + this._hsApi, + this._keyLoader, + this._storage, + txn + ); + if (this._keyBackup) { + for (const room of this._rooms.values()) { + if (room.isEncrypted) { + room.enableKeyBackup(this._keyBackup); + } + } } + this.needsKeyBackup.set(false); + } catch (err) { + log.catch(err); + return false; } - } - this.needsKeyBackup.set(false); + return true; + }); } get keyBackup() { @@ -455,11 +466,12 @@ export class Session { ]); // try set up session backup if we stored the ssss key const ssssKey = await ssssReadKey(txn); + let couldReadKeyBackup = false; if (ssssKey) { // txn will end here as this does a network request - await this._createKeyBackup(ssssKey, txn); + couldReadKeyBackup = await this._createKeyBackup(ssssKey, txn, log); } - this._hasSecretStorageKey.set(!!ssssKey); + this._hasSecretStorageKey.set(couldReadKeyBackup); } // restore unfinished operations, like sending out room keys const opsTxn = await this._storage.readWriteTxn([ From 0b4954a9caa4750aa21dbce555ef28d9c08fea9f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 27 Jan 2022 14:20:04 +0100 Subject: [PATCH 18/52] log key backup upload requests --- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index cd09423c..c5fc7210 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -93,7 +93,7 @@ export class KeyBackup { const roomKeys = roomKeysOrNotFound.filter(k => !!k) as RoomKey[]; if (roomKeys.length) { const payload = await this.encodeKeysForBackup(roomKeys); - const uploadRequest = this.hsApi.uploadRoomKeysToBackup(this.backupInfo.version, payload); + const uploadRequest = this.hsApi.uploadRoomKeysToBackup(this.backupInfo.version, payload, {log}); setAbortable(uploadRequest); await uploadRequest.response(); } From 6f1484005bd883b7f4a32def52d553b6bb9deb11 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 27 Jan 2022 15:14:29 +0100 Subject: [PATCH 19/52] stop key backup when on the wrong version users can then enter the new key in the settings to start backing up again --- src/matrix/Session.js | 7 ++++++- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 15 ++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 5e68829b..9b9d013e 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -646,7 +646,12 @@ export class Session { const operation = this._keyBackup.flush(log); this._keyBackupOperation.set(operation); try { - await operation.result; + const success = await operation.result; + // stop key backup if the version was changed + if (!success) { + this._keyBackup = this._keyBackup.dispose(); + this.needsKeyBackup.set(true); + } } catch (err) { log.catch(err); } diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index c5fc7210..3792ea96 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -69,7 +69,7 @@ export class KeyBackup { } // TODO: protect against having multiple concurrent flushes - flush(log: ILogItem): AbortableOperation, Progress> { + flush(log: ILogItem): AbortableOperation, Progress> { return new AbortableOperation(async (setAbortable, setProgress) => { let total = 0; let amountFinished = 0; @@ -87,7 +87,7 @@ export class KeyBackup { setProgress(new Progress(total, amountFinished)); const keysNeedingBackup = await txn.sessionsNeedingBackup.getFirstEntries(20); if (keysNeedingBackup.length === 0) { - return; + return true; } const roomKeysOrNotFound = await Promise.all(keysNeedingBackup.map(k => keyFromStorage(k.roomId, k.senderKey, k.sessionId, txn))); const roomKeys = roomKeysOrNotFound.filter(k => !!k) as RoomKey[]; @@ -95,7 +95,16 @@ export class KeyBackup { const payload = await this.encodeKeysForBackup(roomKeys); const uploadRequest = this.hsApi.uploadRoomKeysToBackup(this.backupInfo.version, payload, {log}); setAbortable(uploadRequest); - await uploadRequest.response(); + try { + await uploadRequest.response(); + } catch (err) { + if (err.name === "HomeServerError" && err.errcode === "M_WRONG_ROOM_KEYS_VERSION") { + log.set("wrong_version", true); + return false; + } else { + throw err; + } + } } this.removeBackedUpKeys(keysNeedingBackup, setAbortable); amountFinished += keysNeedingBackup.length; From 48e72f9b69e44069bcf97f00d7a9298d6b52a0a6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 27 Jan 2022 16:00:46 +0100 Subject: [PATCH 20/52] replace SessionsNeedingBackup store with backup field on inbound session --- src/matrix/storage/common.ts | 1 - src/matrix/storage/idb/Transaction.ts | 5 -- src/matrix/storage/idb/schema.ts | 35 ++++------- .../idb/stores/InboundGroupSessionStore.ts | 21 +++++++ .../idb/stores/SessionNeedingBackupStore.ts | 60 ------------------- 5 files changed, 31 insertions(+), 91 deletions(-) delete mode 100644 src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts diff --git a/src/matrix/storage/common.ts b/src/matrix/storage/common.ts index 3929c061..23bb0d31 100644 --- a/src/matrix/storage/common.ts +++ b/src/matrix/storage/common.ts @@ -33,7 +33,6 @@ export enum StoreNames { groupSessionDecryptions = "groupSessionDecryptions", operations = "operations", accountData = "accountData", - sessionsNeedingBackup = "sessionsNeedingBackup", } export const STORE_NAMES: Readonly = Object.values(StoreNames); diff --git a/src/matrix/storage/idb/Transaction.ts b/src/matrix/storage/idb/Transaction.ts index 9b765310..80894105 100644 --- a/src/matrix/storage/idb/Transaction.ts +++ b/src/matrix/storage/idb/Transaction.ts @@ -36,7 +36,6 @@ import {OutboundGroupSessionStore} from "./stores/OutboundGroupSessionStore"; import {GroupSessionDecryptionStore} from "./stores/GroupSessionDecryptionStore"; import {OperationStore} from "./stores/OperationStore"; import {AccountDataStore} from "./stores/AccountDataStore"; -import {SessionNeedingBackupStore} from "./stores/SessionNeedingBackupStore"; import type {ILogger, ILogItem} from "../../../logging/types"; export type IDBKey = IDBValidKey | IDBKeyRange; @@ -152,10 +151,6 @@ export class Transaction { get inboundGroupSessions(): InboundGroupSessionStore { return this._store(StoreNames.inboundGroupSessions, idbStore => new InboundGroupSessionStore(idbStore)); } - - get sessionsNeedingBackup(): SessionNeedingBackupStore { - return this._store(StoreNames.sessionsNeedingBackup, idbStore => new SessionNeedingBackupStore(idbStore)); - } get outboundGroupSessions(): OutboundGroupSessionStore { return this._store(StoreNames.outboundGroupSessions, idbStore => new OutboundGroupSessionStore(idbStore)); diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index 2da4d65c..e6644fd0 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -6,8 +6,7 @@ import {addRoomToIdentity} from "../../e2ee/DeviceTracker.js"; import {SESSION_E2EE_KEY_PREFIX} from "../../e2ee/common.js"; import {SummaryData} from "../../room/RoomSummary"; import {RoomMemberStore, MemberData} from "./stores/RoomMemberStore"; -import {encodeKey as encodeBackupKey} from "./stores/SessionNeedingBackupStore"; -import {InboundGroupSessionStore, InboundGroupSessionEntry} from "./stores/InboundGroupSessionStore"; +import {InboundGroupSessionStore, InboundGroupSessionEntry, BackupStatus} from "./stores/InboundGroupSessionStore"; import {RoomStateEntry} from "./stores/RoomStateStore"; import {SessionStore} from "./stores/SessionStore"; import {Store} from "./Store"; @@ -34,7 +33,7 @@ export const schema: MigrationFunc[] = [ changeSSSSKeyPrefix, backupAndRestoreE2EEAccountToLocalStorage, clearAllStores, - createSessionsNeedingBackup + addInboundSessionBackupIndex ]; // TODO: how to deal with git merge conflicts of this array? @@ -279,26 +278,12 @@ async function clearAllStores(db: IDBDatabase, txn: IDBTransaction) { } } -// v15 adds the sessionsNeedingBackup store, for key backup -async function createSessionsNeedingBackup(db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: ILogItem): Promise { - const backupStore = db.createObjectStore("sessionsNeedingBackup", {keyPath: "key"}); - const session = txn.objectStore("session"); - const ssssKey = await reqAsPromise(session.get(`${SESSION_E2EE_KEY_PREFIX}ssssKey`) as IDBRequest); - const keyBackupEnabled = !!ssssKey; - log.set("key backup", keyBackupEnabled); - if (keyBackupEnabled) { - let count = 0; - try { - const inboundGroupSessions = txn.objectStore("inboundGroupSessions"); - await iterateCursor(inboundGroupSessions.openCursor(), session => { - backupStore.add({key: encodeBackupKey(session.roomId, session.senderKey, session.sessionId)}); - count += 1; - return NOT_DONE; - }); - } catch (err) { - txn.abort(); - log.log("could not migrate operations").catch(err); - } - log.set("count", count); - } +// v15 add backup index to inboundGroupSessions +async function addInboundSessionBackupIndex(db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: ILogItem): Promise { + const inboundGroupSessions = txn.objectStore("inboundGroupSessions"); + await iterateCursor(inboundGroupSessions.openCursor(), (value, key, cursor) => { + value.backup = BackupStatus.NotBackedUp; + return NOT_DONE; + }); + inboundGroupSessions.createIndex("byBackup", "backup", {unique: false}); } diff --git a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts index c670302c..72ea6e9a 100644 --- a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts +++ b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts @@ -17,6 +17,11 @@ limitations under the License. import {MIN_UNICODE, MAX_UNICODE} from "./common"; import {Store} from "../Store"; +export enum BackupStatus { + NotBackedUp = 0, + BackedUp = 1 +} + export interface InboundGroupSessionEntry { roomId: string; senderKey: string; @@ -24,6 +29,7 @@ export interface InboundGroupSessionEntry { session?: string; claimedKeys?: { [algorithm : string] : string }; eventIds?: string[]; + backup: BackupStatus } type InboundGroupSessionStorageEntry = InboundGroupSessionEntry & { key: string }; @@ -63,4 +69,19 @@ export class InboundGroupSessionStore { ); this._store.delete(range); } + countNonBackedUpSessions(): Promise { + return this._store.index("byBackup").count(); + } + + getFirstNonBackedUpSessions(amount: number): Promise { + return this._store.index("byBackup").selectLimit(0, amount); + } + + async markAsBackedUp(roomId: string, senderKey: string, sessionId: string): Promise { + const entry = await this._store.get(encodeKey(roomId, senderKey, sessionId)); + if (entry) { + entry.backup = BackupStatus.BackedUp; + this._store.put(entry); + } + } } diff --git a/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts b/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts deleted file mode 100644 index 327fce9a..00000000 --- a/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* -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. -*/ - -import type {Store} from "../Store"; - -export type BackupEntry = { - roomId: string; - senderKey: string; - sessionId: string; -}; - -type StorageEntry = { - key: string -}; - -export function encodeKey(roomId: string, senderKey: string, sessionId: string): string { - return `${roomId}|${senderKey}|${sessionId}`; -} - -function decodeKey(key: string): BackupEntry { - const [roomId, senderKey, sessionId] = key.split("|"); - return {roomId, senderKey, sessionId}; -} - -export class SessionNeedingBackupStore { - constructor(private store: Store) {} - - async getFirstEntries(amount: number): Promise { - const storageEntries = await this.store.selectLimit(undefined, amount); - return storageEntries.map(s => decodeKey(s.key)); - } - - set(roomId: string, senderKey: string, sessionId: string): void { - const storageEntry : StorageEntry = { - key: encodeKey(roomId, senderKey, sessionId), - }; - this.store.put(storageEntry); - } - - remove(roomId: string, senderKey: string, sessionId: string): void { - this.store.delete(encodeKey(roomId, senderKey, sessionId)); - } - - count(): Promise { - return this.store.count(); - } -} From dd2b41ff95f3527bd313d87bb1c11dbc2a5163f2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 27 Jan 2022 16:07:18 +0100 Subject: [PATCH 21/52] use backup flag in key backup rather than separate store --- src/matrix/DeviceMessageHandler.js | 3 +- src/matrix/Session.js | 9 +-- src/matrix/e2ee/megolm/decryption/RoomKey.ts | 12 +++- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 60 +++++++------------ 4 files changed, 35 insertions(+), 49 deletions(-) diff --git a/src/matrix/DeviceMessageHandler.js b/src/matrix/DeviceMessageHandler.js index 0a606841..6ac5ac07 100644 --- a/src/matrix/DeviceMessageHandler.js +++ b/src/matrix/DeviceMessageHandler.js @@ -57,7 +57,8 @@ export class DeviceMessageHandler { async writeSync(prep, txn) { // write olm changes prep.olmDecryptChanges.write(txn); - await Promise.all(prep.newRoomKeys.map(key => this._megolmDecryption.writeRoomKey(key, txn))); + const didWriteValues = await Promise.all(prep.newRoomKeys.map(key => this._megolmDecryption.writeRoomKey(key, txn))); + return didWriteValues.some(didWrite => !!didWrite); } } diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 9b9d013e..dd7a0e1c 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -597,12 +597,7 @@ export class Session { } if (preparation) { - await log.wrap("deviceMsgs", log => this._deviceMessageHandler.writeSync(preparation, txn, log)); - // this should come after the deviceMessageHandler, so the room keys are already written and their - // isBetter property has been checked - if (this._keyBackup) { - changes.shouldFlushKeyBackup = this._keyBackup.writeKeys(preparation.newRoomKeys, txn, log); - } + changes.hasNewRoomKeys = await log.wrap("deviceMsgs", log => this._deviceMessageHandler.writeSync(preparation, txn, log)); } // store account data @@ -641,7 +636,7 @@ export class Session { } } // should flush and not already flushing - if (changes.shouldFlushKeyBackup && this._keyBackup && !this._keyBackupOperation.get()) { + if (changes.hasNewRoomKeys && this._keyBackup && !this._keyBackupOperation.get()) { log.wrapDetached("flush key backup", async log => { const operation = this._keyBackup.flush(log); this._keyBackupOperation.set(operation); diff --git a/src/matrix/e2ee/megolm/decryption/RoomKey.ts b/src/matrix/e2ee/megolm/decryption/RoomKey.ts index 299b0a81..6112ccef 100644 --- a/src/matrix/e2ee/megolm/decryption/RoomKey.ts +++ b/src/matrix/e2ee/megolm/decryption/RoomKey.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import {BackupStatus} from "../../../storage/idb/stores/InboundGroupSessionStore"; import type {InboundGroupSessionEntry} from "../../../storage/idb/stores/InboundGroupSessionStore"; import type {Transaction} from "../../../storage/idb/Transaction"; import type {DecryptionResult} from "../../DecryptionResult"; @@ -81,6 +82,7 @@ export abstract class IncomingRoomKey extends RoomKey { senderKey: this.senderKey, sessionId: this.sessionId, session: pickledSession, + backup: this.backupStatus, claimedKeys: {"ed25519": this.claimedEd25519Key}, }; txn.inboundGroupSessions.set(sessionEntry); @@ -125,6 +127,10 @@ export abstract class IncomingRoomKey extends RoomKey { } return this.isBetter!; } + + protected get backupStatus(): BackupStatus { + return BackupStatus.NotBackedUp; + } } class DeviceMessageRoomKey extends IncomingRoomKey { @@ -162,9 +168,13 @@ class BackupRoomKey extends IncomingRoomKey { loadInto(session) { session.import_session(this.serializationKey); } + + protected get backupStatus(): BackupStatus { + return BackupStatus.BackedUp; + } } -class StoredRoomKey extends RoomKey { +export class StoredRoomKey extends RoomKey { private storageEntry: InboundGroupSessionEntry; constructor(storageEntry: InboundGroupSessionEntry) { diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index 3792ea96..49771d15 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {StoreNames} from "../../../storage/common"; -import {keyFromStorage, keyFromBackup} from "../decryption/RoomKey"; +import {StoredRoomKey, keyFromBackup} from "../decryption/RoomKey"; import {MEGOLM_ALGORITHM} from "../../common"; import * as Curve25519 from "./Curve25519"; import {AbortableOperation} from "../../../../utils/AbortableOperation"; @@ -30,7 +30,6 @@ import type {Storage} from "../../../storage/idb/Storage"; import type {ILogItem} from "../../../../logging/types"; import type {Platform} from "../../../../platform/web/Platform"; import type {Transaction} from "../../../storage/idb/Transaction"; -import type {BackupEntry} from "../../../storage/idb/stores/SessionNeedingBackupStore"; import type * as OlmNamespace from "@matrix-org/olm"; type Olm = typeof OlmNamespace; @@ -57,17 +56,6 @@ export class KeyBackup { } } - writeKeys(roomKeys: IncomingRoomKey[], txn: Transaction): boolean { - let hasBetter = false; - for (const key of roomKeys) { - if (key.isBetter) { - txn.sessionsNeedingBackup.set(key.roomId, key.senderKey, key.sessionId); - hasBetter = true; - } - } - return hasBetter; - } - // TODO: protect against having multiple concurrent flushes flush(log: ILogItem): AbortableOperation, Progress> { return new AbortableOperation(async (setAbortable, setProgress) => { @@ -77,36 +65,30 @@ export class KeyBackup { const timeout = this.platform.clock.createTimeout(this.platform.random() * 10000); setAbortable(timeout); await timeout.elapsed(); - const txn = await this.storage.readTxn([ - StoreNames.sessionsNeedingBackup, - StoreNames.inboundGroupSessions, - ]); + const txn = await this.storage.readTxn([StoreNames.inboundGroupSessions]); setAbortable(txn); // fetch total again on each iteration as while we are flushing, sync might be adding keys - total = await txn.sessionsNeedingBackup.count(); + total = await txn.inboundGroupSessions.countNonBackedUpSessions(); setProgress(new Progress(total, amountFinished)); - const keysNeedingBackup = await txn.sessionsNeedingBackup.getFirstEntries(20); + const keysNeedingBackup = (await txn.inboundGroupSessions.getFirstNonBackedUpSessions(20)) + .map(entry => new StoredRoomKey(entry)); if (keysNeedingBackup.length === 0) { return true; } - const roomKeysOrNotFound = await Promise.all(keysNeedingBackup.map(k => keyFromStorage(k.roomId, k.senderKey, k.sessionId, txn))); - const roomKeys = roomKeysOrNotFound.filter(k => !!k) as RoomKey[]; - if (roomKeys.length) { - const payload = await this.encodeKeysForBackup(roomKeys); - const uploadRequest = this.hsApi.uploadRoomKeysToBackup(this.backupInfo.version, payload, {log}); - setAbortable(uploadRequest); - try { - await uploadRequest.response(); - } catch (err) { - if (err.name === "HomeServerError" && err.errcode === "M_WRONG_ROOM_KEYS_VERSION") { - log.set("wrong_version", true); - return false; - } else { - throw err; - } + const payload = await this.encodeKeysForBackup(keysNeedingBackup); + const uploadRequest = this.hsApi.uploadRoomKeysToBackup(this.backupInfo.version, payload, {log}); + setAbortable(uploadRequest); + try { + await uploadRequest.response(); + } catch (err) { + if (err.name === "HomeServerError" && err.errcode === "M_WRONG_ROOM_KEYS_VERSION") { + log.set("wrong_version", true); + return false; + } else { + throw err; } } - this.removeBackedUpKeys(keysNeedingBackup, setAbortable); + this.markKeysAsBackedUp(keysNeedingBackup, setAbortable); amountFinished += keysNeedingBackup.length; setProgress(new Progress(total, amountFinished)); } @@ -126,15 +108,13 @@ export class KeyBackup { return payload; } - private async removeBackedUpKeys(keysNeedingBackup: BackupEntry[], setAbortable: SetAbortableFn) { + private async markKeysAsBackedUp(roomKeys: RoomKey[], setAbortable: SetAbortableFn) { const txn = await this.storage.readWriteTxn([ - StoreNames.sessionsNeedingBackup, + StoreNames.inboundGroupSessions, ]); setAbortable(txn); try { - for (const key of keysNeedingBackup) { - txn.sessionsNeedingBackup.remove(key.roomId, key.senderKey, key.sessionId); - } + await Promise.all(roomKeys.map(key => txn.inboundGroupSessions.markAsBackedUp(key.roomId, key.senderKey, key.sessionId))); } catch (err) { txn.abort(); throw err; From c81dde53e7dc473ed7f7d3d771e419e5bdfdf9e2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 10:00:41 +0100 Subject: [PATCH 22/52] store key source in inbound session --- src/matrix/e2ee/megolm/decryption/KeyLoader.ts | 5 +++++ src/matrix/e2ee/megolm/decryption/RoomKey.ts | 10 ++++++++-- src/matrix/storage/idb/schema.ts | 7 ++++++- .../storage/idb/stores/InboundGroupSessionStore.ts | 8 +++++++- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/matrix/e2ee/megolm/decryption/KeyLoader.ts b/src/matrix/e2ee/megolm/decryption/KeyLoader.ts index c1925e84..884203a3 100644 --- a/src/matrix/e2ee/megolm/decryption/KeyLoader.ts +++ b/src/matrix/e2ee/megolm/decryption/KeyLoader.ts @@ -213,6 +213,9 @@ class KeyOperation { } } +import {KeySource} from "../../../storage/idb/stores/InboundGroupSessionStore"; + + export function tests() { let instances = 0; @@ -237,6 +240,8 @@ export function tests() { get serializationKey(): string { return `key-${this.sessionId}-${this._firstKnownIndex}`; } get serializationType(): string { return "type"; } get eventIds(): string[] | undefined { return undefined; } + get keySource(): KeySource { return KeySource.DeviceMessage; } + loadInto(session: Olm.InboundGroupSession) { const mockSession = session as MockInboundSession; mockSession.sessionId = this.sessionId; diff --git a/src/matrix/e2ee/megolm/decryption/RoomKey.ts b/src/matrix/e2ee/megolm/decryption/RoomKey.ts index 6112ccef..7b648d92 100644 --- a/src/matrix/e2ee/megolm/decryption/RoomKey.ts +++ b/src/matrix/e2ee/megolm/decryption/RoomKey.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BackupStatus} from "../../../storage/idb/stores/InboundGroupSessionStore"; +import {BackupStatus, KeySource} from "../../../storage/idb/stores/InboundGroupSessionStore"; import type {InboundGroupSessionEntry} from "../../../storage/idb/stores/InboundGroupSessionStore"; import type {Transaction} from "../../../storage/idb/Transaction"; import type {DecryptionResult} from "../../DecryptionResult"; @@ -83,6 +83,7 @@ export abstract class IncomingRoomKey extends RoomKey { sessionId: this.sessionId, session: pickledSession, backup: this.backupStatus, + source: this.keySource, claimedKeys: {"ed25519": this.claimedEd25519Key}, }; txn.inboundGroupSessions.set(sessionEntry); @@ -131,6 +132,8 @@ export abstract class IncomingRoomKey extends RoomKey { protected get backupStatus(): BackupStatus { return BackupStatus.NotBackedUp; } + + protected abstract get keySource(): KeySource; } class DeviceMessageRoomKey extends IncomingRoomKey { @@ -147,10 +150,12 @@ class DeviceMessageRoomKey extends IncomingRoomKey { get claimedEd25519Key() { return this._decryptionResult.claimedEd25519Key; } get serializationKey(): string { return this._decryptionResult.event.content?.["session_key"]; } get serializationType(): string { return "create"; } + protected get keySource(): KeySource { return KeySource.DeviceMessage; } loadInto(session) { session.create(this.serializationKey); } + } class BackupRoomKey extends IncomingRoomKey { @@ -164,7 +169,8 @@ class BackupRoomKey extends IncomingRoomKey { get claimedEd25519Key() { return this._backupInfo["sender_claimed_keys"]?.["ed25519"]; } get serializationKey(): string { return this._backupInfo["session_key"]; } get serializationType(): string { return "import_session"; } - + protected get keySource(): KeySource { return KeySource.Backup; } + loadInto(session) { session.import_session(this.serializationKey); } diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index e6644fd0..9c105a71 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -6,7 +6,7 @@ import {addRoomToIdentity} from "../../e2ee/DeviceTracker.js"; import {SESSION_E2EE_KEY_PREFIX} from "../../e2ee/common.js"; import {SummaryData} from "../../room/RoomSummary"; import {RoomMemberStore, MemberData} from "./stores/RoomMemberStore"; -import {InboundGroupSessionStore, InboundGroupSessionEntry, BackupStatus} from "./stores/InboundGroupSessionStore"; +import {InboundGroupSessionStore, InboundGroupSessionEntry, BackupStatus, KeySource} from "./stores/InboundGroupSessionStore"; import {RoomStateEntry} from "./stores/RoomStateStore"; import {SessionStore} from "./stores/SessionStore"; import {Store} from "./Store"; @@ -283,6 +283,11 @@ async function addInboundSessionBackupIndex(db: IDBDatabase, txn: IDBTransaction const inboundGroupSessions = txn.objectStore("inboundGroupSessions"); await iterateCursor(inboundGroupSessions.openCursor(), (value, key, cursor) => { value.backup = BackupStatus.NotBackedUp; + // we'll also have backup keys in here, we can't tell, + // but the worst thing that can happen is that we try + // to backup keys that were already in backup, which + // the server will ignore + value.source = KeySource.DeviceMessage; return NOT_DONE; }); inboundGroupSessions.createIndex("byBackup", "backup", {unique: false}); diff --git a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts index 72ea6e9a..96b127f7 100644 --- a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts +++ b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts @@ -22,6 +22,11 @@ export enum BackupStatus { BackedUp = 1 } +export enum KeySource { + DeviceMessage = 1, + Backup, +} + export interface InboundGroupSessionEntry { roomId: string; senderKey: string; @@ -29,7 +34,8 @@ export interface InboundGroupSessionEntry { session?: string; claimedKeys?: { [algorithm : string] : string }; eventIds?: string[]; - backup: BackupStatus + backup: BackupStatus, + source: KeySource } type InboundGroupSessionStorageEntry = InboundGroupSessionEntry & { key: string }; From a499689bd857ea5cf391eba074b9d4dc8704c512 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 13:07:49 +0100 Subject: [PATCH 23/52] also write room key that we create ourselves with RoomKey infrastructure so all keys are written in one place and the flags are always correct --- src/matrix/e2ee/megolm/Encryption.js | 33 +++++-------------- src/matrix/e2ee/megolm/decryption/RoomKey.ts | 31 +++++++++++++++++ .../idb/stores/InboundGroupSessionStore.ts | 1 + 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/src/matrix/e2ee/megolm/Encryption.js b/src/matrix/e2ee/megolm/Encryption.js index bd2311d7..eb5f68d3 100644 --- a/src/matrix/e2ee/megolm/Encryption.js +++ b/src/matrix/e2ee/megolm/Encryption.js @@ -15,12 +15,14 @@ limitations under the License. */ import {MEGOLM_ALGORITHM} from "../common.js"; +import {OutboundRoomKey} from "./decryption/RoomKey"; export class Encryption { - constructor({pickleKey, olm, account, storage, now, ownDeviceId}) { + constructor({pickleKey, olm, account, keyLoader, storage, now, ownDeviceId}) { this._pickleKey = pickleKey; this._olm = olm; this._account = account; + this._keyLoader = keyLoader; this._storage = storage; this._now = now; this._ownDeviceId = ownDeviceId; @@ -64,7 +66,7 @@ export class Encryption { let roomKeyMessage; try { let sessionEntry = await txn.outboundGroupSessions.get(roomId); - roomKeyMessage = this._readOrCreateSession(session, sessionEntry, roomId, encryptionParams, txn); + roomKeyMessage = await this._readOrCreateSession(session, sessionEntry, roomId, encryptionParams, txn); if (roomKeyMessage) { this._writeSession(this._now(), session, roomId, txn); } @@ -79,7 +81,7 @@ export class Encryption { } } - _readOrCreateSession(session, sessionEntry, roomId, encryptionParams, txn) { + async _readOrCreateSession(session, sessionEntry, roomId, encryptionParams, txn) { if (sessionEntry) { session.unpickle(this._pickleKey, sessionEntry.session); } @@ -91,7 +93,8 @@ export class Encryption { } session.create(); const roomKeyMessage = this._createRoomKeyMessage(session, roomId); - this._storeAsInboundSession(session, roomId, txn); + const roomKey = new OutboundRoomKey(roomId, session, this._account.identityKeys); + await roomKey.write(this._keyLoader, txn); return roomKeyMessage; } } @@ -123,7 +126,7 @@ export class Encryption { let encryptedContent; try { let sessionEntry = await txn.outboundGroupSessions.get(roomId); - roomKeyMessage = this._readOrCreateSession(session, sessionEntry, roomId, encryptionParams, txn); + roomKeyMessage = await this._readOrCreateSession(session, sessionEntry, roomId, encryptionParams, txn); encryptedContent = this._encryptContent(roomId, session, type, content); // update timestamp when a new session is created const createdAt = roomKeyMessage ? this._now() : sessionEntry.createdAt; @@ -190,26 +193,6 @@ export class Encryption { chain_index: session.message_index() } } - - _storeAsInboundSession(outboundSession, roomId, txn) { - const {identityKeys} = this._account; - const claimedKeys = {ed25519: identityKeys.ed25519}; - const session = new this._olm.InboundGroupSession(); - try { - session.create(outboundSession.session_key()); - const sessionEntry = { - roomId, - senderKey: identityKeys.curve25519, - sessionId: session.session_id(), - session: session.pickle(this._pickleKey), - claimedKeys, - }; - txn.inboundGroupSessions.set(sessionEntry); - return sessionEntry; - } finally { - session.free(); - } - } } /** diff --git a/src/matrix/e2ee/megolm/decryption/RoomKey.ts b/src/matrix/e2ee/megolm/decryption/RoomKey.ts index 7b648d92..f4812acc 100644 --- a/src/matrix/e2ee/megolm/decryption/RoomKey.ts +++ b/src/matrix/e2ee/megolm/decryption/RoomKey.ts @@ -155,7 +155,38 @@ class DeviceMessageRoomKey extends IncomingRoomKey { loadInto(session) { session.create(this.serializationKey); } +} +// a room key we send out ourselves, +// here adapted to write it as an incoming key +// as we don't send it to ourself with a to_device msg +export class OutboundRoomKey extends IncomingRoomKey { + private _sessionKey: string; + + constructor( + private readonly _roomId: string, + private readonly outboundSession: Olm.OutboundGroupSession, + private readonly identityKeys: {[algo: string]: string} + ) { + super(); + // this is a new key, so always better than what might be in storage, no need to check + this.isBetter = true; + // cache this, as it is used by key loader to find a matching key and + // this calls into WASM so is not just reading a prop + this._sessionKey = this.outboundSession.session_key(); + } + + get roomId(): string { return this._roomId; } + get senderKey(): string { return this.identityKeys.curve25519; } + get sessionId(): string { return this.outboundSession.session_id(); } + get claimedEd25519Key(): string { return this.identityKeys.ed25519; } + get serializationKey(): string { return this._sessionKey; } + get serializationType(): string { return "create"; } + protected get keySource(): KeySource { return KeySource.Outbound; } + + loadInto(session: Olm.InboundGroupSession) { + session.create(this.serializationKey); + } } class BackupRoomKey extends IncomingRoomKey { diff --git a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts index 96b127f7..f16872a7 100644 --- a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts +++ b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts @@ -25,6 +25,7 @@ export enum BackupStatus { export enum KeySource { DeviceMessage = 1, Backup, + Outbound } export interface InboundGroupSessionEntry { From b30db544a31dab621e771c29b4dfe27975e80727 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 13:11:32 +0100 Subject: [PATCH 24/52] use idb key range to select non-backed up keys --- src/matrix/storage/idb/stores/InboundGroupSessionStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts index f16872a7..a2ccae95 100644 --- a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts +++ b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts @@ -81,7 +81,7 @@ export class InboundGroupSessionStore { } getFirstNonBackedUpSessions(amount: number): Promise { - return this._store.index("byBackup").selectLimit(0, amount); + return this._store.index("byBackup").selectLimit(this._store.IDBKeyRange.only(BackupStatus.NotBackedUp), amount); } async markAsBackedUp(roomId: string, senderKey: string, sessionId: string): Promise { From ebc7f1ecd77a551f2cc24348bcc8253d528c42e1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 13:11:52 +0100 Subject: [PATCH 25/52] needs to be awaited --- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index 49771d15..2de71491 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -88,7 +88,7 @@ export class KeyBackup { throw err; } } - this.markKeysAsBackedUp(keysNeedingBackup, setAbortable); + await this.markKeysAsBackedUp(keysNeedingBackup, setAbortable); amountFinished += keysNeedingBackup.length; setProgress(new Progress(total, amountFinished)); } From b692b3ec4fde9a36cf74a4c9884e8bcffdd31a67 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 13:13:23 +0100 Subject: [PATCH 26/52] move key backup operation and flush bookkeeping inside KeyBackup so we can flush from other places than Session --- src/matrix/Session.js | 27 ++------------ src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 37 ++++++++++++++++--- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index dd7a0e1c..7209e523 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -76,7 +76,6 @@ export class Session { this._getSyncToken = () => this.syncToken; this._olmWorker = olmWorker; this._keyBackup = null; - this._keyBackupOperation = new ObservableValue(null); this._hasSecretStorageKey = new ObservableValue(null); this._observedRoomStatus = new Map(); @@ -135,15 +134,16 @@ export class Session { olmUtil: this._olmUtil, senderKeyLock }); + this._keyLoader = new MegOlmKeyLoader(this._olm, PICKLE_KEY, 20); this._megolmEncryption = new MegOlmEncryption({ account: this._e2eeAccount, pickleKey: PICKLE_KEY, olm: this._olm, storage: this._storage, + keyLoader: this._keyLoader, now: this._platform.clock.now, ownDeviceId: this._sessionInfo.deviceId, }); - this._keyLoader = new MegOlmKeyLoader(this._olm, PICKLE_KEY, 20); this._megolmDecryption = new MegOlmDecryption(this._keyLoader, this._olmWorker); this._deviceMessageHandler.enableEncryption({olmDecryption, megolmDecryption: this._megolmDecryption}); } @@ -282,10 +282,6 @@ export class Session { return this._keyBackup; } - get keyBackupOperation() { - return this._keyBackupOperation; - } - get hasIdentity() { return !!this._e2eeAccount; } @@ -635,23 +631,8 @@ export class Session { await log.wrap("uploadKeys", log => this._e2eeAccount.uploadKeys(this._storage, false, log)); } } - // should flush and not already flushing - if (changes.hasNewRoomKeys && this._keyBackup && !this._keyBackupOperation.get()) { - log.wrapDetached("flush key backup", async log => { - const operation = this._keyBackup.flush(log); - this._keyBackupOperation.set(operation); - try { - const success = await operation.result; - // stop key backup if the version was changed - if (!success) { - this._keyBackup = this._keyBackup.dispose(); - this.needsKeyBackup.set(true); - } - } catch (err) { - log.catch(err); - } - this._keyBackupOperation.set(null); - }); + if (changes.hasNewRoomKeys) { + this._keyBackup?.flush(log); } } diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index 2de71491..e9a8756d 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -19,6 +19,7 @@ import {StoredRoomKey, keyFromBackup} from "../decryption/RoomKey"; import {MEGOLM_ALGORITHM} from "../../common"; import * as Curve25519 from "./Curve25519"; import {AbortableOperation} from "../../../../utils/AbortableOperation"; +import {ObservableValue} from "../../../../observable/ObservableValue"; import {SetAbortableFn} from "../../../../utils/AbortableOperation"; import type {BackupInfo, SessionData, SessionKeyInfo, SessionInfo, KeyBackupPayload} from "./types"; @@ -33,7 +34,12 @@ import type {Transaction} from "../../../storage/idb/Transaction"; import type * as OlmNamespace from "@matrix-org/olm"; type Olm = typeof OlmNamespace; +const KEYS_PER_REQUEST = 20; + export class KeyBackup { + public readonly operationInProgress = new ObservableValue, Progress> | undefined>(undefined); + public readonly needsNewKey = new ObservableValue(false); + constructor( private readonly backupInfo: BackupInfo, private readonly crypto: Curve25519.BackupEncryption, @@ -56,13 +62,32 @@ export class KeyBackup { } } - // TODO: protect against having multiple concurrent flushes - flush(log: ILogItem): AbortableOperation, Progress> { + flush(log: ILogItem): void { + if (!this.operationInProgress.get()) { + log.wrapDetached("flush key backup", async log => { + const operation = this._flush(log); + this.operationInProgress.set(operation); + try { + const success = await operation.result; + // stop key backup if the version was changed + if (!success) { + this.needsNewKey.set(true); + } + } catch (err) { + log.catch(err); + } + this.operationInProgress.set(undefined); + }); + } + } + + private _flush(log: ILogItem): AbortableOperation, Progress> { return new AbortableOperation(async (setAbortable, setProgress) => { let total = 0; let amountFinished = 0; while (true) { - const timeout = this.platform.clock.createTimeout(this.platform.random() * 10000); + const waitMs = this.platform.random() * 10000; + const timeout = this.platform.clock.createTimeout(waitMs); setAbortable(timeout); await timeout.elapsed(); const txn = await this.storage.readTxn([StoreNames.inboundGroupSessions]); @@ -70,7 +95,7 @@ export class KeyBackup { // fetch total again on each iteration as while we are flushing, sync might be adding keys total = await txn.inboundGroupSessions.countNonBackedUpSessions(); setProgress(new Progress(total, amountFinished)); - const keysNeedingBackup = (await txn.inboundGroupSessions.getFirstNonBackedUpSessions(20)) + const keysNeedingBackup = (await txn.inboundGroupSessions.getFirstNonBackedUpSessions(KEYS_PER_REQUEST)) .map(entry => new StoredRoomKey(entry)); if (keysNeedingBackup.length === 0) { return true; @@ -114,7 +139,9 @@ export class KeyBackup { ]); setAbortable(txn); try { - await Promise.all(roomKeys.map(key => txn.inboundGroupSessions.markAsBackedUp(key.roomId, key.senderKey, key.sessionId))); + await Promise.all(roomKeys.map(key => { + return txn.inboundGroupSessions.markAsBackedUp(key.roomId, key.senderKey, key.sessionId); + })); } catch (err) { txn.abort(); throw err; From c47bdd5715f39305c9305b49cced9a9f561a1f5c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 13:14:11 +0100 Subject: [PATCH 27/52] flush key backup when creating a new room key --- src/matrix/e2ee/RoomEncryption.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index 2e6ee254..80f57507 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -236,6 +236,7 @@ export class RoomEncryption { this._keySharePromise = (async () => { const roomKeyMessage = await this._megolmEncryption.ensureOutboundSession(this._room.id, this._encryptionParams); if (roomKeyMessage) { + this._keyBackup?.flush(log); await log.wrap("share key", log => this._shareNewRoomKey(roomKeyMessage, hsApi, log)); } })(); @@ -254,6 +255,7 @@ export class RoomEncryption { } const megolmResult = await log.wrap("megolm encrypt", () => this._megolmEncryption.encrypt(this._room.id, type, content, this._encryptionParams)); if (megolmResult.roomKeyMessage) { + this._keyBackup?.flush(log); await log.wrap("share key", log => this._shareNewRoomKey(megolmResult.roomKeyMessage, hsApi, log)); } return { From 7d3e3b992b0e15b1ce73585be0d0f6a553b84942 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 13:14:38 +0100 Subject: [PATCH 28/52] some more typing --- src/matrix/e2ee/megolm/decryption/RoomKey.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/e2ee/megolm/decryption/RoomKey.ts b/src/matrix/e2ee/megolm/decryption/RoomKey.ts index f4812acc..b5f75224 100644 --- a/src/matrix/e2ee/megolm/decryption/RoomKey.ts +++ b/src/matrix/e2ee/megolm/decryption/RoomKey.ts @@ -60,7 +60,7 @@ export abstract class IncomingRoomKey extends RoomKey { async write(loader: KeyLoader, txn: Transaction): Promise { // we checked already and we had a better session in storage, so don't write - let pickledSession; + let pickledSession: string | undefined; if (this.isBetter === undefined) { // if this key wasn't used to decrypt any messages in the same sync, // we haven't checked if this is the best key yet, From eb134a6c47df35cc14026bcd304bd2610abcddfc Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 13:18:03 +0100 Subject: [PATCH 29/52] only take into account non-backed up keys for counting --- src/matrix/storage/idb/stores/InboundGroupSessionStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts index a2ccae95..4d3497a4 100644 --- a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts +++ b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts @@ -77,7 +77,7 @@ export class InboundGroupSessionStore { this._store.delete(range); } countNonBackedUpSessions(): Promise { - return this._store.index("byBackup").count(); + return this._store.index("byBackup").count(this._store.IDBKeyRange.only(BackupStatus.NotBackedUp)); } getFirstNonBackedUpSessions(amount: number): Promise { From 504f420293f67e72a4435d1ff314ba6f229493e1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 15:13:58 +0100 Subject: [PATCH 30/52] make keyBackup an observable and don't have separate needs-key flag --- src/matrix/Session.js | 40 +++++++++++++++++----------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 7209e523..ab815f29 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -75,8 +75,7 @@ export class Session { this._megolmDecryption = null; this._getSyncToken = () => this.syncToken; this._olmWorker = olmWorker; - this._keyBackup = null; - this._hasSecretStorageKey = new ObservableValue(null); + this._keyBackup = new ObservableValue(undefined); this._observedRoomStatus = new Map(); if (olm) { @@ -171,10 +170,10 @@ export class Session { megolmEncryption: this._megolmEncryption, megolmDecryption: this._megolmDecryption, storage: this._storage, - keyBackup: this._keyBackup, + keyBackup: this._keyBackup?.get(), encryptionParams, notifyMissingMegolmSession: () => { - if (!this._keyBackup) { + if (!this._keyBackup.get()) { this.needsKeyBackup.set(true) } }, @@ -195,7 +194,7 @@ export class Session { if (!this._olm) { throw new Error("olm required"); } - if (this._keyBackup) { + if (this._keyBackup.get()) { return false; } const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm); @@ -205,7 +204,6 @@ export class Session { ]); if (await this._createKeyBackup(key, readTxn, log)) { await this._writeSSSSKey(key); - this._hasSecretStorageKey.set(true); return key; } }); @@ -237,23 +235,22 @@ export class Session { throw err; } await writeTxn.complete(); - if (this._keyBackup) { + if (this._keyBackup.get()) { for (const room of this._rooms.values()) { if (room.isEncrypted) { room.enableKeyBackup(undefined); } } - this._keyBackup?.dispose(); - this._keyBackup = undefined; + this._keyBackup.get().dispose(); + this._keyBackup.set(undefined); } - this._hasSecretStorageKey.set(false); } _createKeyBackup(ssssKey, txn, log) { return log.wrap("enable key backup", async log => { try { const secretStorage = new SecretStorage({key: ssssKey, platform: this._platform}); - this._keyBackup = await KeyBackup.fromSecretStorage( + const keyBackup = await KeyBackup.fromSecretStorage( this._platform, this._olm, secretStorage, @@ -262,19 +259,18 @@ export class Session { this._storage, txn ); - if (this._keyBackup) { + if (keyBackup) { for (const room of this._rooms.values()) { if (room.isEncrypted) { - room.enableKeyBackup(this._keyBackup); + room.enableKeyBackup(keyBackup); } } + this._keyBackup.set(keyBackup); + return true; } - this.needsKeyBackup.set(false); } catch (err) { log.catch(err); - return false; } - return true; }); } @@ -417,8 +413,8 @@ export class Session { dispose() { this._olmWorker?.dispose(); this._olmWorker = undefined; - this._keyBackup?.dispose(); - this._keyBackup = undefined; + this._keyBackup.get()?.dispose(); + this._keyBackup.set(undefined); this._megolmDecryption?.dispose(); this._megolmDecryption = undefined; this._e2eeAccount?.dispose(); @@ -446,7 +442,7 @@ export class Session { await txn.complete(); } // enable session backup, this requests the latest backup version - if (!this._keyBackup) { + if (!this._keyBackup.get()) { if (dehydratedDevice) { await log.wrap("SSSSKeyFromDehydratedDeviceKey", async log => { const ssssKey = await createSSSSKeyFromDehydratedDeviceKey(dehydratedDevice.key, this._storage, this._platform); @@ -462,12 +458,10 @@ export class Session { ]); // try set up session backup if we stored the ssss key const ssssKey = await ssssReadKey(txn); - let couldReadKeyBackup = false; if (ssssKey) { // txn will end here as this does a network request - couldReadKeyBackup = await this._createKeyBackup(ssssKey, txn, log); + await this._createKeyBackup(ssssKey, txn, log); } - this._hasSecretStorageKey.set(couldReadKeyBackup); } // restore unfinished operations, like sending out room keys const opsTxn = await this._storage.readWriteTxn([ @@ -632,7 +626,7 @@ export class Session { } } if (changes.hasNewRoomKeys) { - this._keyBackup?.flush(log); + this._keyBackup.get()?.flush(log); } } From bd2c70b92326e6444cebfa69d1a09f2e9227ac4e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 15:14:23 +0100 Subject: [PATCH 31/52] adapt key backup view(model) to changes in session, show backup progress --- .../session/settings/KeyBackupViewModel.js | 79 ++++++++++++++++--- .../session/settings/KeyBackupSettingsView.js | 36 ++++++--- 2 files changed, 95 insertions(+), 20 deletions(-) diff --git a/src/domain/session/settings/KeyBackupViewModel.js b/src/domain/session/settings/KeyBackupViewModel.js index 38d37c18..231fb4fe 100644 --- a/src/domain/session/settings/KeyBackupViewModel.js +++ b/src/domain/session/settings/KeyBackupViewModel.js @@ -18,7 +18,7 @@ import {ViewModel} from "../../ViewModel.js"; import {KeyType} from "../../../matrix/ssss/index"; import {createEnum} from "../../../utils/enum"; -export const Status = createEnum("Enabled", "SetupKey", "SetupPhrase", "Pending"); +export const Status = createEnum("Enabled", "SetupKey", "SetupPhrase", "Pending", "NewVersionAvailable"); export class KeyBackupViewModel extends ViewModel { constructor(options) { @@ -28,8 +28,11 @@ export class KeyBackupViewModel extends ViewModel { this._isBusy = false; this._dehydratedDeviceId = undefined; this._status = undefined; + this._needsNewKeySubscription = undefined; + this._operationSubscription = undefined; + this._operationProgressSubscription = undefined; this._reevaluateStatus(); - this.track(this._session.hasSecretStorageKey.subscribe(() => { + this.track(this._session.keyBackup.subscribe(() => { if (this._reevaluateStatus()) { this.emitChange("status"); } @@ -41,14 +44,31 @@ export class KeyBackupViewModel extends ViewModel { return false; } let status; - const hasSecretStorageKey = this._session.hasSecretStorageKey.get(); - if (hasSecretStorageKey === true) { - status = this._session.keyBackup ? Status.Enabled : Status.SetupKey; - } else if (hasSecretStorageKey === false) { - status = Status.SetupKey; + const keyBackup = this._session.keyBackup.get(); + if (keyBackup) { + if (!this._needsNewKeySubscription) { + this._needsNewKeySubscription = this.track(keyBackup.needsNewKey.subscribe(() => this._reevaluateStatus())); + } + if (!this._operationSubscription) { + this._operationSubscription = this.track(keyBackup.operationInProgress.subscribe(op => { + if (op && !this._operationProgressSubscription) { + this._operationProgressSubscription = this.track(op.progress.subscribe(() => this.emitChange("backupPercentage"))); + } else if (!op && this._operationProgressSubscription) { + this._operationProgressSubscription = this.disposeTracked(this._operationProgressSubscription); + } + this.emitChange("isBackingUp"); + })); + } + status = keyBackup.needsNewKey.get() ? Status.NewVersionAvailable : Status.Enabled; } else { + this._needsNewKeySubscription = this.disposeTracked(this._needsNewKeySubscription); + this._operationSubscription = this.disposeTracked(this._operationSubscription); + this._operationProgressSubscription = this.disposeTracked(this._operationProgressSubscription); + status = this.showPhraseSetup() ? Status.SetupPhrase : Status.SetupKey; + } /* TODO: bring back "waiting to get online" + else { status = Status.Pending; - } + } */ const changed = status !== this._status; this._status = status; return changed; @@ -75,7 +95,7 @@ export class KeyBackupViewModel extends ViewModel { } get backupVersion() { - return this._session.keyBackup?.version; + return this._session.keyBackup.get()?.version; } get status() { @@ -144,4 +164,45 @@ export class KeyBackupViewModel extends ViewModel { this.emitChange(""); } } + + get isBackingUp() { + const keyBackup = this._session.keyBackup.get(); + if (keyBackup) { + return !!keyBackup.operationInProgress.get(); + } + return undefined; + } + + get backupPercentage() { + const keyBackup = this._session.keyBackup.get(); + if (keyBackup) { + const op = keyBackup.operationInProgress.get(); + const progress = op.progress.get(); + if (progress) { + return Math.round(progress.finished / progress.total) * 100; + } + } + return 0; + } + + get backupInProgressLabel() { + const keyBackup = this._session.keyBackup.get(); + if (keyBackup) { + const op = keyBackup.operationInProgress.get(); + if (op) { + const progress = op.progress.get(); + if (progress) { + return this.i18n`${progress.finished} of ${progress.total}`; + } else { + return this.i18n`…`; + } + } + } + return undefined; + } + + cancelBackup() { + this._session.keyBackup.get()?.operationInProgress.get()?.abort(); + } } + diff --git a/src/platform/web/ui/session/settings/KeyBackupSettingsView.js b/src/platform/web/ui/session/settings/KeyBackupSettingsView.js index bf3fd296..0d5d27e0 100644 --- a/src/platform/web/ui/session/settings/KeyBackupSettingsView.js +++ b/src/platform/web/ui/session/settings/KeyBackupSettingsView.js @@ -14,19 +14,33 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {TemplateView, InlineTemplateView} from "../../general/TemplateView"; -import {StaticView} from "../../general/StaticView.js"; +import {TemplateView} from "../../general/TemplateView"; export class KeyBackupSettingsView extends TemplateView { - render(t, vm) { - return t.mapView(vm => vm.status, status => { - switch (status) { - case "Enabled": return new InlineTemplateView(vm, renderEnabled) - case "SetupKey": return new InlineTemplateView(vm, renderEnableFromKey) - case "SetupPhrase": return new InlineTemplateView(vm, renderEnableFromPhrase) - case "Pending": return new StaticView(vm, t => t.p(vm.i18n`Waiting to go online…`)) - } - }); + render(t) { + return t.div([ + t.map(vm => vm.status, (status, t, vm) => { + switch (status) { + case "Enabled": return renderEnabled(t, vm); + case "SetupKey": return renderEnableFromKey(t, vm); + case "SetupPhrase": return renderEnableFromPhrase(t, vm); + case "NewVersionAvailable": return t.p(vm.i18n`A new backup version has been created. Disable key backup and enable it again with the new key.`); + case "Pending": return t.p(vm.i18n`Waiting to go online…`); + } + }), + t.map(vm => vm.isBackingUp, (backingUp, t, vm) => { + if (backingUp) { + const progress = t.progress({ + min: 0, + max: 100, + value: vm => vm.backupPercentage, + }); + return t.div([`Backup in progress `, progress, " ", vm => vm.backupInProgressLabel]); + } else { + return t.p("All keys are backed up."); + } + }) + ]); } } From eabd303c8e9fb06d1f14e80a84bfc4d0439dcbd2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 15:14:58 +0100 Subject: [PATCH 32/52] count on the index if we're using one, don't always take the store --- src/matrix/storage/idb/Store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/storage/idb/Store.ts b/src/matrix/storage/idb/Store.ts index 20e6f79a..c9df33b2 100644 --- a/src/matrix/storage/idb/Store.ts +++ b/src/matrix/storage/idb/Store.ts @@ -120,7 +120,7 @@ export class QueryTargetWrapper { count(keyRange?: IDBKeyRange): IDBRequest { try { - return this._qtStore.count(keyRange); + return this._qt.count(keyRange); } catch(err) { throw new IDBRequestAttemptError("count", this._qt, err, [keyRange]); } From c340746a87987c5b631949f792fc08e21de85c18 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 16:04:56 +0100 Subject: [PATCH 33/52] also remove text nodes when updating message body fixes #649 --- src/platform/web/ui/session/room/timeline/TextMessageView.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js index 510a676f..c0c0cfb0 100644 --- a/src/platform/web/ui/session/room/timeline/TextMessageView.js +++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js @@ -42,7 +42,8 @@ export class TextMessageView extends BaseMessageView { } })); - const shouldRemove = (element) => element?.nodeType === Node.ELEMENT_NODE && element.className !== "ReplyPreviewView"; + // exclude comment nodes as they are used by t.map and friends for placeholders + const shouldRemove = (element) => element?.nodeType !== Node.COMMENT_NODE && element.className !== "ReplyPreviewView"; t.mapSideEffect(vm => vm.body, body => { while (shouldRemove(container.lastChild)) { From e0df003aba75f13eb1150ecebefe0eda57e0aaf0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 16:35:49 +0100 Subject: [PATCH 34/52] add flatMap operator on observable value --- src/observable/ObservableValue.ts | 89 +++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/src/observable/ObservableValue.ts b/src/observable/ObservableValue.ts index b3ffa6ee..15d931ff 100644 --- a/src/observable/ObservableValue.ts +++ b/src/observable/ObservableValue.ts @@ -16,6 +16,7 @@ limitations under the License. import {AbortError} from "../utils/error"; import {BaseObservable} from "./BaseObservable"; +import type {SubscriptionHandle} from "./BaseObservable"; // like an EventEmitter, but doesn't have an event type export abstract class BaseObservableValue extends BaseObservable<(value: T) => void> { @@ -34,6 +35,10 @@ export abstract class BaseObservableValue extends BaseObservable<(value: T) = return new WaitForHandle(this, predicate); } } + + flatMap(mapper: (value: T) => (BaseObservableValue | undefined)): BaseObservableValue { + return new FlatMapObservableValue(this, mapper); + } } interface IWaitHandle { @@ -114,6 +119,61 @@ export class RetainedObservableValue extends ObservableValue { } } +export class FlatMapObservableValue extends BaseObservableValue { + private sourceSubscription?: SubscriptionHandle; + private targetSubscription?: SubscriptionHandle; + + constructor( + private readonly source: BaseObservableValue

, + private readonly mapper: (value: P) => (BaseObservableValue | undefined) + ) { + super(); + } + + onUnsubscribeLast() { + super.onUnsubscribeLast(); + this.sourceSubscription = this.sourceSubscription!(); + if (this.targetSubscription) { + this.targetSubscription = this.targetSubscription(); + } + } + + onSubscribeFirst() { + super.onSubscribeFirst(); + this.sourceSubscription = this.source.subscribe(() => { + this.updateTargetSubscription(); + this.emit(this.get()); + }); + this.updateTargetSubscription(); + } + + private updateTargetSubscription() { + const sourceValue = this.source.get(); + if (sourceValue) { + const target = this.mapper(sourceValue); + if (target) { + if (!this.targetSubscription) { + this.targetSubscription = target.subscribe(() => this.emit(this.get())); + } + return; + } + } + // if no sourceValue or target + if (this.targetSubscription) { + this.targetSubscription = this.targetSubscription(); + } + } + + get(): C | undefined { + const sourceValue = this.source.get(); + if (!sourceValue) { + return undefined; + } + const mapped = this.mapper(sourceValue); + return mapped?.get(); + } +} + export function tests() { return { "set emits an update": assert => { @@ -155,5 +215,34 @@ export function tests() { }); await assert.rejects(handle.promise, AbortError); }, + "flatMap.get": assert => { + const a = new ObservableValue}>(undefined); + const countProxy = a.flatMap(a => a.count); + assert.strictEqual(countProxy.get(), undefined); + const count = new ObservableValue(0); + a.set({count}); + assert.strictEqual(countProxy.get(), 0); + }, + "flatMap update from source": assert => { + const a = new ObservableValue}>(undefined); + const updates: (number | undefined)[] = []; + a.flatMap(a => a.count).subscribe(count => { + updates.push(count); + }); + const count = new ObservableValue(0); + a.set({count}); + assert.deepEqual(updates, [0]); + }, + "flatMap update from target": assert => { + const a = new ObservableValue}>(undefined); + const updates: (number | undefined)[] = []; + a.flatMap(a => a.count).subscribe(count => { + updates.push(count); + }); + const count = new ObservableValue(0); + a.set({count}); + count.set(5); + assert.deepEqual(updates, [0, 5]); + } } } From c9b5ce6508c5d664e425abf5a6665dda72caaafd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 16:36:13 +0100 Subject: [PATCH 35/52] clean up key backup vm using flatMap to avoid subscription handling --- .../session/settings/KeyBackupViewModel.js | 55 ++++--------------- 1 file changed, 12 insertions(+), 43 deletions(-) diff --git a/src/domain/session/settings/KeyBackupViewModel.js b/src/domain/session/settings/KeyBackupViewModel.js index 231fb4fe..d88cd797 100644 --- a/src/domain/session/settings/KeyBackupViewModel.js +++ b/src/domain/session/settings/KeyBackupViewModel.js @@ -28,9 +28,10 @@ export class KeyBackupViewModel extends ViewModel { this._isBusy = false; this._dehydratedDeviceId = undefined; this._status = undefined; - this._needsNewKeySubscription = undefined; - this._operationSubscription = undefined; - this._operationProgressSubscription = undefined; + this._backupOperation = this._session.keyBackup.flatMap(keyBackup => keyBackup.operationInProgress); + this._progress = this._backupOperation.flatMap(op => op.progress); + this.track(this._backupOperation.subscribe(() => this.emitChange("isBackingUp"))); + this.track(this._progress.subscribe(() => this.emitChange("backupPercentage"))); this._reevaluateStatus(); this.track(this._session.keyBackup.subscribe(() => { if (this._reevaluateStatus()) { @@ -46,24 +47,8 @@ export class KeyBackupViewModel extends ViewModel { let status; const keyBackup = this._session.keyBackup.get(); if (keyBackup) { - if (!this._needsNewKeySubscription) { - this._needsNewKeySubscription = this.track(keyBackup.needsNewKey.subscribe(() => this._reevaluateStatus())); - } - if (!this._operationSubscription) { - this._operationSubscription = this.track(keyBackup.operationInProgress.subscribe(op => { - if (op && !this._operationProgressSubscription) { - this._operationProgressSubscription = this.track(op.progress.subscribe(() => this.emitChange("backupPercentage"))); - } else if (!op && this._operationProgressSubscription) { - this._operationProgressSubscription = this.disposeTracked(this._operationProgressSubscription); - } - this.emitChange("isBackingUp"); - })); - } status = keyBackup.needsNewKey.get() ? Status.NewVersionAvailable : Status.Enabled; } else { - this._needsNewKeySubscription = this.disposeTracked(this._needsNewKeySubscription); - this._operationSubscription = this.disposeTracked(this._operationSubscription); - this._operationProgressSubscription = this.disposeTracked(this._operationProgressSubscription); status = this.showPhraseSetup() ? Status.SetupPhrase : Status.SetupKey; } /* TODO: bring back "waiting to get online" else { @@ -166,39 +151,23 @@ export class KeyBackupViewModel extends ViewModel { } get isBackingUp() { - const keyBackup = this._session.keyBackup.get(); - if (keyBackup) { - return !!keyBackup.operationInProgress.get(); - } - return undefined; + return !!this._backupOperation.get(); } get backupPercentage() { - const keyBackup = this._session.keyBackup.get(); - if (keyBackup) { - const op = keyBackup.operationInProgress.get(); - const progress = op.progress.get(); - if (progress) { - return Math.round(progress.finished / progress.total) * 100; - } + const progress = this._progress.get(); + if (progress) { + return Math.round(progress.finished / progress.total) * 100; } return 0; } get backupInProgressLabel() { - const keyBackup = this._session.keyBackup.get(); - if (keyBackup) { - const op = keyBackup.operationInProgress.get(); - if (op) { - const progress = op.progress.get(); - if (progress) { - return this.i18n`${progress.finished} of ${progress.total}`; - } else { - return this.i18n`…`; - } - } + const progress = this._progress.get(); + if (progress) { + return this.i18n`${progress.finished} of ${progress.total}`; } - return undefined; + return this.i18n`…`; } cancelBackup() { From a97d235cf56092f819e1a08b307dbac07f4d62b1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 16:36:42 +0100 Subject: [PATCH 36/52] flush after enabling key backup --- src/matrix/Session.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index ab815f29..3d1530c9 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -204,6 +204,7 @@ export class Session { ]); if (await this._createKeyBackup(key, readTxn, log)) { await this._writeSSSSKey(key); + this._keyBackup.get().flush(log); return key; } }); From d9e6164a5cd06542e07a76c2a6961a0eba95600c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 16:40:32 +0100 Subject: [PATCH 37/52] fix ts errors --- src/observable/ObservableValue.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/observable/ObservableValue.ts b/src/observable/ObservableValue.ts index 15d931ff..ad0a226d 100644 --- a/src/observable/ObservableValue.ts +++ b/src/observable/ObservableValue.ts @@ -217,7 +217,7 @@ export function tests() { }, "flatMap.get": assert => { const a = new ObservableValue}>(undefined); - const countProxy = a.flatMap(a => a.count); + const countProxy = a.flatMap(a => a!.count); assert.strictEqual(countProxy.get(), undefined); const count = new ObservableValue(0); a.set({count}); @@ -226,7 +226,7 @@ export function tests() { "flatMap update from source": assert => { const a = new ObservableValue}>(undefined); const updates: (number | undefined)[] = []; - a.flatMap(a => a.count).subscribe(count => { + a.flatMap(a => a!.count).subscribe(count => { updates.push(count); }); const count = new ObservableValue(0); @@ -236,7 +236,7 @@ export function tests() { "flatMap update from target": assert => { const a = new ObservableValue}>(undefined); const updates: (number | undefined)[] = []; - a.flatMap(a => a.count).subscribe(count => { + a.flatMap(a => a!.count).subscribe(count => { updates.push(count); }); const count = new ObservableValue(0); From ae5cc172907a6b6e39f7e4b9267521e2bec6c2e9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 31 Jan 2022 14:36:04 +0100 Subject: [PATCH 38/52] mark all inbound sessions to be backed up again when changing version --- src/matrix/Session.js | 21 ++++++++++++++----- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 4 ++++ src/matrix/ssss/index.ts | 6 +++++- .../idb/stores/InboundGroupSessionStore.ts | 12 +++++++++++ 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 3d1530c9..2a1618a2 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -203,21 +203,32 @@ export class Session { this._storage.storeNames.accountData, ]); if (await this._createKeyBackup(key, readTxn, log)) { - await this._writeSSSSKey(key); + // only after having read a secret, write the key + // as we only find out if it was good if the MAC verification succeeds + await this._writeSSSSKey(key, log); this._keyBackup.get().flush(log); return key; } }); } - async _writeSSSSKey(key) { - // only after having read a secret, write the key - // as we only find out if it was good if the MAC verification succeeds + async _writeSSSSKey(key, log) { + // we're going to write the 4S key, and also the backup version. + // this way, we can detect when we enter a key for a new backup version + // and mark all inbound sessions to be backed up again + const backupVersion = this._keyBackup.get()?.version; const writeTxn = await this._storage.readWriteTxn([ this._storage.storeNames.session, + this._storage.storeNames.inboundGroupSessions, ]); try { - ssssWriteKey(key, writeTxn); + const previousBackupVersion = await ssssWriteKey(key, backupVersion, writeTxn); + log.set("previousBackupVersion", previousBackupVersion); + log.set("backupVersion", backupVersion); + if (typeof previousBackupVersion === "number" && previousBackupVersion !== backupVersion) { + const amountMarked = await this._keyBackup.markAllForBackup(writeTxn); + log.set("amountMarkedForBackup", amountMarked); + } } catch (err) { writeTxn.abort(); throw err; diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index e9a8756d..8f9611dd 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -62,6 +62,10 @@ export class KeyBackup { } } + markAllForBackup(txn: Transaction): Promise { + return txn.inboundGroupSessions.markAllAsNotBackedUp(); + } + flush(log: ILogItem): void { if (!this.operationInProgress.get()) { log.wrapDetached("flush key backup", async log => { diff --git a/src/matrix/ssss/index.ts b/src/matrix/ssss/index.ts index 37f47963..fd4c2245 100644 --- a/src/matrix/ssss/index.ts +++ b/src/matrix/ssss/index.ts @@ -27,6 +27,7 @@ import type * as OlmNamespace from "@matrix-org/olm" type Olm = typeof OlmNamespace; const SSSS_KEY = `${SESSION_E2EE_KEY_PREFIX}ssssKey`; +const BACKUPVERSION_KEY = `${SESSION_E2EE_KEY_PREFIX}keyBackupVersion`; export enum KeyType { "RecoveryKey", @@ -49,8 +50,11 @@ async function readDefaultKeyDescription(storage: Storage): Promise { +export async function writeKey(key: Key, keyBackupVersion: number, txn: Transaction): Promise { + const existingVersion: number | undefined = await txn.session.get(BACKUPVERSION_KEY); + txn.session.set(BACKUPVERSION_KEY, keyBackupVersion); txn.session.set(SSSS_KEY, {id: key.id, binaryKey: key.binaryKey}); + return existingVersion; } export async function readKey(txn: Transaction): Promise { diff --git a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts index 4d3497a4..b78c817e 100644 --- a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts +++ b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts @@ -91,4 +91,16 @@ export class InboundGroupSessionStore { this._store.put(entry); } } + + async markAllAsNotBackedUp(): Promise { + const backedUpKey = this._store.IDBKeyRange.only(BackupStatus.BackedUp); + let count = 0; + await this._store.index("byBackup").iterateValues(backedUpKey, (val: InboundGroupSessionEntry, key: IDBValidKey, cur: IDBCursorWithValue) => { + val.backup = BackupStatus.NotBackedUp; + cur.update(val); + count += 1; + return false; + }); + return count; + } } From 7eb0d347f5b000736d4c3b18b9e16c760d0d4146 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 31 Jan 2022 14:36:35 +0100 Subject: [PATCH 39/52] flush key backup after coming online --- src/matrix/Session.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 2a1618a2..a5270476 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -473,6 +473,7 @@ export class Session { if (ssssKey) { // txn will end here as this does a network request await this._createKeyBackup(ssssKey, txn, log); + this._keyBackup.get()?.flush(); } } // restore unfinished operations, like sending out room keys From a757fb3696029f011a016cb613e70c0a56bfa3f4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 31 Jan 2022 14:37:05 +0100 Subject: [PATCH 40/52] better error handling in key backup, cleanup and not overuse observables --- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 50 +++++++++++-------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index 8f9611dd..c693506d 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -37,8 +37,11 @@ type Olm = typeof OlmNamespace; const KEYS_PER_REQUEST = 20; export class KeyBackup { - public readonly operationInProgress = new ObservableValue, Progress> | undefined>(undefined); - public readonly needsNewKey = new ObservableValue(false); + public readonly operationInProgress = new ObservableValue, Progress> | undefined>(undefined); + + private _cancelled = false; + private _needsNewKey = false; + private _error?: Error; constructor( private readonly backupInfo: BackupInfo, @@ -49,6 +52,10 @@ export class KeyBackup { private readonly platform: Platform, ) {} + get cancelled(): boolean { return this._cancelled; } + get needsNewKey(): boolean { return this._needsNewKey; } + get error(): Error | undefined { return this._error; } + async getRoomKey(roomId: string, sessionId: string, log: ILogItem): Promise { const sessionResponse = await this.hsApi.roomKeyForRoomAndSession(this.backupInfo.version, roomId, sessionId, {log}).response(); if (!sessionResponse.session_data) { @@ -69,15 +76,27 @@ export class KeyBackup { flush(log: ILogItem): void { if (!this.operationInProgress.get()) { log.wrapDetached("flush key backup", async log => { - const operation = this._flush(log); + if (this._needsNewKey) { + log.set("needsNewKey", this._needsNewKey); + return; + } + this._cancelled = false; + this._error = undefined; + const operation = this._runFlushOperation(log); this.operationInProgress.set(operation); try { - const success = await operation.result; - // stop key backup if the version was changed - if (!success) { - this.needsNewKey.set(true); - } + await operation.result; } catch (err) { + if (err.name === "HomeServerError" && err.errcode === "M_WRONG_ROOM_KEYS_VERSION") { + log.set("wrong_version", true); + this._needsNewKey = true; + } else { + this._cancelled = true; + // TODO should really also use AbortError in storage + if (err.name !== "AbortError" || (err.name === "StorageError" && err.errcode === "AbortError")) { + this._error = err; + } + } log.catch(err); } this.operationInProgress.set(undefined); @@ -85,7 +104,7 @@ export class KeyBackup { } } - private _flush(log: ILogItem): AbortableOperation, Progress> { + private _runFlushOperation(log: ILogItem): AbortableOperation, Progress> { return new AbortableOperation(async (setAbortable, setProgress) => { let total = 0; let amountFinished = 0; @@ -102,21 +121,12 @@ export class KeyBackup { const keysNeedingBackup = (await txn.inboundGroupSessions.getFirstNonBackedUpSessions(KEYS_PER_REQUEST)) .map(entry => new StoredRoomKey(entry)); if (keysNeedingBackup.length === 0) { - return true; + return; } const payload = await this.encodeKeysForBackup(keysNeedingBackup); const uploadRequest = this.hsApi.uploadRoomKeysToBackup(this.backupInfo.version, payload, {log}); setAbortable(uploadRequest); - try { - await uploadRequest.response(); - } catch (err) { - if (err.name === "HomeServerError" && err.errcode === "M_WRONG_ROOM_KEYS_VERSION") { - log.set("wrong_version", true); - return false; - } else { - throw err; - } - } + await uploadRequest.response(); await this.markKeysAsBackedUp(keysNeedingBackup, setAbortable); amountFinished += keysNeedingBackup.length; setProgress(new Progress(total, amountFinished)); From dacaa86386e4ac64321deec567b09a34f4feeaa9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 31 Jan 2022 16:22:22 +0100 Subject: [PATCH 41/52] fix percentage calculation --- src/domain/session/settings/KeyBackupViewModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/settings/KeyBackupViewModel.js b/src/domain/session/settings/KeyBackupViewModel.js index d88cd797..391aa09e 100644 --- a/src/domain/session/settings/KeyBackupViewModel.js +++ b/src/domain/session/settings/KeyBackupViewModel.js @@ -157,7 +157,7 @@ export class KeyBackupViewModel extends ViewModel { get backupPercentage() { const progress = this._progress.get(); if (progress) { - return Math.round(progress.finished / progress.total) * 100; + return Math.round((progress.finished / progress.total) * 100); } return 0; } From 6541aacf9877a63d37d3ffa3528998e0a27f9200 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 31 Jan 2022 16:23:48 +0100 Subject: [PATCH 42/52] don't discount already finished keys in total for previous iterations --- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index c693506d..de4a978b 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -116,7 +116,7 @@ export class KeyBackup { const txn = await this.storage.readTxn([StoreNames.inboundGroupSessions]); setAbortable(txn); // fetch total again on each iteration as while we are flushing, sync might be adding keys - total = await txn.inboundGroupSessions.countNonBackedUpSessions(); + total = amountFinished + await txn.inboundGroupSessions.countNonBackedUpSessions(); setProgress(new Progress(total, amountFinished)); const keysNeedingBackup = (await txn.inboundGroupSessions.getFirstNonBackedUpSessions(KEYS_PER_REQUEST)) .map(entry => new StoredRoomKey(entry)); From 06a1421e9769e94406f284d90765624cbffc987d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 31 Jan 2022 16:25:08 +0100 Subject: [PATCH 43/52] add backupWriteStatus so binding can take multiple fields into account --- .../session/settings/KeyBackupViewModel.js | 38 +++++++++++++-- .../session/settings/KeyBackupSettingsView.js | 47 ++++++++++++++----- 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/src/domain/session/settings/KeyBackupViewModel.js b/src/domain/session/settings/KeyBackupViewModel.js index 391aa09e..133139ed 100644 --- a/src/domain/session/settings/KeyBackupViewModel.js +++ b/src/domain/session/settings/KeyBackupViewModel.js @@ -18,7 +18,8 @@ import {ViewModel} from "../../ViewModel.js"; import {KeyType} from "../../../matrix/ssss/index"; import {createEnum} from "../../../utils/enum"; -export const Status = createEnum("Enabled", "SetupKey", "SetupPhrase", "Pending", "NewVersionAvailable"); +export const Status = createEnum("Enabled", "SetupKey", "SetupPhrase", "Pending", "NewVersionAvailable", "Error", "Cancelled"); +export const BackupWriteStatus = createEnum("Writing", "Stopped", "Done", "Pending"); export class KeyBackupViewModel extends ViewModel { constructor(options) { @@ -30,7 +31,11 @@ export class KeyBackupViewModel extends ViewModel { this._status = undefined; this._backupOperation = this._session.keyBackup.flatMap(keyBackup => keyBackup.operationInProgress); this._progress = this._backupOperation.flatMap(op => op.progress); - this.track(this._backupOperation.subscribe(() => this.emitChange("isBackingUp"))); + this.track(this._backupOperation.subscribe(() => { + // see if needsNewKey might be set + this._reevaluateStatus(); + this.emitChange("isBackingUp"); + })); this.track(this._progress.subscribe(() => this.emitChange("backupPercentage"))); this._reevaluateStatus(); this.track(this._session.keyBackup.subscribe(() => { @@ -47,7 +52,7 @@ export class KeyBackupViewModel extends ViewModel { let status; const keyBackup = this._session.keyBackup.get(); if (keyBackup) { - status = keyBackup.needsNewKey.get() ? Status.NewVersionAvailable : Status.Enabled; + status = keyBackup.needsNewKey ? Status.NewVersionAvailable : Status.Enabled; } else { status = this.showPhraseSetup() ? Status.SetupPhrase : Status.SetupKey; } /* TODO: bring back "waiting to get online" @@ -83,6 +88,27 @@ export class KeyBackupViewModel extends ViewModel { return this._session.keyBackup.get()?.version; } + get backupWriteStatus() { + const keyBackup = this._session.keyBackup.get(); + if (!keyBackup) { + return BackupWriteStatus.Pending; + } else if (keyBackup.hasStopped) { + return BackupWriteStatus.Stopped; + } + const operation = keyBackup.operationInProgress.get(); + if (operation) { + return BackupWriteStatus.Writing; + } else if (keyBackup.hasBackedUpAllKeys) { + return BackupWriteStatus.Done; + } else { + return BackupWriteStatus.Pending; + } + } + + get backupError() { + return this._session.keyBackup.get()?.error?.message; + } + get status() { return this._status; } @@ -171,7 +197,11 @@ export class KeyBackupViewModel extends ViewModel { } cancelBackup() { - this._session.keyBackup.get()?.operationInProgress.get()?.abort(); + this._backupOperation.get()?.abort(); + } + + startBackup() { + this._session.keyBackup.get()?.flush(); } } diff --git a/src/platform/web/ui/session/settings/KeyBackupSettingsView.js b/src/platform/web/ui/session/settings/KeyBackupSettingsView.js index 0d5d27e0..3f8812c9 100644 --- a/src/platform/web/ui/session/settings/KeyBackupSettingsView.js +++ b/src/platform/web/ui/session/settings/KeyBackupSettingsView.js @@ -22,22 +22,36 @@ export class KeyBackupSettingsView extends TemplateView { t.map(vm => vm.status, (status, t, vm) => { switch (status) { case "Enabled": return renderEnabled(t, vm); + case "NewVersionAvailable": return renderNewVersionAvailable(t, vm); case "SetupKey": return renderEnableFromKey(t, vm); case "SetupPhrase": return renderEnableFromPhrase(t, vm); - case "NewVersionAvailable": return t.p(vm.i18n`A new backup version has been created. Disable key backup and enable it again with the new key.`); case "Pending": return t.p(vm.i18n`Waiting to go online…`); } }), - t.map(vm => vm.isBackingUp, (backingUp, t, vm) => { - if (backingUp) { - const progress = t.progress({ - min: 0, - max: 100, - value: vm => vm.backupPercentage, - }); - return t.div([`Backup in progress `, progress, " ", vm => vm.backupInProgressLabel]); - } else { - return t.p("All keys are backed up."); + t.map(vm => vm.backupWriteStatus, (status, t, vm) => { + switch (status) { + case "Writing": { + const progress = t.progress({ + min: 0, + max: 100, + value: vm => vm.backupPercentage, + }); + return t.div([`Backup in progress `, progress, " ", vm => vm.backupInProgressLabel]); + } + case "Stopped": { + let label; + const error = vm.backupError; + if (error) { + label = `Backup has stopped because of an error: ${vm.backupError}`; + } else { + label = `Backup has stopped`; + } + return t.p(label, " ", t.button({onClick: () => vm.startBackup()}, `Backup now`)); + } + case "Done": + return t.p(`All keys are backed up.`); + default: + return null; } }) ]); @@ -46,7 +60,7 @@ export class KeyBackupSettingsView extends TemplateView { function renderEnabled(t, vm) { const items = [ - t.p([vm.i18n`Session backup is enabled, using backup version ${vm.backupVersion}. `, t.button({onClick: () => vm.disable()}, vm.i18n`Disable`)]) + t.p([vm.i18n`Key backup is enabled, using backup version ${vm.backupVersion}. `, t.button({onClick: () => vm.disable()}, vm.i18n`Disable`)]) ]; if (vm.dehydratedDeviceId) { items.push(t.p(vm.i18n`A dehydrated device id was set up with id ${vm.dehydratedDeviceId} which you can use during your next login with your secret storage key.`)); @@ -54,6 +68,13 @@ function renderEnabled(t, vm) { return t.div(items); } +function renderNewVersionAvailable(t, vm) { + const items = [ + t.p([vm.i18n`A new backup version has been created from another device. Disable key backup and enable it again with the new key.`, t.button({onClick: () => vm.disable()}, vm.i18n`Disable`)]) + ]; + return t.div(items); +} + function renderEnableFromKey(t, vm) { const useASecurityPhrase = t.button({className: "link", onClick: () => vm.showPhraseSetup()}, vm.i18n`use a security phrase`); return t.div([ @@ -101,7 +122,7 @@ function renderEnableFieldRow(t, vm, label, callback) { function renderError(t) { return t.if(vm => vm.error, (t, vm) => { return t.div([ - t.p({className: "error"}, vm => vm.i18n`Could not enable session backup: ${vm.error}.`), + t.p({className: "error"}, vm => vm.i18n`Could not enable key backup: ${vm.error}.`), t.p(vm.i18n`Try double checking that you did not mix up your security key, security phrase and login password as explained above.`) ]) }); From 830786b2fdb31c17cbd1dbf8d2bbb994f29884ae Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 31 Jan 2022 16:26:14 +0100 Subject: [PATCH 44/52] fixes and cleanup --- src/matrix/Session.js | 14 ++++++++++---- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 19 ++++++++++--------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index a5270476..17c809bd 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -208,6 +208,8 @@ export class Session { await this._writeSSSSKey(key, log); this._keyBackup.get().flush(log); return key; + } else { + throw new Error("Could not read key backup with the given key"); } }); } @@ -216,7 +218,11 @@ export class Session { // we're going to write the 4S key, and also the backup version. // this way, we can detect when we enter a key for a new backup version // and mark all inbound sessions to be backed up again - const backupVersion = this._keyBackup.get()?.version; + const keyBackup = this._keyBackup.get(); + if (!keyBackup) { + return; + } + const backupVersion = keyBackup.version; const writeTxn = await this._storage.readWriteTxn([ this._storage.storeNames.session, this._storage.storeNames.inboundGroupSessions, @@ -225,8 +231,8 @@ export class Session { const previousBackupVersion = await ssssWriteKey(key, backupVersion, writeTxn); log.set("previousBackupVersion", previousBackupVersion); log.set("backupVersion", backupVersion); - if (typeof previousBackupVersion === "number" && previousBackupVersion !== backupVersion) { - const amountMarked = await this._keyBackup.markAllForBackup(writeTxn); + if (!!previousBackupVersion && previousBackupVersion !== backupVersion) { + const amountMarked = await keyBackup.markAllForBackup(writeTxn); log.set("amountMarkedForBackup", amountMarked); } } catch (err) { @@ -473,7 +479,7 @@ export class Session { if (ssssKey) { // txn will end here as this does a network request await this._createKeyBackup(ssssKey, txn, log); - this._keyBackup.get()?.flush(); + this._keyBackup.get()?.flush(log); } } // restore unfinished operations, like sending out room keys diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index de4a978b..e20ccc1a 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -39,8 +39,9 @@ const KEYS_PER_REQUEST = 20; export class KeyBackup { public readonly operationInProgress = new ObservableValue, Progress> | undefined>(undefined); - private _cancelled = false; + private _stopped = false; private _needsNewKey = false; + private _hasBackedUpAllKeys = false; private _error?: Error; constructor( @@ -52,9 +53,11 @@ export class KeyBackup { private readonly platform: Platform, ) {} - get cancelled(): boolean { return this._cancelled; } - get needsNewKey(): boolean { return this._needsNewKey; } + get hasStopped(): boolean { return this._stopped; } get error(): Error | undefined { return this._error; } + get version(): string { return this.backupInfo.version; } + get needsNewKey(): boolean { return this._needsNewKey; } + get hasBackedUpAllKeys(): boolean { return this._hasBackedUpAllKeys; } async getRoomKey(roomId: string, sessionId: string, log: ILogItem): Promise { const sessionResponse = await this.hsApi.roomKeyForRoomAndSession(this.backupInfo.version, roomId, sessionId, {log}).response(); @@ -80,18 +83,20 @@ export class KeyBackup { log.set("needsNewKey", this._needsNewKey); return; } - this._cancelled = false; + this._stopped = false; this._error = undefined; + this._hasBackedUpAllKeys = false; const operation = this._runFlushOperation(log); this.operationInProgress.set(operation); try { await operation.result; + this._hasBackedUpAllKeys = true; } catch (err) { + this._stopped = true; if (err.name === "HomeServerError" && err.errcode === "M_WRONG_ROOM_KEYS_VERSION") { log.set("wrong_version", true); this._needsNewKey = true; } else { - this._cancelled = true; // TODO should really also use AbortError in storage if (err.name !== "AbortError" || (err.name === "StorageError" && err.errcode === "AbortError")) { this._error = err; @@ -176,10 +181,6 @@ export class KeyBackup { }); } - get version(): string { - return this.backupInfo.version; - } - dispose() { this.crypto.dispose(); } From 17275a53903a2592fe0215002f4f22b9dad8b897 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 31 Jan 2022 17:30:15 +0100 Subject: [PATCH 45/52] backup 200 keys per request --- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index e20ccc1a..998fb67f 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -34,7 +34,7 @@ import type {Transaction} from "../../../storage/idb/Transaction"; import type * as OlmNamespace from "@matrix-org/olm"; type Olm = typeof OlmNamespace; -const KEYS_PER_REQUEST = 20; +const KEYS_PER_REQUEST = 200; export class KeyBackup { public readonly operationInProgress = new ObservableValue, Progress> | undefined>(undefined); From 62acd458c6fbf66499ab25ca594975adf3cb1a2c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 31 Jan 2022 17:30:51 +0100 Subject: [PATCH 46/52] also ask for new key if backup version is not found --- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index 998fb67f..e97eb0cf 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -93,7 +93,7 @@ export class KeyBackup { this._hasBackedUpAllKeys = true; } catch (err) { this._stopped = true; - if (err.name === "HomeServerError" && err.errcode === "M_WRONG_ROOM_KEYS_VERSION") { + if (err.name === "HomeServerError" && (err.errcode === "M_WRONG_ROOM_KEYS_VERSION" || err.errcode === "M_NOT_FOUND")) { log.set("wrong_version", true); this._needsNewKey = true; } else { From 9c599d53aa74922fb3821b7ef2ecf6bf601ff1c1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 31 Jan 2022 17:31:01 +0100 Subject: [PATCH 47/52] allow to inject max delay in key backup --- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index e97eb0cf..089d2b0a 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -51,6 +51,7 @@ export class KeyBackup { private readonly keyLoader: KeyLoader, private readonly storage: Storage, private readonly platform: Platform, + private readonly maxDelay: number = 10000 ) {} get hasStopped(): boolean { return this._stopped; } @@ -114,7 +115,7 @@ export class KeyBackup { let total = 0; let amountFinished = 0; while (true) { - const waitMs = this.platform.random() * 10000; + const waitMs = this.platform.random() * this.maxDelay; const timeout = this.platform.clock.createTimeout(waitMs); setAbortable(timeout); await timeout.elapsed(); From 997666164c974843ed9da8fa43a6a3d2a92cafb1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 31 Jan 2022 17:37:44 +0100 Subject: [PATCH 48/52] remove unused enum variants --- src/domain/session/settings/KeyBackupViewModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/settings/KeyBackupViewModel.js b/src/domain/session/settings/KeyBackupViewModel.js index 133139ed..69e87e5d 100644 --- a/src/domain/session/settings/KeyBackupViewModel.js +++ b/src/domain/session/settings/KeyBackupViewModel.js @@ -18,7 +18,7 @@ import {ViewModel} from "../../ViewModel.js"; import {KeyType} from "../../../matrix/ssss/index"; import {createEnum} from "../../../utils/enum"; -export const Status = createEnum("Enabled", "SetupKey", "SetupPhrase", "Pending", "NewVersionAvailable", "Error", "Cancelled"); +export const Status = createEnum("Enabled", "SetupKey", "SetupPhrase", "Pending", "NewVersionAvailable"); export const BackupWriteStatus = createEnum("Writing", "Stopped", "Done", "Pending"); export class KeyBackupViewModel extends ViewModel { From fd4eb6b50da969ed01e73735c5418c0cd11f9fb5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 1 Feb 2022 11:08:13 +0100 Subject: [PATCH 49/52] distinguish between "waiting to go online" vs "backup not configured" --- .../session/settings/KeyBackupViewModel.js | 7 +++---- src/matrix/Session.js | 18 +++++++++++++++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/domain/session/settings/KeyBackupViewModel.js b/src/domain/session/settings/KeyBackupViewModel.js index 69e87e5d..b44de7e5 100644 --- a/src/domain/session/settings/KeyBackupViewModel.js +++ b/src/domain/session/settings/KeyBackupViewModel.js @@ -53,12 +53,11 @@ export class KeyBackupViewModel extends ViewModel { const keyBackup = this._session.keyBackup.get(); if (keyBackup) { status = keyBackup.needsNewKey ? Status.NewVersionAvailable : Status.Enabled; - } else { + } else if (keyBackup === null) { status = this.showPhraseSetup() ? Status.SetupPhrase : Status.SetupKey; - } /* TODO: bring back "waiting to get online" - else { + } else { status = Status.Pending; - } */ + } const changed = status !== this._status; this._status = status; return changed; diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 17c809bd..868a743b 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -289,9 +289,15 @@ export class Session { } catch (err) { log.catch(err); } + return false; }); } + /** + * @type {ObservableValue Date: Tue, 1 Feb 2022 11:26:00 +0100 Subject: [PATCH 50/52] don't block reenabling 4s if already enabled --- src/matrix/Session.js | 3 ++- src/matrix/e2ee/megolm/keybackup/Curve25519.ts | 14 ++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 868a743b..3404a6aa 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -195,7 +195,8 @@ export class Session { throw new Error("olm required"); } if (this._keyBackup.get()) { - return false; + this._keyBackup.get().dispose(); + this._keyBackup.set(undefined); } const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm); // and create key backup, which needs to read from accountData diff --git a/src/matrix/e2ee/megolm/keybackup/Curve25519.ts b/src/matrix/e2ee/megolm/keybackup/Curve25519.ts index 8007d422..7d2ebac7 100644 --- a/src/matrix/e2ee/megolm/keybackup/Curve25519.ts +++ b/src/matrix/e2ee/megolm/keybackup/Curve25519.ts @@ -41,8 +41,8 @@ export type SessionData = { export class BackupEncryption { constructor( - private readonly encryption: Olm.PkEncryption, - private readonly decryption: Olm.PkDecryption + private encryption?: Olm.PkEncryption, + private decryption?: Olm.PkDecryption ) {} static fromAuthData(authData: AuthData, privateKey: Uint8Array, olm: Olm): BackupEncryption { @@ -63,7 +63,7 @@ export class BackupEncryption { } decryptRoomKey(sessionData: SessionData): SessionKeyInfo { - const sessionInfo = this.decryption.decrypt( + const sessionInfo = this.decryption!.decrypt( sessionData.ephemeral, sessionData.mac, sessionData.ciphertext, @@ -79,11 +79,13 @@ export class BackupEncryption { forwarding_curve25519_key_chain: [], session_key: sessionKey }; - return this.encryption.encrypt(JSON.stringify(sessionInfo)) as SessionData; + return this.encryption!.encrypt(JSON.stringify(sessionInfo)) as SessionData; } dispose() { - this.decryption.free(); - this.encryption.free(); + this.decryption?.free(); + this.decryption = undefined; + this.encryption?.free(); + this.encryption = undefined; } } From f4bb420f35fa31e408311fa7fa3b7a0ba61f454b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 1 Feb 2022 11:27:42 +0100 Subject: [PATCH 51/52] mark key backup properly as disabled --- src/matrix/Session.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 3404a6aa..668677b3 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -261,7 +261,7 @@ export class Session { } } this._keyBackup.get().dispose(); - this._keyBackup.set(undefined); + this._keyBackup.set(null); } } From f4fa013ebc7ded486c683fe8431dc2dc1a632079 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 1 Feb 2022 11:32:53 +0100 Subject: [PATCH 52/52] mark as not configured yet when re-enabling key backup --- src/matrix/Session.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 668677b3..a2725bd4 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -196,7 +196,7 @@ export class Session { } if (this._keyBackup.get()) { this._keyBackup.get().dispose(); - this._keyBackup.set(undefined); + this._keyBackup.set(null); } const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm); // and create key backup, which needs to read from accountData