From 747723c8fb7f4d9b26e31de7c9f4b7be06c6d2e4 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 18 May 2021 18:15:22 -0400 Subject: [PATCH 01/10] factor out backup management to a separate module --- src/client.js | 155 ++++---- src/crypto/algorithms/megolm.js | 21 +- src/crypto/backup.ts | 649 ++++++++++++++++++++++++++++++++ src/crypto/index.js | 438 +++------------------ 4 files changed, 769 insertions(+), 494 deletions(-) create mode 100644 src/crypto/backup.ts diff --git a/src/client.js b/src/client.js index 12bfc6a33..30a8454ed 100644 --- a/src/client.js +++ b/src/client.js @@ -2,7 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2018-2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2021 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. @@ -57,6 +57,7 @@ import {encodeBase64, decodeBase64} from "./crypto/olmlib"; import { User } from "./models/user"; import {AutoDiscovery} from "./autodiscovery"; import {DEHYDRATION_ALGORITHM} from "./crypto/dehydration"; +import { BackupManager } from "./crypto/backup"; const SCROLLBACK_DELAY_MS = 3000; export const CRYPTO_ENABLED = isCryptoAvailable(); @@ -258,6 +259,10 @@ function keyFromRecoverySession(session, decryptionKey) { * } * {string} name the name of the value we want to read out of SSSS, for UI purposes. * + * @param {function} [opts.cryptoCallbacks.getBackupKey] + * Optional. Function called when the backup key is required. The callback function + * should return a promise that resolves to the backup key as a Uint8Array. + * * @param {function} [opts.cryptoCallbacks.cacheSecretStorageKey] * Optional. Function called when a new encryption key for secret storage * has been created. This allows the application a chance to cache this key if @@ -1088,7 +1093,7 @@ MatrixClient.prototype.setDeviceVerified = function(userId, deviceId, verified) // check the key backup status, since whether or not we use this depends on // whether it has a signature from a verified device if (userId == this.credentials.userId) { - this._crypto.checkKeyBackup(); + this.checkKeyBackup(); } return prom; }; @@ -1776,7 +1781,7 @@ MatrixClient.prototype.importRoomKeys = function(keys, opts) { * in trustInfo. */ MatrixClient.prototype.checkKeyBackup = function() { - return this._crypto.checkKeyBackup(); + return this._crypto._backupManager.checkKeyBackup(); }; /** @@ -1818,7 +1823,7 @@ MatrixClient.prototype.getKeyBackupVersion = function() { * } */ MatrixClient.prototype.isKeyBackupTrusted = function(info) { - return this._crypto.isKeyBackupTrusted(info); + return this._crypto._backupManager.isKeyBackupTrusted(info); }; /** @@ -1830,10 +1835,7 @@ MatrixClient.prototype.getKeyBackupEnabled = function() { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } - if (!this._crypto._checkedForBackup) { - return null; - } - return Boolean(this._crypto.backupKey); + return this._crypto._backupManager.getKeyBackupEnabled(); }; /** @@ -1847,16 +1849,7 @@ MatrixClient.prototype.enableKeyBackup = function(info) { throw new Error("End-to-end encryption disabled"); } - this._crypto.backupInfo = info; - if (this._crypto.backupKey) this._crypto.backupKey.free(); - this._crypto.backupKey = new global.Olm.PkEncryption(); - this._crypto.backupKey.set_recipient_key(info.auth_data.public_key); - - this.emit('crypto.keyBackupStatus', true); - - // There may be keys left over from a partially completed backup, so - // schedule a send to check. - this._crypto.scheduleKeyBackupSend(); + return this._crypto.backupManager.enableKeyBackup(); }; /** @@ -1867,11 +1860,7 @@ MatrixClient.prototype.disableKeyBackup = function() { throw new Error("End-to-end encryption disabled"); } - this._crypto.backupInfo = null; - if (this._crypto.backupKey) this._crypto.backupKey.free(); - this._crypto.backupKey = null; - - this.emit('crypto.keyBackupStatus', false); + this._crypto.backupManager.disableKeyBackup(); }; /** @@ -1896,26 +1885,19 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function( throw new Error("End-to-end encryption disabled"); } - const { keyInfo, encodedPrivateKey, privateKey } = - await this.createRecoveryKeyFromPassphrase(password); + // eslint-disable-next-line camelcase + const {algorithm, auth_data, recovery_key, privateKey} = + await this._crypto._backupManager.prepareKeyBackupVersion(password); if (secureSecretStorage) { await this.storeSecret("m.megolm_backup.v1", encodeBase64(privateKey)); logger.info("Key backup private key stored in secret storage"); } - // Reshape objects into form expected for key backup - const authData = { - public_key: keyInfo.pubkey, - }; - if (keyInfo.passphrase) { - authData.private_key_salt = keyInfo.passphrase.salt; - authData.private_key_iterations = keyInfo.passphrase.iterations; - } return { - algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, - auth_data: authData, - recovery_key: encodedPrivateKey, + algorithm, + auth_data, + recovery_key, }; }; @@ -1941,6 +1923,8 @@ MatrixClient.prototype.createKeyBackupVersion = async function(info) { throw new Error("End-to-end encryption disabled"); } + await this._crypto._backupManager.createKeyBackupVersion(info); + const data = { algorithm: info.algorithm, auth_data: info.auth_data, @@ -1986,8 +1970,8 @@ MatrixClient.prototype.deleteKeyBackupVersion = function(version) { // If we're currently backing up to this backup... stop. // (We start using it automatically in createKeyBackupVersion // so this is symmetrical). - if (this._crypto.backupInfo && this._crypto.backupInfo.version === version) { - this.disableKeyBackup(); + if (this._crypto._backupManager.version) { + this._crypto._backupManager.disableKeyBackup(); } const path = utils.encodeUri("/room_keys/version/$version", { @@ -2051,7 +2035,7 @@ MatrixClient.prototype.scheduleAllGroupSessionsForBackup = async function() { throw new Error("End-to-end encryption disabled"); } - await this._crypto.scheduleAllGroupSessionsForBackup(); + await this._crypto._backupManager.scheduleAllGroupSessionsForBackup(); }; /** @@ -2064,7 +2048,7 @@ MatrixClient.prototype.flagAllGroupSessionsForBackup = function() { throw new Error("End-to-end encryption disabled"); } - return this._crypto.flagAllGroupSessionsForBackup(); + return this._crypto._backupManager.flagAllGroupSessionsForBackup(); }; MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) { @@ -2208,7 +2192,7 @@ MatrixClient.prototype.restoreKeyBackupWithCache = async function( ); }; -MatrixClient.prototype._restoreKeyBackup = function( +MatrixClient.prototype._restoreKeyBackup = async function( privKey, targetRoomId, targetSessionId, backupInfo, { cacheCompleteCallback, // For sequencing during tests @@ -2225,46 +2209,41 @@ MatrixClient.prototype._restoreKeyBackup = function( targetRoomId, targetSessionId, backupInfo.version, ); - const decryption = new global.Olm.PkDecryption(); - let backupPubKey; + const algorithm = await BackupManager.makeAlgorithm(backupInfo, async () => { return privKey; }); + try { - backupPubKey = decryption.init_with_private_key(privKey); - } catch (e) { - decryption.free(); - throw e; - } + // If the pubkey computed from the private data we've been given + // doesn't match the one in the auth_data, the user has entered + // a different recovery key / the wrong passphrase. + if (!await algorithm.keyMatches(privKey)) { + return Promise.reject({ errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY }); + } - // If the pubkey computed from the private data we've been given - // doesn't match the one in the auth_data, the user has enetered - // a different recovery key / the wrong passphrase. - if (backupPubKey !== backupInfo.auth_data.public_key) { - return Promise.reject({errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY}); - } + // Cache the key, if possible. + // This is async. + this._crypto.storeSessionBackupPrivateKey(privKey) + .catch((e) => { + logger.warn("Error caching session backup key:", e); + }).then(cacheCompleteCallback); - // Cache the key, if possible. - // This is async. - this._crypto.storeSessionBackupPrivateKey(privKey) - .catch((e) => { - logger.warn("Error caching session backup key:", e); - }).then(cacheCompleteCallback); + if (progressCallback) { + progressCallback({ + stage: "fetch", + }); + } - if (progressCallback) { - progressCallback({ - stage: "fetch", - }); - } + const res = await this._http.authedRequest( + undefined, "GET", path.path, path.queryData, undefined, + { prefix: PREFIX_UNSTABLE }, + ); - return this._http.authedRequest( - undefined, "GET", path.path, path.queryData, undefined, - {prefix: PREFIX_UNSTABLE}, - ).then((res) => { if (res.rooms) { for (const [roomId, roomData] of Object.entries(res.rooms)) { if (!roomData.sessions) continue; totalKeyCount += Object.keys(roomData.sessions).length; - const roomKeys = keysFromRecoverySession( - roomData.sessions, decryption, roomId, + const roomKeys = await algorithm.decryptSessions( + roomData.sessions, ); for (const k of roomKeys) { k.room_id = roomId; @@ -2273,13 +2252,18 @@ MatrixClient.prototype._restoreKeyBackup = function( } } else if (res.sessions) { totalKeyCount = Object.keys(res.sessions).length; - keys = keysFromRecoverySession( - res.sessions, decryption, targetRoomId, keys, + keys = await algorithm.decryptSessions( + res.sessions, ); + for (const k of keys) { + k.room_id = targetRoomId; + } } else { totalKeyCount = 1; try { - const key = keyFromRecoverySession(res, decryption); + const [key] = await algorithm.decryptSessions({ + [targetSessionId]: res, + }); key.room_id = targetRoomId; key.session_id = targetSessionId; keys.push(key); @@ -2287,19 +2271,20 @@ MatrixClient.prototype._restoreKeyBackup = function( logger.log("Failed to decrypt megolm session from backup", e); } } + } finally { + algorithm.free(); + } - return this.importRoomKeys(keys, { - progressCallback, - untrusted: true, - source: "backup", - }); - }).then(() => { - return this._crypto.setTrustedBackupPubKey(backupPubKey); - }).then(() => { - return {total: totalKeyCount, imported: keys.length}; - }).finally(() => { - decryption.free(); + await this.importRoomKeys(keys, { + progressCallback, + untrusted: true, + source: "backup", }); + + // await this._crypto.setTrustedBackupPubKey(backupPubKey); + await this.checkKeyBackup(); + + return {total: totalKeyCount, imported: keys.length}; }; MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, version) { diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index c06b6da16..f5f5bbecf 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -415,9 +415,8 @@ MegolmEncryption.prototype._prepareNewSession = async function(sharedHistory) { ); // don't wait for it to complete - this._crypto.backupGroupSession( - this._roomId, this._olmDevice.deviceCurve25519Key, [], - sessionId, key.key, + this._crypto._backupManager.backupGroupSession( + this._olmDevice.deviceCurve25519Key, sessionId, ); return new OutboundSessionInfo(sessionId, sharedHistory); @@ -1428,11 +1427,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { }); }).then(() => { // don't wait for the keys to be backed up for the server - this._crypto.backupGroupSession( - content.room_id, senderKey, forwardingKeyChain, - content.session_id, content.session_key, keysClaimed, - exportFormat, - ); + this._crypto._backupManager.backupGroupSession(senderKey, content.session_id); }).catch((e) => { logger.error(`Error handling m.room_key_event: ${e}`); }); @@ -1648,14 +1643,8 @@ MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) { ).then(() => { if (opts.source !== "backup") { // don't wait for it to complete - this._crypto.backupGroupSession( - session.room_id, - session.sender_key, - session.forwarding_curve25519_key_chain, - session.session_id, - session.session_key, - session.sender_claimed_keys, - true, + this._crypto._backupManager.backupGroupSession( + session.sender_key, session.session_id, ).catch((e) => { // This throws if the upload failed, but this is fine // since it will have written it to the db and will retry. diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts new file mode 100644 index 000000000..01ecf1fc1 --- /dev/null +++ b/src/crypto/backup.ts @@ -0,0 +1,649 @@ +/* +Copyright 2021 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. +*/ + +/** + * @module crypto/backup + * + * Classes for dealing with key backup. + */ + +import {MatrixClient} from "../client"; +import {logger} from "../logger"; +import {MEGOLM_ALGORITHM, verifySignature} from "./olmlib"; +import {DeviceInfo} from "./deviceinfo" +import {DeviceTrustLevel} from './CrossSigning'; +import {keyFromPassphrase} from './key_passphrase'; +import {sleep} from "../utils"; +import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store'; +import {encodeRecoveryKey} from './recoverykey'; + +const KEY_BACKUP_KEYS_PER_REQUEST = 200; + +type AuthData = Record; + +type BackupInfo = { + algorithm: string, + auth_data: AuthData, // eslint-disable-line camelcase + [properties: string]: any, +}; + +type SigInfo = { + deviceId: string, + valid?: boolean | null, // true: valid, false: invalid, null: cannot attempt validation + device?: DeviceInfo | null, + crossSigningId?: boolean, + deviceTrust?: DeviceTrustLevel, +}; + +type TrustInfo = { + usable: boolean, // is the backup trusted, true iff there is a sig that is valid & from a trusted device + sigs: SigInfo[], +}; + +/** A function used to get the secret key for a backup. + */ +type GetKey = () => Promise; + +interface BackupAlgorithmClass { + algorithmName: string; + // initialize from an existing backup + init(authData: AuthData, getKey: GetKey): Promise; + + // prepare a brand new backup + prepare( + key: string | Uint8Array | null, + ): Promise<[Uint8Array, AuthData]>; +} + +interface BackupAlgorithm { + encryptSession(data: Record): Promise; + decryptSessions(ciphertexts: Record): Promise[]>; + authData: AuthData; + keyMatches(key: Uint8Array): Promise; + free(): void; +} + +/** + * Manages the key backup. + */ +export class BackupManager { + private algorithm: BackupAlgorithm | undefined; + private backupInfo: BackupInfo | undefined; // The info dict from /room_keys/version + public checkedForBackup: boolean; // Have we checked the server for a backup we can use? + private sendingBackups: boolean; // Are we currently sending backups? + constructor(private readonly baseApis, readonly getKey: GetKey) { + this.checkedForBackup = false; + this.sendingBackups = false; + } + + get version(): string | undefined { + return this.backupInfo && this.backupInfo.version; + } + + static async makeAlgorithm(info: BackupInfo, getKey: GetKey): Promise { + const Algorithm = algorithmsByName[info.algorithm]; + if (!Algorithm) { + throw new Error("Unknown backup algorithm"); + } + return await Algorithm.init(info.auth_data, getKey); + } + + async enableKeyBackup(info: BackupInfo): Promise { + this.backupInfo = info; + if (this.algorithm) { + this.algorithm.free(); + } + + this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey); + + this.baseApis.emit('crypto.keyBackupStatus', true); + + // There may be keys left over from a partially completed backup, so + // schedule a send to check. + this.scheduleKeyBackupSend(); + } + + /** + * Disable backing up of keys. + */ + disableKeyBackup(): void { + if (this.algorithm) { + this.algorithm.free(); + } + this.algorithm = undefined; + + this.backupInfo = undefined; + + this.baseApis.emit('crypto.keyBackupStatus', false); + } + + getKeyBackupEnabled(): boolean | null { + if (!this.checkedForBackup) { + return null; + } + return Boolean(this.algorithm); + } + + async prepareKeyBackupVersion( + key?: string | Uint8Array | null, + algorithm?: string | undefined, + ): Promise { + const Algorithm = algorithm ? algorithmsByName[algorithm] : DefaultAlgorithm; + if (!Algorithm) { + throw new Error("Unknown backup algorithm"); + } + + const [privateKey, authData] = await Algorithm.prepare(key); + const recoveryKey = encodeRecoveryKey(privateKey); + return { + algorithm: Algorithm.algorithmName, + auth_data: authData, + recovery_key: recoveryKey, + privateKey, + }; + } + + async createKeyBackupVersion(info: BackupInfo): Promise { + this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey); + } + + /** + * Check the server for an active key backup and + * if one is present and has a valid signature from + * one of the user's verified devices, start backing up + * to it. + */ + async checkAndStart(): Promise<{backupInfo: any, trustInfo: TrustInfo}> { + logger.log("Checking key backup status..."); + if (this.baseApis.isGuest()) { + logger.log("Skipping key backup check since user is guest"); + this.checkedForBackup = true; + return null; + } + let backupInfo: BackupInfo; + try { + backupInfo = await this.baseApis.getKeyBackupVersion(); + } catch (e) { + logger.log("Error checking for active key backup", e); + if (e.httpStatus === 404) { + // 404 is returned when the key backup does not exist, so that + // counts as successfully checking. + this.checkedForBackup = true; + } + return null; + } + this.checkedForBackup = true; + + const trustInfo = await this.isKeyBackupTrusted(backupInfo); + + if (trustInfo.usable && !this.backupInfo) { + logger.log( + "Found usable key backup v" + backupInfo.version + + ": enabling key backups", + ); + await this.enableKeyBackup(backupInfo); + } else if (!trustInfo.usable && this.backupInfo) { + logger.log("No usable key backup: disabling key backup"); + this.disableKeyBackup(); + } else if (!trustInfo.usable && !this.backupInfo) { + logger.log("No usable key backup: not enabling key backup"); + } else if (trustInfo.usable && this.backupInfo) { + // may not be the same version: if not, we should switch + if (backupInfo.version !== this.backupInfo.version) { + logger.log( + "On backup version " + this.backupInfo.version + " but found " + + "version " + backupInfo.version + ": switching.", + ); + this.disableKeyBackup(); + await this.enableKeyBackup(backupInfo); + // We're now using a new backup, so schedule all the keys we have to be + // uploaded to the new backup. This is a bit of a workaround to upload + // keys to a new backup in *most* cases, but it won't cover all cases + // because we don't remember what backup version we uploaded keys to: + // see https://github.com/vector-im/element-web/issues/14833 + await this.scheduleAllGroupSessionsForBackup(); + } else { + logger.log("Backup version " + backupInfo.version + " still current"); + } + } + + return {backupInfo, trustInfo}; + } + + /** + * Forces a re-check of the key backup and enables/disables it + * as appropriate. + * + * @return {Object} Object with backup info (as returned by + * getKeyBackupVersion) in backupInfo and + * trust information (as returned by isKeyBackupTrusted) + * in trustInfo. + */ + async checkKeyBackup(): Promise<{backupInfo: AuthData, trustInfo: TrustInfo}> { + this.checkedForBackup = false; + return this.checkAndStart(); + } + + /** + * @param {object} backupInfo key backup info dict from /room_keys/version + * @return {object} { + * usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device + * sigs: [ + * valid: [bool || null], // true: valid, false: invalid, null: cannot attempt validation + * deviceId: [string], + * device: [DeviceInfo || null], + * ] + * } + */ + async isKeyBackupTrusted(backupInfo: any | undefined): Promise { + const ret = { + usable: false, + trusted_locally: false, + sigs: [], + }; + + if ( + !backupInfo || + !backupInfo.algorithm || + !backupInfo.auth_data || + !backupInfo.auth_data.public_key || + !backupInfo.auth_data.signatures + ) { + logger.info("Key backup is absent or missing required data"); + return ret; + } + + const trustedPubkey = this.baseApis._crypto._sessionStore.getLocalTrustedBackupPubKey(); + + if (backupInfo.auth_data.public_key === trustedPubkey) { + logger.info("Backup public key " + trustedPubkey + " is trusted locally"); + ret.trusted_locally = true; + } + + const mySigs = backupInfo.auth_data.signatures[this.baseApis.getUserId()] || []; + + for (const keyId of Object.keys(mySigs)) { + const keyIdParts = keyId.split(':'); + if (keyIdParts[0] !== 'ed25519') { + logger.log("Ignoring unknown signature type: " + keyIdParts[0]); + continue; + } + // Could be a cross-signing master key, but just say this is the device + // ID for backwards compat + const sigInfo: SigInfo = { deviceId: keyIdParts[1] }; + + // first check to see if it's from our cross-signing key + const crossSigningId = this.baseApis._crypto._crossSigningInfo.getId(); + if (crossSigningId === sigInfo.deviceId) { + sigInfo.crossSigningId = true; + try { + await verifySignature( + this.baseApis._crypto._olmDevice, + backupInfo.auth_data, + this.baseApis.getUserId(), + sigInfo.deviceId, + crossSigningId, + ); + sigInfo.valid = true; + } catch (e) { + logger.warn( + "Bad signature from cross signing key " + crossSigningId, e, + ); + sigInfo.valid = false; + } + ret.sigs.push(sigInfo); + continue; + } + + // Now look for a sig from a device + // At some point this can probably go away and we'll just support + // it being signed by the cross-signing master key + const device = this.baseApis._crypto._deviceList.getStoredDevice( + this.baseApis.getUserId(), sigInfo.deviceId, + ); + if (device) { + sigInfo.device = device; + sigInfo.deviceTrust = await this.baseApis.checkDeviceTrust( + this.baseApis.getUserId(), sigInfo.deviceId, + ); + try { + await verifySignature( + this.baseApis._crypto._olmDevice, + backupInfo.auth_data, + this.baseApis.getUserId(), + device.deviceId, + device.getFingerprint(), + ); + sigInfo.valid = true; + } catch (e) { + logger.info( + "Bad signature from key ID " + keyId + " userID " + this.baseApis.getUserId() + + " device ID " + device.deviceId + " fingerprint: " + + device.getFingerprint(), backupInfo.auth_data, e, + ); + sigInfo.valid = false; + } + } else { + sigInfo.valid = null; // Can't determine validity because we don't have the signing device + logger.info("Ignoring signature from unknown key " + keyId); + } + ret.sigs.push(sigInfo); + } + + ret.usable = ret.sigs.some((s) => { + return ( + s.valid && ( + (s.device && s.deviceTrust.isVerified()) || + (s.crossSigningId) + ) + ); + }); + ret.usable ||= ret.trusted_locally; + return ret; + } + + /** + * Schedules sending all keys waiting to be sent to the backup, if not already + * scheduled. Retries if necessary. + * + * @param maxDelay Maximum delay to wait in ms. 0 means no delay. + */ + async scheduleKeyBackupSend(maxDelay = 10000): Promise { + if (this.sendingBackups) return; + + this.sendingBackups = true; + + try { + // wait between 0 and `maxDelay` seconds, to avoid backup + // requests from different clients hitting the server all at + // the same time when a new key is sent + const delay = Math.random() * maxDelay; + await sleep(delay, undefined); + let numFailures = 0; // number of consecutive failures + for (;;) { + if (!this.algorithm) { + return; + } + try { + const numBackedUp = + await this.backupPendingKeys(KEY_BACKUP_KEYS_PER_REQUEST); + if (numBackedUp === 0) { + // no sessions left needing backup: we're done + return; + } + numFailures = 0; + } catch (err) { + numFailures++; + logger.log("Key backup request failed", err); + if (err.data) { + if ( + err.data.errcode == 'M_NOT_FOUND' || + err.data.errcode == 'M_WRONG_ROOM_KEYS_VERSION' + ) { + // Re-check key backup status on error, so we can be + // sure to present the current situation when asked. + await this.checkKeyBackup(); + // Backup version has changed or this backup version + // has been deleted + this.baseApis._crypto.emit("crypto.keyBackupFailed", err.data.errcode); + throw err; + } + } + } + if (numFailures) { + // exponential backoff if we have failures + await sleep(1000 * Math.pow(2, Math.min(numFailures - 1, 4)), undefined); + } + } + } finally { + this.sendingBackups = false; + } + } + + /** + * Take some e2e keys waiting to be backed up and send them + * to the backup. + * + * @param {integer} limit Maximum number of keys to back up + * @returns {integer} Number of sessions backed up + */ + private async backupPendingKeys(limit: number): Promise { + const sessions = await this.baseApis._crypto._cryptoStore.getSessionsNeedingBackup(limit); + if (!sessions.length) { + return 0; + } + + let remaining = await this.baseApis._crypto._cryptoStore.countSessionsNeedingBackup(); + this.baseApis._crypto.emit("crypto.keyBackupSessionsRemaining", remaining); + + const data = {}; + for (const session of sessions) { + const roomId = session.sessionData.room_id; + if (data[roomId] === undefined) { + data[roomId] = {sessions: {}}; + } + + const sessionData = await this.baseApis._crypto._olmDevice.exportInboundGroupSession( + session.senderKey, session.sessionId, session.sessionData, + ); + sessionData.algorithm = MEGOLM_ALGORITHM; + + const forwardedCount = + (sessionData.forwarding_curve25519_key_chain || []).length; + + const userId = this.baseApis._crypto._deviceList.getUserByIdentityKey( + MEGOLM_ALGORITHM, session.senderKey, + ); + const device = this.baseApis._crypto._deviceList.getDeviceByIdentityKey( + MEGOLM_ALGORITHM, session.senderKey, + ); + const verified = this.baseApis._crypto._checkDeviceInfoTrust(userId, device).isVerified(); + + data[roomId]['sessions'][session.sessionId] = { + first_message_index: sessionData.first_known_index, + forwarded_count: forwardedCount, + is_verified: verified, + session_data: await this.algorithm.encryptSession(sessionData), + }; + } + + await this.baseApis.sendKeyBackup( + undefined, undefined, this.backupInfo.version, + {rooms: data}, + ); + + await this.baseApis._crypto._cryptoStore.unmarkSessionsNeedingBackup(sessions); + remaining = await this.baseApis._crypto._cryptoStore.countSessionsNeedingBackup(); + this.baseApis._crypto.emit("crypto.keyBackupSessionsRemaining", remaining); + + return sessions.length; + } + + async backupGroupSession( + senderKey: string, sessionId: string, + ): Promise { + await this.baseApis._crypto._cryptoStore.markSessionsNeedingBackup([{ + senderKey: senderKey, + sessionId: sessionId, + }]); + + if (this.backupInfo) { + // don't wait for this to complete: it will delay so + // happens in the background + this.scheduleKeyBackupSend(); + } + // if this.backupInfo is not set, then the keys will be backed up when + // this.enableKeyBackup is called + } + + /** + * Marks all group sessions as needing to be backed up and schedules them to + * upload in the background as soon as possible. + */ + async scheduleAllGroupSessionsForBackup(): Promise { + await this.flagAllGroupSessionsForBackup(); + + // Schedule keys to upload in the background as soon as possible. + this.scheduleKeyBackupSend(0 /* maxDelay */); + } + + /** + * Marks all group sessions as needing to be backed up without scheduling + * them to upload in the background. + * @returns {Promise} Resolves to the number of sessions now requiring a backup + * (which will be equal to the number of sessions in the store). + */ + async flagAllGroupSessionsForBackup(): Promise { + await this.baseApis._crypto._cryptoStore.doTxn( + 'readwrite', + [ + IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, + IndexedDBCryptoStore.STORE_BACKUP, + ], + (txn) => { + this.baseApis._crypto._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => { + if (session !== null) { + this.baseApis._crypto._cryptoStore.markSessionsNeedingBackup([session], txn); + } + }); + }, + ); + + const remaining = await this.baseApis._crypto._cryptoStore.countSessionsNeedingBackup(); + this.baseApis.emit("crypto.keyBackupSessionsRemaining", remaining); + return remaining; + } + + /** + * Counts the number of end to end session keys that are waiting to be backed up + * @returns {Promise} Resolves to the number of sessions requiring backup + */ + countSessionsNeedingBackup(): Promise { + return this.baseApis._crypto._cryptoStore.countSessionsNeedingBackup(); + } +} + +export class Curve25519 implements BackupAlgorithm { + static algorithmName = "m.megolm_backup.v1.curve25519-aes-sha2"; + + constructor( + public authData: AuthData, + private publicKey: any, // FIXME: PkEncryption + private getKey: () => Promise, + ) {} + + static async init( + authData: AuthData, + getKey: () => Promise, + ): Promise { + if (!authData || !authData.public_key) { + throw new Error("auth_data missing required information"); + } + const publicKey = new global.Olm.PkEncryption(); + publicKey.set_recipient_key(authData.public_key); + return new Curve25519(authData, publicKey, getKey); + } + + static async prepare( + key: string | Uint8Array | null, + ): Promise<[Uint8Array, AuthData]> { + const decryption = new global.Olm.PkDecryption(); + try { + const authData: AuthData = {}; + if (!key) { + authData.public_key = decryption.generate_key(); + } else if (key instanceof Uint8Array) { + authData.public_key = decryption.init_with_private_key(key); + } else { + const derivation = await keyFromPassphrase(key); + authData.private_key_salt = derivation.salt; + authData.private_key_iterations = derivation.iterations; + // FIXME: algorithm? + authData.public_key = decryption.init_with_private_key(derivation.key); + } + const publicKey = new global.Olm.PkEncryption(); + publicKey.set_recipient_key(authData.public_key); + + return [ + decryption.get_private_key(), + authData, + ] + } finally { + decryption.free(); + } + } + + async encryptSession(data: Record): Promise { + const plainText: Record = Object.assign({}, data); + delete plainText.session_id; + delete plainText.room_id; + delete plainText.first_known_index; + return this.publicKey.encrypt(JSON.stringify(plainText)); + } + + async decryptSessions(sessions: Record>): Promise[]> { + const privKey = await this.getKey(); + const decryption = new global.Olm.PkDecryption(); + try { + const backupPubKey = decryption.init_with_private_key(privKey); + + if (backupPubKey !== this.authData.public_key) { + throw {errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY}; + } + + const keys = []; + + for (const [sessionId, sessionData] of Object.entries(sessions)) { + try { + const decrypted = JSON.parse(decryption.decrypt( + sessionData.session_data.ephemeral, + sessionData.session_data.mac, + sessionData.session_data.ciphertext, + )); + decrypted.session_id = sessionId; + keys.push(decrypted); + } catch (e) { + logger.log("Failed to decrypt megolm session from backup", e, sessionData); + } + } + return keys; + } finally { + decryption.free(); + } + } + + async keyMatches(key: Uint8Array): Promise { + const decryption = new global.Olm.PkDecryption(); + let pubKey; + try { + pubKey = decryption.init_with_private_key(key); + } finally { + decryption.free(); + } + + return pubKey === this.authData.public_key; + } + + free(): void { + this.publicKey.free(); + } +} + +export const algorithmsByName: Record = { + [Curve25519.algorithmName]: Curve25519, +}; + +export const DefaultAlgorithm: BackupAlgorithmClass = Curve25519; diff --git a/src/crypto/index.js b/src/crypto/index.js index cdcce3cd5..19b84efd3 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -2,7 +2,7 @@ Copyright 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2018-2019 New Vector Ltd -Copyright 2019-2020 The Matrix.org Foundation C.I.C. +Copyright 2019-2021 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. @@ -58,6 +58,7 @@ import {KeySignatureUploadError} from "../errors"; import {decryptAES, encryptAES} from './aes'; import {DehydrationManager} from './dehydration'; import { MatrixEvent } from "../models/event"; +import { BackupManager } from "./backup"; const DeviceVerification = DeviceInfo.DeviceVerification; @@ -85,7 +86,6 @@ export function isCryptoAvailable() { } const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000; -const KEY_BACKUP_KEYS_PER_REQUEST = 200; /** * Cryptography bits @@ -154,13 +154,36 @@ export function Crypto(baseApis, sessionStore, userId, deviceId, } else { this._verificationMethods = defaultVerificationMethods; } - // track whether this device's megolm keys are being backed up incrementally - // to the server or not. - // XXX: this should probably have a single source of truth from OlmAccount - this.backupInfo = null; // The info dict from /room_keys/version - this.backupKey = null; // The encryption key object - this._checkedForBackup = false; // Have we checked the server for a backup we can use? - this._sendingBackups = false; // Are we currently sending backups? + + this._backupManager = new BackupManager(baseApis, async (algorithm) => { + // try to get key from cache + const cachedKey = await this.getSessionBackupPrivateKey(); + if (cachedKey) { + return cachedKey; + } + + // try to get key from secret storage + const storedKey = await this.getSecret("m.megolm_backup.v1"); + + if (storedKey) { + // ensure that the key is in the right format. If not, fix the key and + // store the fixed version + const fixedKey = fixBackupKey(storedKey); + if (fixedKey) { + const [keyId] = await this._crypto.getSecretStorageKey(); + await this.storeSecret("m.megolm_backup.v1", fixedKey, [keyId]); + } + + return decodeBase64(fixedKey || storedKey); + } + + // try to get key from app + if (this._baseApis._cryptoCallbacks && this._baseApis._cryptoCallbacks.getBackupKey) { + return await this._baseApis._cryptoCallbacks.getBackupKey(algorithm); + } + + throw new Error("Unable to get private key"); + }); this._olmDevice = new OlmDevice(cryptoStore); this._deviceList = new DeviceList( @@ -331,7 +354,7 @@ Crypto.prototype.init = async function(opts) { this._deviceList.startTrackingDeviceList(this._userId); logger.log("Crypto: checking for key backup..."); - this._checkAndStartKeyBackup(); + this._backupManager.checkAndStart(); }; /** @@ -458,7 +481,7 @@ Crypto.prototype.isSecretStorageReady = async function() { this._secretStorage, ); const sessionBackupInStorage = ( - !this._baseApis.getKeyBackupEnabled() || + !this._backupManager.getKeyBackupEnabled() || this._baseApis.isKeyBackupKeyStored() ); @@ -522,9 +545,11 @@ Crypto.prototype.bootstrapCrossSigning = async function({ builder.addKeySignature(this._userId, this._deviceId, deviceSignature); // Sign message key backup with cross-signing master key - if (this.backupInfo) { - await crossSigningInfo.signObject(this.backupInfo.auth_data, "master"); - builder.addSessionBackup(this.backupInfo); + if (this._backupManager.backupInfo) { + await crossSigningInfo.signObject( + this._backupManager.backupInfo.auth_data, "master", + ); + builder.addSessionBackup(this._backupManager.backupInfo); } }; @@ -766,6 +791,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({ keyBackupInfo.auth_data.private_key_salt && keyBackupInfo.auth_data.private_key_iterations ) { + // FIXME: ??? opts.passphrase = { algorithm: "m.pbkdf2", iterations: keyBackupInfo.auth_data.private_key_iterations, @@ -1477,7 +1503,7 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function({ } // Now we may be able to trust our key backup - await this.checkKeyBackup(); + await this._backupManager.checkKeyBackup(); // FIXME: if we previously trusted the backup, should we automatically sign // the backup with the new key (if not already signed)? }; @@ -1539,206 +1565,11 @@ Crypto.prototype._checkDeviceVerifications = async function(userId) { logger.info(`Finished device verification upgrade for ${userId}`); }; -/** - * Check the server for an active key backup and - * if one is present and has a valid signature from - * one of the user's verified devices, start backing up - * to it. - */ -Crypto.prototype._checkAndStartKeyBackup = async function() { - logger.log("Checking key backup status..."); - if (this._baseApis.isGuest()) { - logger.log("Skipping key backup check since user is guest"); - this._checkedForBackup = true; - return null; - } - let backupInfo; - try { - backupInfo = await this._baseApis.getKeyBackupVersion(); - } catch (e) { - logger.log("Error checking for active key backup", e); - if (e.httpStatus === 404) { - // 404 is returned when the key backup does not exist, so that - // counts as successfully checking. - this._checkedForBackup = true; - } - return null; - } - this._checkedForBackup = true; - - const trustInfo = await this.isKeyBackupTrusted(backupInfo); - - if (trustInfo.usable && !this.backupInfo) { - logger.log( - "Found usable key backup v" + backupInfo.version + - ": enabling key backups", - ); - this._baseApis.enableKeyBackup(backupInfo); - } else if (!trustInfo.usable && this.backupInfo) { - logger.log("No usable key backup: disabling key backup"); - this._baseApis.disableKeyBackup(); - } else if (!trustInfo.usable && !this.backupInfo) { - logger.log("No usable key backup: not enabling key backup"); - } else if (trustInfo.usable && this.backupInfo) { - // may not be the same version: if not, we should switch - if (backupInfo.version !== this.backupInfo.version) { - logger.log( - "On backup version " + this.backupInfo.version + " but found " + - "version " + backupInfo.version + ": switching.", - ); - this._baseApis.disableKeyBackup(); - this._baseApis.enableKeyBackup(backupInfo); - // We're now using a new backup, so schedule all the keys we have to be - // uploaded to the new backup. This is a bit of a workaround to upload - // keys to a new backup in *most* cases, but it won't cover all cases - // because we don't remember what backup version we uploaded keys to: - // see https://github.com/vector-im/element-web/issues/14833 - await this.scheduleAllGroupSessionsForBackup(); - } else { - logger.log("Backup version " + backupInfo.version + " still current"); - } - } - - return {backupInfo, trustInfo}; -}; - Crypto.prototype.setTrustedBackupPubKey = async function(trustedPubKey) { // This should be redundant post cross-signing is a thing, so just // plonk it in localStorage for now. this._sessionStore.setLocalTrustedBackupPubKey(trustedPubKey); - await this.checkKeyBackup(); -}; - -/** - * Forces a re-check of the key backup and enables/disables it - * as appropriate. - * - * @return {Object} Object with backup info (as returned by - * getKeyBackupVersion) in backupInfo and - * trust information (as returned by isKeyBackupTrusted) - * in trustInfo. - */ -Crypto.prototype.checkKeyBackup = async function() { - this._checkedForBackup = false; - return this._checkAndStartKeyBackup(); -}; - -/** - * @param {object} backupInfo key backup info dict from /room_keys/version - * @return {object} { - * usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device - * sigs: [ - * valid: [bool || null], // true: valid, false: invalid, null: cannot attempt validation - * deviceId: [string], - * device: [DeviceInfo || null], - * ] - * } - */ -Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { - const ret = { - usable: false, - trusted_locally: false, - sigs: [], - }; - - if ( - !backupInfo || - !backupInfo.algorithm || - !backupInfo.auth_data || - !backupInfo.auth_data.public_key || - !backupInfo.auth_data.signatures - ) { - logger.info("Key backup is absent or missing required data"); - return ret; - } - - const trustedPubkey = this._sessionStore.getLocalTrustedBackupPubKey(); - - if (backupInfo.auth_data.public_key === trustedPubkey) { - logger.info("Backup public key " + trustedPubkey + " is trusted locally"); - ret.trusted_locally = true; - } - - const mySigs = backupInfo.auth_data.signatures[this._userId] || []; - - for (const keyId of Object.keys(mySigs)) { - const keyIdParts = keyId.split(':'); - if (keyIdParts[0] !== 'ed25519') { - logger.log("Ignoring unknown signature type: " + keyIdParts[0]); - continue; - } - // Could be a cross-signing master key, but just say this is the device - // ID for backwards compat - const sigInfo = { deviceId: keyIdParts[1] }; - - // first check to see if it's from our cross-signing key - const crossSigningId = this._crossSigningInfo.getId(); - if (crossSigningId === sigInfo.deviceId) { - sigInfo.crossSigningId = true; - try { - await olmlib.verifySignature( - this._olmDevice, - backupInfo.auth_data, - this._userId, - sigInfo.deviceId, - crossSigningId, - ); - sigInfo.valid = true; - } catch (e) { - logger.warning( - "Bad signature from cross signing key " + crossSigningId, e, - ); - sigInfo.valid = false; - } - ret.sigs.push(sigInfo); - continue; - } - - // Now look for a sig from a device - // At some point this can probably go away and we'll just support - // it being signed by the cross-signing master key - const device = this._deviceList.getStoredDevice( - this._userId, sigInfo.deviceId, - ); - if (device) { - sigInfo.device = device; - sigInfo.deviceTrust = await this.checkDeviceTrust( - this._userId, sigInfo.deviceId, - ); - try { - await olmlib.verifySignature( - this._olmDevice, - backupInfo.auth_data, - this._userId, - device.deviceId, - device.getFingerprint(), - ); - sigInfo.valid = true; - } catch (e) { - logger.info( - "Bad signature from key ID " + keyId + " userID " + this._userId + - " device ID " + device.deviceId + " fingerprint: " + - device.getFingerprint(), backupInfo.auth_data, e, - ); - sigInfo.valid = false; - } - } else { - sigInfo.valid = null; // Can't determine validity because we don't have the signing device - logger.info("Ignoring signature from unknown key " + keyId); - } - ret.sigs.push(sigInfo); - } - - ret.usable = ret.sigs.some((s) => { - return ( - s.valid && ( - (s.device && s.deviceTrust.isVerified()) || - (s.crossSigningId) - ) - ); - }); - ret.usable |= ret.trusted_locally; - return ret; + await this._backupManager.checkKeyBackup(); }; /** @@ -2789,191 +2620,12 @@ Crypto.prototype.importRoomKeys = function(keys, opts = {}) { })); }; -/** - * Schedules sending all keys waiting to be sent to the backup, if not already - * scheduled. Retries if necessary. - * - * @param {number} maxDelay Maximum delay to wait in ms. 0 means no delay. - */ -Crypto.prototype.scheduleKeyBackupSend = async function(maxDelay = 10000) { - if (this._sendingBackups) return; - - this._sendingBackups = true; - - try { - // wait between 0 and `maxDelay` seconds, to avoid backup - // requests from different clients hitting the server all at - // the same time when a new key is sent - const delay = Math.random() * maxDelay; - await sleep(delay); - let numFailures = 0; // number of consecutive failures - while (1) { - if (!this.backupKey) { - return; - } - try { - const numBackedUp = - await this._backupPendingKeys(KEY_BACKUP_KEYS_PER_REQUEST); - if (numBackedUp === 0) { - // no sessions left needing backup: we're done - return; - } - numFailures = 0; - } catch (err) { - numFailures++; - logger.log("Key backup request failed", err); - if (err.data) { - if ( - err.data.errcode == 'M_NOT_FOUND' || - err.data.errcode == 'M_WRONG_ROOM_KEYS_VERSION' - ) { - // Re-check key backup status on error, so we can be - // sure to present the current situation when asked. - await this.checkKeyBackup(); - // Backup version has changed or this backup version - // has been deleted - this.emit("crypto.keyBackupFailed", err.data.errcode); - throw err; - } - } - } - if (numFailures) { - // exponential backoff if we have failures - await sleep(1000 * Math.pow(2, Math.min(numFailures - 1, 4))); - } - } - } finally { - this._sendingBackups = false; - } -}; - -/** - * Take some e2e keys waiting to be backed up and send them - * to the backup. - * - * @param {integer} limit Maximum number of keys to back up - * @returns {integer} Number of sessions backed up - */ -Crypto.prototype._backupPendingKeys = async function(limit) { - const sessions = await this._cryptoStore.getSessionsNeedingBackup(limit); - if (!sessions.length) { - return 0; - } - - let remaining = await this._cryptoStore.countSessionsNeedingBackup(); - this.emit("crypto.keyBackupSessionsRemaining", remaining); - - const data = {}; - for (const session of sessions) { - const roomId = session.sessionData.room_id; - if (data[roomId] === undefined) { - data[roomId] = {sessions: {}}; - } - - const sessionData = await this._olmDevice.exportInboundGroupSession( - session.senderKey, session.sessionId, session.sessionData, - ); - sessionData.algorithm = olmlib.MEGOLM_ALGORITHM; - delete sessionData.session_id; - delete sessionData.room_id; - const firstKnownIndex = sessionData.first_known_index; - delete sessionData.first_known_index; - const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData)); - - const forwardedCount = - (sessionData.forwarding_curve25519_key_chain || []).length; - - const userId = this._deviceList.getUserByIdentityKey( - olmlib.MEGOLM_ALGORITHM, session.senderKey, - ); - const device = this._deviceList.getDeviceByIdentityKey( - olmlib.MEGOLM_ALGORITHM, session.senderKey, - ); - const verified = this._checkDeviceInfoTrust(userId, device).isVerified(); - - data[roomId]['sessions'][session.sessionId] = { - first_message_index: firstKnownIndex, - forwarded_count: forwardedCount, - is_verified: verified, - session_data: encrypted, - }; - } - - await this._baseApis.sendKeyBackup( - undefined, undefined, this.backupInfo.version, - {rooms: data}, - ); - - await this._cryptoStore.unmarkSessionsNeedingBackup(sessions); - remaining = await this._cryptoStore.countSessionsNeedingBackup(); - this.emit("crypto.keyBackupSessionsRemaining", remaining); - - return sessions.length; -}; - -Crypto.prototype.backupGroupSession = async function( - roomId, senderKey, forwardingCurve25519KeyChain, - sessionId, sessionKey, keysClaimed, - exportFormat, -) { - await this._cryptoStore.markSessionsNeedingBackup([{ - senderKey: senderKey, - sessionId: sessionId, - }]); - - if (this.backupInfo) { - // don't wait for this to complete: it will delay so - // happens in the background - this.scheduleKeyBackupSend(); - } - // if this.backupInfo is not set, then the keys will be backed up when - // client.enableKeyBackup is called -}; - -/** - * Marks all group sessions as needing to be backed up and schedules them to - * upload in the background as soon as possible. - */ -Crypto.prototype.scheduleAllGroupSessionsForBackup = async function() { - await this.flagAllGroupSessionsForBackup(); - - // Schedule keys to upload in the background as soon as possible. - this.scheduleKeyBackupSend(0 /* maxDelay */); -}; - -/** - * Marks all group sessions as needing to be backed up without scheduling - * them to upload in the background. - * @returns {Promise} Resolves to the number of sessions now requiring a backup - * (which will be equal to the number of sessions in the store). - */ -Crypto.prototype.flagAllGroupSessionsForBackup = async function() { - await this._cryptoStore.doTxn( - 'readwrite', - [ - IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, - IndexedDBCryptoStore.STORE_BACKUP, - ], - (txn) => { - this._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => { - if (session !== null) { - this._cryptoStore.markSessionsNeedingBackup([session], txn); - } - }); - }, - ); - - const remaining = await this._cryptoStore.countSessionsNeedingBackup(); - this.emit("crypto.keyBackupSessionsRemaining", remaining); - return remaining; -}; - /** * Counts the number of end to end session keys that are waiting to be backed up * @returns {Promise} Resolves to the number of sessions requiring backup */ Crypto.prototype.countSessionsNeedingBackup = function() { - return this._cryptoStore.countSessionsNeedingBackup(); + return this._backupManager.countSessionsNeedingBackup(); }; /** @@ -3347,10 +2999,10 @@ Crypto.prototype._onRoomKeyEvent = function(event) { return; } - if (!this._checkedForBackup) { + if (!this._backupManager.checkedForBackup) { // don't bother awaiting on this - the important thing is that we retry if we // haven't managed to check before - this._checkAndStartKeyBackup(); + this._backupManager.checkAndStart(); } const alg = this._getRoomDecryptor(content.room_id, content.algorithm); From 9e6b72bf384298c110fdeec0659aaf393f46ec2f Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 18 May 2021 18:31:19 -0400 Subject: [PATCH 02/10] some linting --- src/client.js | 30 ++++-------------------------- src/crypto/index.js | 2 +- 2 files changed, 5 insertions(+), 27 deletions(-) diff --git a/src/client.js b/src/client.js index 30a8454ed..cf26e68df 100644 --- a/src/client.js +++ b/src/client.js @@ -35,7 +35,6 @@ import {StubStore} from "./store/stub"; import {createNewMatrixCall} from "./webrtc/call"; import {CallEventHandler} from './webrtc/callEventHandler'; import * as utils from './utils'; -import {sleep} from './utils'; import { MatrixError, PREFIX_MEDIA_R0, @@ -64,29 +63,6 @@ export const CRYPTO_ENABLED = isCryptoAvailable(); const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes -function keysFromRecoverySession(sessions, decryptionKey, roomId) { - const keys = []; - for (const [sessionId, sessionData] of Object.entries(sessions)) { - try { - const decrypted = keyFromRecoverySession(sessionData, decryptionKey); - decrypted.session_id = sessionId; - decrypted.room_id = roomId; - keys.push(decrypted); - } catch (e) { - logger.log("Failed to decrypt megolm session from backup", e); - } - } - return keys; -} - -function keyFromRecoverySession(session, decryptionKey) { - return JSON.parse(decryptionKey.decrypt( - session.session_data.ephemeral, - session.session_data.mac, - session.session_data.ciphertext, - )); -} - /** * Construct a Matrix Client. Only directly construct this if you want to use * custom modules. Normally, {@link createClient} should be used @@ -1843,13 +1819,15 @@ MatrixClient.prototype.getKeyBackupEnabled = function() { * getKeyBackupVersion. * * @param {object} info Backup information object as returned by getKeyBackupVersion + * + * @returns {Promise} */ -MatrixClient.prototype.enableKeyBackup = function(info) { +MatrixClient.prototype.enableKeyBackup = async function(info) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } - return this._crypto.backupManager.enableKeyBackup(); + await this._crypto.backupManager.enableKeyBackup(); }; /** diff --git a/src/crypto/index.js b/src/crypto/index.js index 19b84efd3..a8df354b9 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -174,7 +174,7 @@ export function Crypto(baseApis, sessionStore, userId, deviceId, await this.storeSecret("m.megolm_backup.v1", fixedKey, [keyId]); } - return decodeBase64(fixedKey || storedKey); + return olmlib.decodeBase64(fixedKey || storedKey); } // try to get key from app From 3ec89a89dfa9ec1a22e0ac06758a53455e5fbaa9 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 18 May 2021 18:40:36 -0400 Subject: [PATCH 03/10] fix some types --- src/crypto/backup.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index 01ecf1fc1..df127baef 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -166,7 +166,7 @@ export class BackupManager { * one of the user's verified devices, start backing up * to it. */ - async checkAndStart(): Promise<{backupInfo: any, trustInfo: TrustInfo}> { + async checkAndStart(): Promise<{backupInfo: BackupInfo, trustInfo: TrustInfo}> { logger.log("Checking key backup status..."); if (this.baseApis.isGuest()) { logger.log("Skipping key backup check since user is guest"); @@ -232,7 +232,7 @@ export class BackupManager { * trust information (as returned by isKeyBackupTrusted) * in trustInfo. */ - async checkKeyBackup(): Promise<{backupInfo: AuthData, trustInfo: TrustInfo}> { + async checkKeyBackup(): Promise<{backupInfo: BackupInfo, trustInfo: TrustInfo}> { this.checkedForBackup = false; return this.checkAndStart(); } @@ -248,7 +248,7 @@ export class BackupManager { * ] * } */ - async isKeyBackupTrusted(backupInfo: any | undefined): Promise { + async isKeyBackupTrusted(backupInfo: BackupInfo): Promise { const ret = { usable: false, trusted_locally: false, From 98e2154f0f14a5abde6a8f66434db9710f0c3210 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 25 May 2021 21:59:08 -0400 Subject: [PATCH 04/10] fix unit tests --- spec/unit/crypto/backup.spec.js | 91 ++++++++++++++++++--------------- src/client.js | 4 +- 2 files changed, 51 insertions(+), 44 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index 96623dda3..6d6b59ea6 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -28,6 +28,7 @@ import * as testUtils from "../../test-utils"; import {OlmDevice} from "../../../src/crypto/OlmDevice"; import {Crypto} from "../../../src/crypto"; import {resetCrossSigningKeys} from "./crypto-utils"; +import {BackupManager} from "../../../src/crypto/backup"; const Olm = global.Olm; @@ -73,7 +74,7 @@ const KEY_BACKUP_DATA = { }; const BACKUP_INFO = { - algorithm: "m.megolm_backup.v1", + algorithm: "m.megolm_backup.v1.curve25519-aes-sha2", version: 1, auth_data: { public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", @@ -138,6 +139,7 @@ describe("MegolmBackup", function() { let megolmDecryption; beforeEach(async function() { mockCrypto = testUtils.mock(Crypto, 'Crypto'); + mockCrypto._backupManager = testUtils.mock(BackupManager, "BackupManager"); mockCrypto.backupKey = new Olm.PkEncryption(); mockCrypto.backupKey.set_recipient_key( "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", @@ -215,12 +217,14 @@ describe("MegolmBackup", function() { }; mockCrypto.cancelRoomKeyRequest = function() {}; - mockCrypto.backupGroupSession = jest.fn(); + mockCrypto._backupManager = { + backupGroupSession: jest.fn(), + }; return event.attemptDecryption(mockCrypto).then(() => { return megolmDecryption.onRoomKeyEvent(event); }).then(() => { - expect(mockCrypto.backupGroupSession).toHaveBeenCalled(); + expect(mockCrypto._backupManager.backupGroupSession).toHaveBeenCalled(); }); }); @@ -264,7 +268,7 @@ describe("MegolmBackup", function() { }) .then(() => { client.enableKeyBackup({ - algorithm: "m.megolm_backup.v1", + algorithm: "m.megolm_backup.v1.curve25519-aes-sha2", version: 1, auth_data: { public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", @@ -292,12 +296,9 @@ describe("MegolmBackup", function() { resolve(); return Promise.resolve({}); }; - client._crypto.backupGroupSession( - "roomId", + client._crypto._backupManager.backupGroupSession( "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", - [], groupSession.session_id(), - groupSession.session_key(), ); }).then(() => { expect(numCalls).toBe(1); @@ -335,39 +336,48 @@ describe("MegolmBackup", function() { }); await resetCrossSigningKeys(client); let numCalls = 0; - await new Promise((resolve, reject) => { - client._http.authedRequest = function( - callback, method, path, queryParams, data, opts, - ) { - ++numCalls; - expect(numCalls).toBeLessThanOrEqual(1); - if (numCalls >= 2) { - // exit out of retry loop if there's something wrong - reject(new Error("authedRequest called too many timmes")); - return Promise.resolve({}); - } - expect(method).toBe("POST"); - expect(path).toBe("/room_keys/version"); - try { - // make sure auth_data is signed by the master key - olmlib.pkVerify( - data.auth_data, client.getCrossSigningId(), "@alice:bar", - ); - } catch (e) { - reject(e); - return Promise.resolve({}); - } - resolve(); - return Promise.resolve({}); - }; + await Promise.all([ + new Promise((resolve, reject) => { + let backupInfo; + client._http.authedRequest = function( + callback, method, path, queryParams, data, opts, + ) { + ++numCalls; + expect(numCalls).toBeLessThanOrEqual(2); + if (numCalls === 1) { + expect(method).toBe("POST"); + expect(path).toBe("/room_keys/version"); + try { + // make sure auth_data is signed by the master key + olmlib.pkVerify( + data.auth_data, client.getCrossSigningId(), "@alice:bar", + ); + } catch (e) { + reject(e); + return Promise.resolve({}); + } + backupInfo = data; + return Promise.resolve({}); + } else if (numCalls === 2) { + expect(method).toBe("GET"); + expect(path).toBe("/room_keys/version"); + resolve(); + return Promise.resolve(backupInfo); + } else { + // exit out of retry loop if there's something wrong + reject(new Error("authedRequest called too many times")); + return Promise.resolve({}); + } + }; + }), client.createKeyBackupVersion({ - algorithm: "m.megolm_backup.v1", + algorithm: "m.megolm_backup.v1.curve25519-aes-sha2", auth_data: { public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", }, - }); - }); - expect(numCalls).toBe(1); + }), + ]); + expect(numCalls).toBe(2); }); it('retries when a backup fails', function() { @@ -434,7 +444,7 @@ describe("MegolmBackup", function() { }) .then(() => { client.enableKeyBackup({ - algorithm: "foobar", + algorithm: "m.megolm_backup.v1.curve25519-aes-sha2", version: 1, auth_data: { public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", @@ -468,12 +478,9 @@ describe("MegolmBackup", function() { ); } }; - client._crypto.backupGroupSession( - "roomId", + client._crypto._backupManager.backupGroupSession( "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", - [], groupSession.session_id(), - groupSession.session_key(), ); }).then(() => { expect(numCalls).toBe(2); diff --git a/src/client.js b/src/client.js index cf26e68df..0f1a15cb7 100644 --- a/src/client.js +++ b/src/client.js @@ -1827,7 +1827,7 @@ MatrixClient.prototype.enableKeyBackup = async function(info) { throw new Error("End-to-end encryption disabled"); } - await this._crypto.backupManager.enableKeyBackup(); + await this._crypto._backupManager.enableKeyBackup(info); }; /** @@ -1838,7 +1838,7 @@ MatrixClient.prototype.disableKeyBackup = function() { throw new Error("End-to-end encryption disabled"); } - this._crypto.backupManager.disableKeyBackup(); + this._crypto._backupManager.disableKeyBackup(); }; /** From e73b9690665d1e57dd53623bdc04c6c23ddce7ec Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 25 May 2021 22:10:15 -0400 Subject: [PATCH 05/10] lint --- src/client.js | 1 + src/crypto/backup.ts | 1 + src/crypto/index.js | 1 - 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 0f1a15cb7..60977bd61 100644 --- a/src/client.js +++ b/src/client.js @@ -35,6 +35,7 @@ import {StubStore} from "./store/stub"; import {createNewMatrixCall} from "./webrtc/call"; import {CallEventHandler} from './webrtc/callEventHandler'; import * as utils from './utils'; +import {sleep} from './utils'; import { MatrixError, PREFIX_MEDIA_R0, diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index df127baef..fbd06db25 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -601,6 +601,7 @@ export class Curve25519 implements BackupAlgorithm { const backupPubKey = decryption.init_with_private_key(privKey); if (backupPubKey !== this.authData.public_key) { + // eslint-disable-next-line no-throw-literal throw {errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY}; } diff --git a/src/crypto/index.js b/src/crypto/index.js index a8df354b9..904fba310 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -26,7 +26,6 @@ import {EventEmitter} from 'events'; import {ReEmitter} from '../ReEmitter'; import {logger} from '../logger'; import * as utils from "../utils"; -import {sleep} from "../utils"; import {OlmDevice} from "./OlmDevice"; import * as olmlib from "./olmlib"; import {DeviceList} from "./DeviceList"; From 07bfa5532e11ca94fb05fd70d60683ec5c7e0e9b Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 26 May 2021 18:18:30 -0400 Subject: [PATCH 06/10] fix more unit tests --- spec/unit/crypto/algorithms/megolm.spec.js | 3 +++ spec/unit/crypto/secrets.spec.js | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/unit/crypto/algorithms/megolm.spec.js b/spec/unit/crypto/algorithms/megolm.spec.js index 22c668fcf..c8e8158bb 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.js +++ b/spec/unit/crypto/algorithms/megolm.spec.js @@ -258,6 +258,9 @@ describe("MegolmDecryption", function() { }); it("re-uses sessions for sequential messages", async function() { + mockCrypto._backupManager = { + backupGroupSession: () => {}, + }; const mockStorage = new MockStorageApi(); const cryptoStore = new MemoryCryptoStore(mockStorage); diff --git a/spec/unit/crypto/secrets.spec.js b/spec/unit/crypto/secrets.spec.js index bcf1871da..297caf064 100644 --- a/spec/unit/crypto/secrets.spec.js +++ b/spec/unit/crypto/secrets.spec.js @@ -376,7 +376,7 @@ describe("Secrets", function() { ]); this.emit("accountData", event); }; - bob._crypto.checkKeyBackup = async () => {}; + bob._crypto._backupManager.checkKeyBackup = async () => {}; const crossSigning = bob._crypto._crossSigningInfo; const secretStorage = bob._crypto._secretStorage; From c5433588267d00d3ae26ffd7e9827638f51482bd Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 31 May 2021 21:52:20 -0400 Subject: [PATCH 07/10] add unit test and minor fixes --- spec/unit/crypto/backup.spec.js | 16 ++++++++++++++++ src/crypto/backup.ts | 4 +++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index 6d6b59ea6..53102daed 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -576,5 +576,21 @@ describe("MegolmBackup", function() { const cachedKey = await client._crypto.getSessionBackupPrivateKey(); expect(cachedKey).not.toBeNull(); }); + + it("fails if an known algorithm is used", async function() { + const BAD_BACKUP_INFO = Object.assign({}, BACKUP_INFO, { + algorithm: "this.algorithm.does.not.exist", + }); + client._http.authedRequest = function() { + return Promise.resolve(KEY_BACKUP_DATA); + }; + + await expect(client.restoreKeyBackupWithRecoveryKey( + "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", + ROOM_ID, + SESSION_ID, + BAD_BACKUP_INFO, + )).rejects.toThrow(); + }); }); }); diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index fbd06db25..f470e3c9e 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -238,6 +238,8 @@ export class BackupManager { } /** + * Check if the given backup info is trusted. + * * @param {object} backupInfo key backup info dict from /room_keys/version * @return {object} { * usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device @@ -351,7 +353,7 @@ export class BackupManager { ) ); }); - ret.usable ||= ret.trusted_locally; + ret.usable = ret.usable || ret.trusted_locally; return ret; } From b4eff9b99677eb433047f6c45ae010ba100da530 Mon Sep 17 00:00:00 2001 From: Lars Richard Date: Wed, 2 Jun 2021 11:42:50 +0200 Subject: [PATCH 08/10] Ignore m.room.power_levels events with unknown state_key on room-state initialization Signed-off-by: Lars Richard --- src/models/room-state.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/models/room-state.js b/src/models/room-state.js index e991df57c..f9e76cfc3 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -349,6 +349,11 @@ RoomState.prototype.setStateEvents = function(stateEvents) { self._updateMember(member); self.emit("RoomState.members", event, self, member); } else if (event.getType() === "m.room.power_levels") { + // events with unknown state keys should be ignored + // and should not aggregate onto members power levels + if (event.getStateKey() !== "") { + return; + } const members = Object.values(self.members); members.forEach(function(member) { // We only propagate `RoomState.members` event if the From 2608dd2d642d4a980bc65223b7ba3571fea65e4e Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 3 Jun 2021 18:15:05 -0400 Subject: [PATCH 09/10] mark members as public --- src/crypto/backup.ts | 46 ++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index f470e3c9e..10a6d6356 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -84,16 +84,16 @@ export class BackupManager { private backupInfo: BackupInfo | undefined; // The info dict from /room_keys/version public checkedForBackup: boolean; // Have we checked the server for a backup we can use? private sendingBackups: boolean; // Are we currently sending backups? - constructor(private readonly baseApis, readonly getKey: GetKey) { + constructor(private readonly baseApis, public readonly getKey: GetKey) { this.checkedForBackup = false; this.sendingBackups = false; } - get version(): string | undefined { + public get version(): string | undefined { return this.backupInfo && this.backupInfo.version; } - static async makeAlgorithm(info: BackupInfo, getKey: GetKey): Promise { + public static async makeAlgorithm(info: BackupInfo, getKey: GetKey): Promise { const Algorithm = algorithmsByName[info.algorithm]; if (!Algorithm) { throw new Error("Unknown backup algorithm"); @@ -101,7 +101,7 @@ export class BackupManager { return await Algorithm.init(info.auth_data, getKey); } - async enableKeyBackup(info: BackupInfo): Promise { + public async enableKeyBackup(info: BackupInfo): Promise { this.backupInfo = info; if (this.algorithm) { this.algorithm.free(); @@ -119,7 +119,7 @@ export class BackupManager { /** * Disable backing up of keys. */ - disableKeyBackup(): void { + public disableKeyBackup(): void { if (this.algorithm) { this.algorithm.free(); } @@ -130,14 +130,14 @@ export class BackupManager { this.baseApis.emit('crypto.keyBackupStatus', false); } - getKeyBackupEnabled(): boolean | null { + public getKeyBackupEnabled(): boolean | null { if (!this.checkedForBackup) { return null; } return Boolean(this.algorithm); } - async prepareKeyBackupVersion( + public async prepareKeyBackupVersion( key?: string | Uint8Array | null, algorithm?: string | undefined, ): Promise { @@ -156,7 +156,7 @@ export class BackupManager { }; } - async createKeyBackupVersion(info: BackupInfo): Promise { + public async createKeyBackupVersion(info: BackupInfo): Promise { this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey); } @@ -166,7 +166,7 @@ export class BackupManager { * one of the user's verified devices, start backing up * to it. */ - async checkAndStart(): Promise<{backupInfo: BackupInfo, trustInfo: TrustInfo}> { + public async checkAndStart(): Promise<{backupInfo: BackupInfo, trustInfo: TrustInfo}> { logger.log("Checking key backup status..."); if (this.baseApis.isGuest()) { logger.log("Skipping key backup check since user is guest"); @@ -232,7 +232,7 @@ export class BackupManager { * trust information (as returned by isKeyBackupTrusted) * in trustInfo. */ - async checkKeyBackup(): Promise<{backupInfo: BackupInfo, trustInfo: TrustInfo}> { + public async checkKeyBackup(): Promise<{backupInfo: BackupInfo, trustInfo: TrustInfo}> { this.checkedForBackup = false; return this.checkAndStart(); } @@ -250,7 +250,7 @@ export class BackupManager { * ] * } */ - async isKeyBackupTrusted(backupInfo: BackupInfo): Promise { + public async isKeyBackupTrusted(backupInfo: BackupInfo): Promise { const ret = { usable: false, trusted_locally: false, @@ -363,7 +363,7 @@ export class BackupManager { * * @param maxDelay Maximum delay to wait in ms. 0 means no delay. */ - async scheduleKeyBackupSend(maxDelay = 10000): Promise { + public async scheduleKeyBackupSend(maxDelay = 10000): Promise { if (this.sendingBackups) return; this.sendingBackups = true; @@ -474,7 +474,7 @@ export class BackupManager { return sessions.length; } - async backupGroupSession( + public async backupGroupSession( senderKey: string, sessionId: string, ): Promise { await this.baseApis._crypto._cryptoStore.markSessionsNeedingBackup([{ @@ -495,7 +495,7 @@ export class BackupManager { * Marks all group sessions as needing to be backed up and schedules them to * upload in the background as soon as possible. */ - async scheduleAllGroupSessionsForBackup(): Promise { + public async scheduleAllGroupSessionsForBackup(): Promise { await this.flagAllGroupSessionsForBackup(); // Schedule keys to upload in the background as soon as possible. @@ -508,7 +508,7 @@ export class BackupManager { * @returns {Promise} Resolves to the number of sessions now requiring a backup * (which will be equal to the number of sessions in the store). */ - async flagAllGroupSessionsForBackup(): Promise { + public async flagAllGroupSessionsForBackup(): Promise { await this.baseApis._crypto._cryptoStore.doTxn( 'readwrite', [ @@ -533,13 +533,13 @@ export class BackupManager { * Counts the number of end to end session keys that are waiting to be backed up * @returns {Promise} Resolves to the number of sessions requiring backup */ - countSessionsNeedingBackup(): Promise { + public countSessionsNeedingBackup(): Promise { return this.baseApis._crypto._cryptoStore.countSessionsNeedingBackup(); } } export class Curve25519 implements BackupAlgorithm { - static algorithmName = "m.megolm_backup.v1.curve25519-aes-sha2"; + public static algorithmName = "m.megolm_backup.v1.curve25519-aes-sha2"; constructor( public authData: AuthData, @@ -547,7 +547,7 @@ export class Curve25519 implements BackupAlgorithm { private getKey: () => Promise, ) {} - static async init( + public static async init( authData: AuthData, getKey: () => Promise, ): Promise { @@ -559,7 +559,7 @@ export class Curve25519 implements BackupAlgorithm { return new Curve25519(authData, publicKey, getKey); } - static async prepare( + public static async prepare( key: string | Uint8Array | null, ): Promise<[Uint8Array, AuthData]> { const decryption = new global.Olm.PkDecryption(); @@ -588,7 +588,7 @@ export class Curve25519 implements BackupAlgorithm { } } - async encryptSession(data: Record): Promise { + public async encryptSession(data: Record): Promise { const plainText: Record = Object.assign({}, data); delete plainText.session_id; delete plainText.room_id; @@ -596,7 +596,7 @@ export class Curve25519 implements BackupAlgorithm { return this.publicKey.encrypt(JSON.stringify(plainText)); } - async decryptSessions(sessions: Record>): Promise[]> { + public async decryptSessions(sessions: Record>): Promise[]> { const privKey = await this.getKey(); const decryption = new global.Olm.PkDecryption(); try { @@ -628,7 +628,7 @@ export class Curve25519 implements BackupAlgorithm { } } - async keyMatches(key: Uint8Array): Promise { + public async keyMatches(key: Uint8Array): Promise { const decryption = new global.Olm.PkDecryption(); let pubKey; try { @@ -640,7 +640,7 @@ export class Curve25519 implements BackupAlgorithm { return pubKey === this.authData.public_key; } - free(): void { + public free(): void { this.publicKey.free(); } } From ca85dfc6ff77cbd68bbe7aca817ac420ff591d11 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 3 Jun 2021 18:52:06 -0400 Subject: [PATCH 10/10] re-lint --- src/client.js | 5 ++--- src/crypto/backup.ts | 27 +++++++++++++-------------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/client.js b/src/client.js index 426dd037b..bad90bb52 100644 --- a/src/client.js +++ b/src/client.js @@ -1865,7 +1865,7 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function( } // eslint-disable-next-line camelcase - const {algorithm, auth_data, recovery_key, privateKey} = + const { algorithm, auth_data, recovery_key, privateKey } = await this._crypto._backupManager.prepareKeyBackupVersion(password); if (secureSecretStorage) { @@ -2260,10 +2260,9 @@ MatrixClient.prototype._restoreKeyBackup = async function( source: "backup", }); - // await this._crypto.setTrustedBackupPubKey(backupPubKey); await this.checkKeyBackup(); - return {total: totalKeyCount, imported: keys.length}; + return { total: totalKeyCount, imported: keys.length }; }; MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, version) { diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index 10a6d6356..d513c24aa 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -20,15 +20,15 @@ limitations under the License. * Classes for dealing with key backup. */ -import {MatrixClient} from "../client"; -import {logger} from "../logger"; -import {MEGOLM_ALGORITHM, verifySignature} from "./olmlib"; -import {DeviceInfo} from "./deviceinfo" -import {DeviceTrustLevel} from './CrossSigning'; -import {keyFromPassphrase} from './key_passphrase'; -import {sleep} from "../utils"; -import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store'; -import {encodeRecoveryKey} from './recoverykey'; +import { MatrixClient } from "../client"; +import { logger } from "../logger"; +import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib"; +import { DeviceInfo } from "./deviceinfo" +import { DeviceTrustLevel } from './CrossSigning'; +import { keyFromPassphrase } from './key_passphrase'; +import { sleep } from "../utils"; +import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; +import { encodeRecoveryKey } from './recoverykey'; const KEY_BACKUP_KEYS_PER_REQUEST = 200; @@ -220,7 +220,7 @@ export class BackupManager { } } - return {backupInfo, trustInfo}; + return { backupInfo, trustInfo }; } /** @@ -435,7 +435,7 @@ export class BackupManager { for (const session of sessions) { const roomId = session.sessionData.room_id; if (data[roomId] === undefined) { - data[roomId] = {sessions: {}}; + data[roomId] = { sessions: {} }; } const sessionData = await this.baseApis._crypto._olmDevice.exportInboundGroupSession( @@ -464,7 +464,7 @@ export class BackupManager { await this.baseApis.sendKeyBackup( undefined, undefined, this.backupInfo.version, - {rooms: data}, + { rooms: data }, ); await this.baseApis._crypto._cryptoStore.unmarkSessionsNeedingBackup(sessions); @@ -573,7 +573,6 @@ export class Curve25519 implements BackupAlgorithm { const derivation = await keyFromPassphrase(key); authData.private_key_salt = derivation.salt; authData.private_key_iterations = derivation.iterations; - // FIXME: algorithm? authData.public_key = decryption.init_with_private_key(derivation.key); } const publicKey = new global.Olm.PkEncryption(); @@ -604,7 +603,7 @@ export class Curve25519 implements BackupAlgorithm { if (backupPubKey !== this.authData.public_key) { // eslint-disable-next-line no-throw-literal - throw {errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY}; + throw { errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY }; } const keys = [];