From 933a1b4636c0c05b3b2e38d9ca1a109eedb7dfd0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 25 Jan 2022 18:48:19 +0100 Subject: [PATCH] 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(); + } +}