From fc67dc6497c87d7b6efef25de99da47e4d99a650 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 19 Jun 2021 19:41:45 +0100 Subject: [PATCH 01/10] Convert crypto index to TS --- src/client.ts | 77 +- src/crypto/api.ts | 2 +- src/crypto/backup.ts | 49 +- src/crypto/dehydration.ts | 2 +- src/crypto/index.js | 3651 ------------------------------------ src/crypto/index.ts | 3745 +++++++++++++++++++++++++++++++++++++ src/crypto/keybackup.ts | 13 +- 7 files changed, 3815 insertions(+), 3724 deletions(-) delete mode 100644 src/crypto/index.js create mode 100644 src/crypto/index.ts diff --git a/src/client.ts b/src/client.ts index 1d54523cabd..58a3ab3ff82 100644 --- a/src/client.ts +++ b/src/client.ts @@ -47,7 +47,8 @@ import { PREFIX_UNSTABLE, retryNetworkOperation, } from "./http-api"; -import { Crypto, DeviceInfo, fixBackupKey, isCryptoAvailable } from './crypto'; +import { Crypto, fixBackupKey, IBootstrapCrossSigningOpts, isCryptoAvailable } from './crypto'; +import { DeviceInfo } from "./crypto/DeviceInfo"; import { decodeRecoveryKey } from './crypto/recoverykey'; import { keyFromAuthData } from './crypto/key_passphrase'; import { User } from "./models/user"; @@ -58,7 +59,6 @@ import { IKeyBackupPrepareOpts, IKeyBackupRestoreOpts, IKeyBackupRestoreResult, - IKeyBackupTrustInfo, IKeyBackupVersion, } from "./crypto/keybackup"; import { IIdentityServerProvider } from "./@types/IIdentityServerProvider"; @@ -114,7 +114,7 @@ import url from "url"; import { randomString } from "./randomstring"; import { ReadStream } from "fs"; import { WebStorageSessionStore } from "./store/session/webstorage"; -import { BackupManager } from "./crypto/backup"; +import { BackupManager, IKeyBackupCheck, TrustInfo } from "./crypto/backup"; import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE, MSC3089TreeSpace } from "./models/MSC3089TreeSpace"; export type Store = StubStore | MemoryStore | LocalIndexedDBStoreBackend | RemoteIndexedDBStoreBackend; @@ -141,6 +141,12 @@ interface IExportedDevice { deviceId: string; } +export interface IKeysUploadResponse { + one_time_key_counts: { // eslint-disable-line camelcase + [algorithm: string]: number; + }; +} + export interface ICreateClientOpts { baseUrl: string; @@ -836,7 +842,7 @@ export class MatrixClient extends EventEmitter { return; } // XXX: Private member access. - return await this.crypto._dehydrationManager.setKeyAndQueueDehydration( + return await this.crypto.dehydrationManager.setKeyAndQueueDehydration( key, keyInfo, deviceDisplayName, ); } @@ -859,11 +865,11 @@ export class MatrixClient extends EventEmitter { logger.warn('not dehydrating device if crypto is not enabled'); return; } - await this.crypto._dehydrationManager.setKey( + await this.crypto.dehydrationManager.setKey( key, keyInfo, deviceDisplayName, ); // XXX: Private member access. - return await this.crypto._dehydrationManager.dehydrateDevice(); + return await this.crypto.dehydrationManager.dehydrateDevice(); } public async exportDevice(): Promise { @@ -875,7 +881,7 @@ export class MatrixClient extends EventEmitter { userId: this.credentials.userId, deviceId: this.deviceId, // XXX: Private member access. - olmDevice: await this.crypto._olmDevice.export(), + olmDevice: await this.crypto.olmDevice.export(), }; } @@ -1239,12 +1245,12 @@ export class MatrixClient extends EventEmitter { * Upload the device keys to the homeserver. * @return {Promise} A promise that will resolve when the keys are uploaded. */ - public uploadKeys(): Promise { + public async uploadKeys(): Promise { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.uploadDeviceKeys(); + await this.crypto.uploadDeviceKeys(); } /** @@ -1631,7 +1637,7 @@ export class MatrixClient extends EventEmitter { * return true. * @return {boolean} True if cross-signing is ready to be used on this device */ - public isCrossSigningReady(): boolean { + public isCrossSigningReady(): Promise { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -1658,10 +1664,7 @@ export class MatrixClient extends EventEmitter { * auth data as an object. Can be called multiple times, first with an empty * authDict, to obtain the flows. */ - public bootstrapCrossSigning(opts: { - authUploadDeviceSigningKeys: (makeRequest: (authData: any) => void) => Promise, - setupNewCrossSigning?: boolean, - }) { + public bootstrapCrossSigning(opts: IBootstrapCrossSigningOpts) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -1756,7 +1759,7 @@ export class MatrixClient extends EventEmitter { * * @return {boolean} True if secret storage is ready to be used on this device */ - public isSecretStorageReady(): boolean { + public isSecretStorageReady(): Promise { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -1848,7 +1851,7 @@ export class MatrixClient extends EventEmitter { * * @return {string} the contents of the secret */ - public getSecret(name: string): string { + public getSecret(name: string): Promise { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -1885,7 +1888,7 @@ export class MatrixClient extends EventEmitter { * * @return {string} the contents of the secret */ - public requestSecret(name: string, devices: string[]): string { + public requestSecret(name: string, devices: string[]): any { // TODO types if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -1899,7 +1902,7 @@ export class MatrixClient extends EventEmitter { * * @return {string} The default key ID or null if no default key ID is set */ - public getDefaultSecretStorageKeyId(): string { + public getDefaultSecretStorageKeyId(): Promise { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -2075,8 +2078,8 @@ export class MatrixClient extends EventEmitter { * trust information (as returned by isKeyBackupTrusted) * in trustInfo. */ - public checkKeyBackup(): IKeyBackupVersion { - return this.crypto._backupManager.checkKeyBackup(); + public checkKeyBackup(): Promise { + return this.crypto.backupManager.checkKeyBackup(); } /** @@ -2117,8 +2120,8 @@ export class MatrixClient extends EventEmitter { * ] * } */ - public isKeyBackupTrusted(info: IKeyBackupVersion): IKeyBackupTrustInfo { - return this.crypto._backupManager.isKeyBackupTrusted(info); + public isKeyBackupTrusted(info: IKeyBackupVersion): Promise { + return this.crypto.backupManager.isKeyBackupTrusted(info); } /** @@ -2130,7 +2133,7 @@ export class MatrixClient extends EventEmitter { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto._backupManager.getKeyBackupEnabled(); + return this.crypto.backupManager.getKeyBackupEnabled(); } /** @@ -2145,7 +2148,7 @@ export class MatrixClient extends EventEmitter { throw new Error("End-to-end encryption disabled"); } - return this.crypto._backupManager.enableKeyBackup(info); + return this.crypto.backupManager.enableKeyBackup(info); } /** @@ -2156,7 +2159,7 @@ export class MatrixClient extends EventEmitter { throw new Error("End-to-end encryption disabled"); } - this.crypto._backupManager.disableKeyBackup(); + this.crypto.backupManager.disableKeyBackup(); } /** @@ -2184,7 +2187,7 @@ export class MatrixClient extends EventEmitter { // eslint-disable-next-line camelcase const { algorithm, auth_data, recovery_key, privateKey } = - await this.crypto._backupManager.prepareKeyBackupVersion(password); + await this.crypto.backupManager.prepareKeyBackupVersion(password); if (opts.secureSecretStorage) { await this.storeSecret("m.megolm_backup.v1", encodeBase64(privateKey)); @@ -2221,7 +2224,7 @@ export class MatrixClient extends EventEmitter { throw new Error("End-to-end encryption disabled"); } - await this.crypto._backupManager.createKeyBackupVersion(info); + await this.crypto.backupManager.createKeyBackupVersion(info); const data = { algorithm: info.algorithm, @@ -2232,19 +2235,19 @@ export class MatrixClient extends EventEmitter { // older devices with cross-signing. This can probably go away very soon in // favour of just signing with the cross-singing master key. // XXX: Private member access - await this.crypto._signObject(data.auth_data); + await this.crypto.signObject(data.auth_data); if ( this.cryptoCallbacks.getCrossSigningKey && // XXX: Private member access - this.crypto._crossSigningInfo.getId() + this.crypto.crossSigningInfo.getId() ) { // now also sign the auth data with the cross-signing master key // we check for the callback explicitly here because we still want to be able // to create an un-cross-signed key backup if there is a cross-signing key but // no callback supplied. // XXX: Private member access - await this.crypto._crossSigningInfo.signObject(data.auth_data, "master"); + await this.crypto.crossSigningInfo.signObject(data.auth_data, "master"); } const res = await this.http.authedRequest( @@ -2271,8 +2274,8 @@ export class MatrixClient extends EventEmitter { // If we're currently backing up to this backup... stop. // (We start using it automatically in createKeyBackupVersion // so this is symmetrical). - if (this.crypto._backupManager.version) { - this.crypto._backupManager.disableKeyBackup(); + if (this.crypto.backupManager.version) { + this.crypto.backupManager.disableKeyBackup(); } const path = utils.encodeUri("/room_keys/version/$version", { @@ -2337,7 +2340,7 @@ export class MatrixClient extends EventEmitter { throw new Error("End-to-end encryption disabled"); } - await this.crypto._backupManager.scheduleAllGroupSessionsForBackup(); + await this.crypto.backupManager.scheduleAllGroupSessionsForBackup(); } /** @@ -2350,7 +2353,7 @@ export class MatrixClient extends EventEmitter { throw new Error("End-to-end encryption disabled"); } - return this.crypto._backupManager.flagAllGroupSessionsForBackup(); + return this.crypto.backupManager.flagAllGroupSessionsForBackup(); } public isValidRecoveryKey(recoveryKey: string): boolean { @@ -2633,7 +2636,7 @@ export class MatrixClient extends EventEmitter { } // XXX: Private member access - const alg = this.crypto._getRoomDecryptor(roomId, roomEncryption.algorithm); + const alg = this.crypto.getRoomDecryptor(roomId, roomEncryption.algorithm); if (alg.sendSharedHistoryInboundSessions) { await alg.sendSharedHistoryInboundSessions(devicesByUser); } else { @@ -5708,7 +5711,7 @@ export class MatrixClient extends EventEmitter { */ public getCrossSigningCacheCallbacks(): any { // TODO: Types // XXX: Private member access - return this.crypto?._crossSigningInfo.getCacheCallbacks(); + return this.crypto?.crossSigningInfo.getCacheCallbacks(); } /** @@ -7087,7 +7090,7 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: result object. Rejects: with * an error response ({@link module:http-api.MatrixError}). */ - public uploadKeysRequest(content: any, opts?: any, callback?: Callback): Promise { // TODO: Types + public uploadKeysRequest(content: any, opts?: any, callback?: Callback): Promise { return this.http.authedRequest(callback, "POST", "/keys/upload", undefined, content); } diff --git a/src/crypto/api.ts b/src/crypto/api.ts index 24528486e35..39469a83a56 100644 --- a/src/crypto/api.ts +++ b/src/crypto/api.ts @@ -60,7 +60,7 @@ export interface IEncryptedEventInfo { export interface IRecoveryKey { keyInfo: { - pubkey: Uint8Array; + pubkey: string; passphrase?: { algorithm: string; iterations: number; diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index 79b7e975294..f2b62cc01d0 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -48,11 +48,16 @@ type SigInfo = { deviceTrust?: DeviceTrustLevel, }; -type TrustInfo = { +export type TrustInfo = { usable: boolean, // is the backup trusted, true iff there is a sig that is valid & from a trusted device sigs: SigInfo[], }; +export interface IKeyBackupCheck { + backupInfo: BackupInfo; + trustInfo: TrustInfo; +} + /** A function used to get the secret key for a backup. */ type GetKey = () => Promise; @@ -81,7 +86,7 @@ interface BackupAlgorithm { */ export class BackupManager { private algorithm: BackupAlgorithm | undefined; - private backupInfo: BackupInfo | undefined; // The info dict from /room_keys/version + public 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: MatrixClient, public readonly getKey: GetKey) { @@ -232,7 +237,7 @@ export class BackupManager { * trust information (as returned by isKeyBackupTrusted) * in trustInfo. */ - public async checkKeyBackup(): Promise<{backupInfo: BackupInfo, trustInfo: TrustInfo}> { + public async checkKeyBackup(): Promise { this.checkedForBackup = false; return this.checkAndStart(); } @@ -268,7 +273,7 @@ export class BackupManager { return ret; } - const trustedPubkey = this.baseApis.crypto._sessionStore.getLocalTrustedBackupPubKey(); + const trustedPubkey = this.baseApis.crypto.sessionStore.getLocalTrustedBackupPubKey(); if (backupInfo.auth_data.public_key === trustedPubkey) { logger.info("Backup public key " + trustedPubkey + " is trusted locally"); @@ -288,12 +293,12 @@ export class BackupManager { 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(); + const crossSigningId = this.baseApis.crypto.crossSigningInfo.getId(); if (crossSigningId === sigInfo.deviceId) { sigInfo.crossSigningId = true; try { await verifySignature( - this.baseApis.crypto._olmDevice, + this.baseApis.crypto.olmDevice, backupInfo.auth_data, this.baseApis.getUserId(), sigInfo.deviceId, @@ -313,7 +318,7 @@ export class BackupManager { // 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( + const device = this.baseApis.crypto.deviceList.getStoredDevice( this.baseApis.getUserId(), sigInfo.deviceId, ); if (device) { @@ -323,7 +328,7 @@ export class BackupManager { ); try { await verifySignature( - this.baseApis.crypto._olmDevice, + this.baseApis.crypto.olmDevice, backupInfo.auth_data, this.baseApis.getUserId(), device.deviceId, @@ -423,12 +428,12 @@ export class BackupManager { * @returns {integer} Number of sessions backed up */ private async backupPendingKeys(limit: number): Promise { - const sessions = await this.baseApis.crypto._cryptoStore.getSessionsNeedingBackup(limit); + const sessions = await this.baseApis.crypto.cryptoStore.getSessionsNeedingBackup(limit); if (!sessions.length) { return 0; } - let remaining = await this.baseApis.crypto._cryptoStore.countSessionsNeedingBackup(); + let remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup(); this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining); const data = {}; @@ -438,7 +443,7 @@ export class BackupManager { data[roomId] = { sessions: {} }; } - const sessionData = await this.baseApis.crypto._olmDevice.exportInboundGroupSession( + const sessionData = await this.baseApis.crypto.olmDevice.exportInboundGroupSession( session.senderKey, session.sessionId, session.sessionData, ); sessionData.algorithm = MEGOLM_ALGORITHM; @@ -446,13 +451,13 @@ export class BackupManager { const forwardedCount = (sessionData.forwarding_curve25519_key_chain || []).length; - const userId = this.baseApis.crypto._deviceList.getUserByIdentityKey( + const userId = this.baseApis.crypto.deviceList.getUserByIdentityKey( MEGOLM_ALGORITHM, session.senderKey, ); - const device = this.baseApis.crypto._deviceList.getDeviceByIdentityKey( + const device = this.baseApis.crypto.deviceList.getDeviceByIdentityKey( MEGOLM_ALGORITHM, session.senderKey, ); - const verified = this.baseApis.crypto._checkDeviceInfoTrust(userId, device).isVerified(); + const verified = this.baseApis.crypto.checkDeviceInfoTrust(userId, device).isVerified(); data[roomId]['sessions'][session.sessionId] = { first_message_index: sessionData.first_known_index, @@ -467,8 +472,8 @@ export class BackupManager { { rooms: data }, ); - await this.baseApis.crypto._cryptoStore.unmarkSessionsNeedingBackup(sessions); - remaining = await this.baseApis.crypto._cryptoStore.countSessionsNeedingBackup(); + await this.baseApis.crypto.cryptoStore.unmarkSessionsNeedingBackup(sessions); + remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup(); this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining); return sessions.length; @@ -477,7 +482,7 @@ export class BackupManager { public async backupGroupSession( senderKey: string, sessionId: string, ): Promise { - await this.baseApis.crypto._cryptoStore.markSessionsNeedingBackup([{ + await this.baseApis.crypto.cryptoStore.markSessionsNeedingBackup([{ senderKey: senderKey, sessionId: sessionId, }]); @@ -509,22 +514,22 @@ export class BackupManager { * (which will be equal to the number of sessions in the store). */ public async flagAllGroupSessionsForBackup(): Promise { - await this.baseApis.crypto._cryptoStore.doTxn( + await this.baseApis.crypto.cryptoStore.doTxn( 'readwrite', [ IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, IndexedDBCryptoStore.STORE_BACKUP, ], (txn) => { - this.baseApis.crypto._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => { + this.baseApis.crypto.cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => { if (session !== null) { - this.baseApis.crypto._cryptoStore.markSessionsNeedingBackup([session], txn); + this.baseApis.crypto.cryptoStore.markSessionsNeedingBackup([session], txn); } }); }, ); - const remaining = await this.baseApis.crypto._cryptoStore.countSessionsNeedingBackup(); + const remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup(); this.baseApis.emit("crypto.keyBackupSessionsRemaining", remaining); return remaining; } @@ -534,7 +539,7 @@ export class BackupManager { * @returns {Promise} Resolves to the number of sessions requiring backup */ public countSessionsNeedingBackup(): Promise { - return this.baseApis.crypto._cryptoStore.countSessionsNeedingBackup(); + return this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup(); } } diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index f32daaeb71e..eb003f75bdf 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -292,7 +292,7 @@ export class DehydrationManager { } } - private stop() { + public stop() { if (this.timeoutId) { global.clearTimeout(this.timeoutId); this.timeoutId = undefined; diff --git a/src/crypto/index.js b/src/crypto/index.js deleted file mode 100644 index a1171b5f51b..00000000000 --- a/src/crypto/index.js +++ /dev/null @@ -1,3651 +0,0 @@ -/* -Copyright 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2018-2019 New Vector Ltd -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. -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 - */ - -import anotherjson from "another-json"; -import { EventEmitter } from 'events'; -import { ReEmitter } from '../ReEmitter'; -import { logger } from '../logger'; -import * as utils from "../utils"; -import { OlmDevice } from "./OlmDevice"; -import * as olmlib from "./olmlib"; -import { DeviceList } from "./DeviceList"; -import { DeviceInfo } from "./deviceinfo"; -import * as algorithms from "./algorithms"; -import { - CrossSigningInfo, - DeviceTrustLevel, - UserTrustLevel, - createCryptoStoreCacheCallbacks, -} from './CrossSigning'; -import { EncryptionSetupBuilder } from "./EncryptionSetup"; -import { SECRET_STORAGE_ALGORITHM_V1_AES, SecretStorage } from './SecretStorage'; -import { OutgoingRoomKeyRequestManager } from './OutgoingRoomKeyRequestManager'; -import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; -import { - ReciprocateQRCode, - SCAN_QR_CODE_METHOD, - SHOW_QR_CODE_METHOD, -} from './verification/QRCode'; -import { SAS } from './verification/SAS'; -import { keyFromPassphrase } from './key_passphrase'; -import { encodeRecoveryKey, decodeRecoveryKey } from './recoverykey'; -import { VerificationRequest } from "./verification/request/VerificationRequest"; -import { InRoomChannel, InRoomRequests } from "./verification/request/InRoomChannel"; -import { ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDeviceChannel"; -import { IllegalMethod } from "./verification/IllegalMethod"; -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; - -const defaultVerificationMethods = { - [ReciprocateQRCode.NAME]: ReciprocateQRCode, - [SAS.NAME]: SAS, - - // These two can't be used for actual verification, but we do - // need to be able to define them here for the verification flows - // to start. - [SHOW_QR_CODE_METHOD]: IllegalMethod, - [SCAN_QR_CODE_METHOD]: IllegalMethod, -}; - -/** - * verification method names - */ -export const verificationMethods = { - RECIPROCATE_QR_CODE: ReciprocateQRCode.NAME, - SAS: SAS.NAME, -}; - -export function isCryptoAvailable() { - return Boolean(global.Olm); -} - -const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000; - -/** - * Cryptography bits - * - * This module is internal to the js-sdk; the public API is via MatrixClient. - * - * @constructor - * @alias module:crypto - * - * @internal - * - * @param {MatrixClient} baseApis base matrix api interface - * - * @param {module:store/session/webstorage~WebStorageSessionStore} sessionStore - * Store to be used for end-to-end crypto session data - * - * @param {string} userId The user ID for the local user - * - * @param {string} deviceId The identifier for this device. - * - * @param {Object} clientStore the MatrixClient data store. - * - * @param {module:crypto/store/base~CryptoStore} cryptoStore - * storage for the crypto layer. - * - * @param {RoomList} roomList An initialised RoomList object - * - * @param {Array} verificationMethods Array of verification methods to use. - * Each element can either be a string from MatrixClient.verificationMethods - * or a class that implements a verification method. - */ -export function Crypto(baseApis, sessionStore, userId, deviceId, - clientStore, cryptoStore, roomList, verificationMethods) { - this._onDeviceListUserCrossSigningUpdated = - this._onDeviceListUserCrossSigningUpdated.bind(this); - - this._trustCrossSignedDevices = true; - - this._reEmitter = new ReEmitter(this); - this._baseApis = baseApis; - this._sessionStore = sessionStore; - this._userId = userId; - this._deviceId = deviceId; - this._clientStore = clientStore; - this._cryptoStore = cryptoStore; - this._roomList = roomList; - if (verificationMethods) { - this._verificationMethods = new Map(); - for (const method of verificationMethods) { - if (typeof method === "string") { - if (defaultVerificationMethods[method]) { - this._verificationMethods.set( - method, - defaultVerificationMethods[method], - ); - } - } else if (method.NAME) { - this._verificationMethods.set( - method.NAME, - method, - ); - } else { - logger.warn(`Excluding unknown verification method ${method}`); - } - } - } else { - this._verificationMethods = defaultVerificationMethods; - } - - 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 olmlib.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( - baseApis, cryptoStore, this._olmDevice, - ); - // XXX: This isn't removed at any point, but then none of the event listeners - // this class sets seem to be removed at any point... :/ - this._deviceList.on( - 'userCrossSigningUpdated', this._onDeviceListUserCrossSigningUpdated, - ); - this._reEmitter.reEmit(this._deviceList, [ - "crypto.devicesUpdated", "crypto.willUpdateDevices", - ]); - - // the last time we did a check for the number of one-time-keys on the - // server. - this._lastOneTimeKeyCheck = null; - this._oneTimeKeyCheckInProgress = false; - - // EncryptionAlgorithm instance for each room - this._roomEncryptors = {}; - - // map from algorithm to DecryptionAlgorithm instance, for each room - this._roomDecryptors = {}; - - this._supportedAlgorithms = Object.keys( - algorithms.DECRYPTION_CLASSES, - ); - - this._deviceKeys = {}; - - this._globalBlacklistUnverifiedDevices = false; - this._globalErrorOnUnknownDevices = true; - - this._outgoingRoomKeyRequestManager = new OutgoingRoomKeyRequestManager( - baseApis, this._deviceId, this._cryptoStore, - ); - - // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations - // we received in the current sync. - this._receivedRoomKeyRequests = []; - this._receivedRoomKeyRequestCancellations = []; - // true if we are currently processing received room key requests - this._processingRoomKeyRequests = false; - // controls whether device tracking is delayed - // until calling encryptEvent or trackRoomDevices, - // or done immediately upon enabling room encryption. - this._lazyLoadMembers = false; - // in case _lazyLoadMembers is true, - // track if an initial tracking of all the room members - // has happened for a given room. This is delayed - // to avoid loading room members as long as possible. - this._roomDeviceTrackingState = {}; - - // The timestamp of the last time we forced establishment - // of a new session for each device, in milliseconds. - // { - // userId: { - // deviceId: 1234567890000, - // }, - // } - this._lastNewSessionForced = {}; - - this._toDeviceVerificationRequests = new ToDeviceRequests(); - this._inRoomVerificationRequests = new InRoomRequests(); - - // This flag will be unset whilst the client processes a sync response - // so that we don't start requesting keys until we've actually finished - // processing the response. - this._sendKeyRequestsImmediately = false; - - const cryptoCallbacks = this._baseApis.cryptoCallbacks || {}; - const cacheCallbacks = createCryptoStoreCacheCallbacks(cryptoStore, this._olmDevice); - - this._crossSigningInfo = new CrossSigningInfo( - userId, - cryptoCallbacks, - cacheCallbacks, - ); - - this._secretStorage = new SecretStorage( - baseApis, cryptoCallbacks, - ); - - this._dehydrationManager = new DehydrationManager(this); - - // Assuming no app-supplied callback, default to getting from SSSS. - if (!cryptoCallbacks.getCrossSigningKey && cryptoCallbacks.getSecretStorageKey) { - cryptoCallbacks.getCrossSigningKey = async (type) => { - return CrossSigningInfo.getFromSecretStorage(type, this._secretStorage); - }; - } -} -utils.inherits(Crypto, EventEmitter); - -/** - * Initialise the crypto module so that it is ready for use - * - * Returns a promise which resolves once the crypto module is ready for use. - * - * @param {Object} opts keyword arguments. - * @param {string} opts.exportedOlmDevice (Optional) data from exported device - * that must be re-created. - */ -Crypto.prototype.init = async function(opts) { - const { - exportedOlmDevice, - pickleKey, - } = opts || {}; - - logger.log("Crypto: initialising Olm..."); - await global.Olm.init(); - logger.log( - exportedOlmDevice - ? "Crypto: initialising Olm device from exported device..." - : "Crypto: initialising Olm device...", - ); - await this._olmDevice.init({ fromExportedDevice: exportedOlmDevice, pickleKey }); - logger.log("Crypto: loading device list..."); - await this._deviceList.load(); - - // build our device keys: these will later be uploaded - this._deviceKeys["ed25519:" + this._deviceId] = - this._olmDevice.deviceEd25519Key; - this._deviceKeys["curve25519:" + this._deviceId] = - this._olmDevice.deviceCurve25519Key; - - logger.log("Crypto: fetching own devices..."); - let myDevices = this._deviceList.getRawStoredDevicesForUser( - this._userId, - ); - - if (!myDevices) { - myDevices = {}; - } - - if (!myDevices[this._deviceId]) { - // add our own deviceinfo to the cryptoStore - logger.log("Crypto: adding this device to the store..."); - const deviceInfo = { - keys: this._deviceKeys, - algorithms: this._supportedAlgorithms, - verified: DeviceVerification.VERIFIED, - known: true, - }; - - myDevices[this._deviceId] = deviceInfo; - this._deviceList.storeDevicesForUser( - this._userId, myDevices, - ); - this._deviceList.saveIfDirty(); - } - - await this._cryptoStore.doTxn( - 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], - (txn) => { - this._cryptoStore.getCrossSigningKeys(txn, (keys) => { - // can be an empty object after resetting cross-signing keys, see _storeTrustedSelfKeys - if (keys && Object.keys(keys).length !== 0) { - logger.log("Loaded cross-signing public keys from crypto store"); - this._crossSigningInfo.setKeys(keys); - } - }); - }, - ); - // make sure we are keeping track of our own devices - // (this is important for key backups & things) - this._deviceList.startTrackingDeviceList(this._userId); - - logger.log("Crypto: checking for key backup..."); - this._backupManager.checkAndStart(); -}; - -/** - * Whether to trust a others users signatures of their devices. - * If false, devices will only be considered 'verified' if we have - * verified that device individually (effectively disabling cross-signing). - * - * Default: true - * - * @return {bool} True if trusting cross-signed devices - */ -Crypto.prototype.getCryptoTrustCrossSignedDevices = function() { - return this._trustCrossSignedDevices; -}; - -/** - * See getCryptoTrustCrossSignedDevices - - * This may be set before initCrypto() is called to ensure no races occur. - * - * @param {bool} val True to trust cross-signed devices - */ -Crypto.prototype.setCryptoTrustCrossSignedDevices = function(val) { - this._trustCrossSignedDevices = val; - - for (const userId of this._deviceList.getKnownUserIds()) { - const devices = this._deviceList.getRawStoredDevicesForUser(userId); - for (const deviceId of Object.keys(devices)) { - const deviceTrust = this.checkDeviceTrust(userId, deviceId); - // If the device is locally verified then isVerified() is always true, - // so this will only have caused the value to change if the device is - // cross-signing verified but not locally verified - if ( - !deviceTrust.isLocallyVerified() && - deviceTrust.isCrossSigningVerified() - ) { - const deviceObj = this._deviceList.getStoredDevice(userId, deviceId); - this.emit("deviceVerificationChanged", userId, deviceId, deviceObj); - } - } - } -}; - -/** - * Create a recovery key from a user-supplied passphrase. - * - * @param {string} password Passphrase string that can be entered by the user - * when restoring the backup as an alternative to entering the recovery key. - * Optional. - * @returns {Promise} Object with public key metadata, encoded private - * recovery key which should be disposed of after displaying to the user, - * and raw private key to avoid round tripping if needed. - */ -Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) { - const decryption = new global.Olm.PkDecryption(); - try { - const keyInfo = {}; - if (password) { - const derivation = await keyFromPassphrase(password); - keyInfo.passphrase = { - algorithm: "m.pbkdf2", - iterations: derivation.iterations, - salt: derivation.salt, - }; - keyInfo.pubkey = decryption.init_with_private_key(derivation.key); - } else { - keyInfo.pubkey = decryption.generate_key(); - } - const privateKey = decryption.get_private_key(); - const encodedPrivateKey = encodeRecoveryKey(privateKey); - return { keyInfo, encodedPrivateKey, privateKey }; - } finally { - if (decryption) decryption.free(); - } -}; - -/** - * Checks whether cross signing: - * - is enabled on this account and trusted by this device - * - has private keys either cached locally or stored in secret storage - * - * If this function returns false, bootstrapCrossSigning() can be used - * to fix things such that it returns true. That is to say, after - * bootstrapCrossSigning() completes successfully, this function should - * return true. - * - * The cross-signing API is currently UNSTABLE and may change without notice. - * - * @return {bool} True if cross-signing is ready to be used on this device - */ -Crypto.prototype.isCrossSigningReady = async function() { - const publicKeysOnDevice = this._crossSigningInfo.getId(); - const privateKeysExistSomewhere = ( - await this._crossSigningInfo.isStoredInKeyCache() || - await this._crossSigningInfo.isStoredInSecretStorage( - this._secretStorage, - ) - ); - - return !!( - publicKeysOnDevice && - privateKeysExistSomewhere - ); -}; - -/** - * Checks whether secret storage: - * - is enabled on this account - * - is storing cross-signing private keys - * - is storing session backup key (if enabled) - * - * If this function returns false, bootstrapSecretStorage() can be used - * to fix things such that it returns true. That is to say, after - * bootstrapSecretStorage() completes successfully, this function should - * return true. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @return {bool} True if secret storage is ready to be used on this device - */ -Crypto.prototype.isSecretStorageReady = async function() { - const secretStorageKeyInAccount = await this._secretStorage.hasKey(); - const privateKeysInStorage = await this._crossSigningInfo.isStoredInSecretStorage( - this._secretStorage, - ); - const sessionBackupInStorage = ( - !this._backupManager.getKeyBackupEnabled() || - this._baseApis.isKeyBackupKeyStored() - ); - - return !!( - secretStorageKeyInAccount && - privateKeysInStorage && - sessionBackupInStorage - ); -}; - -/** - * Bootstrap cross-signing by creating keys if needed. If everything is already - * set up, then no changes are made, so this is safe to run to ensure - * cross-signing is ready for use. - * - * This function: - * - creates new cross-signing keys if they are not found locally cached nor in - * secret storage (if it has been setup) - * - * The cross-signing API is currently UNSTABLE and may change without notice. - * - * @param {function} opts.authUploadDeviceSigningKeys Function - * called to await an interactive auth flow when uploading device signing keys. - * @param {bool} [opts.setupNewCrossSigning] Optional. Reset even if keys - * already exist. - * Args: - * {function} A function that makes the request requiring auth. Receives the - * auth data as an object. Can be called multiple times, first with an empty - * authDict, to obtain the flows. - */ -Crypto.prototype.bootstrapCrossSigning = async function({ - authUploadDeviceSigningKeys, - setupNewCrossSigning, -} = {}) { - logger.log("Bootstrapping cross-signing"); - - const delegateCryptoCallbacks = this._baseApis.cryptoCallbacks; - const builder = new EncryptionSetupBuilder( - this._baseApis.store.accountData, - delegateCryptoCallbacks, - ); - const crossSigningInfo = new CrossSigningInfo( - this._userId, - builder.crossSigningCallbacks, - builder.crossSigningCallbacks, - ); - - // Reset the cross-signing keys - const resetCrossSigning = async () => { - crossSigningInfo.resetKeys(); - // Sign master key with device key - await this._signObject(crossSigningInfo.keys.master); - - // Store auth flow helper function, as we need to call it when uploading - // to ensure we handle auth errors properly. - builder.addCrossSigningKeys(authUploadDeviceSigningKeys, crossSigningInfo.keys); - - // Cross-sign own device - const device = this._deviceList.getStoredDevice(this._userId, this._deviceId); - const deviceSignature = await crossSigningInfo.signDevice(this._userId, device); - builder.addKeySignature(this._userId, this._deviceId, deviceSignature); - - // Sign message key backup with cross-signing master key - if (this._backupManager.backupInfo) { - await crossSigningInfo.signObject( - this._backupManager.backupInfo.auth_data, "master", - ); - builder.addSessionBackup(this._backupManager.backupInfo); - } - }; - - const publicKeysOnDevice = this._crossSigningInfo.getId(); - const privateKeysInCache = await this._crossSigningInfo.isStoredInKeyCache(); - const privateKeysInStorage = await this._crossSigningInfo.isStoredInSecretStorage( - this._secretStorage, - ); - const privateKeysExistSomewhere = ( - privateKeysInCache || - privateKeysInStorage - ); - - // Log all relevant state for easier parsing of debug logs. - logger.log({ - setupNewCrossSigning, - publicKeysOnDevice, - privateKeysInCache, - privateKeysInStorage, - privateKeysExistSomewhere, - }); - - if (!privateKeysExistSomewhere || setupNewCrossSigning) { - logger.log( - "Cross-signing private keys not found locally or in secret storage, " + - "creating new keys", - ); - // If a user has multiple devices, it important to only call bootstrap - // as part of some UI flow (and not silently during startup), as they - // may have setup cross-signing on a platform which has not saved keys - // to secret storage, and this would reset them. In such a case, you - // should prompt the user to verify any existing devices first (and - // request private keys from those devices) before calling bootstrap. - await resetCrossSigning(); - } else if (publicKeysOnDevice && privateKeysInCache) { - logger.log( - "Cross-signing public keys trusted and private keys found locally", - ); - } else if (privateKeysInStorage) { - logger.log( - "Cross-signing private keys not found locally, but they are available " + - "in secret storage, reading storage and caching locally", - ); - await this.checkOwnCrossSigningTrust({ - allowPrivateKeyRequests: true, - }); - } - - // Assuming no app-supplied callback, default to storing new private keys in - // secret storage if it exists. If it does not, it is assumed this will be - // done as part of setting up secret storage later. - const crossSigningPrivateKeys = builder.crossSigningCallbacks.privateKeys; - if ( - crossSigningPrivateKeys.size && - !this._baseApis.cryptoCallbacks.saveCrossSigningKeys - ) { - const secretStorage = new SecretStorage( - builder.accountDataClientAdapter, - builder.ssssCryptoCallbacks); - if (await secretStorage.hasKey()) { - logger.log("Storing new cross-signing private keys in secret storage"); - // This is writing to in-memory account data in - // builder.accountDataClientAdapter so won't fail - await CrossSigningInfo.storeInSecretStorage( - crossSigningPrivateKeys, - secretStorage, - ); - } - } - - const operation = builder.buildOperation(); - await operation.apply(this); - // This persists private keys and public keys as trusted, - // only do this if apply succeeded for now as retry isn't in place yet - await builder.persist(this); - - logger.log("Cross-signing ready"); -}; - -/** - * Bootstrap Secure Secret Storage if needed by creating a default key. If everything is - * already set up, then no changes are made, so this is safe to run to ensure secret - * storage is ready for use. - * - * This function - * - creates a new Secure Secret Storage key if no default key exists - * - if a key backup exists, it is migrated to store the key in the Secret - * Storage - * - creates a backup if none exists, and one is requested - * - migrates Secure Secret Storage to use the latest algorithm, if an outdated - * algorithm is found - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @param {function} [opts.createSecretStorageKey] Optional. Function - * called to await a secret storage key creation flow. - * Returns: - * {Promise} Object with public key metadata, encoded private - * recovery key which should be disposed of after displaying to the user, - * and raw private key to avoid round tripping if needed. - * @param {object} [opts.keyBackupInfo] The current key backup object. If passed, - * the passphrase and recovery key from this backup will be used. - * @param {bool} [opts.setupNewKeyBackup] If true, a new key backup version will be - * created and the private key stored in the new SSSS store. Ignored if keyBackupInfo - * is supplied. - * @param {bool} [opts.setupNewSecretStorage] Optional. Reset even if keys already exist. - * @param {func} [opts.getKeyBackupPassphrase] Optional. Function called to get the user's - * current key backup passphrase. Should return a promise that resolves with a Buffer - * containing the key, or rejects if the key cannot be obtained. - * Returns: - * {Promise} A promise which resolves to key creation data for - * SecretStorage#addKey: an object with `passphrase` etc fields. - */ -Crypto.prototype.bootstrapSecretStorage = async function({ - createSecretStorageKey = async () => ({ }), - keyBackupInfo, - setupNewKeyBackup, - setupNewSecretStorage, - getKeyBackupPassphrase, -} = {}) { - logger.log("Bootstrapping Secure Secret Storage"); - const delegateCryptoCallbacks = this._baseApis.cryptoCallbacks; - const builder = new EncryptionSetupBuilder( - this._baseApis.store.accountData, - delegateCryptoCallbacks, - ); - const secretStorage = new SecretStorage( - builder.accountDataClientAdapter, - builder.ssssCryptoCallbacks, - ); - - // the ID of the new SSSS key, if we create one - let newKeyId = null; - - // create a new SSSS key and set it as default - const createSSSS = async (opts, privateKey) => { - opts = opts || {}; - if (privateKey) { - opts.key = privateKey; - } - - const { keyId, keyInfo } = await secretStorage.addKey( - SECRET_STORAGE_ALGORITHM_V1_AES, opts, - ); - - if (privateKey) { - // make the private key available to encrypt 4S secrets - builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey); - } - - await secretStorage.setDefaultKeyId(keyId); - return keyId; - }; - - const ensureCanCheckPassphrase = async (keyId, keyInfo) => { - if (!keyInfo.mac) { - const key = await this._baseApis.cryptoCallbacks.getSecretStorageKey( - { keys: { [keyId]: keyInfo } }, "", - ); - if (key) { - const privateKey = key[1]; - builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey); - const { iv, mac } = await SecretStorage._calculateKeyCheck(privateKey); - keyInfo.iv = iv; - keyInfo.mac = mac; - - await builder.setAccountData( - `m.secret_storage.key.${keyId}`, keyInfo, - ); - } - } - }; - - const signKeyBackupWithCrossSigning = async (keyBackupAuthData) => { - if ( - this._crossSigningInfo.getId() && - await this._crossSigningInfo.isStoredInKeyCache("master") - ) { - try { - logger.log("Adding cross-signing signature to key backup"); - await this._crossSigningInfo.signObject(keyBackupAuthData, "master"); - } catch (e) { - // This step is not critical (just helpful), so we catch here - // and continue if it fails. - logger.error("Signing key backup with cross-signing keys failed", e); - } - } else { - logger.warn( - "Cross-signing keys not available, skipping signature on key backup", - ); - } - }; - - const oldSSSSKey = await this.getSecretStorageKey(); - const [oldKeyId, oldKeyInfo] = oldSSSSKey || [null, null]; - const storageExists = ( - !setupNewSecretStorage && - oldKeyInfo && - oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES - ); - - // Log all relevant state for easier parsing of debug logs. - logger.log({ - keyBackupInfo, - setupNewKeyBackup, - setupNewSecretStorage, - storageExists, - oldKeyInfo, - }); - - if (!storageExists && !keyBackupInfo) { - // either we don't have anything, or we've been asked to restart - // from scratch - logger.log( - "Secret storage does not exist, creating new storage key", - ); - - // if we already have a usable default SSSS key and aren't resetting - // SSSS just use it. otherwise, create a new one - // Note: we leave the old SSSS key in place: there could be other - // secrets using it, in theory. We could move them to the new key but a) - // that would mean we'd need to prompt for the old passphrase, and b) - // it's not clear that would be the right thing to do anyway. - const { keyInfo, privateKey } = await createSecretStorageKey(); - newKeyId = await createSSSS(keyInfo, privateKey); - } else if (!storageExists && keyBackupInfo) { - // we have an existing backup, but no SSSS - logger.log("Secret storage does not exist, using key backup key"); - - // if we have the backup key already cached, use it; otherwise use the - // callback to prompt for the key - const backupKey = await this.getSessionBackupPrivateKey() || - await getKeyBackupPassphrase(); - - // create a new SSSS key and use the backup key as the new SSSS key - const opts = {}; - - if ( - 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, - salt: keyBackupInfo.auth_data.private_key_salt, - bits: 256, - }; - } - - newKeyId = await createSSSS(opts, backupKey); - - // store the backup key in secret storage - await secretStorage.store( - "m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId], - ); - - // The backup is trusted because the user provided the private key. - // Sign the backup with the cross-signing key so the key backup can - // be trusted via cross-signing. - await signKeyBackupWithCrossSigning(keyBackupInfo.auth_data); - - builder.addSessionBackup(keyBackupInfo); - } else { - // 4S is already set up - logger.log("Secret storage exists"); - - if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { - // make sure that the default key has the information needed to - // check the passphrase - await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo); - } - } - - // If we have cross-signing private keys cached, store them in secret - // storage if they are not there already. - if ( - !this._baseApis.cryptoCallbacks.saveCrossSigningKeys && - await this.isCrossSigningReady() && - (newKeyId || !await this._crossSigningInfo.isStoredInSecretStorage(secretStorage)) - ) { - logger.log("Copying cross-signing private keys from cache to secret storage"); - const crossSigningPrivateKeys = - await this._crossSigningInfo.getCrossSigningKeysFromCache(); - // This is writing to in-memory account data in - // builder.accountDataClientAdapter so won't fail - await CrossSigningInfo.storeInSecretStorage( - crossSigningPrivateKeys, - secretStorage, - ); - } - - if (setupNewKeyBackup && !keyBackupInfo) { - logger.log("Creating new message key backup version"); - const info = await this._baseApis.prepareKeyBackupVersion( - null /* random key */, - // don't write to secret storage, as it will write to this._secretStorage. - // Here, we want to capture all the side-effects of bootstrapping, - // and want to write to the local secretStorage object - { secureSecretStorage: false }, - ); - // write the key ourselves to 4S - const privateKey = decodeRecoveryKey(info.recovery_key); - await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(privateKey)); - - // create keyBackupInfo object to add to builder - const data = { - algorithm: info.algorithm, - auth_data: info.auth_data, - }; - - // Sign with cross-signing master key - await signKeyBackupWithCrossSigning(data.auth_data); - - // sign with the device fingerprint - await this._signObject(data.auth_data); - - builder.addSessionBackup(data); - } - - // Cache the session backup key - const sessionBackupKey = await secretStorage.get('m.megolm_backup.v1'); - if (sessionBackupKey) { - logger.info("Got session backup key from secret storage: caching"); - // fix up the backup key if it's in the wrong format, and replace - // in secret storage - const fixedBackupKey = fixBackupKey(sessionBackupKey); - if (fixedBackupKey) { - await secretStorage.store("m.megolm_backup.v1", - fixedBackupKey, [newKeyId || oldKeyId], - ); - } - const decodedBackupKey = new Uint8Array(olmlib.decodeBase64( - fixedBackupKey || sessionBackupKey, - )); - await builder.addSessionBackupPrivateKeyToCache(decodedBackupKey); - } - - const operation = builder.buildOperation(); - await operation.apply(this); - // this persists private keys and public keys as trusted, - // only do this if apply succeeded for now as retry isn't in place yet - await builder.persist(this); - - logger.log("Secure Secret Storage ready"); -}; - -/** - * Fix up the backup key, that may be in the wrong format due to a bug in a - * migration step. Some backup keys were stored as a comma-separated list of - * integers, rather than a base64-encoded byte array. If this function is - * passed a string that looks like a list of integers rather than a base64 - * string, it will attempt to convert it to the right format. - * - * @param {string} key the key to check - * @returns {null | string} If the key is in the wrong format, then the fixed - * key will be returned. Otherwise null will be returned. - * - */ -export function fixBackupKey(key) { - if (typeof key !== "string" || key.indexOf(",") < 0) { - return null; - } - const fixedKey = Uint8Array.from(key.split(","), x => parseInt(x)); - return olmlib.encodeBase64(fixedKey); -} - -Crypto.prototype.addSecretStorageKey = function(algorithm, opts, keyID) { - return this._secretStorage.addKey(algorithm, opts, keyID); -}; - -Crypto.prototype.hasSecretStorageKey = function(keyID) { - return this._secretStorage.hasKey(keyID); -}; - -Crypto.prototype.getSecretStorageKey = function(keyID) { - return this._secretStorage.getKey(keyID); -}; - -Crypto.prototype.storeSecret = function(name, secret, keys) { - return this._secretStorage.store(name, secret, keys); -}; - -Crypto.prototype.getSecret = function(name) { - return this._secretStorage.get(name); -}; - -Crypto.prototype.isSecretStored = function(name, checkKey) { - return this._secretStorage.isStored(name, checkKey); -}; - -Crypto.prototype.requestSecret = function(name, devices) { - if (!devices) { - devices = Object.keys(this._deviceList.getRawStoredDevicesForUser(this._userId)); - } - return this._secretStorage.request(name, devices); -}; - -Crypto.prototype.getDefaultSecretStorageKeyId = function() { - return this._secretStorage.getDefaultKeyId(); -}; - -Crypto.prototype.setDefaultSecretStorageKeyId = function(k) { - return this._secretStorage.setDefaultKeyId(k); -}; - -Crypto.prototype.checkSecretStorageKey = function(key, info) { - return this._secretStorage.checkKey(key, info); -}; - -/** - * Checks that a given secret storage private key matches a given public key. - * This can be used by the getSecretStorageKey callback to verify that the - * private key it is about to supply is the one that was requested. - * - * @param {Uint8Array} privateKey The private key - * @param {string} expectedPublicKey The public key - * @returns {boolean} true if the key matches, otherwise false - */ -Crypto.prototype.checkSecretStoragePrivateKey = function(privateKey, expectedPublicKey) { - let decryption = null; - try { - decryption = new global.Olm.PkDecryption(); - const gotPubkey = decryption.init_with_private_key(privateKey); - // make sure it agrees with the given pubkey - return gotPubkey === expectedPublicKey; - } finally { - if (decryption) decryption.free(); - } -}; - -/** - * Fetches the backup private key, if cached - * @returns {Promise} the key, if any, or null - */ -Crypto.prototype.getSessionBackupPrivateKey = async function() { - let key = await new Promise((resolve) => { - this._cryptoStore.doTxn( - 'readonly', - [IndexedDBCryptoStore.STORE_ACCOUNT], - (txn) => { - this._cryptoStore.getSecretStorePrivateKey( - txn, - resolve, - "m.megolm_backup.v1", - ); - }, - ); - }); - - // make sure we have a Uint8Array, rather than a string - if (key && typeof key === "string") { - key = new Uint8Array(olmlib.decodeBase64(fixBackupKey(key) || key)); - await this.storeSessionBackupPrivateKey(key); - } - if (key && key.ciphertext) { - const pickleKey = Buffer.from(this._olmDevice._pickleKey); - const decrypted = await decryptAES(key, pickleKey, "m.megolm_backup.v1"); - key = olmlib.decodeBase64(decrypted); - } - return key; -}; - -/** - * Stores the session backup key to the cache - * @param {Uint8Array} key the private key - * @returns {Promise} so you can catch failures - */ -Crypto.prototype.storeSessionBackupPrivateKey = async function(key) { - if (!(key instanceof Uint8Array)) { - throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`); - } - const pickleKey = Buffer.from(this._olmDevice._pickleKey); - key = await encryptAES(olmlib.encodeBase64(key), pickleKey, "m.megolm_backup.v1"); - return this._cryptoStore.doTxn( - 'readwrite', - [IndexedDBCryptoStore.STORE_ACCOUNT], - (txn) => { - this._cryptoStore.storeSecretStorePrivateKey(txn, "m.megolm_backup.v1", key); - }, - ); -}; - -/** - * Checks that a given cross-signing private key matches a given public key. - * This can be used by the getCrossSigningKey callback to verify that the - * private key it is about to supply is the one that was requested. - * - * @param {Uint8Array} privateKey The private key - * @param {string} expectedPublicKey The public key - * @returns {boolean} true if the key matches, otherwise false - */ -Crypto.prototype.checkCrossSigningPrivateKey = function(privateKey, expectedPublicKey) { - let signing = null; - try { - signing = new global.Olm.PkSigning(); - const gotPubkey = signing.init_with_seed(privateKey); - // make sure it agrees with the given pubkey - return gotPubkey === expectedPublicKey; - } finally { - if (signing) signing.free(); - } -}; - -/** - * Run various follow-up actions after cross-signing keys have changed locally - * (either by resetting the keys for the account or by getting them from secret - * storage), such as signing the current device, upgrading device - * verifications, etc. - */ -Crypto.prototype._afterCrossSigningLocalKeyChange = async function() { - logger.info("Starting cross-signing key change post-processing"); - - // sign the current device with the new key, and upload to the server - const device = this._deviceList.getStoredDevice(this._userId, this._deviceId); - const signedDevice = await this._crossSigningInfo.signDevice(this._userId, device); - logger.info(`Starting background key sig upload for ${this._deviceId}`); - - const upload = ({ shouldEmit }) => { - return this._baseApis.uploadKeySignatures({ - [this._userId]: { - [this._deviceId]: signedDevice, - }, - }).then((response) => { - const { failures } = response || {}; - if (Object.keys(failures || []).length > 0) { - if (shouldEmit) { - this._baseApis.emit( - "crypto.keySignatureUploadFailure", - failures, - "_afterCrossSigningLocalKeyChange", - upload, // continuation - ); - } - throw new KeySignatureUploadError("Key upload failed", { failures }); - } - logger.info(`Finished background key sig upload for ${this._deviceId}`); - }).catch(e => { - logger.error( - `Error during background key sig upload for ${this._deviceId}`, - e, - ); - }); - }; - upload({ shouldEmit: true }); - - const shouldUpgradeCb = ( - this._baseApis.cryptoCallbacks.shouldUpgradeDeviceVerifications - ); - if (shouldUpgradeCb) { - logger.info("Starting device verification upgrade"); - - // Check all users for signatures if upgrade callback present - // FIXME: do this in batches - const users = {}; - for (const [userId, crossSigningInfo] - of Object.entries(this._deviceList._crossSigningInfo)) { - const upgradeInfo = await this._checkForDeviceVerificationUpgrade( - userId, CrossSigningInfo.fromStorage(crossSigningInfo, userId), - ); - if (upgradeInfo) { - users[userId] = upgradeInfo; - } - } - - if (Object.keys(users).length > 0) { - logger.info(`Found ${Object.keys(users).length} verif users to upgrade`); - try { - const usersToUpgrade = await shouldUpgradeCb({ users: users }); - if (usersToUpgrade) { - for (const userId of usersToUpgrade) { - if (userId in users) { - await this._baseApis.setDeviceVerified( - userId, users[userId].crossSigningInfo.getId(), - ); - } - } - } - } catch (e) { - logger.log( - "shouldUpgradeDeviceVerifications threw an error: not upgrading", e, - ); - } - } - - logger.info("Finished device verification upgrade"); - } - - logger.info("Finished cross-signing key change post-processing"); -}; - -/** - * Check if a user's cross-signing key is a candidate for upgrading from device - * verification. - * - * @param {string} userId the user whose cross-signing information is to be checked - * @param {object} crossSigningInfo the cross-signing information to check - */ -Crypto.prototype._checkForDeviceVerificationUpgrade = async function( - userId, crossSigningInfo, -) { - // only upgrade if this is the first cross-signing key that we've seen for - // them, and if their cross-signing key isn't already verified - const trustLevel = this._crossSigningInfo.checkUserTrust(crossSigningInfo); - if (crossSigningInfo.firstUse && !trustLevel.verified) { - const devices = this._deviceList.getRawStoredDevicesForUser(userId); - const deviceIds = await this._checkForValidDeviceSignature( - userId, crossSigningInfo.keys.master, devices, - ); - if (deviceIds.length) { - return { - devices: deviceIds.map( - deviceId => DeviceInfo.fromStorage(devices[deviceId], deviceId), - ), - crossSigningInfo, - }; - } - } -}; - -/** - * Check if the cross-signing key is signed by a verified device. - * - * @param {string} userId the user ID whose key is being checked - * @param {object} key the key that is being checked - * @param {object} devices the user's devices. Should be a map from device ID - * to device info - */ -Crypto.prototype._checkForValidDeviceSignature = async function(userId, key, devices) { - const deviceIds = []; - if (devices && key.signatures && key.signatures[userId]) { - for (const signame of Object.keys(key.signatures[userId])) { - const [, deviceId] = signame.split(':', 2); - if (deviceId in devices - && devices[deviceId].verified === DeviceVerification.VERIFIED) { - try { - await olmlib.verifySignature( - this._olmDevice, - key, - userId, - deviceId, - devices[deviceId].keys[signame], - ); - deviceIds.push(deviceId); - } catch (e) {} - } - } - } - return deviceIds; -}; - -/** - * Get the user's cross-signing key ID. - * - * @param {string} [type=master] The type of key to get the ID of. One of - * "master", "self_signing", or "user_signing". Defaults to "master". - * - * @returns {string} the key ID - */ -Crypto.prototype.getCrossSigningId = function(type) { - return this._crossSigningInfo.getId(type); -}; - -/** - * Get the cross signing information for a given user. - * - * @param {string} userId the user ID to get the cross-signing info for. - * - * @returns {CrossSigningInfo} the cross signing informmation for the user. - */ -Crypto.prototype.getStoredCrossSigningForUser = function(userId) { - return this._deviceList.getStoredCrossSigningForUser(userId); -}; - -/** - * Check whether a given user is trusted. - * - * @param {string} userId The ID of the user to check. - * - * @returns {UserTrustLevel} - */ -Crypto.prototype.checkUserTrust = function(userId) { - const userCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId); - if (!userCrossSigning) { - return new UserTrustLevel(false, false, false); - } - return this._crossSigningInfo.checkUserTrust(userCrossSigning); -}; - -/** - * Check whether a given device is trusted. - * - * @param {string} userId The ID of the user whose devices is to be checked. - * @param {string} deviceId The ID of the device to check - * - * @returns {DeviceTrustLevel} - */ -Crypto.prototype.checkDeviceTrust = function(userId, deviceId) { - const device = this._deviceList.getStoredDevice(userId, deviceId); - return this._checkDeviceInfoTrust(userId, device); -}; - -/** - * Check whether a given deviceinfo is trusted. - * - * @param {string} userId The ID of the user whose devices is to be checked. - * @param {module:crypto/deviceinfo?} device The device info object to check - * - * @returns {DeviceTrustLevel} - */ -Crypto.prototype._checkDeviceInfoTrust = function(userId, device) { - const trustedLocally = !!(device && device.isVerified()); - - const userCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId); - if (device && userCrossSigning) { - // The _trustCrossSignedDevices only affects trust of other people's cross-signing - // signatures - const trustCrossSig = this._trustCrossSignedDevices || userId === this._userId; - return this._crossSigningInfo.checkDeviceTrust( - userCrossSigning, device, trustedLocally, trustCrossSig, - ); - } else { - return new DeviceTrustLevel(false, false, trustedLocally, false); - } -}; - -/* - * Event handler for DeviceList's userNewDevices event - */ -Crypto.prototype._onDeviceListUserCrossSigningUpdated = async function(userId) { - if (userId === this._userId) { - // An update to our own cross-signing key. - // Get the new key first: - const newCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId); - const seenPubkey = newCrossSigning ? newCrossSigning.getId() : null; - const currentPubkey = this._crossSigningInfo.getId(); - const changed = currentPubkey !== seenPubkey; - - if (currentPubkey && seenPubkey && !changed) { - // If it's not changed, just make sure everything is up to date - await this.checkOwnCrossSigningTrust(); - } else { - // We'll now be in a state where cross-signing on the account is not trusted - // because our locally stored cross-signing keys will not match the ones - // on the server for our account. So we clear our own stored cross-signing keys, - // effectively disabling cross-signing until the user gets verified by the device - // that reset the keys - this._storeTrustedSelfKeys(null); - // emit cross-signing has been disabled - this.emit("crossSigning.keysChanged", {}); - // as the trust for our own user has changed, - // also emit an event for this - this.emit("userTrustStatusChanged", - this._userId, this.checkUserTrust(userId)); - } - } else { - await this._checkDeviceVerifications(userId); - - // Update verified before latch using the current state and save the new - // latch value in the device list store. - const crossSigning = this._deviceList.getStoredCrossSigningForUser(userId); - if (crossSigning) { - crossSigning.updateCrossSigningVerifiedBefore( - this.checkUserTrust(userId).isCrossSigningVerified(), - ); - this._deviceList.setRawStoredCrossSigningForUser( - userId, crossSigning.toStorage(), - ); - } - - this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId)); - } -}; - -/** - * Check the copy of our cross-signing key that we have in the device list and - * see if we can get the private key. If so, mark it as trusted. - */ -Crypto.prototype.checkOwnCrossSigningTrust = async function({ - allowPrivateKeyRequests = false, -} = {}) { - const userId = this._userId; - - // Before proceeding, ensure our cross-signing public keys have been - // downloaded via the device list. - await this.downloadKeys([this._userId]); - - // Also check which private keys are locally cached. - const crossSigningPrivateKeys = - await this._crossSigningInfo.getCrossSigningKeysFromCache(); - - // If we see an update to our own master key, check it against the master - // key we have and, if it matches, mark it as verified - - // First, get the new cross-signing info - const newCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId); - if (!newCrossSigning) { - logger.error( - "Got cross-signing update event for user " + userId + - " but no new cross-signing information found!", - ); - return; - } - - const seenPubkey = newCrossSigning.getId(); - const masterChanged = this._crossSigningInfo.getId() !== seenPubkey; - const masterExistsNotLocallyCached = - newCrossSigning.getId() && !crossSigningPrivateKeys.has("master"); - if (masterChanged) { - logger.info("Got new master public key", seenPubkey); - } - if ( - allowPrivateKeyRequests && - (masterChanged || masterExistsNotLocallyCached) - ) { - logger.info("Attempting to retrieve cross-signing master private key"); - let signing = null; - // It's important for control flow that we leave any errors alone for - // higher levels to handle so that e.g. cancelling access properly - // aborts any larger operation as well. - try { - const ret = await this._crossSigningInfo.getCrossSigningKey( - 'master', seenPubkey, - ); - signing = ret[1]; - logger.info("Got cross-signing master private key"); - } finally { - if (signing) signing.free(); - } - } - - const oldSelfSigningId = this._crossSigningInfo.getId("self_signing"); - const oldUserSigningId = this._crossSigningInfo.getId("user_signing"); - - // Update the version of our keys in our cross-signing object and the local store - this._storeTrustedSelfKeys(newCrossSigning.keys); - - const selfSigningChanged = oldSelfSigningId !== newCrossSigning.getId("self_signing"); - const userSigningChanged = oldUserSigningId !== newCrossSigning.getId("user_signing"); - - const selfSigningExistsNotLocallyCached = ( - newCrossSigning.getId("self_signing") && - !crossSigningPrivateKeys.has("self_signing") - ); - const userSigningExistsNotLocallyCached = ( - newCrossSigning.getId("user_signing") && - !crossSigningPrivateKeys.has("user_signing") - ); - - const keySignatures = {}; - - if (selfSigningChanged) { - logger.info("Got new self-signing key", newCrossSigning.getId("self_signing")); - } - if ( - allowPrivateKeyRequests && - (selfSigningChanged || selfSigningExistsNotLocallyCached) - ) { - logger.info("Attempting to retrieve cross-signing self-signing private key"); - let signing = null; - try { - const ret = await this._crossSigningInfo.getCrossSigningKey( - "self_signing", newCrossSigning.getId("self_signing"), - ); - signing = ret[1]; - logger.info("Got cross-signing self-signing private key"); - } finally { - if (signing) signing.free(); - } - - const device = this._deviceList.getStoredDevice(this._userId, this._deviceId); - const signedDevice = await this._crossSigningInfo.signDevice( - this._userId, device, - ); - keySignatures[this._deviceId] = signedDevice; - } - if (userSigningChanged) { - logger.info("Got new user-signing key", newCrossSigning.getId("user_signing")); - } - if ( - allowPrivateKeyRequests && - (userSigningChanged || userSigningExistsNotLocallyCached) - ) { - logger.info("Attempting to retrieve cross-signing user-signing private key"); - let signing = null; - try { - const ret = await this._crossSigningInfo.getCrossSigningKey( - "user_signing", newCrossSigning.getId("user_signing"), - ); - signing = ret[1]; - logger.info("Got cross-signing user-signing private key"); - } finally { - if (signing) signing.free(); - } - } - - if (masterChanged) { - const masterKey = this._crossSigningInfo.keys.master; - await this._signObject(masterKey); - const deviceSig = masterKey.signatures[this._userId]["ed25519:" + this._deviceId]; - // Include only the _new_ device signature in the upload. - // We may have existing signatures from deleted devices, which will cause - // the entire upload to fail. - keySignatures[this._crossSigningInfo.getId()] = Object.assign( - {}, - masterKey, - { - signatures: { - [this._userId]: { - ["ed25519:" + this._deviceId]: deviceSig, - }, - }, - }, - ); - } - - const keysToUpload = Object.keys(keySignatures); - if (keysToUpload.length) { - const upload = ({ shouldEmit }) => { - logger.info(`Starting background key sig upload for ${keysToUpload}`); - return this._baseApis.uploadKeySignatures({ [this._userId]: keySignatures }) - .then((response) => { - const { failures } = response || {}; - logger.info(`Finished background key sig upload for ${keysToUpload}`); - if (Object.keys(failures || []).length > 0) { - if (shouldEmit) { - this._baseApis.emit( - "crypto.keySignatureUploadFailure", - failures, - "checkOwnCrossSigningTrust", - upload, - ); - } - throw new KeySignatureUploadError("Key upload failed", { failures }); - } - }).catch(e => { - logger.error( - `Error during background key sig upload for ${keysToUpload}`, - e, - ); - }); - }; - upload({ shouldEmit: true }); - } - - this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId)); - - if (masterChanged) { - this._baseApis.emit("crossSigning.keysChanged", {}); - await this._afterCrossSigningLocalKeyChange(); - } - - // Now we may be able to trust our key backup - 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)? -}; - -/** - * Store a set of keys as our own, trusted, cross-signing keys. - * - * @param {object} keys The new trusted set of keys - */ -Crypto.prototype._storeTrustedSelfKeys = async function(keys) { - if (keys) { - this._crossSigningInfo.setKeys(keys); - } else { - this._crossSigningInfo.clearKeys(); - } - await this._cryptoStore.doTxn( - 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], - (txn) => { - this._cryptoStore.storeCrossSigningKeys(txn, this._crossSigningInfo.keys); - }, - ); -}; - -/** - * Check if the master key is signed by a verified device, and if so, prompt - * the application to mark it as verified. - * - * @param {string} userId the user ID whose key should be checked - */ -Crypto.prototype._checkDeviceVerifications = async function(userId) { - const shouldUpgradeCb = ( - this._baseApis.cryptoCallbacks.shouldUpgradeDeviceVerifications - ); - if (!shouldUpgradeCb) { - // Upgrading skipped when callback is not present. - return; - } - logger.info(`Starting device verification upgrade for ${userId}`); - if (this._crossSigningInfo.keys.user_signing) { - const crossSigningInfo = this._deviceList.getStoredCrossSigningForUser(userId); - if (crossSigningInfo) { - const upgradeInfo = await this._checkForDeviceVerificationUpgrade( - userId, crossSigningInfo, - ); - if (upgradeInfo) { - const usersToUpgrade = await shouldUpgradeCb({ - users: { - [userId]: upgradeInfo, - }, - }); - if (usersToUpgrade.includes(userId)) { - await this._baseApis.setDeviceVerified( - userId, crossSigningInfo.getId(), - ); - } - } - } - } - logger.info(`Finished device verification upgrade for ${userId}`); -}; - -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._backupManager.checkKeyBackup(); -}; - -/** - */ -Crypto.prototype.enableLazyLoading = function() { - this._lazyLoadMembers = true; -}; - -/** - * Tell the crypto module to register for MatrixClient events which it needs to - * listen for - * - * @param {external:EventEmitter} eventEmitter event source where we can register - * for event notifications - */ -Crypto.prototype.registerEventHandlers = function(eventEmitter) { - const crypto = this; - - eventEmitter.on("RoomMember.membership", function(event, member, oldMembership) { - try { - crypto._onRoomMembership(event, member, oldMembership); - } catch (e) { - logger.error("Error handling membership change:", e); - } - }); - - eventEmitter.on("toDeviceEvent", crypto._onToDeviceEvent.bind(crypto)); - - const timelineHandler = crypto._onTimelineEvent.bind(crypto); - - eventEmitter.on("Room.timeline", timelineHandler); - - eventEmitter.on("Event.decrypted", timelineHandler); -}; - -/** Start background processes related to crypto */ -Crypto.prototype.start = function() { - this._outgoingRoomKeyRequestManager.start(); -}; - -/** Stop background processes related to crypto */ -Crypto.prototype.stop = function() { - this._outgoingRoomKeyRequestManager.stop(); - this._deviceList.stop(); - this._dehydrationManager.stop(); -}; - -/** - * @return {string} The version of Olm. - */ -Crypto.getOlmVersion = function() { - return OlmDevice.getOlmVersion(); -}; - -/** - * Get the Ed25519 key for this device - * - * @return {string} base64-encoded ed25519 key. - */ -Crypto.prototype.getDeviceEd25519Key = function() { - return this._olmDevice.deviceEd25519Key; -}; - -/** - * Get the Curve25519 key for this device - * - * @return {string} base64-encoded curve25519 key. - */ -Crypto.prototype.getDeviceCurve25519Key = function() { - return this._olmDevice.deviceCurve25519Key; -}; - -/** - * Set the global override for whether the client should ever send encrypted - * messages to unverified devices. This provides the default for rooms which - * do not specify a value. - * - * @param {boolean} value whether to blacklist all unverified devices by default - */ -Crypto.prototype.setGlobalBlacklistUnverifiedDevices = function(value) { - this._globalBlacklistUnverifiedDevices = value; -}; - -/** - * @return {boolean} whether to blacklist all unverified devices by default - */ -Crypto.prototype.getGlobalBlacklistUnverifiedDevices = function() { - return this._globalBlacklistUnverifiedDevices; -}; - -/** - * Set whether sendMessage in a room with unknown and unverified devices - * should throw an error and not send them message. This has 'Global' for - * symmertry with setGlobalBlacklistUnverifiedDevices but there is currently - * no room-level equivalent for this setting. - * - * This API is currently UNSTABLE and may change or be removed without notice. - * - * @param {boolean} value whether error on unknown devices - */ -Crypto.prototype.setGlobalErrorOnUnknownDevices = function(value) { - this._globalErrorOnUnknownDevices = value; -}; - -/** - * @return {boolean} whether to error on unknown devices - * - * This API is currently UNSTABLE and may change or be removed without notice. - */ -Crypto.prototype.getGlobalErrorOnUnknownDevices = function() { - return this._globalErrorOnUnknownDevices; -}; - -/** - * Upload the device keys to the homeserver. - * @return {object} A promise that will resolve when the keys are uploaded. - */ -Crypto.prototype.uploadDeviceKeys = function() { - const crypto = this; - const userId = crypto._userId; - const deviceId = crypto._deviceId; - - const deviceKeys = { - algorithms: crypto._supportedAlgorithms, - device_id: deviceId, - keys: crypto._deviceKeys, - user_id: userId, - }; - - return crypto._signObject(deviceKeys).then(() => { - return crypto._baseApis.uploadKeysRequest({ - device_keys: deviceKeys, - }); - }); -}; - -/** - * Stores the current one_time_key count which will be handled later (in a call of - * onSyncCompleted). The count is e.g. coming from a /sync response. - * - * @param {Number} currentCount The current count of one_time_keys to be stored - */ -Crypto.prototype.updateOneTimeKeyCount = function(currentCount) { - if (isFinite(currentCount)) { - this._oneTimeKeyCount = currentCount; - } else { - throw new TypeError("Parameter for updateOneTimeKeyCount has to be a number"); - } -}; - -Crypto.prototype.setNeedsNewFallback = function(needsNewFallback) { - this._needsNewFallback = !!needsNewFallback; -}; - -Crypto.prototype.getNeedsNewFallback = function() { - return this._needsNewFallback; -}; - -// check if it's time to upload one-time keys, and do so if so. -function _maybeUploadOneTimeKeys(crypto) { - // frequency with which to check & upload one-time keys - const uploadPeriod = 1000 * 60; // one minute - - // max number of keys to upload at once - // Creating keys can be an expensive operation so we limit the - // number we generate in one go to avoid blocking the application - // for too long. - const maxKeysPerCycle = 5; - - if (crypto._oneTimeKeyCheckInProgress) { - return; - } - - const now = Date.now(); - if (crypto._lastOneTimeKeyCheck !== null && - now - crypto._lastOneTimeKeyCheck < uploadPeriod - ) { - // we've done a key upload recently. - return; - } - - crypto._lastOneTimeKeyCheck = now; - - // We need to keep a pool of one time public keys on the server so that - // other devices can start conversations with us. But we can only store - // a finite number of private keys in the olm Account object. - // To complicate things further then can be a delay between a device - // claiming a public one time key from the server and it sending us a - // message. We need to keep the corresponding private key locally until - // we receive the message. - // But that message might never arrive leaving us stuck with duff - // private keys clogging up our local storage. - // So we need some kind of enginering compromise to balance all of - // these factors. - - // Check how many keys we can store in the Account object. - const maxOneTimeKeys = crypto._olmDevice.maxNumberOfOneTimeKeys(); - // Try to keep at most half that number on the server. This leaves the - // rest of the slots free to hold keys that have been claimed from the - // server but we haven't recevied a message for. - // If we run out of slots when generating new keys then olm will - // discard the oldest private keys first. This will eventually clean - // out stale private keys that won't receive a message. - const keyLimit = Math.floor(maxOneTimeKeys / 2); - - async function uploadLoop(keyCount) { - while (keyLimit > keyCount || crypto.getNeedsNewFallback()) { - // Ask olm to generate new one time keys, then upload them to synapse. - if (keyLimit > keyCount) { - logger.info("generating oneTimeKeys"); - const keysThisLoop = Math.min(keyLimit - keyCount, maxKeysPerCycle); - await crypto._olmDevice.generateOneTimeKeys(keysThisLoop); - } - - if (crypto.getNeedsNewFallback()) { - logger.info("generating fallback key"); - await crypto._olmDevice.generateFallbackKey(); - } - - logger.info("calling _uploadOneTimeKeys"); - const res = await _uploadOneTimeKeys(crypto); - if (res.one_time_key_counts && res.one_time_key_counts.signed_curve25519) { - // if the response contains a more up to date value use this - // for the next loop - keyCount = res.one_time_key_counts.signed_curve25519; - } else { - throw new Error("response for uploading keys does not contain " + - "one_time_key_counts.signed_curve25519"); - } - } - } - - crypto._oneTimeKeyCheckInProgress = true; - Promise.resolve().then(() => { - if (crypto._oneTimeKeyCount !== undefined) { - // We already have the current one_time_key count from a /sync response. - // Use this value instead of asking the server for the current key count. - return Promise.resolve(crypto._oneTimeKeyCount); - } - // ask the server how many keys we have - return crypto._baseApis.uploadKeysRequest({}).then((res) => { - return res.one_time_key_counts.signed_curve25519 || 0; - }); - }).then((keyCount) => { - // Start the uploadLoop with the current keyCount. The function checks if - // we need to upload new keys or not. - // If there are too many keys on the server then we don't need to - // create any more keys. - return uploadLoop(keyCount); - }).catch((e) => { - logger.error("Error uploading one-time keys", e.stack || e); - }).finally(() => { - // reset _oneTimeKeyCount to prevent start uploading based on old data. - // it will be set again on the next /sync-response - crypto._oneTimeKeyCount = undefined; - crypto._oneTimeKeyCheckInProgress = false; - }); -} - -// returns a promise which resolves to the response -async function _uploadOneTimeKeys(crypto) { - const promises = []; - - const fallbackJson = {}; - if (crypto.getNeedsNewFallback()) { - const fallbackKeys = await crypto._olmDevice.getFallbackKey(); - for (const [keyId, key] of Object.entries(fallbackKeys.curve25519)) { - const k = { key, fallback: true }; - fallbackJson["signed_curve25519:" + keyId] = k; - promises.push(crypto._signObject(k)); - } - crypto.setNeedsNewFallback(false); - } - - const oneTimeKeys = await crypto._olmDevice.getOneTimeKeys(); - const oneTimeJson = {}; - - for (const keyId in oneTimeKeys.curve25519) { - if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) { - const k = { - key: oneTimeKeys.curve25519[keyId], - }; - oneTimeJson["signed_curve25519:" + keyId] = k; - promises.push(crypto._signObject(k)); - } - } - - await Promise.all(promises); - - const res = await crypto._baseApis.uploadKeysRequest({ - "one_time_keys": oneTimeJson, - "org.matrix.msc2732.fallback_keys": fallbackJson, - }); - - await crypto._olmDevice.markKeysAsPublished(); - return res; -} - -/** - * Download the keys for a list of users and stores the keys in the session - * store. - * @param {Array} userIds The users to fetch. - * @param {bool} forceDownload Always download the keys even if cached. - * - * @return {Promise} A promise which resolves to a map userId->deviceId->{@link - * module:crypto/deviceinfo|DeviceInfo}. - */ -Crypto.prototype.downloadKeys = function(userIds, forceDownload) { - return this._deviceList.downloadKeys(userIds, forceDownload); -}; - -/** - * Get the stored device keys for a user id - * - * @param {string} userId the user to list keys for. - * - * @return {module:crypto/deviceinfo[]|null} list of devices, or null if we haven't - * managed to get a list of devices for this user yet. - */ -Crypto.prototype.getStoredDevicesForUser = function(userId) { - return this._deviceList.getStoredDevicesForUser(userId); -}; - -/** - * Get the stored keys for a single device - * - * @param {string} userId - * @param {string} deviceId - * - * @return {module:crypto/deviceinfo?} device, or undefined - * if we don't know about this device - */ -Crypto.prototype.getStoredDevice = function(userId, deviceId) { - return this._deviceList.getStoredDevice(userId, deviceId); -}; - -/** - * Save the device list, if necessary - * - * @param {integer} delay Time in ms before which the save actually happens. - * By default, the save is delayed for a short period in order to batch - * multiple writes, but this behaviour can be disabled by passing 0. - * - * @return {Promise} true if the data was saved, false if - * it was not (eg. because no changes were pending). The promise - * will only resolve once the data is saved, so may take some time - * to resolve. - */ -Crypto.prototype.saveDeviceList = function(delay) { - return this._deviceList.saveIfDirty(delay); -}; - -/** - * Update the blocked/verified state of the given device - * - * @param {string} userId owner of the device - * @param {string} deviceId unique identifier for the device or user's - * cross-signing public key ID. - * - * @param {?boolean} verified whether to mark the device as verified. Null to - * leave unchanged. - * - * @param {?boolean} blocked whether to mark the device as blocked. Null to - * leave unchanged. - * - * @param {?boolean} known whether to mark that the user has been made aware of - * the existence of this device. Null to leave unchanged - * - * @return {Promise} updated DeviceInfo - */ -Crypto.prototype.setDeviceVerification = async function( - userId, deviceId, verified, blocked, known, -) { - // get rid of any `undefined`s here so we can just check - // for null rather than null or undefined - if (verified === undefined) verified = null; - if (blocked === undefined) blocked = null; - if (known === undefined) known = null; - - // Check if the 'device' is actually a cross signing key - // The js-sdk's verification treats cross-signing keys as devices - // and so uses this method to mark them verified. - const xsk = this._deviceList.getStoredCrossSigningForUser(userId); - if (xsk && xsk.getId() === deviceId) { - if (blocked !== null || known !== null) { - throw new Error("Cannot set blocked or known for a cross-signing key"); - } - if (!verified) { - throw new Error("Cannot set a cross-signing key as unverified"); - } - - if (!this._crossSigningInfo.getId() && userId === this._crossSigningInfo.userId) { - this._storeTrustedSelfKeys(xsk.keys); - // This will cause our own user trust to change, so emit the event - this.emit( - "userTrustStatusChanged", this._userId, this.checkUserTrust(userId), - ); - } - - // Now sign the master key with our user signing key (unless it's ourself) - if (userId !== this._userId) { - logger.info( - "Master key " + xsk.getId() + " for " + userId + - " marked verified. Signing...", - ); - const device = await this._crossSigningInfo.signUser(xsk); - if (device) { - const upload = async ({ shouldEmit }) => { - logger.info("Uploading signature for " + userId + "..."); - const response = await this._baseApis.uploadKeySignatures({ - [userId]: { - [deviceId]: device, - }, - }); - const { failures } = response || {}; - if (Object.keys(failures || []).length > 0) { - if (shouldEmit) { - this._baseApis.emit( - "crypto.keySignatureUploadFailure", - failures, - "setDeviceVerification", - upload, - ); - } - /* Throwing here causes the process to be cancelled and the other - * user to be notified */ - throw new KeySignatureUploadError( - "Key upload failed", - { failures }, - ); - } - }; - await upload({ shouldEmit: true }); - - // This will emit events when it comes back down the sync - // (we could do local echo to speed things up) - } - return device; - } else { - return xsk; - } - } - - const devices = this._deviceList.getRawStoredDevicesForUser(userId); - if (!devices || !devices[deviceId]) { - throw new Error("Unknown device " + userId + ":" + deviceId); - } - - const dev = devices[deviceId]; - let verificationStatus = dev.verified; - - if (verified) { - verificationStatus = DeviceVerification.VERIFIED; - } else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) { - verificationStatus = DeviceVerification.UNVERIFIED; - } - - if (blocked) { - verificationStatus = DeviceVerification.BLOCKED; - } else if (blocked !== null && verificationStatus == DeviceVerification.BLOCKED) { - verificationStatus = DeviceVerification.UNVERIFIED; - } - - let knownStatus = dev.known; - if (known !== null) { - knownStatus = known; - } - - if (dev.verified !== verificationStatus || dev.known !== knownStatus) { - dev.verified = verificationStatus; - dev.known = knownStatus; - this._deviceList.storeDevicesForUser(userId, devices); - this._deviceList.saveIfDirty(); - } - - // do cross-signing - if (verified && userId === this._userId) { - logger.info("Own device " + deviceId + " marked verified: signing"); - - // Signing only needed if other device not already signed - let device; - const deviceTrust = this.checkDeviceTrust(userId, deviceId); - if (deviceTrust.isCrossSigningVerified()) { - logger.log(`Own device ${deviceId} already cross-signing verified`); - } else { - device = await this._crossSigningInfo.signDevice( - userId, DeviceInfo.fromStorage(dev, deviceId), - ); - } - - if (device) { - const upload = async ({ shouldEmit }) => { - logger.info("Uploading signature for " + deviceId); - const response = await this._baseApis.uploadKeySignatures({ - [userId]: { - [deviceId]: device, - }, - }); - const { failures } = response || {}; - if (Object.keys(failures || []).length > 0) { - if (shouldEmit) { - this._baseApis.emit( - "crypto.keySignatureUploadFailure", - failures, - "setDeviceVerification", - upload, // continuation - ); - } - throw new KeySignatureUploadError("Key upload failed", { failures }); - } - }; - await upload({ shouldEmit: true }); - // XXX: we'll need to wait for the device list to be updated - } - } - - const deviceObj = DeviceInfo.fromStorage(dev, deviceId); - this.emit("deviceVerificationChanged", userId, deviceId, deviceObj); - return deviceObj; -}; - -Crypto.prototype.findVerificationRequestDMInProgress = function(roomId) { - return this._inRoomVerificationRequests.findRequestInProgress(roomId); -}; - -Crypto.prototype.getVerificationRequestsToDeviceInProgress = function(userId) { - return this._toDeviceVerificationRequests.getRequestsInProgress(userId); -}; - -Crypto.prototype.requestVerificationDM = function(userId, roomId) { - const existingRequest = this._inRoomVerificationRequests. - findRequestInProgress(roomId); - if (existingRequest) { - return Promise.resolve(existingRequest); - } - const channel = new InRoomChannel(this._baseApis, roomId, userId); - return this._requestVerificationWithChannel( - userId, - channel, - this._inRoomVerificationRequests, - ); -}; - -Crypto.prototype.requestVerification = function(userId, devices) { - if (!devices) { - devices = Object.keys(this._deviceList.getRawStoredDevicesForUser(userId)); - } - const existingRequest = this._toDeviceVerificationRequests - .findRequestInProgress(userId, devices); - if (existingRequest) { - return Promise.resolve(existingRequest); - } - const channel = new ToDeviceChannel(this._baseApis, userId, devices, - ToDeviceChannel.makeTransactionId()); - return this._requestVerificationWithChannel( - userId, - channel, - this._toDeviceVerificationRequests, - ); -}; - -Crypto.prototype._requestVerificationWithChannel = async function( - userId, channel, requestsMap, -) { - let request = new VerificationRequest( - channel, this._verificationMethods, this._baseApis); - // if transaction id is already known, add request - if (channel.transactionId) { - requestsMap.setRequestByChannel(channel, request); - } - await request.sendRequest(); - // don't replace the request created by a racing remote echo - const racingRequest = requestsMap.getRequestByChannel(channel); - if (racingRequest) { - request = racingRequest; - } else { - logger.log(`Crypto: adding new request to ` + - `requestsByTxnId with id ${channel.transactionId} ${channel.roomId}`); - requestsMap.setRequestByChannel(channel, request); - } - return request; -}; - -Crypto.prototype.beginKeyVerification = function( - method, userId, deviceId, transactionId = null, -) { - let request; - if (transactionId) { - request = this._toDeviceVerificationRequests.getRequestBySenderAndTxnId( - userId, transactionId); - if (!request) { - throw new Error( - `No request found for user ${userId} with ` + - `transactionId ${transactionId}`); - } - } else { - transactionId = ToDeviceChannel.makeTransactionId(); - const channel = new ToDeviceChannel( - this._baseApis, userId, [deviceId], transactionId, deviceId); - request = new VerificationRequest( - channel, this._verificationMethods, this._baseApis); - this._toDeviceVerificationRequests.setRequestBySenderAndTxnId( - userId, transactionId, request); - } - return request.beginKeyVerification(method, { userId, deviceId }); -}; - -Crypto.prototype.legacyDeviceVerification = async function( - userId, deviceId, method, -) { - const transactionId = ToDeviceChannel.makeTransactionId(); - const channel = new ToDeviceChannel( - this._baseApis, userId, [deviceId], transactionId, deviceId); - const request = new VerificationRequest( - channel, this._verificationMethods, this._baseApis); - this._toDeviceVerificationRequests.setRequestBySenderAndTxnId( - userId, transactionId, request); - const verifier = request.beginKeyVerification(method, { userId, deviceId }); - // either reject by an error from verify() while sending .start - // or resolve when the request receives the - // local (fake remote) echo for sending the .start event - await Promise.race([ - verifier.verify(), - request.waitFor(r => r.started), - ]); - return request; -}; - -/** - * Get information on the active olm sessions with a user - *

- * Returns a map from device id to an object with keys 'deviceIdKey' (the - * device's curve25519 identity key) and 'sessions' (an array of objects in the - * same format as that returned by - * {@link module:crypto/OlmDevice#getSessionInfoForDevice}). - *

- * This method is provided for debugging purposes. - * - * @param {string} userId id of user to inspect - * - * @return {Promise>} - */ -Crypto.prototype.getOlmSessionsForUser = async function(userId) { - const devices = this.getStoredDevicesForUser(userId) || []; - const result = {}; - for (let j = 0; j < devices.length; ++j) { - const device = devices[j]; - const deviceKey = device.getIdentityKey(); - const sessions = await this._olmDevice.getSessionInfoForDevice(deviceKey); - - result[device.deviceId] = { - deviceIdKey: deviceKey, - sessions: sessions, - }; - } - return result; -}; - -/** - * Get the device which sent an event - * - * @param {module:models/event.MatrixEvent} event event to be checked - * - * @return {module:crypto/deviceinfo?} - */ -Crypto.prototype.getEventSenderDeviceInfo = function(event) { - const senderKey = event.getSenderKey(); - const algorithm = event.getWireContent().algorithm; - - if (!senderKey || !algorithm) { - return null; - } - - const forwardingChain = event.getForwardingCurve25519KeyChain(); - if (forwardingChain.length > 0) { - // we got the key this event from somewhere else - // TODO: check if we can trust the forwarders. - return null; - } - - if (event.isKeySourceUntrusted()) { - // we got the key for this event from a source that we consider untrusted - return null; - } - - // senderKey is the Curve25519 identity key of the device which the event - // was sent from. In the case of Megolm, it's actually the Curve25519 - // identity key of the device which set up the Megolm session. - - const device = this._deviceList.getDeviceByIdentityKey( - algorithm, senderKey, - ); - - if (device === null) { - // we haven't downloaded the details of this device yet. - return null; - } - - // so far so good, but now we need to check that the sender of this event - // hadn't advertised someone else's Curve25519 key as their own. We do that - // by checking the Ed25519 claimed by the event (or, in the case of megolm, - // the event which set up the megolm session), to check that it matches the - // fingerprint of the purported sending device. - // - // (see https://github.com/vector-im/vector-web/issues/2215) - - const claimedKey = event.getClaimedEd25519Key(); - if (!claimedKey) { - logger.warn("Event " + event.getId() + " claims no ed25519 key: " + - "cannot verify sending device"); - return null; - } - - if (claimedKey !== device.getFingerprint()) { - logger.warn( - "Event " + event.getId() + " claims ed25519 key " + claimedKey + - " but sender device has key " + device.getFingerprint()); - return null; - } - - return device; -}; - -/** - * Get information about the encryption of an event - * - * @param {module:models/event.MatrixEvent} event event to be checked - * - * @return {object} An object with the fields: - * - encrypted: whether the event is encrypted (if not encrypted, some of the - * other properties may not be set) - * - senderKey: the sender's key - * - algorithm: the algorithm used to encrypt the event - * - authenticated: whether we can be sure that the owner of the senderKey - * sent the event - * - sender: the sender's device information, if available - * - mismatchedSender: if the event's ed25519 and curve25519 keys don't match - * (only meaningful if `sender` is set) - */ -Crypto.prototype.getEventEncryptionInfo = function(event) { - const ret = {}; - - ret.senderKey = event.getSenderKey(); - ret.algorithm = event.getWireContent().algorithm; - - if (!ret.senderKey || !ret.algorithm) { - ret.encrypted = false; - return ret; - } - ret.encrypted = true; - - const forwardingChain = event.getForwardingCurve25519KeyChain(); - if (forwardingChain.length > 0 || event.isKeySourceUntrusted()) { - // we got the key this event from somewhere else - // TODO: check if we can trust the forwarders. - ret.authenticated = false; - } else { - ret.authenticated = true; - } - - // senderKey is the Curve25519 identity key of the device which the event - // was sent from. In the case of Megolm, it's actually the Curve25519 - // identity key of the device which set up the Megolm session. - - ret.sender = this._deviceList.getDeviceByIdentityKey( - ret.algorithm, ret.senderKey, - ); - - // so far so good, but now we need to check that the sender of this event - // hadn't advertised someone else's Curve25519 key as their own. We do that - // by checking the Ed25519 claimed by the event (or, in the case of megolm, - // the event which set up the megolm session), to check that it matches the - // fingerprint of the purported sending device. - // - // (see https://github.com/vector-im/vector-web/issues/2215) - - const claimedKey = event.getClaimedEd25519Key(); - if (!claimedKey) { - logger.warn("Event " + event.getId() + " claims no ed25519 key: " + - "cannot verify sending device"); - ret.mismatchedSender = true; - } - - if (ret.sender && claimedKey !== ret.sender.getFingerprint()) { - logger.warn( - "Event " + event.getId() + " claims ed25519 key " + claimedKey + - "but sender device has key " + ret.sender.getFingerprint()); - ret.mismatchedSender = true; - } - - return ret; -}; - -/** - * Forces the current outbound group session to be discarded such - * that another one will be created next time an event is sent. - * - * @param {string} roomId The ID of the room to discard the session for - * - * This should not normally be necessary. - */ -Crypto.prototype.forceDiscardSession = function(roomId) { - const alg = this._roomEncryptors[roomId]; - if (alg === undefined) throw new Error("Room not encrypted"); - if (alg.forceDiscardSession === undefined) { - throw new Error("Room encryption algorithm doesn't support session discarding"); - } - alg.forceDiscardSession(); -}; - -/** - * Configure a room to use encryption (ie, save a flag in the cryptoStore). - * - * @param {string} roomId The room ID to enable encryption in. - * - * @param {object} config The encryption config for the room. - * - * @param {boolean=} inhibitDeviceQuery true to suppress device list query for - * users in the room (for now). In case lazy loading is enabled, - * the device query is always inhibited as the members are not tracked. - */ -Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDeviceQuery) { - // ignore crypto events with no algorithm defined - // This will happen if a crypto event is redacted before we fetch the room state - // It would otherwise just throw later as an unknown algorithm would, but we may - // as well catch this here - if (!config.algorithm) { - logger.log("Ignoring setRoomEncryption with no algorithm"); - return; - } - - // if state is being replayed from storage, we might already have a configuration - // for this room as they are persisted as well. - // We just need to make sure the algorithm is initialized in this case. - // However, if the new config is different, - // we should bail out as room encryption can't be changed once set. - const existingConfig = this._roomList.getRoomEncryption(roomId); - if (existingConfig) { - if (JSON.stringify(existingConfig) != JSON.stringify(config)) { - logger.error("Ignoring m.room.encryption event which requests " + - "a change of config in " + roomId); - return; - } - } - // if we already have encryption in this room, we should ignore this event, - // as it would reset the encryption algorithm. - // This is at least expected to be called twice, as sync calls onCryptoEvent - // for both the timeline and state sections in the /sync response, - // the encryption event would appear in both. - // If it's called more than twice though, - // it signals a bug on client or server. - const existingAlg = this._roomEncryptors[roomId]; - if (existingAlg) { - return; - } - - // _roomList.getRoomEncryption will not race with _roomList.setRoomEncryption - // because it first stores in memory. We should await the promise only - // after all the in-memory state (_roomEncryptors and _roomList) has been updated - // to avoid races when calling this method multiple times. Hence keep a hold of the promise. - let storeConfigPromise = null; - if (!existingConfig) { - storeConfigPromise = this._roomList.setRoomEncryption(roomId, config); - } - - const AlgClass = algorithms.ENCRYPTION_CLASSES[config.algorithm]; - if (!AlgClass) { - throw new Error("Unable to encrypt with " + config.algorithm); - } - - const alg = new AlgClass({ - userId: this._userId, - deviceId: this._deviceId, - crypto: this, - olmDevice: this._olmDevice, - baseApis: this._baseApis, - roomId: roomId, - config: config, - }); - this._roomEncryptors[roomId] = alg; - - if (storeConfigPromise) { - await storeConfigPromise; - } - - if (!this._lazyLoadMembers) { - logger.log("Enabling encryption in " + roomId + "; " + - "starting to track device lists for all users therein"); - - await this.trackRoomDevices(roomId); - // TODO: this flag is only not used from MatrixClient::setRoomEncryption - // which is never used (inside Element at least) - // but didn't want to remove it as it technically would - // be a breaking change. - if (!this.inhibitDeviceQuery) { - this._deviceList.refreshOutdatedDeviceLists(); - } - } else { - logger.log("Enabling encryption in " + roomId); - } -}; - -/** - * Make sure we are tracking the device lists for all users in this room. - * - * @param {string} roomId The room ID to start tracking devices in. - * @returns {Promise} when all devices for the room have been fetched and marked to track - */ -Crypto.prototype.trackRoomDevices = function(roomId) { - const trackMembers = async () => { - // not an encrypted room - if (!this._roomEncryptors[roomId]) { - return; - } - const room = this._clientStore.getRoom(roomId); - if (!room) { - throw new Error(`Unable to start tracking devices in unknown room ${roomId}`); - } - logger.log(`Starting to track devices for room ${roomId} ...`); - const members = await room.getEncryptionTargetMembers(); - members.forEach((m) => { - this._deviceList.startTrackingDeviceList(m.userId); - }); - }; - - let promise = this._roomDeviceTrackingState[roomId]; - if (!promise) { - promise = trackMembers(); - this._roomDeviceTrackingState[roomId] = promise.catch(err => { - this._roomDeviceTrackingState[roomId] = null; - throw err; - }); - } - return promise; -}; - -/** - * @typedef {Object} module:crypto~OlmSessionResult - * @property {module:crypto/deviceinfo} device device info - * @property {string?} sessionId base64 olm session id; null if no session - * could be established - */ - -/** - * Try to make sure we have established olm sessions for all known devices for - * the given users. - * - * @param {string[]} users list of user ids - * - * @return {Promise} resolves once the sessions are complete, to - * an Object mapping from userId to deviceId to - * {@link module:crypto~OlmSessionResult} - */ -Crypto.prototype.ensureOlmSessionsForUsers = function(users) { - const devicesByUser = {}; - - for (let i = 0; i < users.length; ++i) { - const userId = users[i]; - devicesByUser[userId] = []; - - const devices = this.getStoredDevicesForUser(userId) || []; - for (let j = 0; j < devices.length; ++j) { - const deviceInfo = devices[j]; - - const key = deviceInfo.getIdentityKey(); - if (key == this._olmDevice.deviceCurve25519Key) { - // don't bother setting up session to ourself - continue; - } - if (deviceInfo.verified == DeviceVerification.BLOCKED) { - // don't bother setting up sessions with blocked users - continue; - } - - devicesByUser[userId].push(deviceInfo); - } - } - - return olmlib.ensureOlmSessionsForDevices( - this._olmDevice, this._baseApis, devicesByUser, - ); -}; - -/** - * Get a list containing all of the room keys - * - * @return {module:crypto/OlmDevice.MegolmSessionData[]} a list of session export objects - */ -Crypto.prototype.exportRoomKeys = async function() { - const exportedSessions = []; - await this._cryptoStore.doTxn( - 'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => { - this._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (s) => { - if (s === null) return; - - const sess = this._olmDevice.exportInboundGroupSession( - s.senderKey, s.sessionId, s.sessionData, - ); - delete sess.first_known_index; - sess.algorithm = olmlib.MEGOLM_ALGORITHM; - exportedSessions.push(sess); - }); - }, - ); - - return exportedSessions; -}; - -/** - * Import a list of room keys previously exported by exportRoomKeys - * - * @param {Object[]} keys a list of session export objects - * @param {Object} opts - * @param {Function} opts.progressCallback called with an object which has a stage param - * @return {Promise} a promise which resolves once the keys have been imported - */ -Crypto.prototype.importRoomKeys = function(keys, opts = {}) { - let successes = 0; - let failures = 0; - const total = keys.length; - - function updateProgress() { - opts.progressCallback({ - stage: "load_keys", - successes, - failures, - total, - }); - } - - return Promise.all(keys.map((key) => { - if (!key.room_id || !key.algorithm) { - logger.warn("ignoring room key entry with missing fields", key); - failures++; - if (opts.progressCallback) { updateProgress(); } - return null; - } - - const alg = this._getRoomDecryptor(key.room_id, key.algorithm); - return alg.importRoomKey(key, opts).finally((r) => { - successes++; - if (opts.progressCallback) { updateProgress(); } - }); - })); -}; - -/** - * 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._backupManager.countSessionsNeedingBackup(); -}; - -/** - * Perform any background tasks that can be done before a message is ready to - * send, in order to speed up sending of the message. - * - * @param {module:models/room} room the room the event is in - */ -Crypto.prototype.prepareToEncrypt = function(room) { - const roomId = room.roomId; - const alg = this._roomEncryptors[roomId]; - if (alg) { - alg.prepareToEncrypt(room); - } -}; - -/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307 -/** - * Encrypt an event according to the configuration of the room. - * - * @param {module:models/event.MatrixEvent} event event to be sent - * - * @param {module:models/room} room destination room. - * - * @return {Promise?} Promise which resolves when the event has been - * encrypted, or null if nothing was needed - */ -/* eslint-enable valid-jsdoc */ -Crypto.prototype.encryptEvent = async function(event, room) { - if (!room) { - throw new Error("Cannot send encrypted messages in unknown rooms"); - } - - const roomId = event.getRoomId(); - - const alg = this._roomEncryptors[roomId]; - if (!alg) { - // MatrixClient has already checked that this room should be encrypted, - // so this is an unexpected situation. - throw new Error( - "Room was previously configured to use encryption, but is " + - "no longer. Perhaps the homeserver is hiding the " + - "configuration event.", - ); - } - - if (!this._roomDeviceTrackingState[roomId]) { - this.trackRoomDevices(roomId); - } - // wait for all the room devices to be loaded - await this._roomDeviceTrackingState[roomId]; - - let content = event.getContent(); - // If event has an m.relates_to then we need - // to put this on the wrapping event instead - const mRelatesTo = content['m.relates_to']; - if (mRelatesTo) { - // Clone content here so we don't remove `m.relates_to` from the local-echo - content = Object.assign({}, content); - delete content['m.relates_to']; - } - - const encryptedContent = await alg.encryptMessage( - room, event.getType(), content); - - if (mRelatesTo) { - encryptedContent['m.relates_to'] = mRelatesTo; - } - - event.makeEncrypted( - "m.room.encrypted", - encryptedContent, - this._olmDevice.deviceCurve25519Key, - this._olmDevice.deviceEd25519Key, - ); -}; - -/** - * Decrypt a received event - * - * @param {MatrixEvent} event - * - * @return {Promise} resolves once we have - * finished decrypting. Rejects with an `algorithms.DecryptionError` if there - * is a problem decrypting the event. - */ -Crypto.prototype.decryptEvent = async function(event) { - if (event.isRedacted()) { - const redactionEvent = new MatrixEvent(event.getUnsigned().redacted_because); - const decryptedEvent = await this.decryptEvent(redactionEvent); - - return { - clearEvent: { - room_id: event.getRoomId(), - type: "m.room.message", - content: {}, - unsigned: { - redacted_because: decryptedEvent.clearEvent, - }, - }, - }; - } else { - const content = event.getWireContent(); - const alg = this._getRoomDecryptor(event.getRoomId(), content.algorithm); - return await alg.decryptEvent(event); - } -}; - -/** - * Handle the notification from /sync or /keys/changes that device lists have - * been changed. - * - * @param {Object} syncData Object containing sync tokens associated with this sync - * @param {Object} syncDeviceLists device_lists field from /sync, or response from - * /keys/changes - */ -Crypto.prototype.handleDeviceListChanges = async function(syncData, syncDeviceLists) { - // Initial syncs don't have device change lists. We'll either get the complete list - // of changes for the interval or will have invalidated everything in willProcessSync - if (!syncData.oldSyncToken) return; - - // Here, we're relying on the fact that we only ever save the sync data after - // sucessfully saving the device list data, so we're guaranteed that the device - // list store is at least as fresh as the sync token from the sync store, ie. - // any device changes received in sync tokens prior to the 'next' token here - // have been processed and are reflected in the current device list. - // If we didn't make this assumption, we'd have to use the /keys/changes API - // to get key changes between the sync token in the device list and the 'old' - // sync token used here to make sure we didn't miss any. - await this._evalDeviceListChanges(syncDeviceLists); -}; - -/** - * Send a request for some room keys, if we have not already done so - * - * @param {module:crypto~RoomKeyRequestBody} requestBody - * @param {Array<{userId: string, deviceId: string}>} recipients - * @param {boolean} resend whether to resend the key request if there is - * already one - * - * @return {Promise} a promise that resolves when the key request is queued - */ -Crypto.prototype.requestRoomKey = function(requestBody, recipients, resend=false) { - return this._outgoingRoomKeyRequestManager.queueRoomKeyRequest( - requestBody, recipients, resend, - ).then(() => { - if (this._sendKeyRequestsImmediately) { - this._outgoingRoomKeyRequestManager.sendQueuedRequests(); - } - }).catch((e) => { - // this normally means we couldn't talk to the store - logger.error( - 'Error requesting key for event', e, - ); - }); -}; - -/** - * Cancel any earlier room key request - * - * @param {module:crypto~RoomKeyRequestBody} requestBody - * parameters to match for cancellation - */ -Crypto.prototype.cancelRoomKeyRequest = function(requestBody) { - this._outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody) - .catch((e) => { - logger.warn("Error clearing pending room key requests", e); - }); -}; - -/** - * Re-send any outgoing key requests, eg after verification - * @returns {Promise} - */ -Crypto.prototype.cancelAndResendAllOutgoingKeyRequests = function() { - return this._outgoingRoomKeyRequestManager.cancelAndResendAllOutgoingRequests(); -}; - -/** - * handle an m.room.encryption event - * - * @param {module:models/event.MatrixEvent} event encryption event - */ -Crypto.prototype.onCryptoEvent = async function(event) { - const roomId = event.getRoomId(); - const content = event.getContent(); - - try { - // inhibit the device list refresh for now - it will happen once we've - // finished processing the sync, in onSyncCompleted. - await this.setRoomEncryption(roomId, content, true); - } catch (e) { - logger.error("Error configuring encryption in room " + roomId + - ":", e); - } -}; - -/** - * Called before the result of a sync is procesed - * - * @param {Object} syncData the data from the 'MatrixClient.sync' event - */ -Crypto.prototype.onSyncWillProcess = async function(syncData) { - if (!syncData.oldSyncToken) { - // If there is no old sync token, we start all our tracking from - // scratch, so mark everything as untracked. onCryptoEvent will - // be called for all e2e rooms during the processing of the sync, - // at which point we'll start tracking all the users of that room. - logger.log("Initial sync performed - resetting device tracking state"); - this._deviceList.stopTrackingAllDeviceLists(); - // we always track our own device list (for key backups etc) - this._deviceList.startTrackingDeviceList(this._userId); - this._roomDeviceTrackingState = {}; - } - - this._sendKeyRequestsImmediately = false; -}; - -/** - * handle the completion of a /sync - * - * This is called after the processing of each successful /sync response. - * It is an opportunity to do a batch process on the information received. - * - * @param {Object} syncData the data from the 'MatrixClient.sync' event - */ -Crypto.prototype.onSyncCompleted = async function(syncData) { - const nextSyncToken = syncData.nextSyncToken; - - this._deviceList.setSyncToken(syncData.nextSyncToken); - this._deviceList.saveIfDirty(); - - // catch up on any new devices we got told about during the sync. - this._deviceList.lastKnownSyncToken = nextSyncToken; - - // we always track our own device list (for key backups etc) - this._deviceList.startTrackingDeviceList(this._userId); - - this._deviceList.refreshOutdatedDeviceLists(); - - // we don't start uploading one-time keys until we've caught up with - // to-device messages, to help us avoid throwing away one-time-keys that we - // are about to receive messages for - // (https://github.com/vector-im/element-web/issues/2782). - if (!syncData.catchingUp) { - _maybeUploadOneTimeKeys(this); - this._processReceivedRoomKeyRequests(); - - // likewise don't start requesting keys until we've caught up - // on to_device messages, otherwise we'll request keys that we're - // just about to get. - this._outgoingRoomKeyRequestManager.sendQueuedRequests(); - - // Sync has finished so send key requests straight away. - this._sendKeyRequestsImmediately = true; - } -}; - -/** - * Trigger the appropriate invalidations and removes for a given - * device list - * - * @param {Object} deviceLists device_lists field from /sync, or response from - * /keys/changes - */ -Crypto.prototype._evalDeviceListChanges = async function(deviceLists) { - if (deviceLists.changed && Array.isArray(deviceLists.changed)) { - deviceLists.changed.forEach((u) => { - this._deviceList.invalidateUserDeviceList(u); - }); - } - - if (deviceLists.left && Array.isArray(deviceLists.left) && - deviceLists.left.length) { - // Check we really don't share any rooms with these users - // any more: the server isn't required to give us the - // exact correct set. - const e2eUserIds = new Set(await this._getTrackedE2eUsers()); - - deviceLists.left.forEach((u) => { - if (!e2eUserIds.has(u)) { - this._deviceList.stopTrackingDeviceList(u); - } - }); - } -}; - -/** - * Get a list of all the IDs of users we share an e2e room with - * for which we are tracking devices already - * - * @returns {string[]} List of user IDs - */ -Crypto.prototype._getTrackedE2eUsers = async function() { - const e2eUserIds = []; - for (const room of this._getTrackedE2eRooms()) { - const members = await room.getEncryptionTargetMembers(); - for (const member of members) { - e2eUserIds.push(member.userId); - } - } - return e2eUserIds; -}; - -/** - * Get a list of the e2e-enabled rooms we are members of, - * and for which we are already tracking the devices - * - * @returns {module:models.Room[]} - */ -Crypto.prototype._getTrackedE2eRooms = function() { - return this._clientStore.getRooms().filter((room) => { - // check for rooms with encryption enabled - const alg = this._roomEncryptors[room.roomId]; - if (!alg) { - return false; - } - if (!this._roomDeviceTrackingState[room.roomId]) { - return false; - } - - // ignore any rooms which we have left - const myMembership = room.getMyMembership(); - return myMembership === "join" || myMembership === "invite"; - }); -}; - -Crypto.prototype._onToDeviceEvent = function(event) { - try { - logger.log(`received to_device ${event.getType()} from: ` + - `${event.getSender()} id: ${event.getId()}`); - - if (event.getType() == "m.room_key" - || event.getType() == "m.forwarded_room_key") { - this._onRoomKeyEvent(event); - } else if (event.getType() == "m.room_key_request") { - this._onRoomKeyRequestEvent(event); - } else if (event.getType() === "m.secret.request") { - this._secretStorage._onRequestReceived(event); - } else if (event.getType() === "m.secret.send") { - this._secretStorage._onSecretReceived(event); - } else if (event.getType() === "org.matrix.room_key.withheld") { - this._onRoomKeyWithheldEvent(event); - } else if (event.getContent().transaction_id) { - this._onKeyVerificationMessage(event); - } else if (event.getContent().msgtype === "m.bad.encrypted") { - this._onToDeviceBadEncrypted(event); - } else if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) { - if (!event.isBeingDecrypted()) { - event.attemptDecryption(this); - } - // once the event has been decrypted, try again - event.once('Event.decrypted', (ev) => { - this._onToDeviceEvent(ev); - }); - } - } catch (e) { - logger.error("Error handling toDeviceEvent:", e); - } -}; - -/** - * Handle a key event - * - * @private - * @param {module:models/event.MatrixEvent} event key event - */ -Crypto.prototype._onRoomKeyEvent = function(event) { - const content = event.getContent(); - - if (!content.room_id || !content.algorithm) { - logger.error("key event is missing fields"); - return; - } - - 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._backupManager.checkAndStart(); - } - - const alg = this._getRoomDecryptor(content.room_id, content.algorithm); - alg.onRoomKeyEvent(event); -}; - -/** - * Handle a key withheld event - * - * @private - * @param {module:models/event.MatrixEvent} event key withheld event - */ -Crypto.prototype._onRoomKeyWithheldEvent = function(event) { - const content = event.getContent(); - - if ((content.code !== "m.no_olm" && (!content.room_id || !content.session_id)) - || !content.algorithm || !content.sender_key) { - logger.error("key withheld event is missing fields"); - return; - } - - logger.info( - `Got room key withheld event from ${event.getSender()} (${content.sender_key}) ` - + `for ${content.algorithm}/${content.room_id}/${content.session_id} ` - + `with reason ${content.code} (${content.reason})`, - ); - - const alg = this._getRoomDecryptor(content.room_id, content.algorithm); - if (alg.onRoomKeyWithheldEvent) { - alg.onRoomKeyWithheldEvent(event); - } - if (!content.room_id) { - // retry decryption for all events sent by the sender_key. This will - // update the events to show a message indicating that the olm session was - // wedged. - const roomDecryptors = this._getRoomDecryptors(content.algorithm); - for (const decryptor of roomDecryptors) { - decryptor.retryDecryptionFromSender(content.sender_key); - } - } -}; - -/** - * Handle a general key verification event. - * - * @private - * @param {module:models/event.MatrixEvent} event verification start event - */ -Crypto.prototype._onKeyVerificationMessage = function(event) { - if (!ToDeviceChannel.validateEvent(event, this._baseApis)) { - return; - } - const createRequest = event => { - if (!ToDeviceChannel.canCreateRequest(ToDeviceChannel.getEventType(event))) { - return; - } - const content = event.getContent(); - const deviceId = content && content.from_device; - if (!deviceId) { - return; - } - const userId = event.getSender(); - const channel = new ToDeviceChannel( - this._baseApis, - userId, - [deviceId], - ); - return new VerificationRequest( - channel, this._verificationMethods, this._baseApis); - }; - this._handleVerificationEvent( - event, - this._toDeviceVerificationRequests, - createRequest, - ); -}; - -/** - * Handle key verification requests sent as timeline events - * - * @private - * @param {module:models/event.MatrixEvent} event the timeline event - * @param {module:models/Room} room not used - * @param {bool} atStart not used - * @param {bool} removed not used - * @param {bool} data.liveEvent whether this is a live event - */ -Crypto.prototype._onTimelineEvent = function( - event, room, atStart, removed, { liveEvent } = {}, -) { - if (!InRoomChannel.validateEvent(event, this._baseApis)) { - return; - } - const createRequest = event => { - const channel = new InRoomChannel( - this._baseApis, - event.getRoomId(), - ); - return new VerificationRequest( - channel, this._verificationMethods, this._baseApis); - }; - this._handleVerificationEvent( - event, - this._inRoomVerificationRequests, - createRequest, - liveEvent, - ); -}; - -Crypto.prototype._handleVerificationEvent = async function( - event, requestsMap, createRequest, isLiveEvent = true, -) { - let request = requestsMap.getRequest(event); - let isNewRequest = false; - if (!request) { - request = createRequest(event); - // a request could not be made from this event, so ignore event - if (!request) { - logger.log(`Crypto: could not find VerificationRequest for ` + - `${event.getType()}, and could not create one, so ignoring.`); - return; - } - isNewRequest = true; - requestsMap.setRequest(event, request); - } - event.setVerificationRequest(request); - try { - await request.channel.handleEvent(event, request, isLiveEvent); - } catch (err) { - logger.error("error while handling verification event: " + err.message); - } - const shouldEmit = isNewRequest && - !request.initiatedByMe && - !request.invalid && // check it has enough events to pass the UNSENT stage - !request.observeOnly; - if (shouldEmit) { - this._baseApis.emit("crypto.verification.request", request); - } -}; - -/** - * Handle a toDevice event that couldn't be decrypted - * - * @private - * @param {module:models/event.MatrixEvent} event undecryptable event - */ -Crypto.prototype._onToDeviceBadEncrypted = async function(event) { - const content = event.getWireContent(); - const sender = event.getSender(); - const algorithm = content.algorithm; - const deviceKey = content.sender_key; - - // retry decryption for all events sent by the sender_key. This will - // update the events to show a message indicating that the olm session was - // wedged. - const retryDecryption = () => { - const roomDecryptors = this._getRoomDecryptors(olmlib.MEGOLM_ALGORITHM); - for (const decryptor of roomDecryptors) { - decryptor.retryDecryptionFromSender(deviceKey); - } - }; - - if (sender === undefined || deviceKey === undefined || deviceKey === undefined) { - return; - } - - // check when we last forced a new session with this device: if we've already done so - // recently, don't do it again. - this._lastNewSessionForced[sender] = this._lastNewSessionForced[sender] || {}; - const lastNewSessionForced = this._lastNewSessionForced[sender][deviceKey] || 0; - if (lastNewSessionForced + MIN_FORCE_SESSION_INTERVAL_MS > Date.now()) { - logger.debug( - "New session already forced with device " + sender + ":" + deviceKey + - " at " + lastNewSessionForced + ": not forcing another", - ); - await this._olmDevice.recordSessionProblem(deviceKey, "wedged", true); - retryDecryption(); - return; - } - - // establish a new olm session with this device since we're failing to decrypt messages - // on a current session. - // Note that an undecryptable message from another device could easily be spoofed - - // is there anything we can do to mitigate this? - let device = this._deviceList.getDeviceByIdentityKey(algorithm, deviceKey); - if (!device) { - // if we don't know about the device, fetch the user's devices again - // and retry before giving up - await this.downloadKeys([sender], false); - device = this._deviceList.getDeviceByIdentityKey(algorithm, deviceKey); - if (!device) { - logger.info( - "Couldn't find device for identity key " + deviceKey + - ": not re-establishing session", - ); - await this._olmDevice.recordSessionProblem(deviceKey, "wedged", false); - retryDecryption(); - return; - } - } - const devicesByUser = {}; - devicesByUser[sender] = [device]; - await olmlib.ensureOlmSessionsForDevices( - this._olmDevice, this._baseApis, devicesByUser, true, - ); - - this._lastNewSessionForced[sender][deviceKey] = Date.now(); - - // Now send a blank message on that session so the other side knows about it. - // (The keyshare request is sent in the clear so that won't do) - // We send this first such that, as long as the toDevice messages arrive in the - // same order we sent them, the other end will get this first, set up the new session, - // then get the keyshare request and send the key over this new session (because it - // is the session it has most recently received a message on). - const encryptedContent = { - algorithm: olmlib.OLM_ALGORITHM, - sender_key: this._olmDevice.deviceCurve25519Key, - ciphertext: {}, - }; - await olmlib.encryptMessageForDevice( - encryptedContent.ciphertext, - this._userId, - this._deviceId, - this._olmDevice, - sender, - device, - { type: "m.dummy" }, - ); - - await this._olmDevice.recordSessionProblem(deviceKey, "wedged", true); - retryDecryption(); - - await this._baseApis.sendToDevice("m.room.encrypted", { - [sender]: { - [device.deviceId]: encryptedContent, - }, - }); - - // Most of the time this probably won't be necessary since we'll have queued up a key request when - // we failed to decrypt the message and will be waiting a bit for the key to arrive before sending - // it. This won't always be the case though so we need to re-send any that have already been sent - // to avoid races. - const requestsToResend = - await this._outgoingRoomKeyRequestManager.getOutgoingSentRoomKeyRequest( - sender, device.deviceId, - ); - for (const keyReq of requestsToResend) { - this.requestRoomKey(keyReq.requestBody, keyReq.recipients, true); - } -}; - -/** - * Handle a change in the membership state of a member of a room - * - * @private - * @param {module:models/event.MatrixEvent} event event causing the change - * @param {module:models/room-member} member user whose membership changed - * @param {string=} oldMembership previous membership - */ -Crypto.prototype._onRoomMembership = function(event, member, oldMembership) { - // this event handler is registered on the *client* (as opposed to the room - // member itself), which means it is only called on changes to the *live* - // membership state (ie, it is not called when we back-paginate, nor when - // we load the state in the initialsync). - // - // Further, it is automatically registered and called when new members - // arrive in the room. - - const roomId = member.roomId; - - const alg = this._roomEncryptors[roomId]; - if (!alg) { - // not encrypting in this room - return; - } - // only mark users in this room as tracked if we already started tracking in this room - // this way we don't start device queries after sync on behalf of this room which we won't use - // the result of anyway, as we'll need to do a query again once all the members are fetched - // by calling _trackRoomDevices - if (this._roomDeviceTrackingState[roomId]) { - if (member.membership == 'join') { - logger.log('Join event for ' + member.userId + ' in ' + roomId); - // make sure we are tracking the deviceList for this user - this._deviceList.startTrackingDeviceList(member.userId); - } else if (member.membership == 'invite' && - this._clientStore.getRoom(roomId).shouldEncryptForInvitedMembers()) { - logger.log('Invite event for ' + member.userId + ' in ' + roomId); - this._deviceList.startTrackingDeviceList(member.userId); - } - } - - alg.onRoomMembership(event, member, oldMembership); -}; - -/** - * Called when we get an m.room_key_request event. - * - * @private - * @param {module:models/event.MatrixEvent} event key request event - */ -Crypto.prototype._onRoomKeyRequestEvent = function(event) { - const content = event.getContent(); - if (content.action === "request") { - // Queue it up for now, because they tend to arrive before the room state - // events at initial sync, and we want to see if we know anything about the - // room before passing them on to the app. - const req = new IncomingRoomKeyRequest(event); - this._receivedRoomKeyRequests.push(req); - } else if (content.action === "request_cancellation") { - const req = new IncomingRoomKeyRequestCancellation(event); - this._receivedRoomKeyRequestCancellations.push(req); - } -}; - -/** - * Process any m.room_key_request events which were queued up during the - * current sync. - * - * @private - */ -Crypto.prototype._processReceivedRoomKeyRequests = async function() { - if (this._processingRoomKeyRequests) { - // we're still processing last time's requests; keep queuing new ones - // up for now. - return; - } - this._processingRoomKeyRequests = true; - - try { - // we need to grab and clear the queues in the synchronous bit of this method, - // so that we don't end up racing with the next /sync. - const requests = this._receivedRoomKeyRequests; - this._receivedRoomKeyRequests = []; - const cancellations = this._receivedRoomKeyRequestCancellations; - this._receivedRoomKeyRequestCancellations = []; - - // Process all of the requests, *then* all of the cancellations. - // - // This makes sure that if we get a request and its cancellation in the - // same /sync result, then we process the request before the - // cancellation (and end up with a cancelled request), rather than the - // cancellation before the request (and end up with an outstanding - // request which should have been cancelled.) - await Promise.all(requests.map((req) => - this._processReceivedRoomKeyRequest(req))); - await Promise.all(cancellations.map((cancellation) => - this._processReceivedRoomKeyRequestCancellation(cancellation))); - } catch (e) { - logger.error(`Error processing room key requsts: ${e}`); - } finally { - this._processingRoomKeyRequests = false; - } -}; - -/** - * Helper for processReceivedRoomKeyRequests - * - * @param {IncomingRoomKeyRequest} req - */ -Crypto.prototype._processReceivedRoomKeyRequest = async function(req) { - const userId = req.userId; - const deviceId = req.deviceId; - - const body = req.requestBody; - const roomId = body.room_id; - const alg = body.algorithm; - - logger.log(`m.room_key_request from ${userId}:${deviceId}` + - ` for ${roomId} / ${body.session_id} (id ${req.requestId})`); - - if (userId !== this._userId) { - if (!this._roomEncryptors[roomId]) { - logger.debug(`room key request for unencrypted room ${roomId}`); - return; - } - const encryptor = this._roomEncryptors[roomId]; - const device = this._deviceList.getStoredDevice(userId, deviceId); - if (!device) { - logger.debug(`Ignoring keyshare for unknown device ${userId}:${deviceId}`); - return; - } - - try { - await encryptor.reshareKeyWithDevice( - body.sender_key, body.session_id, userId, device, - ); - } catch (e) { - logger.warn( - "Failed to re-share keys for session " + body.session_id + - " with device " + userId + ":" + device.deviceId, e, - ); - } - return; - } - - if (deviceId === this._deviceId) { - // We'll always get these because we send room key requests to - // '*' (ie. 'all devices') which includes the sending device, - // so ignore requests from ourself because apart from it being - // very silly, it won't work because an Olm session cannot send - // messages to itself. - // The log here is probably superfluous since we know this will - // always happen, but let's log anyway for now just in case it - // causes issues. - logger.log("Ignoring room key request from ourselves"); - return; - } - - // todo: should we queue up requests we don't yet have keys for, - // in case they turn up later? - - // if we don't have a decryptor for this room/alg, we don't have - // the keys for the requested events, and can drop the requests. - if (!this._roomDecryptors[roomId]) { - logger.log(`room key request for unencrypted room ${roomId}`); - return; - } - - const decryptor = this._roomDecryptors[roomId][alg]; - if (!decryptor) { - logger.log(`room key request for unknown alg ${alg} in room ${roomId}`); - return; - } - - if (!await decryptor.hasKeysForKeyRequest(req)) { - logger.log( - `room key request for unknown session ${roomId} / ` + - body.session_id, - ); - return; - } - - req.share = () => { - decryptor.shareKeysWithDevice(req); - }; - - // if the device is verified already, share the keys - if (this.checkDeviceTrust(userId, deviceId).isVerified()) { - logger.log('device is already verified: sharing keys'); - req.share(); - return; - } - - this.emit("crypto.roomKeyRequest", req); -}; - -/** - * Helper for processReceivedRoomKeyRequests - * - * @param {IncomingRoomKeyRequestCancellation} cancellation - */ -Crypto.prototype._processReceivedRoomKeyRequestCancellation = async function( - cancellation, -) { - logger.log( - `m.room_key_request cancellation for ${cancellation.userId}:` + - `${cancellation.deviceId} (id ${cancellation.requestId})`, - ); - - // we should probably only notify the app of cancellations we told it - // about, but we don't currently have a record of that, so we just pass - // everything through. - this.emit("crypto.roomKeyRequestCancellation", cancellation); -}; - -/** - * Get a decryptor for a given room and algorithm. - * - * If we already have a decryptor for the given room and algorithm, return - * it. Otherwise try to instantiate it. - * - * @private - * - * @param {string?} roomId room id for decryptor. If undefined, a temporary - * decryptor is instantiated. - * - * @param {string} algorithm crypto algorithm - * - * @return {module:crypto.algorithms.base.DecryptionAlgorithm} - * - * @raises {module:crypto.algorithms.DecryptionError} if the algorithm is - * unknown - */ -Crypto.prototype._getRoomDecryptor = function(roomId, algorithm) { - let decryptors; - let alg; - - roomId = roomId || null; - if (roomId) { - decryptors = this._roomDecryptors[roomId]; - if (!decryptors) { - this._roomDecryptors[roomId] = decryptors = {}; - } - - alg = decryptors[algorithm]; - if (alg) { - return alg; - } - } - - const AlgClass = algorithms.DECRYPTION_CLASSES[algorithm]; - if (!AlgClass) { - throw new algorithms.DecryptionError( - 'UNKNOWN_ENCRYPTION_ALGORITHM', - 'Unknown encryption algorithm "' + algorithm + '".', - ); - } - alg = new AlgClass({ - userId: this._userId, - crypto: this, - olmDevice: this._olmDevice, - baseApis: this._baseApis, - roomId: roomId, - }); - - if (decryptors) { - decryptors[algorithm] = alg; - } - return alg; -}; - -/** - * Get all the room decryptors for a given encryption algorithm. - * - * @param {string} algorithm The encryption algorithm - * - * @return {array} An array of room decryptors - */ -Crypto.prototype._getRoomDecryptors = function(algorithm) { - const decryptors = []; - for (const d of Object.values(this._roomDecryptors)) { - if (algorithm in d) { - decryptors.push(d[algorithm]); - } - } - return decryptors; -}; - -/** - * sign the given object with our ed25519 key - * - * @param {Object} obj Object to which we will add a 'signatures' property - */ -Crypto.prototype._signObject = async function(obj) { - const sigs = obj.signatures || {}; - const unsigned = obj.unsigned; - - delete obj.signatures; - delete obj.unsigned; - - sigs[this._userId] = sigs[this._userId] || {}; - sigs[this._userId]["ed25519:" + this._deviceId] = - await this._olmDevice.sign(anotherjson.stringify(obj)); - obj.signatures = sigs; - if (unsigned !== undefined) obj.unsigned = unsigned; -}; - -/** - * The parameters of a room key request. The details of the request may - * vary with the crypto algorithm, but the management and storage layers for - * outgoing requests expect it to have 'room_id' and 'session_id' properties. - * - * @typedef {Object} RoomKeyRequestBody - */ - -/** - * Represents a received m.room_key_request event - * - * @property {string} userId user requesting the key - * @property {string} deviceId device requesting the key - * @property {string} requestId unique id for the request - * @property {module:crypto~RoomKeyRequestBody} requestBody - * @property {function()} share callback which, when called, will ask - * the relevant crypto algorithm implementation to share the keys for - * this request. - */ -class IncomingRoomKeyRequest { - constructor(event) { - const content = event.getContent(); - - this.userId = event.getSender(); - this.deviceId = content.requesting_device_id; - this.requestId = content.request_id; - this.requestBody = content.body || {}; - this.share = () => { - throw new Error("don't know how to share keys for this request yet"); - }; - } -} - -/** - * Represents a received m.room_key_request cancellation - * - * @property {string} userId user requesting the cancellation - * @property {string} deviceId device requesting the cancellation - * @property {string} requestId unique id for the request to be cancelled - */ -class IncomingRoomKeyRequestCancellation { - constructor(event) { - const content = event.getContent(); - - this.userId = event.getSender(); - this.deviceId = content.requesting_device_id; - this.requestId = content.request_id; - } -} - -/** - * The result of a (successful) call to decryptEvent. - * - * @typedef {Object} EventDecryptionResult - * - * @property {Object} clearEvent The plaintext payload for the event - * (typically containing type and content fields). - * - * @property {?string} senderCurve25519Key Key owned by the sender of this - * event. See {@link module:models/event.MatrixEvent#getSenderKey}. - * - * @property {?string} claimedEd25519Key ed25519 key claimed by the sender of - * this event. See - * {@link module:models/event.MatrixEvent#getClaimedEd25519Key}. - * - * @property {?Array} forwardingCurve25519KeyChain list of curve25519 - * keys involved in telling us about the senderCurve25519Key and - * claimedEd25519Key. See - * {@link module:models/event.MatrixEvent#getForwardingCurve25519KeyChain}. - */ - -/** - * Fires when we receive a room key request - * - * @event module:client~MatrixClient#"crypto.roomKeyRequest" - * @param {module:crypto~IncomingRoomKeyRequest} req request details - */ - -/** - * Fires when we receive a room key request cancellation - * - * @event module:client~MatrixClient#"crypto.roomKeyRequestCancellation" - * @param {module:crypto~IncomingRoomKeyRequestCancellation} req - */ - -/** - * Fires when the app may wish to warn the user about something related - * the end-to-end crypto. - * - * @event module:client~MatrixClient#"crypto.warning" - * @param {string} type One of the strings listed above - */ diff --git a/src/crypto/index.ts b/src/crypto/index.ts new file mode 100644 index 00000000000..e3ab46e6c06 --- /dev/null +++ b/src/crypto/index.ts @@ -0,0 +1,3745 @@ +/* +Copyright 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd +Copyright 2018-2019 New Vector Ltd +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. +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 + */ + +import anotherjson from "another-json"; +import { EventEmitter } from 'events'; + +import { ReEmitter } from '../ReEmitter'; +import { logger } from '../logger'; +import { OlmDevice } from "./OlmDevice"; +import * as olmlib from "./olmlib"; +import { DeviceList } from "./DeviceList"; +import { DeviceInfo } from "./deviceinfo"; +import * as algorithms from "./algorithms"; +import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from './CrossSigning'; +import { EncryptionSetupBuilder } from "./EncryptionSetup"; +import { SECRET_STORAGE_ALGORITHM_V1_AES, SecretStorage } from './SecretStorage'; +import { OutgoingRoomKeyRequestManager } from './OutgoingRoomKeyRequestManager'; +import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; +import { ReciprocateQRCode, SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD } from './verification/QRCode'; +import { SAS } from './verification/SAS'; +import { keyFromPassphrase } from './key_passphrase'; +import { decodeRecoveryKey, encodeRecoveryKey } from './recoverykey'; +import { VerificationRequest } from "./verification/request/VerificationRequest"; +import { InRoomChannel, InRoomRequests } from "./verification/request/InRoomChannel"; +import { ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDeviceChannel"; +import { IllegalMethod } from "./verification/IllegalMethod"; +import { KeySignatureUploadError } from "../errors"; +import { decryptAES, encryptAES } from './aes'; +import { DehydrationManager } from './dehydration'; +import { BackupManager } from "./backup"; +import { IStore } from "../store"; +import { Room, RoomMember, MatrixEvent, MatrixClient, IKeysUploadResponse } from ".."; +import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base"; +import type { RoomList } from "./RoomList"; +import { IRecoveryKey, IEncryptedEventInfo } from "./api"; + +const DeviceVerification = DeviceInfo.DeviceVerification; + +const defaultVerificationMethods = { + [ReciprocateQRCode.NAME]: ReciprocateQRCode, + [SAS.NAME]: SAS, + + // These two can't be used for actual verification, but we do + // need to be able to define them here for the verification flows + // to start. + [SHOW_QR_CODE_METHOD]: IllegalMethod, + [SCAN_QR_CODE_METHOD]: IllegalMethod, +} + +/** + * verification method names + */ +export const verificationMethods = { + RECIPROCATE_QR_CODE: ReciprocateQRCode.NAME, + SAS: SAS.NAME, +} + +export function isCryptoAvailable(): boolean { + return Boolean(global.Olm); +} + +const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000; + +interface IInitOpts { + exportedOlmDevice?: any; // TODO types + pickleKey?: string; +} + +export interface IBootstrapCrossSigningOpts { + setupNewCrossSigning?: boolean; + authUploadDeviceSigningKeys?(makeRequest: (authData: any) => void): Promise; +} + +interface IBootstrapSecretStorageOpts { + keyBackupInfo?: any; // TODO types + setupNewKeyBackup?: boolean; + setupNewSecretStorage?: boolean; + createSecretStorageKey?(): Promise<{ + keyInfo?: any; // TODO types + privateKey?: Uint8Array; + }>; + getKeyBackupPassphrase?(): Promise; +} + +/* eslint-disable camelcase */ +interface IRoomKey { + room_id: string; + algorithm: string; +} + +interface IRoomKeyRequestBody extends IRoomKey { + session_id: string; + sender_key: string +} + +interface IMegolmSessionData { + sender_key: string; + forwarding_curve25519_key_chain: string[]; + sender_claimed_keys: Record; + room_id: string; + session_id: string; + session_key: string; +} +/* eslint-enable camelcase */ + +interface IDeviceVerificationUpgrade { + devices: DeviceInfo[]; + crossSigningInfo: CrossSigningInfo; +} + +/** + * @typedef {Object} module:crypto~OlmSessionResult + * @property {module:crypto/deviceinfo} device device info + * @property {string?} sessionId base64 olm session id; null if no session + * could be established + */ + +interface IOlmSessionResult { + device: DeviceInfo; + sessionId?: string; +} + +interface IUserOlmSession { + deviceIdKey: string; + sessions: { + sessionId: string; + hasReceivedMessage: boolean; + }[]; +} + +interface ISyncData { + oldSyncToken?: string; + nextSyncToken: string; + catchingUp?: boolean; +} + +interface ISyncDeviceLists { + changed: string[]; + left: string[]; +} + +interface IRoomKeyRequestRecipient { + userId: string; + deviceId: string; +} + +interface ISignableObject { + signatures?: object; + unsigned?: object +} + +interface IEventDecryptionResult { + clearEvent: object; + senderCurve25519Key?: string; + claimedEd25519Key?: string; + forwardingCurve25519KeyChain?: string[]; + untrusted?: boolean; +} + +export class Crypto extends EventEmitter { + /** + * @return {string} The version of Olm. + */ + static getOlmVersion(): string { + return OlmDevice.getOlmVersion(); + } + + public readonly backupManager: BackupManager; + public readonly crossSigningInfo: CrossSigningInfo; + public readonly olmDevice: OlmDevice; + public readonly deviceList: DeviceList; + public readonly dehydrationManager: DehydrationManager; + public readonly secretStorage: SecretStorage; + + private readonly reEmitter: ReEmitter; + private readonly verificationMethods: any; // TODO types + private readonly supportedAlgorithms: DecryptionAlgorithm[]; + private readonly outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager; + private readonly toDeviceVerificationRequests: ToDeviceRequests; + private readonly inRoomVerificationRequests: InRoomRequests; + + private trustCrossSignedDevices = true; + // the last time we did a check for the number of one-time-keys on the server. + private lastOneTimeKeyCheck: number = null; + private oneTimeKeyCheckInProgress = false; + + // EncryptionAlgorithm instance for each room + private roomEncryptors: Record = {}; + // map from algorithm to DecryptionAlgorithm instance, for each room + private roomDecryptors: Record> = {}; + + private deviceKeys: Record = {}; // type: key + + private globalBlacklistUnverifiedDevices = false; + private globalErrorOnUnknownDevices = true; + + // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations + // we received in the current sync. + private receivedRoomKeyRequests: IncomingRoomKeyRequest[] = []; + private receivedRoomKeyRequestCancellations: IncomingRoomKeyRequestCancellation[] = []; + // true if we are currently processing received room key requests + private processingRoomKeyRequests = false; + // controls whether device tracking is delayed + // until calling encryptEvent or trackRoomDevices, + // or done immediately upon enabling room encryption. + private lazyLoadMembers = false; + // in case lazyLoadMembers is true, + // track if an initial tracking of all the room members + // has happened for a given room. This is delayed + // to avoid loading room members as long as possible. + private roomDeviceTrackingState: Record> = {}; // roomId: Promise> = {}; + + // This flag will be unset whilst the client processes a sync response + // so that we don't start requesting keys until we've actually finished + // processing the response. + private sendKeyRequestsImmediately = false; + + private oneTimeKeyCount: number; + private needsNewFallback: boolean; + + /** + * Cryptography bits + * + * This module is internal to the js-sdk; the public API is via MatrixClient. + * + * @constructor + * @alias module:crypto + * + * @internal + * + * @param {MatrixClient} baseApis base matrix api interface + * + * @param {module:store/session/webstorage~WebStorageSessionStore} sessionStore + * Store to be used for end-to-end crypto session data + * + * @param {string} userId The user ID for the local user + * + * @param {string} deviceId The identifier for this device. + * + * @param {Object} clientStore the MatrixClient data store. + * + * @param {module:crypto/store/base~CryptoStore} cryptoStore + * storage for the crypto layer. + * + * @param {RoomList} roomList An initialised RoomList object + * + * @param {Array} verificationMethods Array of verification methods to use. + * Each element can either be a string from MatrixClient.verificationMethods + * or a class that implements a verification method. + */ + constructor( + private readonly baseApis: MatrixClient, + public readonly sessionStore: any, // TODO types + private readonly userId: string, + private readonly deviceId: string, + private readonly clientStore: IStore, + public readonly cryptoStore: any, // TODO types + private readonly roomList: RoomList, + verificationMethods: any[], // TODO types + ) { + super(); + this.reEmitter = new ReEmitter(this); + + if (verificationMethods) { + this.verificationMethods = new Map(); + for (const method of verificationMethods) { + if (typeof method === "string") { + if (defaultVerificationMethods[method]) { + this.verificationMethods.set( + method, + defaultVerificationMethods[method], + ); + } + } else if (method.NAME) { + this.verificationMethods.set( + method.NAME, + method, + ); + } else { + logger.warn(`Excluding unknown verification method ${method}`); + } + } + } else { + this.verificationMethods = defaultVerificationMethods; + } + + this.backupManager = new BackupManager(baseApis, async () => { + // 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.getSecretStorageKey(); + await this.storeSecret("m.megolm_backup.v1", fixedKey, [keyId]); + } + + return olmlib.decodeBase64(fixedKey || storedKey); + } + + // try to get key from app + if (this.baseApis.cryptoCallbacks && this.baseApis.cryptoCallbacks.getBackupKey) { + return await this.baseApis.cryptoCallbacks.getBackupKey(); + } + + throw new Error("Unable to get private key"); + }); + + this.olmDevice = new OlmDevice(cryptoStore); + this.deviceList = new DeviceList(baseApis, cryptoStore, this.olmDevice); + + // XXX: This isn't removed at any point, but then none of the event listeners + // this class sets seem to be removed at any point... :/ + this.deviceList.on('userCrossSigningUpdated', this.onDeviceListUserCrossSigningUpdated); + this.reEmitter.reEmit(this.deviceList, ["crypto.devicesUpdated", "crypto.willUpdateDevices"]); + + this.supportedAlgorithms = Object.keys(algorithms.DECRYPTION_CLASSES); + + this.outgoingRoomKeyRequestManager = new OutgoingRoomKeyRequestManager( + baseApis, this.deviceId, this.cryptoStore, + ); + + this.toDeviceVerificationRequests = new ToDeviceRequests(); + this.inRoomVerificationRequests = new InRoomRequests(); + + const cryptoCallbacks = this.baseApis.cryptoCallbacks || {}; + const cacheCallbacks = createCryptoStoreCacheCallbacks(cryptoStore, this.olmDevice); + + this.crossSigningInfo = new CrossSigningInfo(userId, cryptoCallbacks, cacheCallbacks); + this.secretStorage = new SecretStorage(baseApis, cryptoCallbacks); + this.dehydrationManager = new DehydrationManager(this); + + // Assuming no app-supplied callback, default to getting from SSSS. + if (!cryptoCallbacks.getCrossSigningKey && cryptoCallbacks.getSecretStorageKey) { + cryptoCallbacks.getCrossSigningKey = async (type) => { + return CrossSigningInfo.getFromSecretStorage(type, this.secretStorage); + }; + } + } + + /** + * Initialise the crypto module so that it is ready for use + * + * Returns a promise which resolves once the crypto module is ready for use. + * + * @param {Object} opts keyword arguments. + * @param {string} opts.exportedOlmDevice (Optional) data from exported device + * that must be re-created. + */ + public async init({ exportedOlmDevice, pickleKey }: IInitOpts = {}): Promise { + logger.log("Crypto: initialising Olm..."); + await global.Olm.init(); + logger.log(exportedOlmDevice + ? "Crypto: initialising Olm device from exported device..." + : "Crypto: initialising Olm device...", + ); + await this.olmDevice.init({ fromExportedDevice: exportedOlmDevice, pickleKey }); + logger.log("Crypto: loading device list..."); + await this.deviceList.load(); + + // build our device keys: these will later be uploaded + this.deviceKeys["ed25519:" + this.deviceId] = this.olmDevice.deviceEd25519Key; + this.deviceKeys["curve25519:" + this.deviceId] = this.olmDevice.deviceCurve25519Key; + + logger.log("Crypto: fetching own devices..."); + let myDevices = this.deviceList.getRawStoredDevicesForUser(this.userId); + + if (!myDevices) { + myDevices = {}; + } + + if (!myDevices[this.deviceId]) { + // add our own deviceinfo to the cryptoStore + logger.log("Crypto: adding this device to the store..."); + const deviceInfo = { + keys: this.deviceKeys, + algorithms: this.supportedAlgorithms, + verified: DeviceVerification.VERIFIED, + known: true, + }; + + myDevices[this.deviceId] = deviceInfo; + this.deviceList.storeDevicesForUser( + this.userId, myDevices, + ); + this.deviceList.saveIfDirty(); + } + + await this.cryptoStore.doTxn( + 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], + (txn) => { + this.cryptoStore.getCrossSigningKeys(txn, (keys) => { + // can be an empty object after resetting cross-signing keys, see storeTrustedSelfKeys + if (keys && Object.keys(keys).length !== 0) { + logger.log("Loaded cross-signing public keys from crypto store"); + this.crossSigningInfo.setKeys(keys); + } + }); + }, + ); + // make sure we are keeping track of our own devices + // (this is important for key backups & things) + this.deviceList.startTrackingDeviceList(this.userId); + + logger.log("Crypto: checking for key backup..."); + this.backupManager.checkAndStart(); + } + + /** + * Whether to trust a others users signatures of their devices. + * If false, devices will only be considered 'verified' if we have + * verified that device individually (effectively disabling cross-signing). + * + * Default: true + * + * @return {boolean} True if trusting cross-signed devices + */ + public getCryptoTrustCrossSignedDevices(): boolean { + return this.trustCrossSignedDevices; + } + + /** + * See getCryptoTrustCrossSignedDevices + + * This may be set before initCrypto() is called to ensure no races occur. + * + * @param {boolean} val True to trust cross-signed devices + */ + public setCryptoTrustCrossSignedDevices(val: boolean): void { + this.trustCrossSignedDevices = val; + + for (const userId of this.deviceList.getKnownUserIds()) { + const devices = this.deviceList.getRawStoredDevicesForUser(userId); + for (const deviceId of Object.keys(devices)) { + const deviceTrust = this.checkDeviceTrust(userId, deviceId); + // If the device is locally verified then isVerified() is always true, + // so this will only have caused the value to change if the device is + // cross-signing verified but not locally verified + if ( + !deviceTrust.isLocallyVerified() && + deviceTrust.isCrossSigningVerified() + ) { + const deviceObj = this.deviceList.getStoredDevice(userId, deviceId); + this.emit("deviceVerificationChanged", userId, deviceId, deviceObj); + } + } + } + } + + /** + * Create a recovery key from a user-supplied passphrase. + * + * @param {string} password Passphrase string that can be entered by the user + * when restoring the backup as an alternative to entering the recovery key. + * Optional. + * @returns {Promise} Object with public key metadata, encoded private + * recovery key which should be disposed of after displaying to the user, + * and raw private key to avoid round tripping if needed. + */ + public async createRecoveryKeyFromPassphrase(password: string): Promise { + const decryption = new global.Olm.PkDecryption(); + try { + const keyInfo: Partial = {}; + if (password) { + const derivation = await keyFromPassphrase(password); + keyInfo.passphrase = { + algorithm: "m.pbkdf2", + iterations: derivation.iterations, + salt: derivation.salt, + }; + keyInfo.pubkey = decryption.init_with_private_key(derivation.key); + } else { + keyInfo.pubkey = decryption.generate_key(); + } + const privateKey = decryption.get_private_key(); + const encodedPrivateKey = encodeRecoveryKey(privateKey); + return { + keyInfo: keyInfo as IRecoveryKey["keyInfo"], + encodedPrivateKey, + privateKey, + }; + } finally { + if (decryption) decryption.free(); + } + } + + /** + * Checks whether cross signing: + * - is enabled on this account and trusted by this device + * - has private keys either cached locally or stored in secret storage + * + * If this function returns false, bootstrapCrossSigning() can be used + * to fix things such that it returns true. That is to say, after + * bootstrapCrossSigning() completes successfully, this function should + * return true. + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @return {boolean} True if cross-signing is ready to be used on this device + */ + public async isCrossSigningReady(): Promise { + const publicKeysOnDevice = this.crossSigningInfo.getId(); + const privateKeysExistSomewhere = ( + await this.crossSigningInfo.isStoredInKeyCache() || + await this.crossSigningInfo.isStoredInSecretStorage(this.secretStorage) + ); + + return !!(publicKeysOnDevice && privateKeysExistSomewhere); + } + + /** + * Checks whether secret storage: + * - is enabled on this account + * - is storing cross-signing private keys + * - is storing session backup key (if enabled) + * + * If this function returns false, bootstrapSecretStorage() can be used + * to fix things such that it returns true. That is to say, after + * bootstrapSecretStorage() completes successfully, this function should + * return true. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @return {boolean} True if secret storage is ready to be used on this device + */ + public async isSecretStorageReady(): Promise { + const secretStorageKeyInAccount = await this.secretStorage.hasKey(); + const privateKeysInStorage = await this.crossSigningInfo.isStoredInSecretStorage( + this.secretStorage, + ); + const sessionBackupInStorage = ( + !this.backupManager.getKeyBackupEnabled() || + this.baseApis.isKeyBackupKeyStored() + ); + + return !!( + secretStorageKeyInAccount && + privateKeysInStorage && + sessionBackupInStorage + ); + } + + /** + * Bootstrap cross-signing by creating keys if needed. If everything is already + * set up, then no changes are made, so this is safe to run to ensure + * cross-signing is ready for use. + * + * This function: + * - creates new cross-signing keys if they are not found locally cached nor in + * secret storage (if it has been setup) + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @param {function} opts.authUploadDeviceSigningKeys Function + * called to await an interactive auth flow when uploading device signing keys. + * @param {boolean} [opts.setupNewCrossSigning] Optional. Reset even if keys + * already exist. + * Args: + * {function} A function that makes the request requiring auth. Receives the + * auth data as an object. Can be called multiple times, first with an empty + * authDict, to obtain the flows. + */ + public async bootstrapCrossSigning({ + authUploadDeviceSigningKeys, + setupNewCrossSigning, + }: IBootstrapCrossSigningOpts = {}): Promise { + logger.log("Bootstrapping cross-signing"); + + const delegateCryptoCallbacks = this.baseApis.cryptoCallbacks; + const builder = new EncryptionSetupBuilder( + this.baseApis.store.accountData, + delegateCryptoCallbacks, + ); + const crossSigningInfo = new CrossSigningInfo( + this.userId, + builder.crossSigningCallbacks, + builder.crossSigningCallbacks, + ); + + // Reset the cross-signing keys + const resetCrossSigning = async () => { + crossSigningInfo.resetKeys(); + // Sign master key with device key + await this.signObject(crossSigningInfo.keys.master); + + // Store auth flow helper function, as we need to call it when uploading + // to ensure we handle auth errors properly. + builder.addCrossSigningKeys(authUploadDeviceSigningKeys, crossSigningInfo.keys); + + // Cross-sign own device + const device = this.deviceList.getStoredDevice(this.userId, this.deviceId); + const deviceSignature = await crossSigningInfo.signDevice(this.userId, device); + builder.addKeySignature(this.userId, this.deviceId, deviceSignature); + + // Sign message key backup with cross-signing master key + if (this.backupManager.backupInfo) { + await crossSigningInfo.signObject( + this.backupManager.backupInfo.auth_data, "master", + ); + builder.addSessionBackup(this.backupManager.backupInfo); + } + }; + + const publicKeysOnDevice = this.crossSigningInfo.getId(); + const privateKeysInCache = await this.crossSigningInfo.isStoredInKeyCache(); + const privateKeysInStorage = await this.crossSigningInfo.isStoredInSecretStorage( + this.secretStorage, + ); + const privateKeysExistSomewhere = ( + privateKeysInCache || + privateKeysInStorage + ); + + // Log all relevant state for easier parsing of debug logs. + logger.log({ + setupNewCrossSigning, + publicKeysOnDevice, + privateKeysInCache, + privateKeysInStorage, + privateKeysExistSomewhere, + }); + + if (!privateKeysExistSomewhere || setupNewCrossSigning) { + logger.log( + "Cross-signing private keys not found locally or in secret storage, " + + "creating new keys", + ); + // If a user has multiple devices, it important to only call bootstrap + // as part of some UI flow (and not silently during startup), as they + // may have setup cross-signing on a platform which has not saved keys + // to secret storage, and this would reset them. In such a case, you + // should prompt the user to verify any existing devices first (and + // request private keys from those devices) before calling bootstrap. + await resetCrossSigning(); + } else if (publicKeysOnDevice && privateKeysInCache) { + logger.log( + "Cross-signing public keys trusted and private keys found locally", + ); + } else if (privateKeysInStorage) { + logger.log( + "Cross-signing private keys not found locally, but they are available " + + "in secret storage, reading storage and caching locally", + ); + await this.checkOwnCrossSigningTrust({ + allowPrivateKeyRequests: true, + }); + } + + // Assuming no app-supplied callback, default to storing new private keys in + // secret storage if it exists. If it does not, it is assumed this will be + // done as part of setting up secret storage later. + const crossSigningPrivateKeys = builder.crossSigningCallbacks.privateKeys; + if ( + crossSigningPrivateKeys.size && + !this.baseApis.cryptoCallbacks.saveCrossSigningKeys + ) { + const secretStorage = new SecretStorage( + builder.accountDataClientAdapter, + builder.ssssCryptoCallbacks); + if (await secretStorage.hasKey()) { + logger.log("Storing new cross-signing private keys in secret storage"); + // This is writing to in-memory account data in + // builder.accountDataClientAdapter so won't fail + await CrossSigningInfo.storeInSecretStorage( + crossSigningPrivateKeys, + secretStorage, + ); + } + } + + const operation = builder.buildOperation(); + await operation.apply(this); + // This persists private keys and public keys as trusted, + // only do this if apply succeeded for now as retry isn't in place yet + await builder.persist(this); + + logger.log("Cross-signing ready"); + } + + /** + * Bootstrap Secure Secret Storage if needed by creating a default key. If everything is + * already set up, then no changes are made, so this is safe to run to ensure secret + * storage is ready for use. + * + * This function + * - creates a new Secure Secret Storage key if no default key exists + * - if a key backup exists, it is migrated to store the key in the Secret + * Storage + * - creates a backup if none exists, and one is requested + * - migrates Secure Secret Storage to use the latest algorithm, if an outdated + * algorithm is found + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @param {function} [opts.createSecretStorageKey] Optional. Function + * called to await a secret storage key creation flow. + * Returns: + * {Promise} Object with public key metadata, encoded private + * recovery key which should be disposed of after displaying to the user, + * and raw private key to avoid round tripping if needed. + * @param {object} [opts.keyBackupInfo] The current key backup object. If passed, + * the passphrase and recovery key from this backup will be used. + * @param {boolean} [opts.setupNewKeyBackup] If true, a new key backup version will be + * created and the private key stored in the new SSSS store. Ignored if keyBackupInfo + * is supplied. + * @param {boolean} [opts.setupNewSecretStorage] Optional. Reset even if keys already exist. + * @param {func} [opts.getKeyBackupPassphrase] Optional. Function called to get the user's + * current key backup passphrase. Should return a promise that resolves with a Buffer + * containing the key, or rejects if the key cannot be obtained. + * Returns: + * {Promise} A promise which resolves to key creation data for + * SecretStorage#addKey: an object with `passphrase` etc fields. + */ + // TODO this does not resolve with what it says it does + public async bootstrapSecretStorage({ + createSecretStorageKey = async () => ({ }), + keyBackupInfo, + setupNewKeyBackup, + setupNewSecretStorage, + getKeyBackupPassphrase, + }: IBootstrapSecretStorageOpts = {}) { + logger.log("Bootstrapping Secure Secret Storage"); + const delegateCryptoCallbacks = this.baseApis.cryptoCallbacks; + const builder = new EncryptionSetupBuilder( + this.baseApis.store.accountData, + delegateCryptoCallbacks, + ); + const secretStorage = new SecretStorage( + builder.accountDataClientAdapter, + builder.ssssCryptoCallbacks, + ); + + // the ID of the new SSSS key, if we create one + let newKeyId = null; + + // create a new SSSS key and set it as default + const createSSSS = async (opts, privateKey: Uint8Array) => { + opts = opts || {}; + if (privateKey) { + opts.key = privateKey; + } + + const { keyId, keyInfo } = await secretStorage.addKey(SECRET_STORAGE_ALGORITHM_V1_AES, opts); + + if (privateKey) { + // make the private key available to encrypt 4S secrets + builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey); + } + + await secretStorage.setDefaultKeyId(keyId); + return keyId; + }; + + const ensureCanCheckPassphrase = async (keyId, keyInfo) => { + if (!keyInfo.mac) { + const key = await this.baseApis.cryptoCallbacks.getSecretStorageKey( + { keys: { [keyId]: keyInfo } }, "", + ); + if (key) { + const privateKey = key[1]; + builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey); + const { iv, mac } = await SecretStorage._calculateKeyCheck(privateKey); + keyInfo.iv = iv; + keyInfo.mac = mac; + + await builder.setAccountData( + `m.secret_storage.key.${keyId}`, keyInfo, + ); + } + } + }; + + const signKeyBackupWithCrossSigning = async (keyBackupAuthData) => { + if ( + this.crossSigningInfo.getId() && + await this.crossSigningInfo.isStoredInKeyCache("master") + ) { + try { + logger.log("Adding cross-signing signature to key backup"); + await this.crossSigningInfo.signObject(keyBackupAuthData, "master"); + } catch (e) { + // This step is not critical (just helpful), so we catch here + // and continue if it fails. + logger.error("Signing key backup with cross-signing keys failed", e); + } + } else { + logger.warn( + "Cross-signing keys not available, skipping signature on key backup", + ); + } + }; + + const oldSSSSKey = await this.getSecretStorageKey(); + const [oldKeyId, oldKeyInfo] = oldSSSSKey || [null, null]; + const storageExists = ( + !setupNewSecretStorage && + oldKeyInfo && + oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES + ); + + // Log all relevant state for easier parsing of debug logs. + logger.log({ + keyBackupInfo, + setupNewKeyBackup, + setupNewSecretStorage, + storageExists, + oldKeyInfo, + }); + + if (!storageExists && !keyBackupInfo) { + // either we don't have anything, or we've been asked to restart + // from scratch + logger.log( + "Secret storage does not exist, creating new storage key", + ); + + // if we already have a usable default SSSS key and aren't resetting + // SSSS just use it. otherwise, create a new one + // Note: we leave the old SSSS key in place: there could be other + // secrets using it, in theory. We could move them to the new key but a) + // that would mean we'd need to prompt for the old passphrase, and b) + // it's not clear that would be the right thing to do anyway. + const { keyInfo, privateKey } = await createSecretStorageKey(); + newKeyId = await createSSSS(keyInfo, privateKey); + } else if (!storageExists && keyBackupInfo) { + // we have an existing backup, but no SSSS + logger.log("Secret storage does not exist, using key backup key"); + + // if we have the backup key already cached, use it; otherwise use the + // callback to prompt for the key + const backupKey = await this.getSessionBackupPrivateKey() || await getKeyBackupPassphrase(); + + // create a new SSSS key and use the backup key as the new SSSS key + const opts: any = {}; // TODO types + + if ( + 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, + salt: keyBackupInfo.auth_data.private_key_salt, + bits: 256, + }; + } + + newKeyId = await createSSSS(opts, backupKey); + + // store the backup key in secret storage + await secretStorage.store( + "m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId], + ); + + // The backup is trusted because the user provided the private key. + // Sign the backup with the cross-signing key so the key backup can + // be trusted via cross-signing. + await signKeyBackupWithCrossSigning(keyBackupInfo.auth_data); + + builder.addSessionBackup(keyBackupInfo); + } else { + // 4S is already set up + logger.log("Secret storage exists"); + + if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { + // make sure that the default key has the information needed to + // check the passphrase + await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo); + } + } + + // If we have cross-signing private keys cached, store them in secret + // storage if they are not there already. + if ( + !this.baseApis.cryptoCallbacks.saveCrossSigningKeys && + await this.isCrossSigningReady() && + (newKeyId || !await this.crossSigningInfo.isStoredInSecretStorage(secretStorage)) + ) { + logger.log("Copying cross-signing private keys from cache to secret storage"); + const crossSigningPrivateKeys = + await this.crossSigningInfo.getCrossSigningKeysFromCache(); + // This is writing to in-memory account data in + // builder.accountDataClientAdapter so won't fail + await CrossSigningInfo.storeInSecretStorage( + crossSigningPrivateKeys, + secretStorage, + ); + } + + if (setupNewKeyBackup && !keyBackupInfo) { + logger.log("Creating new message key backup version"); + const info = await this.baseApis.prepareKeyBackupVersion( + null /* random key */, + // don't write to secret storage, as it will write to this.secretStorage. + // Here, we want to capture all the side-effects of bootstrapping, + // and want to write to the local secretStorage object + { secureSecretStorage: false }, + ); + // write the key ourselves to 4S + const privateKey = decodeRecoveryKey(info.recovery_key); + await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(privateKey)); + + // create keyBackupInfo object to add to builder + const data = { + algorithm: info.algorithm, + auth_data: info.auth_data, + }; + + // Sign with cross-signing master key + await signKeyBackupWithCrossSigning(data.auth_data); + + // sign with the device fingerprint + await this.signObject(data.auth_data); + + builder.addSessionBackup(data); + } + + // Cache the session backup key + const sessionBackupKey = await secretStorage.get('m.megolm_backup.v1'); + if (sessionBackupKey) { + logger.info("Got session backup key from secret storage: caching"); + // fix up the backup key if it's in the wrong format, and replace + // in secret storage + const fixedBackupKey = fixBackupKey(sessionBackupKey); + if (fixedBackupKey) { + await secretStorage.store("m.megolm_backup.v1", + fixedBackupKey, [newKeyId || oldKeyId], + ); + } + const decodedBackupKey = new Uint8Array(olmlib.decodeBase64( + fixedBackupKey || sessionBackupKey, + )); + await builder.addSessionBackupPrivateKeyToCache(decodedBackupKey); + } + + const operation = builder.buildOperation(); + await operation.apply(this); + // this persists private keys and public keys as trusted, + // only do this if apply succeeded for now as retry isn't in place yet + await builder.persist(this); + + logger.log("Secure Secret Storage ready"); + } + + public addSecretStorageKey(algorithm: string, opts: any, keyID: string): any { // TODO types + return this.secretStorage.addKey(algorithm, opts, keyID); + } + + public hasSecretStorageKey(keyID: string): boolean { + return this.secretStorage.hasKey(keyID); + } + + public getSecretStorageKey(keyID?: string): any { // TODO types + return this.secretStorage.getKey(keyID); + } + + public storeSecret(name: string, secret: string, keys?: string[]): Promise { + return this.secretStorage.store(name, secret, keys); + } + + public getSecret(name: string): Promise { + return this.secretStorage.get(name); + } + + public isSecretStored(name: string, checkKey?: boolean): any { // TODO types + return this.secretStorage.isStored(name, checkKey); + } + + public requestSecret(name: string, devices: string[]): Promise { // TODO types + if (!devices) { + devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(this.userId)); + } + return this.secretStorage.request(name, devices); + } + + public getDefaultSecretStorageKeyId(): Promise { + return this.secretStorage.getDefaultKeyId(); + } + + public setDefaultSecretStorageKeyId(k: string): Promise { + return this.secretStorage.setDefaultKeyId(k); + } + + public checkSecretStorageKey(key: string, info: any): Promise { // TODO types + return this.secretStorage.checkKey(key, info); + } + + /** + * Checks that a given secret storage private key matches a given public key. + * This can be used by the getSecretStorageKey callback to verify that the + * private key it is about to supply is the one that was requested. + * + * @param {Uint8Array} privateKey The private key + * @param {string} expectedPublicKey The public key + * @returns {boolean} true if the key matches, otherwise false + */ + public checkSecretStoragePrivateKey(privateKey: Uint8Array, expectedPublicKey: string): boolean { + let decryption = null; + try { + decryption = new global.Olm.PkDecryption(); + const gotPubkey = decryption.init_with_private_key(privateKey); + // make sure it agrees with the given pubkey + return gotPubkey === expectedPublicKey; + } finally { + if (decryption) decryption.free(); + } + } + + /** + * Fetches the backup private key, if cached + * @returns {Promise} the key, if any, or null + */ + public async getSessionBackupPrivateKey(): Promise { + let key = await new Promise((resolve) => { // TODO types + this.cryptoStore.doTxn( + 'readonly', + [IndexedDBCryptoStore.STORE_ACCOUNT], + (txn) => { + this.cryptoStore.getSecretStorePrivateKey( + txn, + resolve, + "m.megolm_backup.v1", + ); + }, + ); + }); + + // make sure we have a Uint8Array, rather than a string + if (key && typeof key === "string") { + key = new Uint8Array(olmlib.decodeBase64(fixBackupKey(key) || key)); + await this.storeSessionBackupPrivateKey(key); + } + if (key && key.ciphertext) { + const pickleKey = Buffer.from(this.olmDevice._pickleKey); + const decrypted = await decryptAES(key, pickleKey, "m.megolm_backup.v1"); + key = olmlib.decodeBase64(decrypted); + } + return key; + } + + /** + * Stores the session backup key to the cache + * @param {Uint8Array} key the private key + * @returns {Promise} so you can catch failures + */ + public async storeSessionBackupPrivateKey(key: Uint8Array): Promise { + if (!(key instanceof Uint8Array)) { + throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`); + } + const pickleKey = Buffer.from(this.olmDevice._pickleKey); + key = await encryptAES(olmlib.encodeBase64(key), pickleKey, "m.megolm_backup.v1"); + return this.cryptoStore.doTxn( + 'readwrite', + [IndexedDBCryptoStore.STORE_ACCOUNT], + (txn) => { + this.cryptoStore.storeSecretStorePrivateKey(txn, "m.megolm_backup.v1", key); + }, + ); + } + + /** + * Checks that a given cross-signing private key matches a given public key. + * This can be used by the getCrossSigningKey callback to verify that the + * private key it is about to supply is the one that was requested. + * + * @param {Uint8Array} privateKey The private key + * @param {string} expectedPublicKey The public key + * @returns {boolean} true if the key matches, otherwise false + */ + public checkCrossSigningPrivateKey(privateKey: Uint8Array, expectedPublicKey: string): boolean { + let signing = null; + try { + signing = new global.Olm.PkSigning(); + const gotPubkey = signing.init_with_seed(privateKey); + // make sure it agrees with the given pubkey + return gotPubkey === expectedPublicKey; + } finally { + if (signing) signing.free(); + } + } + + /** + * Run various follow-up actions after cross-signing keys have changed locally + * (either by resetting the keys for the account or by getting them from secret + * storage), such as signing the current device, upgrading device + * verifications, etc. + */ + private async afterCrossSigningLocalKeyChange(): Promise { + logger.info("Starting cross-signing key change post-processing"); + + // sign the current device with the new key, and upload to the server + const device = this.deviceList.getStoredDevice(this.userId, this.deviceId); + const signedDevice = await this.crossSigningInfo.signDevice(this.userId, device); + logger.info(`Starting background key sig upload for ${this.deviceId}`); + + const upload = ({ shouldEmit }) => { + return this.baseApis.uploadKeySignatures({ + [this.userId]: { + [this.deviceId]: signedDevice, + }, + }).then((response) => { + const { failures } = response || {}; + if (Object.keys(failures || []).length > 0) { + if (shouldEmit) { + this.baseApis.emit( + "crypto.keySignatureUploadFailure", + failures, + "afterCrossSigningLocalKeyChange", + upload, // continuation + ); + } + throw new KeySignatureUploadError("Key upload failed", { failures }); + } + logger.info(`Finished background key sig upload for ${this.deviceId}`); + }).catch(e => { + logger.error( + `Error during background key sig upload for ${this.deviceId}`, + e, + ); + }); + }; + upload({ shouldEmit: true }); + + const shouldUpgradeCb = ( + this.baseApis.cryptoCallbacks.shouldUpgradeDeviceVerifications + ); + if (shouldUpgradeCb) { + logger.info("Starting device verification upgrade"); + + // Check all users for signatures if upgrade callback present + // FIXME: do this in batches + const users = {}; + for (const [userId, crossSigningInfo] + of Object.entries(this.deviceList._crossSigningInfo)) { + const upgradeInfo = await this.checkForDeviceVerificationUpgrade( + userId, CrossSigningInfo.fromStorage(crossSigningInfo, userId), + ); + if (upgradeInfo) { + users[userId] = upgradeInfo; + } + } + + if (Object.keys(users).length > 0) { + logger.info(`Found ${Object.keys(users).length} verif users to upgrade`); + try { + const usersToUpgrade = await shouldUpgradeCb({ users: users }); + if (usersToUpgrade) { + for (const userId of usersToUpgrade) { + if (userId in users) { + await this.baseApis.setDeviceVerified( + userId, users[userId].crossSigningInfo.getId(), + ); + } + } + } + } catch (e) { + logger.log( + "shouldUpgradeDeviceVerifications threw an error: not upgrading", e, + ); + } + } + + logger.info("Finished device verification upgrade"); + } + + logger.info("Finished cross-signing key change post-processing"); + } + + /** + * Check if a user's cross-signing key is a candidate for upgrading from device + * verification. + * + * @param {string} userId the user whose cross-signing information is to be checked + * @param {object} crossSigningInfo the cross-signing information to check + */ + private async checkForDeviceVerificationUpgrade( + userId: string, + crossSigningInfo: CrossSigningInfo, + ): Promise { + // only upgrade if this is the first cross-signing key that we've seen for + // them, and if their cross-signing key isn't already verified + const trustLevel = this.crossSigningInfo.checkUserTrust(crossSigningInfo); + if (crossSigningInfo.firstUse && !trustLevel.verified) { + const devices = this.deviceList.getRawStoredDevicesForUser(userId); + const deviceIds = await this.checkForValidDeviceSignature( + userId, crossSigningInfo.keys.master, devices, + ); + if (deviceIds.length) { + return { + devices: deviceIds.map( + deviceId => DeviceInfo.fromStorage(devices[deviceId], deviceId), + ), + crossSigningInfo, + }; + } + } + } + + /** + * Check if the cross-signing key is signed by a verified device. + * + * @param {string} userId the user ID whose key is being checked + * @param {object} key the key that is being checked + * @param {object} devices the user's devices. Should be a map from device ID + * to device info + */ + private async checkForValidDeviceSignature( + userId: string, + key: any, // TODO types + devices: Record, + ): Promise { + const deviceIds: string[] = []; + if (devices && key.signatures && key.signatures[userId]) { + for (const signame of Object.keys(key.signatures[userId])) { + const [, deviceId] = signame.split(':', 2); + if (deviceId in devices + && devices[deviceId].verified === DeviceVerification.VERIFIED) { + try { + await olmlib.verifySignature( + this.olmDevice, + key, + userId, + deviceId, + devices[deviceId].keys[signame], + ); + deviceIds.push(deviceId); + } catch (e) {} + } + } + } + return deviceIds; + } + + /** + * Get the user's cross-signing key ID. + * + * @param {string} [type=master] The type of key to get the ID of. One of + * "master", "self_signing", or "user_signing". Defaults to "master". + * + * @returns {string} the key ID + */ + public getCrossSigningId(type: string): string { + return this.crossSigningInfo.getId(type); + } + + /** + * Get the cross signing information for a given user. + * + * @param {string} userId the user ID to get the cross-signing info for. + * + * @returns {CrossSigningInfo} the cross signing information for the user. + */ + public getStoredCrossSigningForUser(userId: string): CrossSigningInfo { + return this.deviceList.getStoredCrossSigningForUser(userId); + } + + /** + * Check whether a given user is trusted. + * + * @param {string} userId The ID of the user to check. + * + * @returns {UserTrustLevel} + */ + public checkUserTrust(userId: string): UserTrustLevel { + const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(userId); + if (!userCrossSigning) { + return new UserTrustLevel(false, false, false); + } + return this.crossSigningInfo.checkUserTrust(userCrossSigning); + } + + /** + * Check whether a given device is trusted. + * + * @param {string} userId The ID of the user whose devices is to be checked. + * @param {string} deviceId The ID of the device to check + * + * @returns {DeviceTrustLevel} + */ + public checkDeviceTrust(userId: string, deviceId: string): DeviceTrustLevel { + const device = this.deviceList.getStoredDevice(userId, deviceId); + return this.checkDeviceInfoTrust(userId, device); + } + + /** + * Check whether a given deviceinfo is trusted. + * + * @param {string} userId The ID of the user whose devices is to be checked. + * @param {module:crypto/deviceinfo?} device The device info object to check + * + * @returns {DeviceTrustLevel} + */ + public checkDeviceInfoTrust(userId: string, device: DeviceInfo): DeviceTrustLevel { + const trustedLocally = !!(device && device.isVerified()); + + const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(userId); + if (device && userCrossSigning) { + // The trustCrossSignedDevices only affects trust of other people's cross-signing + // signatures + const trustCrossSig = this.trustCrossSignedDevices || userId === this.userId; + return this.crossSigningInfo.checkDeviceTrust( + userCrossSigning, device, trustedLocally, trustCrossSig, + ); + } else { + return new DeviceTrustLevel(false, false, trustedLocally, false); + } + } + + /* + * Event handler for DeviceList's userNewDevices event + */ + private onDeviceListUserCrossSigningUpdated = async (userId: string) => { + if (userId === this.userId) { + // An update to our own cross-signing key. + // Get the new key first: + const newCrossSigning = this.deviceList.getStoredCrossSigningForUser(userId); + const seenPubkey = newCrossSigning ? newCrossSigning.getId() : null; + const currentPubkey = this.crossSigningInfo.getId(); + const changed = currentPubkey !== seenPubkey; + + if (currentPubkey && seenPubkey && !changed) { + // If it's not changed, just make sure everything is up to date + await this.checkOwnCrossSigningTrust(); + } else { + // We'll now be in a state where cross-signing on the account is not trusted + // because our locally stored cross-signing keys will not match the ones + // on the server for our account. So we clear our own stored cross-signing keys, + // effectively disabling cross-signing until the user gets verified by the device + // that reset the keys + this.storeTrustedSelfKeys(null); + // emit cross-signing has been disabled + this.emit("crossSigning.keysChanged", {}); + // as the trust for our own user has changed, + // also emit an event for this + this.emit("userTrustStatusChanged", + this.userId, this.checkUserTrust(userId)); + } + } else { + await this.checkDeviceVerifications(userId); + + // Update verified before latch using the current state and save the new + // latch value in the device list store. + const crossSigning = this.deviceList.getStoredCrossSigningForUser(userId); + if (crossSigning) { + crossSigning.updateCrossSigningVerifiedBefore( + this.checkUserTrust(userId).isCrossSigningVerified(), + ); + this.deviceList.setRawStoredCrossSigningForUser( + userId, crossSigning.toStorage(), + ); + } + + this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId)); + } + }; + + /** + * Check the copy of our cross-signing key that we have in the device list and + * see if we can get the private key. If so, mark it as trusted. + */ + async checkOwnCrossSigningTrust({ + allowPrivateKeyRequests = false, + } = {}) { + const userId = this.userId; + + // Before proceeding, ensure our cross-signing public keys have been + // downloaded via the device list. + await this.downloadKeys([this.userId]); + + // Also check which private keys are locally cached. + const crossSigningPrivateKeys = + await this.crossSigningInfo.getCrossSigningKeysFromCache(); + + // If we see an update to our own master key, check it against the master + // key we have and, if it matches, mark it as verified + + // First, get the new cross-signing info + const newCrossSigning = this.deviceList.getStoredCrossSigningForUser(userId); + if (!newCrossSigning) { + logger.error( + "Got cross-signing update event for user " + userId + + " but no new cross-signing information found!", + ); + return; + } + + const seenPubkey = newCrossSigning.getId(); + const masterChanged = this.crossSigningInfo.getId() !== seenPubkey; + const masterExistsNotLocallyCached = + newCrossSigning.getId() && !crossSigningPrivateKeys.has("master"); + if (masterChanged) { + logger.info("Got new master public key", seenPubkey); + } + if ( + allowPrivateKeyRequests && + (masterChanged || masterExistsNotLocallyCached) + ) { + logger.info("Attempting to retrieve cross-signing master private key"); + let signing = null; + // It's important for control flow that we leave any errors alone for + // higher levels to handle so that e.g. cancelling access properly + // aborts any larger operation as well. + try { + const ret = await this.crossSigningInfo.getCrossSigningKey( + 'master', seenPubkey, + ); + signing = ret[1]; + logger.info("Got cross-signing master private key"); + } finally { + if (signing) signing.free(); + } + } + + const oldSelfSigningId = this.crossSigningInfo.getId("self_signing"); + const oldUserSigningId = this.crossSigningInfo.getId("user_signing"); + + // Update the version of our keys in our cross-signing object and the local store + this.storeTrustedSelfKeys(newCrossSigning.keys); + + const selfSigningChanged = oldSelfSigningId !== newCrossSigning.getId("self_signing"); + const userSigningChanged = oldUserSigningId !== newCrossSigning.getId("user_signing"); + + const selfSigningExistsNotLocallyCached = ( + newCrossSigning.getId("self_signing") && + !crossSigningPrivateKeys.has("self_signing") + ); + const userSigningExistsNotLocallyCached = ( + newCrossSigning.getId("user_signing") && + !crossSigningPrivateKeys.has("user_signing") + ); + + const keySignatures = {}; + + if (selfSigningChanged) { + logger.info("Got new self-signing key", newCrossSigning.getId("self_signing")); + } + if ( + allowPrivateKeyRequests && + (selfSigningChanged || selfSigningExistsNotLocallyCached) + ) { + logger.info("Attempting to retrieve cross-signing self-signing private key"); + let signing = null; + try { + const ret = await this.crossSigningInfo.getCrossSigningKey( + "self_signing", newCrossSigning.getId("self_signing"), + ); + signing = ret[1]; + logger.info("Got cross-signing self-signing private key"); + } finally { + if (signing) signing.free(); + } + + const device = this.deviceList.getStoredDevice(this.userId, this.deviceId); + const signedDevice = await this.crossSigningInfo.signDevice( + this.userId, device, + ); + keySignatures[this.deviceId] = signedDevice; + } + if (userSigningChanged) { + logger.info("Got new user-signing key", newCrossSigning.getId("user_signing")); + } + if ( + allowPrivateKeyRequests && + (userSigningChanged || userSigningExistsNotLocallyCached) + ) { + logger.info("Attempting to retrieve cross-signing user-signing private key"); + let signing = null; + try { + const ret = await this.crossSigningInfo.getCrossSigningKey( + "user_signing", newCrossSigning.getId("user_signing"), + ); + signing = ret[1]; + logger.info("Got cross-signing user-signing private key"); + } finally { + if (signing) signing.free(); + } + } + + if (masterChanged) { + const masterKey = this.crossSigningInfo.keys.master; + await this.signObject(masterKey); + const deviceSig = masterKey.signatures[this.userId]["ed25519:" + this.deviceId]; + // Include only the _new_ device signature in the upload. + // We may have existing signatures from deleted devices, which will cause + // the entire upload to fail. + keySignatures[this.crossSigningInfo.getId()] = Object.assign( + {}, + masterKey, + { + signatures: { + [this.userId]: { + ["ed25519:" + this.deviceId]: deviceSig, + }, + }, + }, + ); + } + + const keysToUpload = Object.keys(keySignatures); + if (keysToUpload.length) { + const upload = ({ shouldEmit }) => { + logger.info(`Starting background key sig upload for ${keysToUpload}`); + return this.baseApis.uploadKeySignatures({ [this.userId]: keySignatures }) + .then((response) => { + const { failures } = response || {}; + logger.info(`Finished background key sig upload for ${keysToUpload}`); + if (Object.keys(failures || []).length > 0) { + if (shouldEmit) { + this.baseApis.emit( + "crypto.keySignatureUploadFailure", + failures, + "checkOwnCrossSigningTrust", + upload, + ); + } + throw new KeySignatureUploadError("Key upload failed", { failures }); + } + }).catch(e => { + logger.error( + `Error during background key sig upload for ${keysToUpload}`, + e, + ); + }); + }; + upload({ shouldEmit: true }); + } + + this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId)); + + if (masterChanged) { + this.baseApis.emit("crossSigning.keysChanged", {}); + await this.afterCrossSigningLocalKeyChange(); + } + + // Now we may be able to trust our key backup + 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)? + } + + /** + * Store a set of keys as our own, trusted, cross-signing keys. + * + * @param {object} keys The new trusted set of keys + */ + private async storeTrustedSelfKeys(keys: any): Promise { // TODO types + if (keys) { + this.crossSigningInfo.setKeys(keys); + } else { + this.crossSigningInfo.clearKeys(); + } + await this.cryptoStore.doTxn( + 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], + (txn) => { + this.cryptoStore.storeCrossSigningKeys(txn, this.crossSigningInfo.keys); + }, + ); + } + + /** + * Check if the master key is signed by a verified device, and if so, prompt + * the application to mark it as verified. + * + * @param {string} userId the user ID whose key should be checked + */ + private async checkDeviceVerifications(userId: string): Promise { + const shouldUpgradeCb = ( + this.baseApis.cryptoCallbacks.shouldUpgradeDeviceVerifications + ); + if (!shouldUpgradeCb) { + // Upgrading skipped when callback is not present. + return; + } + logger.info(`Starting device verification upgrade for ${userId}`); + if (this.crossSigningInfo.keys.user_signing) { + const crossSigningInfo = this.deviceList.getStoredCrossSigningForUser(userId); + if (crossSigningInfo) { + const upgradeInfo = await this.checkForDeviceVerificationUpgrade( + userId, crossSigningInfo, + ); + if (upgradeInfo) { + const usersToUpgrade = await shouldUpgradeCb({ + users: { + [userId]: upgradeInfo, + }, + }); + if (usersToUpgrade.includes(userId)) { + await this.baseApis.setDeviceVerified( + userId, crossSigningInfo.getId(), + ); + } + } + } + } + logger.info(`Finished device verification upgrade for ${userId}`); + } + + public async setTrustedBackupPubKey(trustedPubKey: string): Promise { + // This should be redundant post cross-signing is a thing, so just + // plonk it in localStorage for now. + this.sessionStore.setLocalTrustedBackupPubKey(trustedPubKey); + await this.backupManager.checkKeyBackup(); + } + + /** + */ + public enableLazyLoading(): void { + this.lazyLoadMembers = true; + } + + /** + * Tell the crypto module to register for MatrixClient events which it needs to + * listen for + * + * @param {external:EventEmitter} eventEmitter event source where we can register + * for event notifications + */ + public registerEventHandlers(eventEmitter: EventEmitter): void { + eventEmitter.on("RoomMember.membership", (event: MatrixEvent, member: RoomMember, oldMembership?: string) => { + try { + this.onRoomMembership(event, member, oldMembership); + } catch (e) { + logger.error("Error handling membership change:", e); + } + }); + + eventEmitter.on("toDeviceEvent", this.onToDeviceEvent); + eventEmitter.on("Room.timeline", this.onTimelineEvent); + eventEmitter.on("Event.decrypted", this.onTimelineEvent); + } + + /** Start background processes related to crypto */ + public start(): void { + this.outgoingRoomKeyRequestManager.start(); + } + + /** Stop background processes related to crypto */ + public stop(): void { + this.outgoingRoomKeyRequestManager.stop(); + this.deviceList.stop(); + this.dehydrationManager.stop(); + } + + /** + * Get the Ed25519 key for this device + * + * @return {string} base64-encoded ed25519 key. + */ + public getDeviceEd25519Key(): string { + return this.olmDevice.deviceEd25519Key; + } + + /** + * Get the Curve25519 key for this device + * + * @return {string} base64-encoded curve25519 key. + */ + public getDeviceCurve25519Key(): string { + return this.olmDevice.deviceCurve25519Key; + } + + /** + * Set the global override for whether the client should ever send encrypted + * messages to unverified devices. This provides the default for rooms which + * do not specify a value. + * + * @param {boolean} value whether to blacklist all unverified devices by default + */ + public setGlobalBlacklistUnverifiedDevices(value: boolean): void { + this.globalBlacklistUnverifiedDevices = value; + } + + /** + * @return {boolean} whether to blacklist all unverified devices by default + */ + public getGlobalBlacklistUnverifiedDevices(): boolean { + return this.globalBlacklistUnverifiedDevices; + } + + /** + * Set whether sendMessage in a room with unknown and unverified devices + * should throw an error and not send them message. This has 'Global' for + * symmetry with setGlobalBlacklistUnverifiedDevices but there is currently + * no room-level equivalent for this setting. + * + * This API is currently UNSTABLE and may change or be removed without notice. + * + * @param {boolean} value whether error on unknown devices + */ + public setGlobalErrorOnUnknownDevices(value: boolean): void { + this.globalErrorOnUnknownDevices = value; + } + + /** + * @return {boolean} whether to error on unknown devices + * + * This API is currently UNSTABLE and may change or be removed without notice. + */ + public getGlobalErrorOnUnknownDevices(): boolean { + return this.globalErrorOnUnknownDevices; + } + + /** + * Upload the device keys to the homeserver. + * @return {object} A promise that will resolve when the keys are uploaded. + */ + public uploadDeviceKeys(): Promise { + const deviceKeys = { + algorithms: this.supportedAlgorithms, + device_id: this.deviceId, + keys: this.deviceKeys, + user_id: this.userId, + }; + + return this.signObject(deviceKeys).then(() => { + return this.baseApis.uploadKeysRequest({ + device_keys: deviceKeys, + }); + }); + } + + /** + * Stores the current one_time_key count which will be handled later (in a call of + * onSyncCompleted). The count is e.g. coming from a /sync response. + * + * @param {Number} currentCount The current count of one_time_keys to be stored + */ + public updateOneTimeKeyCount(currentCount: number): void { + if (isFinite(currentCount)) { + this.oneTimeKeyCount = currentCount; + } else { + throw new TypeError("Parameter for updateOneTimeKeyCount has to be a number"); + } + } + + public setNeedsNewFallback(needsNewFallback: boolean) { + this.needsNewFallback = !!needsNewFallback; + } + + public getNeedsNewFallback(): boolean { + return this.needsNewFallback; + } + + // check if it's time to upload one-time keys, and do so if so. + private maybeUploadOneTimeKeys() { + // frequency with which to check & upload one-time keys + const uploadPeriod = 1000 * 60; // one minute + + // max number of keys to upload at once + // Creating keys can be an expensive operation so we limit the + // number we generate in one go to avoid blocking the application + // for too long. + const maxKeysPerCycle = 5; + + if (this.oneTimeKeyCheckInProgress) { + return; + } + + const now = Date.now(); + if (this.lastOneTimeKeyCheck !== null && + now - this.lastOneTimeKeyCheck < uploadPeriod + ) { + // we've done a key upload recently. + return; + } + + this.lastOneTimeKeyCheck = now; + + // We need to keep a pool of one time public keys on the server so that + // other devices can start conversations with us. But we can only store + // a finite number of private keys in the olm Account object. + // To complicate things further then can be a delay between a device + // claiming a public one time key from the server and it sending us a + // message. We need to keep the corresponding private key locally until + // we receive the message. + // But that message might never arrive leaving us stuck with duff + // private keys clogging up our local storage. + // So we need some kind of engineering compromise to balance all of + // these factors. + + // Check how many keys we can store in the Account object. + const maxOneTimeKeys = this.olmDevice.maxNumberOfOneTimeKeys(); + // Try to keep at most half that number on the server. This leaves the + // rest of the slots free to hold keys that have been claimed from the + // server but we haven't received a message for. + // If we run out of slots when generating new keys then olm will + // discard the oldest private keys first. This will eventually clean + // out stale private keys that won't receive a message. + const keyLimit = Math.floor(maxOneTimeKeys / 2); + + const uploadLoop = async (keyCount: number) => { + while (keyLimit > keyCount || this.getNeedsNewFallback()) { + // Ask olm to generate new one time keys, then upload them to synapse. + if (keyLimit > keyCount) { + logger.info("generating oneTimeKeys"); + const keysThisLoop = Math.min(keyLimit - keyCount, maxKeysPerCycle); + await this.olmDevice.generateOneTimeKeys(keysThisLoop); + } + + if (this.getNeedsNewFallback()) { + logger.info("generating fallback key"); + await this.olmDevice.generateFallbackKey(); + } + + logger.info("calling uploadOneTimeKeys"); + const res = await this.uploadOneTimeKeys(); + if (res.one_time_key_counts && res.one_time_key_counts.signed_curve25519) { + // if the response contains a more up to date value use this + // for the next loop + keyCount = res.one_time_key_counts.signed_curve25519; + } else { + throw new Error("response for uploading keys does not contain " + + "one_time_key_counts.signed_curve25519"); + } + } + }; + + this.oneTimeKeyCheckInProgress = true; + Promise.resolve().then(() => { + if (this.oneTimeKeyCount !== undefined) { + // We already have the current one_time_key count from a /sync response. + // Use this value instead of asking the server for the current key count. + return Promise.resolve(this.oneTimeKeyCount); + } + // ask the server how many keys we have + return this.baseApis.uploadKeysRequest({}).then((res) => { + return res.one_time_key_counts.signed_curve25519 || 0; + }); + }).then((keyCount) => { + // Start the uploadLoop with the current keyCount. The function checks if + // we need to upload new keys or not. + // If there are too many keys on the server then we don't need to + // create any more keys. + return uploadLoop(keyCount); + }).catch((e) => { + logger.error("Error uploading one-time keys", e.stack || e); + }).finally(() => { + // reset oneTimeKeyCount to prevent start uploading based on old data. + // it will be set again on the next /sync-response + this.oneTimeKeyCount = undefined; + this.oneTimeKeyCheckInProgress = false; + }); + } + + // returns a promise which resolves to the response + private async uploadOneTimeKeys() { + const promises = []; + + const fallbackJson = {}; + if (this.getNeedsNewFallback()) { + const fallbackKeys = await this.olmDevice.getFallbackKey(); + for (const [keyId, key] of Object.entries(fallbackKeys.curve25519)) { + const k = { key, fallback: true }; + fallbackJson["signed_curve25519:" + keyId] = k; + promises.push(this.signObject(k)); + } + this.setNeedsNewFallback(false); + } + + const oneTimeKeys = await this.olmDevice.getOneTimeKeys(); + const oneTimeJson = {}; + + for (const keyId in oneTimeKeys.curve25519) { + if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) { + const k = { + key: oneTimeKeys.curve25519[keyId], + }; + oneTimeJson["signed_curve25519:" + keyId] = k; + promises.push(this.signObject(k)); + } + } + + await Promise.all(promises); + + const res = await this.baseApis.uploadKeysRequest({ + "one_time_keys": oneTimeJson, + "org.matrix.msc2732.fallback_keys": fallbackJson, + }); + + await this.olmDevice.markKeysAsPublished(); + return res; + } + + /** + * Download the keys for a list of users and stores the keys in the session + * store. + * @param {Array} userIds The users to fetch. + * @param {boolean} forceDownload Always download the keys even if cached. + * + * @return {Promise} A promise which resolves to a map userId->deviceId->{@link + * module:crypto/deviceinfo|DeviceInfo}. + */ + public downloadKeys( + userIds: string[], + forceDownload?: boolean, + ): Promise>> { + return this.deviceList.downloadKeys(userIds, forceDownload); + } + + /** + * Get the stored device keys for a user id + * + * @param {string} userId the user to list keys for. + * + * @return {module:crypto/deviceinfo[]|null} list of devices, or null if we haven't + * managed to get a list of devices for this user yet. + */ + public getStoredDevicesForUser(userId: string): Array | null { + return this.deviceList.getStoredDevicesForUser(userId); + } + + /** + * Get the stored keys for a single device + * + * @param {string} userId + * @param {string} deviceId + * + * @return {module:crypto/deviceinfo?} device, or undefined + * if we don't know about this device + */ + public getStoredDevice(userId: string, deviceId: string): DeviceInfo | undefined { + return this.deviceList.getStoredDevice(userId, deviceId); + } + + /** + * Save the device list, if necessary + * + * @param {number} delay Time in ms before which the save actually happens. + * By default, the save is delayed for a short period in order to batch + * multiple writes, but this behaviour can be disabled by passing 0. + * + * @return {Promise} true if the data was saved, false if + * it was not (eg. because no changes were pending). The promise + * will only resolve once the data is saved, so may take some time + * to resolve. + */ + public saveDeviceList(delay: number): Promise { + return this.deviceList.saveIfDirty(delay); + } + + /** + * Update the blocked/verified state of the given device + * + * @param {string} userId owner of the device + * @param {string} deviceId unique identifier for the device or user's + * cross-signing public key ID. + * + * @param {?boolean} verified whether to mark the device as verified. Null to + * leave unchanged. + * + * @param {?boolean} blocked whether to mark the device as blocked. Null to + * leave unchanged. + * + * @param {?boolean} known whether to mark that the user has been made aware of + * the existence of this device. Null to leave unchanged + * + * @return {Promise} updated DeviceInfo + */ + public async setDeviceVerification( + userId: string, + deviceId: string, + verified?: boolean, + blocked?: boolean, + known?: boolean, + ): Promise { + // get rid of any `undefined`s here so we can just check + // for null rather than null or undefined + if (verified === undefined) verified = null; + if (blocked === undefined) blocked = null; + if (known === undefined) known = null; + + // Check if the 'device' is actually a cross signing key + // The js-sdk's verification treats cross-signing keys as devices + // and so uses this method to mark them verified. + const xsk = this.deviceList.getStoredCrossSigningForUser(userId); + if (xsk && xsk.getId() === deviceId) { + if (blocked !== null || known !== null) { + throw new Error("Cannot set blocked or known for a cross-signing key"); + } + if (!verified) { + throw new Error("Cannot set a cross-signing key as unverified"); + } + + if (!this.crossSigningInfo.getId() && userId === this.crossSigningInfo.userId) { + this.storeTrustedSelfKeys(xsk.keys); + // This will cause our own user trust to change, so emit the event + this.emit( + "userTrustStatusChanged", this.userId, this.checkUserTrust(userId), + ); + } + + // Now sign the master key with our user signing key (unless it's ourself) + if (userId !== this.userId) { + logger.info( + "Master key " + xsk.getId() + " for " + userId + + " marked verified. Signing...", + ); + const device = await this.crossSigningInfo.signUser(xsk); + if (device) { + const upload = async ({ shouldEmit }) => { + logger.info("Uploading signature for " + userId + "..."); + const response = await this.baseApis.uploadKeySignatures({ + [userId]: { + [deviceId]: device, + }, + }); + const { failures } = response || {}; + if (Object.keys(failures || []).length > 0) { + if (shouldEmit) { + this.baseApis.emit( + "crypto.keySignatureUploadFailure", + failures, + "setDeviceVerification", + upload, + ); + } + /* Throwing here causes the process to be cancelled and the other + * user to be notified */ + throw new KeySignatureUploadError( + "Key upload failed", + { failures }, + ); + } + }; + await upload({ shouldEmit: true }); + + // This will emit events when it comes back down the sync + // (we could do local echo to speed things up) + } + return device; + } else { + return xsk; + } + } + + const devices = this.deviceList.getRawStoredDevicesForUser(userId); + if (!devices || !devices[deviceId]) { + throw new Error("Unknown device " + userId + ":" + deviceId); + } + + const dev = devices[deviceId]; + let verificationStatus = dev.verified; + + if (verified) { + verificationStatus = DeviceVerification.VERIFIED; + } else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) { + verificationStatus = DeviceVerification.UNVERIFIED; + } + + if (blocked) { + verificationStatus = DeviceVerification.BLOCKED; + } else if (blocked !== null && verificationStatus == DeviceVerification.BLOCKED) { + verificationStatus = DeviceVerification.UNVERIFIED; + } + + let knownStatus = dev.known; + if (known !== null) { + knownStatus = known; + } + + if (dev.verified !== verificationStatus || dev.known !== knownStatus) { + dev.verified = verificationStatus; + dev.known = knownStatus; + this.deviceList.storeDevicesForUser(userId, devices); + this.deviceList.saveIfDirty(); + } + + // do cross-signing + if (verified && userId === this.userId) { + logger.info("Own device " + deviceId + " marked verified: signing"); + + // Signing only needed if other device not already signed + let device; + const deviceTrust = this.checkDeviceTrust(userId, deviceId); + if (deviceTrust.isCrossSigningVerified()) { + logger.log(`Own device ${deviceId} already cross-signing verified`); + } else { + device = await this.crossSigningInfo.signDevice( + userId, DeviceInfo.fromStorage(dev, deviceId), + ); + } + + if (device) { + const upload = async ({ shouldEmit }) => { + logger.info("Uploading signature for " + deviceId); + const response = await this.baseApis.uploadKeySignatures({ + [userId]: { + [deviceId]: device, + }, + }); + const { failures } = response || {}; + if (Object.keys(failures || []).length > 0) { + if (shouldEmit) { + this.baseApis.emit( + "crypto.keySignatureUploadFailure", + failures, + "setDeviceVerification", + upload, // continuation + ); + } + throw new KeySignatureUploadError("Key upload failed", { failures }); + } + }; + await upload({ shouldEmit: true }); + // XXX: we'll need to wait for the device list to be updated + } + } + + const deviceObj = DeviceInfo.fromStorage(dev, deviceId); + this.emit("deviceVerificationChanged", userId, deviceId, deviceObj); + return deviceObj; + } + + public findVerificationRequestDMInProgress(roomId: string): VerificationRequest { + return this.inRoomVerificationRequests.findRequestInProgress(roomId); + } + + public getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest { + return this.toDeviceVerificationRequests.getRequestsInProgress(userId); + } + + public requestVerificationDM(userId: string, roomId: string): VerificationRequest { + const existingRequest = this.inRoomVerificationRequests.findRequestInProgress(roomId); + if (existingRequest) { + return Promise.resolve(existingRequest); + } + const channel = new InRoomChannel(this.baseApis, roomId, userId); + return this.requestVerificationWithChannel( + userId, + channel, + this.inRoomVerificationRequests, + ); + } + + public requestVerification(userId: string, devices: string[]): VerificationRequest { + if (!devices) { + devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(userId)); + } + const existingRequest = this.toDeviceVerificationRequests.findRequestInProgress(userId, devices); + if (existingRequest) { + return Promise.resolve(existingRequest); + } + const channel = new ToDeviceChannel(this.baseApis, userId, devices, ToDeviceChannel.makeTransactionId()); + return this.requestVerificationWithChannel( + userId, + channel, + this.toDeviceVerificationRequests, + ); + } + + private async requestVerificationWithChannel( + userId: string, + channel: any, // TODO types + requestsMap: any, // TODO types + ): VerificationRequest { + let request = new VerificationRequest(channel, this.verificationMethods, this.baseApis); + // if transaction id is already known, add request + if (channel.transactionId) { + requestsMap.setRequestByChannel(channel, request); + } + await request.sendRequest(); + // don't replace the request created by a racing remote echo + const racingRequest = requestsMap.getRequestByChannel(channel); + if (racingRequest) { + request = racingRequest; + } else { + logger.log(`Crypto: adding new request to ` + + `requestsByTxnId with id ${channel.transactionId} ${channel.roomId}`); + requestsMap.setRequestByChannel(channel, request); + } + return request; + } + + public beginKeyVerification( + method: string, + userId: string, + deviceId: string, + transactionId: string = null, + ): any { // TODO types + let request; + if (transactionId) { + request = this.toDeviceVerificationRequests.getRequestBySenderAndTxnId(userId, transactionId); + if (!request) { + throw new Error( + `No request found for user ${userId} with ` + + `transactionId ${transactionId}`); + } + } else { + transactionId = ToDeviceChannel.makeTransactionId(); + const channel = new ToDeviceChannel(this.baseApis, userId, [deviceId], transactionId, deviceId); + request = new VerificationRequest(channel, this.verificationMethods, this.baseApis); + this.toDeviceVerificationRequests.setRequestBySenderAndTxnId(userId, transactionId, request); + } + return request.beginKeyVerification(method, { userId, deviceId }); + } + + public async legacyDeviceVerification( + userId: string, + deviceId: string, + method: string, + ): VerificationRequest { + const transactionId = ToDeviceChannel.makeTransactionId(); + const channel = new ToDeviceChannel( + this.baseApis, userId, [deviceId], transactionId, deviceId); + const request = new VerificationRequest( + channel, this.verificationMethods, this.baseApis); + this.toDeviceVerificationRequests.setRequestBySenderAndTxnId( + userId, transactionId, request); + const verifier = request.beginKeyVerification(method, { userId, deviceId }); + // either reject by an error from verify() while sending .start + // or resolve when the request receives the + // local (fake remote) echo for sending the .start event + await Promise.race([ + verifier.verify(), + request.waitFor(r => r.started), + ]); + return request; + } + + /** + * Get information on the active olm sessions with a user + *

+ * Returns a map from device id to an object with keys 'deviceIdKey' (the + * device's curve25519 identity key) and 'sessions' (an array of objects in the + * same format as that returned by + * {@link module:crypto/OlmDevice#getSessionInfoForDevice}). + *

+ * This method is provided for debugging purposes. + * + * @param {string} userId id of user to inspect + * + * @return {Promise>} + */ + public async getOlmSessionsForUser(userId: string): Promise> { + const devices = this.getStoredDevicesForUser(userId) || []; + const result = {}; + for (let j = 0; j < devices.length; ++j) { + const device = devices[j]; + const deviceKey = device.getIdentityKey(); + const sessions = await this.olmDevice.getSessionInfoForDevice(deviceKey); + + result[device.deviceId] = { + deviceIdKey: deviceKey, + sessions: sessions, + }; + } + return result; + } + + /** + * Get the device which sent an event + * + * @param {module:models/event.MatrixEvent} event event to be checked + * + * @return {module:crypto/deviceinfo?} + */ + public getEventSenderDeviceInfo(event: MatrixEvent): DeviceInfo | null { + const senderKey = event.getSenderKey(); + const algorithm = event.getWireContent().algorithm; + + if (!senderKey || !algorithm) { + return null; + } + + const forwardingChain = event.getForwardingCurve25519KeyChain(); + if (forwardingChain.length > 0) { + // we got the key this event from somewhere else + // TODO: check if we can trust the forwarders. + return null; + } + + if (event.isKeySourceUntrusted()) { + // we got the key for this event from a source that we consider untrusted + return null; + } + + // senderKey is the Curve25519 identity key of the device which the event + // was sent from. In the case of Megolm, it's actually the Curve25519 + // identity key of the device which set up the Megolm session. + + const device = this.deviceList.getDeviceByIdentityKey( + algorithm, senderKey, + ); + + if (device === null) { + // we haven't downloaded the details of this device yet. + return null; + } + + // so far so good, but now we need to check that the sender of this event + // hadn't advertised someone else's Curve25519 key as their own. We do that + // by checking the Ed25519 claimed by the event (or, in the case of megolm, + // the event which set up the megolm session), to check that it matches the + // fingerprint of the purported sending device. + // + // (see https://github.com/vector-im/vector-web/issues/2215) + + const claimedKey = event.getClaimedEd25519Key(); + if (!claimedKey) { + logger.warn("Event " + event.getId() + " claims no ed25519 key: " + + "cannot verify sending device"); + return null; + } + + if (claimedKey !== device.getFingerprint()) { + logger.warn( + "Event " + event.getId() + " claims ed25519 key " + claimedKey + + " but sender device has key " + device.getFingerprint()); + return null; + } + + return device; + } + + /** + * Get information about the encryption of an event + * + * @param {module:models/event.MatrixEvent} event event to be checked + * + * @return {object} An object with the fields: + * - encrypted: whether the event is encrypted (if not encrypted, some of the + * other properties may not be set) + * - senderKey: the sender's key + * - algorithm: the algorithm used to encrypt the event + * - authenticated: whether we can be sure that the owner of the senderKey + * sent the event + * - sender: the sender's device information, if available + * - mismatchedSender: if the event's ed25519 and curve25519 keys don't match + * (only meaningful if `sender` is set) + */ + public getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo { + const ret: Partial = {}; + + ret.senderKey = event.getSenderKey(); + ret.algorithm = event.getWireContent().algorithm; + + if (!ret.senderKey || !ret.algorithm) { + ret.encrypted = false; + return ret as IEncryptedEventInfo; + } + ret.encrypted = true; + + const forwardingChain = event.getForwardingCurve25519KeyChain(); + if (forwardingChain.length > 0 || event.isKeySourceUntrusted()) { + // we got the key this event from somewhere else + // TODO: check if we can trust the forwarders. + ret.authenticated = false; + } else { + ret.authenticated = true; + } + + // senderKey is the Curve25519 identity key of the device which the event + // was sent from. In the case of Megolm, it's actually the Curve25519 + // identity key of the device which set up the Megolm session. + + ret.sender = this.deviceList.getDeviceByIdentityKey(ret.algorithm, ret.senderKey); + + // so far so good, but now we need to check that the sender of this event + // hadn't advertised someone else's Curve25519 key as their own. We do that + // by checking the Ed25519 claimed by the event (or, in the case of megolm, + // the event which set up the megolm session), to check that it matches the + // fingerprint of the purported sending device. + // + // (see https://github.com/vector-im/vector-web/issues/2215) + + const claimedKey = event.getClaimedEd25519Key(); + if (!claimedKey) { + logger.warn("Event " + event.getId() + " claims no ed25519 key: " + + "cannot verify sending device"); + ret.mismatchedSender = true; + } + + if (ret.sender && claimedKey !== ret.sender.getFingerprint()) { + logger.warn( + "Event " + event.getId() + " claims ed25519 key " + claimedKey + + "but sender device has key " + ret.sender.getFingerprint()); + ret.mismatchedSender = true; + } + + return ret as IEncryptedEventInfo; + } + + /** + * Forces the current outbound group session to be discarded such + * that another one will be created next time an event is sent. + * + * @param {string} roomId The ID of the room to discard the session for + * + * This should not normally be necessary. + */ + public forceDiscardSession(roomId: string): void { + const alg = this.roomEncryptors[roomId]; + if (alg === undefined) throw new Error("Room not encrypted"); + if (alg.forceDiscardSession === undefined) { + throw new Error("Room encryption algorithm doesn't support session discarding"); + } + alg.forceDiscardSession(); + } + + /** + * Configure a room to use encryption (ie, save a flag in the cryptoStore). + * + * @param {string} roomId The room ID to enable encryption in. + * + * @param {object} config The encryption config for the room. + * + * @param {boolean=} inhibitDeviceQuery true to suppress device list query for + * users in the room (for now). In case lazy loading is enabled, + * the device query is always inhibited as the members are not tracked. + */ + public async setRoomEncryption( + roomId: string, + config: any, // TODO types + inhibitDeviceQuery?: boolean, + ): Promise { + // ignore crypto events with no algorithm defined + // This will happen if a crypto event is redacted before we fetch the room state + // It would otherwise just throw later as an unknown algorithm would, but we may + // as well catch this here + if (!config.algorithm) { + logger.log("Ignoring setRoomEncryption with no algorithm"); + return; + } + + // if state is being replayed from storage, we might already have a configuration + // for this room as they are persisted as well. + // We just need to make sure the algorithm is initialized in this case. + // However, if the new config is different, + // we should bail out as room encryption can't be changed once set. + const existingConfig = this.roomList.getRoomEncryption(roomId); + if (existingConfig) { + if (JSON.stringify(existingConfig) != JSON.stringify(config)) { + logger.error("Ignoring m.room.encryption event which requests " + + "a change of config in " + roomId); + return; + } + } + // if we already have encryption in this room, we should ignore this event, + // as it would reset the encryption algorithm. + // This is at least expected to be called twice, as sync calls onCryptoEvent + // for both the timeline and state sections in the /sync response, + // the encryption event would appear in both. + // If it's called more than twice though, + // it signals a bug on client or server. + const existingAlg = this.roomEncryptors[roomId]; + if (existingAlg) { + return; + } + + // _roomList.getRoomEncryption will not race with _roomList.setRoomEncryption + // because it first stores in memory. We should await the promise only + // after all the in-memory state (roomEncryptors and _roomList) has been updated + // to avoid races when calling this method multiple times. Hence keep a hold of the promise. + let storeConfigPromise = null; + if (!existingConfig) { + storeConfigPromise = this.roomList.setRoomEncryption(roomId, config); + } + + const AlgClass = algorithms.ENCRYPTION_CLASSES[config.algorithm]; + if (!AlgClass) { + throw new Error("Unable to encrypt with " + config.algorithm); + } + + const alg = new AlgClass({ + userId: this.userId, + deviceId: this.deviceId, + crypto: this, + olmDevice: this.olmDevice, + baseApis: this.baseApis, + roomId: roomId, + config: config, + }); + this.roomEncryptors[roomId] = alg; + + if (storeConfigPromise) { + await storeConfigPromise; + } + + if (!this.lazyLoadMembers) { + logger.log("Enabling encryption in " + roomId + "; " + + "starting to track device lists for all users therein"); + + await this.trackRoomDevices(roomId); + // TODO: this flag is only not used from MatrixClient::setRoomEncryption + // which is never used (inside Element at least) + // but didn't want to remove it as it technically would + // be a breaking change. + if (!inhibitDeviceQuery) { + this.deviceList.refreshOutdatedDeviceLists(); + } + } else { + logger.log("Enabling encryption in " + roomId); + } + } + + /** + * Make sure we are tracking the device lists for all users in this room. + * + * @param {string} roomId The room ID to start tracking devices in. + * @returns {Promise} when all devices for the room have been fetched and marked to track + */ + public trackRoomDevices(roomId: string): Promise { + const trackMembers = async () => { + // not an encrypted room + if (!this.roomEncryptors[roomId]) { + return; + } + const room = this.clientStore.getRoom(roomId); + if (!room) { + throw new Error(`Unable to start tracking devices in unknown room ${roomId}`); + } + logger.log(`Starting to track devices for room ${roomId} ...`); + const members = await room.getEncryptionTargetMembers(); + members.forEach((m) => { + this.deviceList.startTrackingDeviceList(m.userId); + }); + }; + + let promise = this.roomDeviceTrackingState[roomId]; + if (!promise) { + promise = trackMembers(); + this.roomDeviceTrackingState[roomId] = promise.catch(err => { + this.roomDeviceTrackingState[roomId] = null; + throw err; + }); + } + return promise; + } + + /** + * Try to make sure we have established olm sessions for all known devices for + * the given users. + * + * @param {string[]} users list of user ids + * + * @return {Promise} resolves once the sessions are complete, to + * an Object mapping from userId to deviceId to + * {@link module:crypto~OlmSessionResult} + */ + ensureOlmSessionsForUsers(users: string[]): Promise { + const devicesByUser = {}; + + for (let i = 0; i < users.length; ++i) { + const userId = users[i]; + devicesByUser[userId] = []; + + const devices = this.getStoredDevicesForUser(userId) || []; + for (let j = 0; j < devices.length; ++j) { + const deviceInfo = devices[j]; + + const key = deviceInfo.getIdentityKey(); + if (key == this.olmDevice.deviceCurve25519Key) { + // don't bother setting up session to ourself + continue; + } + if (deviceInfo.verified == DeviceVerification.BLOCKED) { + // don't bother setting up sessions with blocked users + continue; + } + + devicesByUser[userId].push(deviceInfo); + } + } + + return olmlib.ensureOlmSessionsForDevices( + this.olmDevice, this.baseApis, devicesByUser, + ); + } + + /** + * Get a list containing all of the room keys + * + * @return {module:crypto/OlmDevice.MegolmSessionData[]} a list of session export objects + */ + public async exportRoomKeys(): Promise { + const exportedSessions = []; + await this.cryptoStore.doTxn( + 'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => { + this.cryptoStore.getAllEndToEndInboundGroupSessions(txn, (s) => { + if (s === null) return; + + const sess = this.olmDevice.exportInboundGroupSession( + s.senderKey, s.sessionId, s.sessionData, + ); + delete sess.first_known_index; + sess.algorithm = olmlib.MEGOLM_ALGORITHM; + exportedSessions.push(sess); + }); + }, + ); + + return exportedSessions; + } + + /** + * Import a list of room keys previously exported by exportRoomKeys + * + * @param {Object[]} keys a list of session export objects + * @param {Object} opts + * @param {Function} opts.progressCallback called with an object which has a stage param + * @return {Promise} a promise which resolves once the keys have been imported + */ + public importRoomKeys(keys: IRoomKey[], opts: any = {}): Promise { // TODO types + let successes = 0; + let failures = 0; + const total = keys.length; + + function updateProgress() { + opts.progressCallback({ + stage: "load_keys", + successes, + failures, + total, + }); + } + + return Promise.all(keys.map((key) => { + if (!key.room_id || !key.algorithm) { + logger.warn("ignoring room key entry with missing fields", key); + failures++; + if (opts.progressCallback) { updateProgress(); } + return null; + } + + const alg = this.getRoomDecryptor(key.room_id, key.algorithm); + return alg.importRoomKey(key, opts).finally((r) => { + successes++; + if (opts.progressCallback) { updateProgress(); } + }); + })); + } + + /** + * 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 + */ + public countSessionsNeedingBackup(): Promise { + return this.backupManager.countSessionsNeedingBackup(); + } + + /** + * Perform any background tasks that can be done before a message is ready to + * send, in order to speed up sending of the message. + * + * @param {module:models/room} room the room the event is in + */ + public prepareToEncrypt(room: Room): void { + const alg = this.roomEncryptors[room.roomId]; + if (alg) { + alg.prepareToEncrypt(room); + } + } + + /* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307 + /** + * Encrypt an event according to the configuration of the room. + * + * @param {module:models/event.MatrixEvent} event event to be sent + * + * @param {module:models/room} room destination room. + * + * @return {Promise?} Promise which resolves when the event has been + * encrypted, or null if nothing was needed + */ + /* eslint-enable valid-jsdoc */ + // TODO this return type lies + public async encryptEvent(event: MatrixEvent, room: Room): Promise { + if (!room) { + throw new Error("Cannot send encrypted messages in unknown rooms"); + } + + const roomId = event.getRoomId(); + + const alg = this.roomEncryptors[roomId]; + if (!alg) { + // MatrixClient has already checked that this room should be encrypted, + // so this is an unexpected situation. + throw new Error( + "Room was previously configured to use encryption, but is " + + "no longer. Perhaps the homeserver is hiding the " + + "configuration event.", + ); + } + + if (!this.roomDeviceTrackingState[roomId]) { + this.trackRoomDevices(roomId); + } + // wait for all the room devices to be loaded + await this.roomDeviceTrackingState[roomId]; + + let content = event.getContent(); + // If event has an m.relates_to then we need + // to put this on the wrapping event instead + const mRelatesTo = content['m.relates_to']; + if (mRelatesTo) { + // Clone content here so we don't remove `m.relates_to` from the local-echo + content = Object.assign({}, content); + delete content['m.relates_to']; + } + + const encryptedContent = await alg.encryptMessage( + room, event.getType(), content); + + if (mRelatesTo) { + encryptedContent['m.relates_to'] = mRelatesTo; + } + + event.makeEncrypted( + "m.room.encrypted", + encryptedContent, + this.olmDevice.deviceCurve25519Key, + this.olmDevice.deviceEd25519Key, + ); + } + + /** + * Decrypt a received event + * + * @param {MatrixEvent} event + * + * @return {Promise} resolves once we have + * finished decrypting. Rejects with an `algorithms.DecryptionError` if there + * is a problem decrypting the event. + */ + public async decryptEvent(event: MatrixEvent): Promise { + if (event.isRedacted()) { + const redactionEvent = new MatrixEvent(event.getUnsigned().redacted_because); + const decryptedEvent = await this.decryptEvent(redactionEvent); + + return { + clearEvent: { + room_id: event.getRoomId(), + type: "m.room.message", + content: {}, + unsigned: { + redacted_because: decryptedEvent.clearEvent, + }, + }, + }; + } else { + const content = event.getWireContent(); + const alg = this.getRoomDecryptor(event.getRoomId(), content.algorithm); + return await alg.decryptEvent(event); + } + } + + /** + * Handle the notification from /sync or /keys/changes that device lists have + * been changed. + * + * @param {Object} syncData Object containing sync tokens associated with this sync + * @param {Object} syncDeviceLists device_lists field from /sync, or response from + * /keys/changes + */ + public async handleDeviceListChanges(syncData: ISyncData, syncDeviceLists: ISyncDeviceLists): Promise { + // Initial syncs don't have device change lists. We'll either get the complete list + // of changes for the interval or will have invalidated everything in willProcessSync + if (!syncData.oldSyncToken) return; + + // Here, we're relying on the fact that we only ever save the sync data after + // sucessfully saving the device list data, so we're guaranteed that the device + // list store is at least as fresh as the sync token from the sync store, ie. + // any device changes received in sync tokens prior to the 'next' token here + // have been processed and are reflected in the current device list. + // If we didn't make this assumption, we'd have to use the /keys/changes API + // to get key changes between the sync token in the device list and the 'old' + // sync token used here to make sure we didn't miss any. + await this.evalDeviceListChanges(syncDeviceLists); + } + + /** + * Send a request for some room keys, if we have not already done so + * + * @param {module:crypto~RoomKeyRequestBody} requestBody + * @param {Array<{userId: string, deviceId: string}>} recipients + * @param {boolean} resend whether to resend the key request if there is + * already one + * + * @return {Promise} a promise that resolves when the key request is queued + */ + public requestRoomKey( + requestBody: IRoomKeyRequestBody, + recipients: IRoomKeyRequestRecipient[], + resend = false, + ): Promise { + return this.outgoingRoomKeyRequestManager.queueRoomKeyRequest( + requestBody, recipients, resend, + ).then(() => { + if (this.sendKeyRequestsImmediately) { + this.outgoingRoomKeyRequestManager.sendQueuedRequests(); + } + }).catch((e) => { + // this normally means we couldn't talk to the store + logger.error( + 'Error requesting key for event', e, + ); + }); + } + + /** + * Cancel any earlier room key request + * + * @param {module:crypto~RoomKeyRequestBody} requestBody + * parameters to match for cancellation + */ + public cancelRoomKeyRequest(requestBody: IRoomKeyRequestBody): void { + this.outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody) + .catch((e) => { + logger.warn("Error clearing pending room key requests", e); + }); + } + + /** + * Re-send any outgoing key requests, eg after verification + * @returns {Promise} + */ + public cancelAndResendAllOutgoingKeyRequests(): Promise { + return this.outgoingRoomKeyRequestManager.cancelAndResendAllOutgoingRequests(); + } + + /** + * handle an m.room.encryption event + * + * @param {module:models/event.MatrixEvent} event encryption event + */ + public async onCryptoEvent(event: MatrixEvent): Promise { + const roomId = event.getRoomId(); + const content = event.getContent(); + + try { + // inhibit the device list refresh for now - it will happen once we've + // finished processing the sync, in onSyncCompleted. + await this.setRoomEncryption(roomId, content, true); + } catch (e) { + logger.error("Error configuring encryption in room " + roomId + + ":", e); + } + } + + /** + * Called before the result of a sync is processed + * + * @param {Object} syncData the data from the 'MatrixClient.sync' event + */ + public async onSyncWillProcess(syncData: ISyncData): Promise { + if (!syncData.oldSyncToken) { + // If there is no old sync token, we start all our tracking from + // scratch, so mark everything as untracked. onCryptoEvent will + // be called for all e2e rooms during the processing of the sync, + // at which point we'll start tracking all the users of that room. + logger.log("Initial sync performed - resetting device tracking state"); + this.deviceList.stopTrackingAllDeviceLists(); + // we always track our own device list (for key backups etc) + this.deviceList.startTrackingDeviceList(this.userId); + this.roomDeviceTrackingState = {}; + } + + this.sendKeyRequestsImmediately = false; + } + + /** + * handle the completion of a /sync + * + * This is called after the processing of each successful /sync response. + * It is an opportunity to do a batch process on the information received. + * + * @param {Object} syncData the data from the 'MatrixClient.sync' event + */ + public async onSyncCompleted(syncData: ISyncData): Promise { + const nextSyncToken = syncData.nextSyncToken; + + this.deviceList.setSyncToken(syncData.nextSyncToken); + this.deviceList.saveIfDirty(); + + // catch up on any new devices we got told about during the sync. + this.deviceList.lastKnownSyncToken = nextSyncToken; + + // we always track our own device list (for key backups etc) + this.deviceList.startTrackingDeviceList(this.userId); + + this.deviceList.refreshOutdatedDeviceLists(); + + // we don't start uploading one-time keys until we've caught up with + // to-device messages, to help us avoid throwing away one-time-keys that we + // are about to receive messages for + // (https://github.com/vector-im/element-web/issues/2782). + if (!syncData.catchingUp) { + this.maybeUploadOneTimeKeys(); + this.processReceivedRoomKeyRequests(); + + // likewise don't start requesting keys until we've caught up + // on to_device messages, otherwise we'll request keys that we're + // just about to get. + this.outgoingRoomKeyRequestManager.sendQueuedRequests(); + + // Sync has finished so send key requests straight away. + this.sendKeyRequestsImmediately = true; + } + } + + /** + * Trigger the appropriate invalidations and removes for a given + * device list + * + * @param {Object} deviceLists device_lists field from /sync, or response from + * /keys/changes + */ + private async evalDeviceListChanges(deviceLists: ISyncDeviceLists): Promise { + if (deviceLists.changed && Array.isArray(deviceLists.changed)) { + deviceLists.changed.forEach((u) => { + this.deviceList.invalidateUserDeviceList(u); + }); + } + + if (deviceLists.left && Array.isArray(deviceLists.left) && + deviceLists.left.length) { + // Check we really don't share any rooms with these users + // any more: the server isn't required to give us the + // exact correct set. + const e2eUserIds = new Set(await this.getTrackedE2eUsers()); + + deviceLists.left.forEach((u) => { + if (!e2eUserIds.has(u)) { + this.deviceList.stopTrackingDeviceList(u); + } + }); + } + } + + /** + * Get a list of all the IDs of users we share an e2e room with + * for which we are tracking devices already + * + * @returns {string[]} List of user IDs + */ + private async getTrackedE2eUsers(): Promise { + const e2eUserIds = []; + for (const room of this.getTrackedE2eRooms()) { + const members = await room.getEncryptionTargetMembers(); + for (const member of members) { + e2eUserIds.push(member.userId); + } + } + return e2eUserIds; + } + + /** + * Get a list of the e2e-enabled rooms we are members of, + * and for which we are already tracking the devices + * + * @returns {module:models.Room[]} + */ + private getTrackedE2eRooms(): Room[] { + return this.clientStore.getRooms().filter((room) => { + // check for rooms with encryption enabled + const alg = this.roomEncryptors[room.roomId]; + if (!alg) { + return false; + } + if (!this.roomDeviceTrackingState[room.roomId]) { + return false; + } + + // ignore any rooms which we have left + const myMembership = room.getMyMembership(); + return myMembership === "join" || myMembership === "invite"; + }); + } + + private onToDeviceEvent = (event: MatrixEvent): void => { + try { + logger.log(`received to_device ${event.getType()} from: ` + + `${event.getSender()} id: ${event.getId()}`); + + if (event.getType() == "m.room_key" + || event.getType() == "m.forwarded_room_key") { + this.onRoomKeyEvent(event); + } else if (event.getType() == "m.room_key_request") { + this.onRoomKeyRequestEvent(event); + } else if (event.getType() === "m.secret.request") { + this.secretStorage._onRequestReceived(event); + } else if (event.getType() === "m.secret.send") { + this.secretStorage._onSecretReceived(event); + } else if (event.getType() === "org.matrix.room_key.withheld") { + this.onRoomKeyWithheldEvent(event); + } else if (event.getContent().transaction_id) { + this.onKeyVerificationMessage(event); + } else if (event.getContent().msgtype === "m.bad.encrypted") { + this.onToDeviceBadEncrypted(event); + } else if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) { + if (!event.isBeingDecrypted()) { + event.attemptDecryption(this); + } + // once the event has been decrypted, try again + event.once('Event.decrypted', (ev) => { + this.onToDeviceEvent(ev); + }); + } + } catch (e) { + logger.error("Error handling toDeviceEvent:", e); + } + }; + + /** + * Handle a key event + * + * @private + * @param {module:models/event.MatrixEvent} event key event + */ + private onRoomKeyEvent(event: MatrixEvent): void { + const content = event.getContent(); + + if (!content.room_id || !content.algorithm) { + logger.error("key event is missing fields"); + return; + } + + 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.backupManager.checkAndStart(); + } + + const alg = this.getRoomDecryptor(content.room_id, content.algorithm); + alg.onRoomKeyEvent(event); + } + + /** + * Handle a key withheld event + * + * @private + * @param {module:models/event.MatrixEvent} event key withheld event + */ + private onRoomKeyWithheldEvent(event: MatrixEvent): void { + const content = event.getContent(); + + if ((content.code !== "m.no_olm" && (!content.room_id || !content.session_id)) + || !content.algorithm || !content.sender_key) { + logger.error("key withheld event is missing fields"); + return; + } + + logger.info( + `Got room key withheld event from ${event.getSender()} (${content.sender_key}) ` + + `for ${content.algorithm}/${content.room_id}/${content.session_id} ` + + `with reason ${content.code} (${content.reason})`, + ); + + const alg = this.getRoomDecryptor(content.room_id, content.algorithm); + if (alg.onRoomKeyWithheldEvent) { + alg.onRoomKeyWithheldEvent(event); + } + if (!content.room_id) { + // retry decryption for all events sent by the sender_key. This will + // update the events to show a message indicating that the olm session was + // wedged. + const roomDecryptors = this.getRoomDecryptors(content.algorithm); + for (const decryptor of roomDecryptors) { + decryptor.retryDecryptionFromSender(content.sender_key); + } + } + } + + /** + * Handle a general key verification event. + * + * @private + * @param {module:models/event.MatrixEvent} event verification start event + */ + private onKeyVerificationMessage(event: MatrixEvent): void { + if (!ToDeviceChannel.validateEvent(event, this.baseApis)) { + return; + } + const createRequest = event => { + if (!ToDeviceChannel.canCreateRequest(ToDeviceChannel.getEventType(event))) { + return; + } + const content = event.getContent(); + const deviceId = content && content.from_device; + if (!deviceId) { + return; + } + const userId = event.getSender(); + const channel = new ToDeviceChannel( + this.baseApis, + userId, + [deviceId], + ); + return new VerificationRequest( + channel, this.verificationMethods, this.baseApis); + }; + this.handleVerificationEvent( + event, + this.toDeviceVerificationRequests, + createRequest, + ); + } + + /** + * Handle key verification requests sent as timeline events + * + * @private + * @param {module:models/event.MatrixEvent} event the timeline event + * @param {module:models/Room} room not used + * @param {boolean} atStart not used + * @param {boolean} removed not used + * @param {boolean} { liveEvent } whether this is a live event + */ + private onTimelineEvent = ( + event: MatrixEvent, + room: Room, + atStart: boolean, + removed: boolean, + { liveEvent = true } = {}, + ): void => { + if (!InRoomChannel.validateEvent(event, this.baseApis)) { + return; + } + const createRequest = event => { + const channel = new InRoomChannel( + this.baseApis, + event.getRoomId(), + ); + return new VerificationRequest( + channel, this.verificationMethods, this.baseApis); + }; + this.handleVerificationEvent( + event, + this.inRoomVerificationRequests, + createRequest, + liveEvent, + ); + }; + + private async handleVerificationEvent( + event: MatrixEvent, + requestsMap: any, // TODO types + createRequest: any, // TODO types + isLiveEvent = true, + ): Promise { + let request = requestsMap.getRequest(event); + let isNewRequest = false; + if (!request) { + request = createRequest(event); + // a request could not be made from this event, so ignore event + if (!request) { + logger.log(`Crypto: could not find VerificationRequest for ` + + `${event.getType()}, and could not create one, so ignoring.`); + return; + } + isNewRequest = true; + requestsMap.setRequest(event, request); + } + event.setVerificationRequest(request); + try { + await request.channel.handleEvent(event, request, isLiveEvent); + } catch (err) { + logger.error("error while handling verification event: " + err.message); + } + const shouldEmit = isNewRequest && + !request.initiatedByMe && + !request.invalid && // check it has enough events to pass the UNSENT stage + !request.observeOnly; + if (shouldEmit) { + this.baseApis.emit("crypto.verification.request", request); + } + } + + /** + * Handle a toDevice event that couldn't be decrypted + * + * @private + * @param {module:models/event.MatrixEvent} event undecryptable event + */ + private async onToDeviceBadEncrypted(event: MatrixEvent): Promise { + const content = event.getWireContent(); + const sender = event.getSender(); + const algorithm = content.algorithm; + const deviceKey = content.sender_key; + + // retry decryption for all events sent by the sender_key. This will + // update the events to show a message indicating that the olm session was + // wedged. + const retryDecryption = () => { + const roomDecryptors = this.getRoomDecryptors(olmlib.MEGOLM_ALGORITHM); + for (const decryptor of roomDecryptors) { + decryptor.retryDecryptionFromSender(deviceKey); + } + }; + + if (sender === undefined || deviceKey === undefined || deviceKey === undefined) { + return; + } + + // check when we last forced a new session with this device: if we've already done so + // recently, don't do it again. + this.lastNewSessionForced[sender] = this.lastNewSessionForced[sender] || {}; + const lastNewSessionForced = this.lastNewSessionForced[sender][deviceKey] || 0; + if (lastNewSessionForced + MIN_FORCE_SESSION_INTERVAL_MS > Date.now()) { + logger.debug( + "New session already forced with device " + sender + ":" + deviceKey + + " at " + lastNewSessionForced + ": not forcing another", + ); + await this.olmDevice.recordSessionProblem(deviceKey, "wedged", true); + retryDecryption(); + return; + } + + // establish a new olm session with this device since we're failing to decrypt messages + // on a current session. + // Note that an undecryptable message from another device could easily be spoofed - + // is there anything we can do to mitigate this? + let device = this.deviceList.getDeviceByIdentityKey(algorithm, deviceKey); + if (!device) { + // if we don't know about the device, fetch the user's devices again + // and retry before giving up + await this.downloadKeys([sender], false); + device = this.deviceList.getDeviceByIdentityKey(algorithm, deviceKey); + if (!device) { + logger.info( + "Couldn't find device for identity key " + deviceKey + + ": not re-establishing session", + ); + await this.olmDevice.recordSessionProblem(deviceKey, "wedged", false); + retryDecryption(); + return; + } + } + const devicesByUser = {}; + devicesByUser[sender] = [device]; + await olmlib.ensureOlmSessionsForDevices( + this.olmDevice, this.baseApis, devicesByUser, true, + ); + + this.lastNewSessionForced[sender][deviceKey] = Date.now(); + + // Now send a blank message on that session so the other side knows about it. + // (The keyshare request is sent in the clear so that won't do) + // We send this first such that, as long as the toDevice messages arrive in the + // same order we sent them, the other end will get this first, set up the new session, + // then get the keyshare request and send the key over this new session (because it + // is the session it has most recently received a message on). + const encryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: this.olmDevice.deviceCurve25519Key, + ciphertext: {}, + }; + await olmlib.encryptMessageForDevice( + encryptedContent.ciphertext, + this.userId, + this.deviceId, + this.olmDevice, + sender, + device, + { type: "m.dummy" }, + ); + + await this.olmDevice.recordSessionProblem(deviceKey, "wedged", true); + retryDecryption(); + + await this.baseApis.sendToDevice("m.room.encrypted", { + [sender]: { + [device.deviceId]: encryptedContent, + }, + }); + + // Most of the time this probably won't be necessary since we'll have queued up a key request when + // we failed to decrypt the message and will be waiting a bit for the key to arrive before sending + // it. This won't always be the case though so we need to re-send any that have already been sent + // to avoid races. + const requestsToResend = + await this.outgoingRoomKeyRequestManager.getOutgoingSentRoomKeyRequest( + sender, device.deviceId, + ); + for (const keyReq of requestsToResend) { + this.requestRoomKey(keyReq.requestBody, keyReq.recipients, true); + } + } + + /** + * Handle a change in the membership state of a member of a room + * + * @private + * @param {module:models/event.MatrixEvent} event event causing the change + * @param {module:models/room-member} member user whose membership changed + * @param {string=} oldMembership previous membership + */ + private onRoomMembership(event: MatrixEvent, member: RoomMember, oldMembership?: string): void { + // this event handler is registered on the *client* (as opposed to the room + // member itself), which means it is only called on changes to the *live* + // membership state (ie, it is not called when we back-paginate, nor when + // we load the state in the initialsync). + // + // Further, it is automatically registered and called when new members + // arrive in the room. + + const roomId = member.roomId; + + const alg = this.roomEncryptors[roomId]; + if (!alg) { + // not encrypting in this room + return; + } + // only mark users in this room as tracked if we already started tracking in this room + // this way we don't start device queries after sync on behalf of this room which we won't use + // the result of anyway, as we'll need to do a query again once all the members are fetched + // by calling _trackRoomDevices + if (this.roomDeviceTrackingState[roomId]) { + if (member.membership == 'join') { + logger.log('Join event for ' + member.userId + ' in ' + roomId); + // make sure we are tracking the deviceList for this user + this.deviceList.startTrackingDeviceList(member.userId); + } else if (member.membership == 'invite' && + this.clientStore.getRoom(roomId).shouldEncryptForInvitedMembers()) { + logger.log('Invite event for ' + member.userId + ' in ' + roomId); + this.deviceList.startTrackingDeviceList(member.userId); + } + } + + alg.onRoomMembership(event, member, oldMembership); + } + + /** + * Called when we get an m.room_key_request event. + * + * @private + * @param {module:models/event.MatrixEvent} event key request event + */ + private onRoomKeyRequestEvent(event: MatrixEvent): void { + const content = event.getContent(); + if (content.action === "request") { + // Queue it up for now, because they tend to arrive before the room state + // events at initial sync, and we want to see if we know anything about the + // room before passing them on to the app. + const req = new IncomingRoomKeyRequest(event); + this.receivedRoomKeyRequests.push(req); + } else if (content.action === "request_cancellation") { + const req = new IncomingRoomKeyRequestCancellation(event); + this.receivedRoomKeyRequestCancellations.push(req); + } + } + + /** + * Process any m.room_key_request events which were queued up during the + * current sync. + * + * @private + */ + private async processReceivedRoomKeyRequests(): Promise { + if (this.processingRoomKeyRequests) { + // we're still processing last time's requests; keep queuing new ones + // up for now. + return; + } + this.processingRoomKeyRequests = true; + + try { + // we need to grab and clear the queues in the synchronous bit of this method, + // so that we don't end up racing with the next /sync. + const requests = this.receivedRoomKeyRequests; + this.receivedRoomKeyRequests = []; + const cancellations = this.receivedRoomKeyRequestCancellations; + this.receivedRoomKeyRequestCancellations = []; + + // Process all of the requests, *then* all of the cancellations. + // + // This makes sure that if we get a request and its cancellation in the + // same /sync result, then we process the request before the + // cancellation (and end up with a cancelled request), rather than the + // cancellation before the request (and end up with an outstanding + // request which should have been cancelled.) + await Promise.all(requests.map((req) => + this.processReceivedRoomKeyRequest(req))); + await Promise.all(cancellations.map((cancellation) => + this.processReceivedRoomKeyRequestCancellation(cancellation))); + } catch (e) { + logger.error(`Error processing room key requsts: ${e}`); + } finally { + this.processingRoomKeyRequests = false; + } + } + + /** + * Helper for processReceivedRoomKeyRequests + * + * @param {IncomingRoomKeyRequest} req + */ + private async processReceivedRoomKeyRequest(req: IncomingRoomKeyRequest): Promise { + const userId = req.userId; + const deviceId = req.deviceId; + + const body = req.requestBody; + const roomId = body.room_id; + const alg = body.algorithm; + + logger.log(`m.room_key_request from ${userId}:${deviceId}` + + ` for ${roomId} / ${body.session_id} (id ${req.requestId})`); + + if (userId !== this.userId) { + if (!this.roomEncryptors[roomId]) { + logger.debug(`room key request for unencrypted room ${roomId}`); + return; + } + const encryptor = this.roomEncryptors[roomId]; + const device = this.deviceList.getStoredDevice(userId, deviceId); + if (!device) { + logger.debug(`Ignoring keyshare for unknown device ${userId}:${deviceId}`); + return; + } + + try { + await encryptor.reshareKeyWithDevice( + body.sender_key, body.session_id, userId, device, + ); + } catch (e) { + logger.warn( + "Failed to re-share keys for session " + body.session_id + + " with device " + userId + ":" + device.deviceId, e, + ); + } + return; + } + + if (deviceId === this.deviceId) { + // We'll always get these because we send room key requests to + // '*' (ie. 'all devices') which includes the sending device, + // so ignore requests from ourself because apart from it being + // very silly, it won't work because an Olm session cannot send + // messages to itself. + // The log here is probably superfluous since we know this will + // always happen, but let's log anyway for now just in case it + // causes issues. + logger.log("Ignoring room key request from ourselves"); + return; + } + + // todo: should we queue up requests we don't yet have keys for, + // in case they turn up later? + + // if we don't have a decryptor for this room/alg, we don't have + // the keys for the requested events, and can drop the requests. + if (!this.roomDecryptors[roomId]) { + logger.log(`room key request for unencrypted room ${roomId}`); + return; + } + + const decryptor = this.roomDecryptors[roomId][alg]; + if (!decryptor) { + logger.log(`room key request for unknown alg ${alg} in room ${roomId}`); + return; + } + + if (!await decryptor.hasKeysForKeyRequest(req)) { + logger.log( + `room key request for unknown session ${roomId} / ` + + body.session_id, + ); + return; + } + + req.share = () => { + decryptor.shareKeysWithDevice(req); + }; + + // if the device is verified already, share the keys + if (this.checkDeviceTrust(userId, deviceId).isVerified()) { + logger.log('device is already verified: sharing keys'); + req.share(); + return; + } + + this.emit("crypto.roomKeyRequest", req); + } + + /** + * Helper for processReceivedRoomKeyRequests + * + * @param {IncomingRoomKeyRequestCancellation} cancellation + */ + private async processReceivedRoomKeyRequestCancellation( + cancellation: IncomingRoomKeyRequestCancellation, + ): Promise { + logger.log( + `m.room_key_request cancellation for ${cancellation.userId}:` + + `${cancellation.deviceId} (id ${cancellation.requestId})`, + ); + + // we should probably only notify the app of cancellations we told it + // about, but we don't currently have a record of that, so we just pass + // everything through. + this.emit("crypto.roomKeyRequestCancellation", cancellation); + } + + /** + * Get a decryptor for a given room and algorithm. + * + * If we already have a decryptor for the given room and algorithm, return + * it. Otherwise try to instantiate it. + * + * @private + * + * @param {string?} roomId room id for decryptor. If undefined, a temporary + * decryptor is instantiated. + * + * @param {string} algorithm crypto algorithm + * + * @return {module:crypto.algorithms.base.DecryptionAlgorithm} + * + * @raises {module:crypto.algorithms.DecryptionError} if the algorithm is + * unknown + */ + public getRoomDecryptor(roomId: string, algorithm: string): DecryptionAlgorithm { + let decryptors: Record; + let alg: DecryptionAlgorithm; + + roomId = roomId || null; + if (roomId) { + decryptors = this.roomDecryptors[roomId]; + if (!decryptors) { + this.roomDecryptors[roomId] = decryptors = {}; + } + + alg = decryptors[algorithm]; + if (alg) { + return alg; + } + } + + const AlgClass = algorithms.DECRYPTION_CLASSES[algorithm]; + if (!AlgClass) { + throw new algorithms.DecryptionError( + 'UNKNOWN_ENCRYPTION_ALGORITHM', + 'Unknown encryption algorithm "' + algorithm + '".', + ); + } + alg = new AlgClass({ + userId: this.userId, + crypto: this, + olmDevice: this.olmDevice, + baseApis: this.baseApis, + roomId: roomId, + }); + + if (decryptors) { + decryptors[algorithm] = alg; + } + return alg; + } + + /** + * Get all the room decryptors for a given encryption algorithm. + * + * @param {string} algorithm The encryption algorithm + * + * @return {array} An array of room decryptors + */ + private getRoomDecryptors(algorithm: string): DecryptionAlgorithm[] { + const decryptors = []; + for (const d of Object.values(this.roomDecryptors)) { + if (algorithm in d) { + decryptors.push(d[algorithm]); + } + } + return decryptors; + } + + /** + * sign the given object with our ed25519 key + * + * @param {Object} obj Object to which we will add a 'signatures' property + */ + public async signObject(obj: object & ISignableObject): Promise { + const sigs = obj.signatures || {}; + const unsigned = obj.unsigned; + + delete obj.signatures; + delete obj.unsigned; + + sigs[this.userId] = sigs[this.userId] || {}; + sigs[this.userId]["ed25519:" + this.deviceId] = await this.olmDevice.sign(anotherjson.stringify(obj)); + obj.signatures = sigs; + if (unsigned !== undefined) obj.unsigned = unsigned; + } +} + +/** + * Fix up the backup key, that may be in the wrong format due to a bug in a + * migration step. Some backup keys were stored as a comma-separated list of + * integers, rather than a base64-encoded byte array. If this function is + * passed a string that looks like a list of integers rather than a base64 + * string, it will attempt to convert it to the right format. + * + * @param {string} key the key to check + * @returns {null | string} If the key is in the wrong format, then the fixed + * key will be returned. Otherwise null will be returned. + * + */ +export function fixBackupKey(key: string): string | null { + if (typeof key !== "string" || key.indexOf(",") < 0) { + return null; + } + const fixedKey = Uint8Array.from(key.split(","), x => parseInt(x)); + return olmlib.encodeBase64(fixedKey); +} + +/** + * The parameters of a room key request. The details of the request may + * vary with the crypto algorithm, but the management and storage layers for + * outgoing requests expect it to have 'room_id' and 'session_id' properties. + * + * @typedef {Object} RoomKeyRequestBody + */ + +/** + * Represents a received m.room_key_request event + * + * @property {string} userId user requesting the key + * @property {string} deviceId device requesting the key + * @property {string} requestId unique id for the request + * @property {module:crypto~RoomKeyRequestBody} requestBody + * @property {function()} share callback which, when called, will ask + * the relevant crypto algorithm implementation to share the keys for + * this request. + */ +class IncomingRoomKeyRequest { + public readonly userId: string; + public readonly deviceId: string; + public readonly requestId: string; + public readonly requestBody: IRoomKeyRequestBody; + public share: () => void; + + constructor(event: MatrixEvent) { + const content = event.getContent(); + + this.userId = event.getSender(); + this.deviceId = content.requesting_device_id; + this.requestId = content.request_id; + this.requestBody = content.body || {}; + this.share = () => { + throw new Error("don't know how to share keys for this request yet"); + }; + } +} + +/** + * Represents a received m.room_key_request cancellation + * + * @property {string} userId user requesting the cancellation + * @property {string} deviceId device requesting the cancellation + * @property {string} requestId unique id for the request to be cancelled + */ +class IncomingRoomKeyRequestCancellation { + public readonly userId: string; + public readonly deviceId: string; + public readonly requestId: string; + + constructor(event: MatrixEvent) { + const content = event.getContent(); + + this.userId = event.getSender(); + this.deviceId = content.requesting_device_id; + this.requestId = content.request_id; + } +} + +/** + * The result of a (successful) call to decryptEvent. + * + * @typedef {Object} EventDecryptionResult + * + * @property {Object} clearEvent The plaintext payload for the event + * (typically containing type and content fields). + * + * @property {?string} senderCurve25519Key Key owned by the sender of this + * event. See {@link module:models/event.MatrixEvent#getSenderKey}. + * + * @property {?string} claimedEd25519Key ed25519 key claimed by the sender of + * this event. See + * {@link module:models/event.MatrixEvent#getClaimedEd25519Key}. + * + * @property {?Array} forwardingCurve25519KeyChain list of curve25519 + * keys involved in telling us about the senderCurve25519Key and + * claimedEd25519Key. See + * {@link module:models/event.MatrixEvent#getForwardingCurve25519KeyChain}. + */ + +/** + * Fires when we receive a room key request + * + * @event module:client~MatrixClient#"crypto.roomKeyRequest" + * @param {module:crypto~IncomingRoomKeyRequest} req request details + */ + +/** + * Fires when we receive a room key request cancellation + * + * @event module:client~MatrixClient#"crypto.roomKeyRequestCancellation" + * @param {module:crypto~IncomingRoomKeyRequestCancellation} req + */ + +/** + * Fires when the app may wish to warn the user about something related + * the end-to-end crypto. + * + * @event module:client~MatrixClient#"crypto.warning" + * @param {string} type One of the strings listed above + */ diff --git a/src/crypto/keybackup.ts b/src/crypto/keybackup.ts index c5a7979f2f7..e67db035cda 100644 --- a/src/crypto/keybackup.ts +++ b/src/crypto/keybackup.ts @@ -41,18 +41,7 @@ export interface IKeyBackupVersion { count: number; etag: string; version: string; // number contained within -} - -// TODO: Verify types -export interface IKeyBackupTrustInfo { - /** - * is the backup trusted, true if there is a sig that is valid & from a trusted device - */ - usable: boolean[]; - sigs: { - valid: boolean[]; - device: DeviceInfo[]; - }[]; + recovery_key: string; // eslint-disable-line camelcase } export interface IKeyBackupPrepareOpts { From 27a6d1f87882adfe9c4fdea7369ffcb68e216cb6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 19 Jun 2021 19:44:47 +0100 Subject: [PATCH 02/10] Fix missing await identified by TS conversion --- src/crypto/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/crypto/index.ts b/src/crypto/index.ts index e3ab46e6c06..25108382aeb 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -567,7 +567,7 @@ export class Crypto extends EventEmitter { ); const sessionBackupInStorage = ( !this.backupManager.getKeyBackupEnabled() || - this.baseApis.isKeyBackupKeyStored() + await this.baseApis.isKeyBackupKeyStored() ); return !!( @@ -2661,7 +2661,7 @@ export class Crypto extends EventEmitter { } const alg = this.getRoomDecryptor(key.room_id, key.algorithm); - return alg.importRoomKey(key, opts).finally((r) => { + return alg.importRoomKey(key, opts).finally(() => { successes++; if (opts.progressCallback) { updateProgress(); } }); From 66b17aa0194a8e55952562117dc7215b5bf1adf7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 19 Jun 2021 19:45:29 +0100 Subject: [PATCH 03/10] Add type for another-json dep --- src/@types/another-json.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/@types/another-json.ts diff --git a/src/@types/another-json.ts b/src/@types/another-json.ts new file mode 100644 index 00000000000..070332a5c88 --- /dev/null +++ b/src/@types/another-json.ts @@ -0,0 +1,19 @@ +/* +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. +*/ + +declare module "another-json" { + export function stringify(o: object): string; +} From 69050ed33887e8afabe917776fa924551de0b8b4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Jun 2021 13:12:26 +0100 Subject: [PATCH 04/10] Fix import cycle --- src/crypto/index.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 25108382aeb..1a5401e65e0 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -49,7 +49,10 @@ import { decryptAES, encryptAES } from './aes'; import { DehydrationManager } from './dehydration'; import { BackupManager } from "./backup"; import { IStore } from "../store"; -import { Room, RoomMember, MatrixEvent, MatrixClient, IKeysUploadResponse } from ".."; +import { Room } from "../models/room"; +import { RoomMember } from "../models/room-member"; +import { MatrixEvent } from "../models/event"; +import { MatrixClient, IKeysUploadResponse } from "../client"; import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base"; import type { RoomList } from "./RoomList"; import { IRecoveryKey, IEncryptedEventInfo } from "./api"; @@ -65,7 +68,7 @@ const defaultVerificationMethods = { // to start. [SHOW_QR_CODE_METHOD]: IllegalMethod, [SCAN_QR_CODE_METHOD]: IllegalMethod, -} +}; /** * verification method names @@ -73,7 +76,7 @@ const defaultVerificationMethods = { export const verificationMethods = { RECIPROCATE_QR_CODE: ReciprocateQRCode.NAME, SAS: SAS.NAME, -} +}; export function isCryptoAvailable(): boolean { return Boolean(global.Olm); From 6017fead19cf5519cc7e3e85ba1ff5143a04390a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Jun 2021 13:33:56 +0100 Subject: [PATCH 05/10] Fix imports --- src/client.ts | 2 +- src/crypto/index.ts | 6 +++--- src/crypto/keybackup.ts | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/client.ts b/src/client.ts index 58a3ab3ff82..d6b4aecdaee 100644 --- a/src/client.ts +++ b/src/client.ts @@ -48,7 +48,7 @@ import { retryNetworkOperation, } from "./http-api"; import { Crypto, fixBackupKey, IBootstrapCrossSigningOpts, isCryptoAvailable } from './crypto'; -import { DeviceInfo } from "./crypto/DeviceInfo"; +import { DeviceInfo } from "./crypto/deviceinfo"; import { decodeRecoveryKey } from './crypto/recoverykey'; import { keyFromAuthData } from './crypto/key_passphrase'; import { User } from "./models/user"; diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 1a5401e65e0..205a2195256 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -52,7 +52,7 @@ import { IStore } from "../store"; import { Room } from "../models/room"; import { RoomMember } from "../models/room-member"; import { MatrixEvent } from "../models/event"; -import { MatrixClient, IKeysUploadResponse } from "../client"; +import { MatrixClient, IKeysUploadResponse, SessionStore, CryptoStore } from "../client"; import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base"; import type { RoomList } from "./RoomList"; import { IRecoveryKey, IEncryptedEventInfo } from "./api"; @@ -282,11 +282,11 @@ export class Crypto extends EventEmitter { */ constructor( private readonly baseApis: MatrixClient, - public readonly sessionStore: any, // TODO types + public readonly sessionStore: SessionStore, private readonly userId: string, private readonly deviceId: string, private readonly clientStore: IStore, - public readonly cryptoStore: any, // TODO types + public readonly cryptoStore: CryptoStore, private readonly roomList: RoomList, verificationMethods: any[], // TODO types ) { diff --git a/src/crypto/keybackup.ts b/src/crypto/keybackup.ts index e67db035cda..c2fe3a1ce9c 100644 --- a/src/crypto/keybackup.ts +++ b/src/crypto/keybackup.ts @@ -15,7 +15,6 @@ limitations under the License. */ import { ISignatures } from "../@types/signed"; -import { DeviceInfo } from "./deviceinfo"; export interface IKeyBackupSession { first_message_index: number; // eslint-disable-line camelcase From 5a8299f1a5655e382df1b3407ebaa803d4c1f7a6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Jun 2021 14:47:25 +0100 Subject: [PATCH 06/10] Convert more of js-sdk crypto and fix underscored field accesses --- spec/integ/devicelist-integ-spec.js | 16 +- spec/integ/matrix-client-crypto.spec.js | 2 +- spec/integ/matrix-client-methods.spec.js | 2 +- spec/integ/megolm-integ.spec.js | 2 +- spec/unit/crypto.spec.js | 8 +- spec/unit/crypto/algorithms/megolm.spec.js | 30 +- spec/unit/crypto/backup.spec.js | 4 +- spec/unit/crypto/cross-signing.spec.js | 54 +- spec/unit/crypto/crypto-utils.js | 10 +- spec/unit/crypto/secrets.spec.js | 34 +- spec/unit/crypto/verification/request.spec.js | 2 +- spec/unit/crypto/verification/sas.spec.js | 12 +- src/client.ts | 20 +- .../{CrossSigning.js => CrossSigning.ts} | 271 ++++----- src/crypto/{DeviceList.js => DeviceList.ts} | 555 +++++++++--------- src/crypto/EncryptionSetup.js | 2 +- src/crypto/{RoomList.js => RoomList.ts} | 41 +- src/crypto/SecretStorage.js | 8 +- src/crypto/dehydration.ts | 34 +- src/crypto/deviceinfo.js | 168 ------ src/crypto/deviceinfo.ts | 175 ++++++ src/crypto/index.ts | 21 +- .../{key_passphrase.js => key_passphrase.ts} | 28 +- src/crypto/keybackup.ts | 11 +- src/crypto/verification/Base.js | 2 +- src/matrix.ts | 13 +- src/utils.ts | 10 +- 27 files changed, 790 insertions(+), 745 deletions(-) rename src/crypto/{CrossSigning.js => CrossSigning.ts} (75%) rename src/crypto/{DeviceList.js => DeviceList.ts} (62%) rename src/crypto/{RoomList.js => RoomList.ts} (52%) delete mode 100644 src/crypto/deviceinfo.js create mode 100644 src/crypto/deviceinfo.ts rename src/crypto/{key_passphrase.js => key_passphrase.ts} (76%) diff --git a/spec/integ/devicelist-integ-spec.js b/spec/integ/devicelist-integ-spec.js index cdad8a90512..2ca459119b9 100644 --- a/spec/integ/devicelist-integ-spec.js +++ b/spec/integ/devicelist-integ-spec.js @@ -165,7 +165,7 @@ describe("DeviceList management:", function() { aliceTestClient.httpBackend.flush('/keys/query', 1).then( () => aliceTestClient.httpBackend.flush('/send/', 1), ), - aliceTestClient.client.crypto._deviceList.saveIfDirty(), + aliceTestClient.client.crypto.deviceList.saveIfDirty(), ]); }).then(() => { aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { @@ -202,7 +202,7 @@ describe("DeviceList management:", function() { return aliceTestClient.httpBackend.flush('/keys/query', 1); }).then((flushed) => { expect(flushed).toEqual(0); - return aliceTestClient.client.crypto._deviceList.saveIfDirty(); + return aliceTestClient.client.crypto.deviceList.saveIfDirty(); }).then(() => { aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; @@ -235,7 +235,7 @@ describe("DeviceList management:", function() { // wait for the client to stop processing the response return aliceTestClient.client.downloadKeys(['@bob:xyz']); }).then(() => { - return aliceTestClient.client.crypto._deviceList.saveIfDirty(); + return aliceTestClient.client.crypto.deviceList.saveIfDirty(); }).then(() => { aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; @@ -256,7 +256,7 @@ describe("DeviceList management:", function() { // wait for the client to stop processing the response return aliceTestClient.client.downloadKeys(['@chris:abc']); }).then(() => { - return aliceTestClient.client.crypto._deviceList.saveIfDirty(); + return aliceTestClient.client.crypto.deviceList.saveIfDirty(); }).then(() => { aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; @@ -286,7 +286,7 @@ describe("DeviceList management:", function() { }, ); await aliceTestClient.httpBackend.flush('/keys/query', 1); - await aliceTestClient.client.crypto._deviceList.saveIfDirty(); + await aliceTestClient.client.crypto.deviceList.saveIfDirty(); aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; @@ -322,7 +322,7 @@ describe("DeviceList management:", function() { ); await aliceTestClient.flushSync(); - await aliceTestClient.client.crypto._deviceList.saveIfDirty(); + await aliceTestClient.client.crypto.deviceList.saveIfDirty(); aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; @@ -358,7 +358,7 @@ describe("DeviceList management:", function() { ); await aliceTestClient.flushSync(); - await aliceTestClient.client.crypto._deviceList.saveIfDirty(); + await aliceTestClient.client.crypto.deviceList.saveIfDirty(); aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; @@ -379,7 +379,7 @@ describe("DeviceList management:", function() { anotherTestClient.httpBackend.when('GET', '/sync').respond( 200, getSyncResponse([])); await anotherTestClient.flushSync(); - await anotherTestClient.client.crypto._deviceList.saveIfDirty(); + await anotherTestClient.client.crypto.deviceList.saveIfDirty(); anotherTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; diff --git a/spec/integ/matrix-client-crypto.spec.js b/spec/integ/matrix-client-crypto.spec.js index 6bb1a494bc8..eb87c5193f4 100644 --- a/spec/integ/matrix-client-crypto.spec.js +++ b/spec/integ/matrix-client-crypto.spec.js @@ -159,7 +159,7 @@ function aliDownloadsKeys() { // check that the localStorage is updated as we expect (not sure this is // an integration test, but meh) return Promise.all([p1, p2]).then(() => { - return aliTestClient.client.crypto._deviceList.saveIfDirty(); + return aliTestClient.client.crypto.deviceList.saveIfDirty(); }).then(() => { aliTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const devices = data.devices[bobUserId]; diff --git a/spec/integ/matrix-client-methods.spec.js b/spec/integ/matrix-client-methods.spec.js index b133b456e7d..acf353970c9 100644 --- a/spec/integ/matrix-client-methods.spec.js +++ b/spec/integ/matrix-client-methods.spec.js @@ -336,7 +336,7 @@ describe("MatrixClient", function() { var b = JSON.parse(JSON.stringify(o)); delete(b.signatures); delete(b.unsigned); - return client.crypto._olmDevice.sign(anotherjson.stringify(b)); + return client.crypto.olmDevice.sign(anotherjson.stringify(b)); }; logger.log("Ed25519: " + ed25519key); diff --git a/spec/integ/megolm-integ.spec.js b/spec/integ/megolm-integ.spec.js index d129590e11b..e2bc34c2503 100644 --- a/spec/integ/megolm-integ.spec.js +++ b/spec/integ/megolm-integ.spec.js @@ -1013,7 +1013,7 @@ describe("megolm", function() { event: true, }); event.senderCurve25519Key = testSenderKey; - return testClient.client.crypto._onRoomKeyEvent(event); + return testClient.client.crypto.onRoomKeyEvent(event); }).then(() => { const event = testUtils.mkEvent({ event: true, diff --git a/spec/unit/crypto.spec.js b/spec/unit/crypto.spec.js index bda03089a09..816b952b101 100644 --- a/spec/unit/crypto.spec.js +++ b/spec/unit/crypto.spec.js @@ -65,7 +65,7 @@ describe("Crypto", function() { 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI'; device.keys["ed25519:FLIBBLE"] = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; - client.crypto._deviceList.getDeviceByIdentityKey = () => device; + client.crypto.deviceList.getDeviceByIdentityKey = () => device; encryptionInfo = client.getEventEncryptionInfo(event); expect(encryptionInfo.encrypted).toBeTruthy(); @@ -213,7 +213,7 @@ describe("Crypto", function() { async function keyshareEventForEvent(event, index) { const eventContent = event.getWireContent(); - const key = await aliceClient.crypto._olmDevice + const key = await aliceClient.crypto.olmDevice .getInboundGroupSessionKey( roomId, eventContent.sender_key, eventContent.session_id, index, @@ -285,7 +285,7 @@ describe("Crypto", function() { } })); - const bobDecryptor = bobClient.crypto._getRoomDecryptor( + const bobDecryptor = bobClient.crypto.getRoomDecryptor( roomId, olmlib.MEGOLM_ALGORITHM, ); @@ -377,7 +377,7 @@ describe("Crypto", function() { // key requests get queued until the sync has finished, but we don't // let the client set up enough for that to happen, so gut-wrench a bit // to force it to send now. - aliceClient.crypto._outgoingRoomKeyRequestManager.sendQueuedRequests(); + aliceClient.crypto.outgoingRoomKeyRequestManager.sendQueuedRequests(); jest.runAllTimers(); await Promise.resolve(); expect(aliceClient.sendToDevice).toBeCalledTimes(1); diff --git a/spec/unit/crypto/algorithms/megolm.spec.js b/spec/unit/crypto/algorithms/megolm.spec.js index 8b56b93b36c..b3afc3e6c98 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.js +++ b/spec/unit/crypto/algorithms/megolm.spec.js @@ -365,9 +365,9 @@ describe("MegolmDecryption", function() { bobClient1.initCrypto(), bobClient2.initCrypto(), ]); - const aliceDevice = aliceClient.crypto._olmDevice; - const bobDevice1 = bobClient1.crypto._olmDevice; - const bobDevice2 = bobClient2.crypto._olmDevice; + const aliceDevice = aliceClient.crypto.olmDevice; + const bobDevice1 = bobClient1.crypto.olmDevice; + const bobDevice2 = bobClient2.crypto.olmDevice; const encryptionCfg = { "algorithm": "m.megolm.v1.aes-sha2", @@ -404,10 +404,10 @@ describe("MegolmDecryption", function() { }, }; - aliceClient.crypto._deviceList.storeDevicesForUser( + aliceClient.crypto.deviceList.storeDevicesForUser( "@bob:example.com", BOB_DEVICES, ); - aliceClient.crypto._deviceList.downloadKeys = async function(userIds) { + aliceClient.crypto.deviceList.downloadKeys = async function(userIds) { return this._getDevicesFromStore(userIds); }; @@ -468,8 +468,8 @@ describe("MegolmDecryption", function() { aliceClient.initCrypto(), bobClient.initCrypto(), ]); - const aliceDevice = aliceClient.crypto._olmDevice; - const bobDevice = bobClient.crypto._olmDevice; + const aliceDevice = aliceClient.crypto.olmDevice; + const bobDevice = bobClient.crypto.olmDevice; const encryptionCfg = { "algorithm": "m.megolm.v1.aes-sha2", @@ -508,10 +508,10 @@ describe("MegolmDecryption", function() { }, }; - aliceClient.crypto._deviceList.storeDevicesForUser( + aliceClient.crypto.deviceList.storeDevicesForUser( "@bob:example.com", BOB_DEVICES, ); - aliceClient.crypto._deviceList.downloadKeys = async function(userIds) { + aliceClient.crypto.deviceList.downloadKeys = async function(userIds) { return this._getDevicesFromStore(userIds); }; @@ -561,11 +561,11 @@ describe("MegolmDecryption", function() { aliceClient.initCrypto(), bobClient.initCrypto(), ]); - const bobDevice = bobClient.crypto._olmDevice; + const bobDevice = bobClient.crypto.olmDevice; const roomId = "!someroom"; - aliceClient.crypto._onToDeviceEvent(new MatrixEvent({ + aliceClient.crypto.onToDeviceEvent(new MatrixEvent({ type: "org.matrix.room_key.withheld", sender: "@bob:example.com", content: { @@ -605,13 +605,13 @@ describe("MegolmDecryption", function() { bobClient.initCrypto(), ]); aliceClient.crypto.downloadKeys = async () => {}; - const bobDevice = bobClient.crypto._olmDevice; + const bobDevice = bobClient.crypto.olmDevice; const roomId = "!someroom"; const now = Date.now(); - aliceClient.crypto._onToDeviceEvent(new MatrixEvent({ + aliceClient.crypto.onToDeviceEvent(new MatrixEvent({ type: "org.matrix.room_key.withheld", sender: "@bob:example.com", content: { @@ -655,7 +655,7 @@ describe("MegolmDecryption", function() { aliceClient.initCrypto(), bobClient.initCrypto(), ]); - const bobDevice = bobClient.crypto._olmDevice; + const bobDevice = bobClient.crypto.olmDevice; aliceClient.crypto.downloadKeys = async () => {}; const roomId = "!someroom"; @@ -663,7 +663,7 @@ describe("MegolmDecryption", function() { const now = Date.now(); // pretend we got an event that we can't decrypt - aliceClient.crypto._onToDeviceEvent(new MatrixEvent({ + aliceClient.crypto.onToDeviceEvent(new MatrixEvent({ type: "m.room.encrypted", sender: "@bob:example.com", content: { diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index 1bb8a39f8ec..df65475d40f 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -296,7 +296,7 @@ describe("MegolmBackup", function() { resolve(); return Promise.resolve({}); }; - client.crypto._backupManager.backupGroupSession( + client.crypto.backupManager.backupGroupSession( "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", groupSession.session_id(), ); @@ -478,7 +478,7 @@ describe("MegolmBackup", function() { ); } }; - client.crypto._backupManager.backupGroupSession( + client.crypto.backupManager.backupGroupSession( "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", groupSession.session_id(), ); diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index 56b86b26b7f..3118e636565 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -64,8 +64,8 @@ describe("Cross Signing", function() { ); alice.uploadDeviceSigningKeys = jest.fn(async (auth, keys) => { await olmlib.verifySignature( - alice.crypto._olmDevice, keys.master_key, "@alice:example.com", - "Osborne2", alice.crypto._olmDevice.deviceEd25519Key, + alice.crypto.olmDevice, keys.master_key, "@alice:example.com", + "Osborne2", alice.crypto.olmDevice.deviceEd25519Key, ); }); alice.uploadKeySignatures = async () => {}; @@ -138,7 +138,7 @@ describe("Cross Signing", function() { // set Alice's cross-signing key await resetCrossSigningKeys(alice); // Alice downloads Bob's device key - alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { master: { user_id: "@bob:example.com", @@ -203,12 +203,12 @@ describe("Cross Signing", function() { alice.uploadKeySignatures = jest.fn(async (content) => { try { await olmlib.verifySignature( - alice.crypto._olmDevice, + alice.crypto.olmDevice, content["@alice:example.com"][ "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk" ], "@alice:example.com", - "Osborne2", alice.crypto._olmDevice.deviceEd25519Key, + "Osborne2", alice.crypto.olmDevice.deviceEd25519Key, ); olmlib.pkVerify( content["@alice:example.com"]["Osborne2"], @@ -222,7 +222,7 @@ describe("Cross Signing", function() { }); }); - const deviceInfo = alice.crypto._deviceList._devices["@alice:example.com"] + const deviceInfo = alice.crypto.deviceList._devices["@alice:example.com"] .Osborne2; const aliceDevice = { user_id: "@alice:example.com", @@ -230,7 +230,7 @@ describe("Cross Signing", function() { }; aliceDevice.keys = deviceInfo.keys; aliceDevice.algorithms = deviceInfo.algorithms; - await alice.crypto._signObject(aliceDevice); + await alice.crypto.signObject(aliceDevice); olmlib.pkSign(aliceDevice, selfSigningKey, "@alice:example.com"); // feed sync result that includes master key, ssk, device key @@ -358,7 +358,7 @@ describe("Cross Signing", function() { ["ed25519:" + bobMasterPubkey]: sskSig, }, }; - alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { master: { user_id: "@bob:example.com", @@ -387,7 +387,7 @@ describe("Cross Signing", function() { ["ed25519:" + bobPubkey]: sig, }, }; - alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", { + alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", { Dynabook: bobDevice, }); // Bob's device key should be TOFU @@ -421,8 +421,8 @@ describe("Cross Signing", function() { null, aliceKeys, ); - alice.crypto._deviceList.startTrackingDeviceList("@bob:example.com"); - alice.crypto._deviceList.stopTrackingAllDeviceLists = () => {}; + alice.crypto.deviceList.startTrackingDeviceList("@bob:example.com"); + alice.crypto.deviceList.stopTrackingAllDeviceLists = () => {}; alice.uploadDeviceSigningKeys = async () => {}; alice.uploadKeySignatures = async () => {}; @@ -437,14 +437,14 @@ describe("Cross Signing", function() { ]); const keyChangePromise = new Promise((resolve, reject) => { - alice.crypto._deviceList.once("userCrossSigningUpdated", (userId) => { + alice.crypto.deviceList.once("userCrossSigningUpdated", (userId) => { if (userId === "@bob:example.com") { resolve(); } }); }); - const deviceInfo = alice.crypto._deviceList._devices["@alice:example.com"] + const deviceInfo = alice.crypto.deviceList._devices["@alice:example.com"] .Osborne2; const aliceDevice = { user_id: "@alice:example.com", @@ -452,7 +452,7 @@ describe("Cross Signing", function() { }; aliceDevice.keys = deviceInfo.keys; aliceDevice.algorithms = deviceInfo.algorithms; - await alice.crypto._signObject(aliceDevice); + await alice.crypto.signObject(aliceDevice); const bobOlmAccount = new global.Olm.Account(); bobOlmAccount.create(); @@ -606,7 +606,7 @@ describe("Cross Signing", function() { ["ed25519:" + bobMasterPubkey]: sskSig, }, }; - alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { master: { user_id: "@bob:example.com", @@ -629,7 +629,7 @@ describe("Cross Signing", function() { "ed25519:Dynabook": "someOtherPubkey", }, }; - alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", { + alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", { Dynabook: bobDevice, }); // Bob's device key should be untrusted @@ -673,7 +673,7 @@ describe("Cross Signing", function() { ["ed25519:" + bobMasterPubkey]: sskSig, }, }; - alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { master: { user_id: "@bob:example.com", @@ -701,7 +701,7 @@ describe("Cross Signing", function() { bobDevice.signatures = {}; bobDevice.signatures["@bob:example.com"] = {}; bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey] = sig; - alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", { + alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", { Dynabook: bobDevice, }); // Alice verifies Bob's SSK @@ -733,7 +733,7 @@ describe("Cross Signing", function() { ["ed25519:" + bobMasterPubkey2]: sskSig2, }, }; - alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { master: { user_id: "@bob:example.com", @@ -770,7 +770,7 @@ describe("Cross Signing", function() { // Alice gets new signature for device const sig2 = bobSigning2.sign(bobDeviceString); bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2; - alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", { + alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", { Dynabook: bobDevice, }); @@ -805,20 +805,20 @@ describe("Cross Signing", function() { bob.uploadKeySignatures = async () => {}; // set Bob's cross-signing key await resetCrossSigningKeys(bob); - alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", { + alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", { Dynabook: { algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], keys: { - "curve25519:Dynabook": bob.crypto._olmDevice.deviceCurve25519Key, - "ed25519:Dynabook": bob.crypto._olmDevice.deviceEd25519Key, + "curve25519:Dynabook": bob.crypto.olmDevice.deviceCurve25519Key, + "ed25519:Dynabook": bob.crypto.olmDevice.deviceEd25519Key, }, verified: 1, known: true, }, }); - alice.crypto._deviceList.storeCrossSigningForUser( + alice.crypto.deviceList.storeCrossSigningForUser( "@bob:example.com", - bob.crypto._crossSigningInfo.toStorage(), + bob.crypto.crossSigningInfo.toStorage(), ); alice.uploadDeviceSigningKeys = async () => {}; @@ -838,7 +838,7 @@ describe("Cross Signing", function() { expect(bobTrust.isTofu()).toBeTruthy(); // "forget" that Bob is trusted - delete alice.crypto._deviceList._crossSigningInfo["@bob:example.com"] + delete alice.crypto.deviceList.crossSigningInfo["@bob:example.com"] .keys.master.signatures["@alice:example.com"]; const bobTrust2 = alice.checkUserTrust("@bob:example.com"); @@ -848,7 +848,7 @@ describe("Cross Signing", function() { upgradePromise = new Promise((resolve) => { upgradeResolveFunc = resolve; }); - alice.crypto._deviceList.emit("userCrossSigningUpdated", "@bob:example.com"); + alice.crypto.deviceList.emit("userCrossSigningUpdated", "@bob:example.com"); await new Promise((resolve) => { alice.crypto.on("userTrustStatusChanged", resolve); }); diff --git a/spec/unit/crypto/crypto-utils.js b/spec/unit/crypto/crypto-utils.js index dcc9db16a75..dbf6cec6546 100644 --- a/spec/unit/crypto/crypto-utils.js +++ b/spec/unit/crypto/crypto-utils.js @@ -8,22 +8,22 @@ export async function resetCrossSigningKeys(client, { } = {}) { const crypto = client.crypto; - const oldKeys = Object.assign({}, crypto._crossSigningInfo.keys); + const oldKeys = Object.assign({}, crypto.crossSigningInfo.keys); try { - await crypto._crossSigningInfo.resetKeys(level); - await crypto._signObject(crypto._crossSigningInfo.keys.master); + await crypto.crossSigningInfo.resetKeys(level); + await crypto._signObject(crypto.crossSigningInfo.keys.master); // write a copy locally so we know these are trusted keys await crypto._cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { crypto._cryptoStore.storeCrossSigningKeys( - txn, crypto._crossSigningInfo.keys); + txn, crypto.crossSigningInfo.keys); }, ); } catch (e) { // If anything failed here, revert the keys so we know to try again from the start // next time. - crypto._crossSigningInfo.keys = oldKeys; + crypto.crossSigningInfo.keys = oldKeys; throw e; } crypto._baseApis.emit("crossSigning.keysChanged", {}); diff --git a/spec/unit/crypto/secrets.spec.js b/spec/unit/crypto/secrets.spec.js index fc82a3259bd..2a86dfaa1fc 100644 --- a/spec/unit/crypto/secrets.spec.js +++ b/spec/unit/crypto/secrets.spec.js @@ -99,11 +99,11 @@ describe("Secrets", function() { }, }, ); - alice.crypto._crossSigningInfo.setKeys({ + alice.crypto.crossSigningInfo.setKeys({ master: signingkeyInfo, }); - const secretStorage = alice.crypto._secretStorage; + const secretStorage = alice.crypto.secretStorage; alice.setAccountData = async function(eventType, contents, callback) { alice.store.storeAccountDataEvents([ @@ -120,7 +120,7 @@ describe("Secrets", function() { const keyAccountData = { algorithm: SECRET_STORAGE_ALGORITHM_V1_AES, }; - await alice.crypto._crossSigningInfo.signObject(keyAccountData, 'master'); + await alice.crypto.crossSigningInfo.signObject(keyAccountData, 'master'); alice.store.storeAccountDataEvents([ new MatrixEvent({ @@ -234,11 +234,11 @@ describe("Secrets", function() { }, ); - const vaxDevice = vax.client.crypto._olmDevice; - const osborne2Device = osborne2.client.crypto._olmDevice; - const secretStorage = osborne2.client.crypto._secretStorage; + const vaxDevice = vax.client.crypto.olmDevice; + const osborne2Device = osborne2.client.crypto.olmDevice; + const secretStorage = osborne2.client.crypto.secretStorage; - osborne2.client.crypto._deviceList.storeDevicesForUser("@alice:example.com", { + osborne2.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", { "VAX": { user_id: "@alice:example.com", device_id: "VAX", @@ -249,7 +249,7 @@ describe("Secrets", function() { }, }, }); - vax.client.crypto._deviceList.storeDevicesForUser("@alice:example.com", { + vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", { "Osborne2": { user_id: "@alice:example.com", device_id: "Osborne2", @@ -265,7 +265,7 @@ describe("Secrets", function() { const otks = (await osborne2Device.getOneTimeKeys()).curve25519; await osborne2Device.markKeysAsPublished(); - await vax.client.crypto._olmDevice.createOutboundSession( + await vax.client.crypto.olmDevice.createOutboundSession( osborne2Device.deviceCurve25519Key, Object.values(otks)[0], ); @@ -334,8 +334,8 @@ describe("Secrets", function() { createSecretStorageKey, }); - const crossSigning = bob.crypto._crossSigningInfo; - const secretStorage = bob.crypto._secretStorage; + const crossSigning = bob.crypto.crossSigningInfo; + const secretStorage = bob.crypto.secretStorage; expect(crossSigning.getId()).toBeTruthy(); expect(await crossSigning.isStoredInSecretStorage(secretStorage)) @@ -376,10 +376,10 @@ describe("Secrets", function() { ]); this.emit("accountData", event); }; - bob.crypto._backupManager.checkKeyBackup = async () => {}; + bob.crypto.backupManager.checkKeyBackup = async () => {}; - const crossSigning = bob.crypto._crossSigningInfo; - const secretStorage = bob.crypto._secretStorage; + const crossSigning = bob.crypto.crossSigningInfo; + const secretStorage = bob.crypto.secretStorage; // Set up cross-signing keys from scratch with specific storage key await bob.bootstrapCrossSigning({ @@ -394,7 +394,7 @@ describe("Secrets", function() { }); // Clear local cross-signing keys and read from secret storage - bob.crypto._deviceList.storeCrossSigningForUser( + bob.crypto.deviceList.storeCrossSigningForUser( "@bob:example.com", crossSigning.toStorage(), ); @@ -479,7 +479,7 @@ describe("Secrets", function() { }, }), ]); - alice.crypto._deviceList.storeCrossSigningForUser("@alice:example.com", { + alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", { keys: { master: { user_id: "@alice:example.com", @@ -619,7 +619,7 @@ describe("Secrets", function() { }, }), ]); - alice.crypto._deviceList.storeCrossSigningForUser("@alice:example.com", { + alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", { keys: { master: { user_id: "@alice:example.com", diff --git a/spec/unit/crypto/verification/request.spec.js b/spec/unit/crypto/verification/request.spec.js index 11275e6fcdc..3ad4fe5621a 100644 --- a/spec/unit/crypto/verification/request.spec.js +++ b/spec/unit/crypto/verification/request.spec.js @@ -49,7 +49,7 @@ describe("verification request integration tests with crypto layer", function() verificationMethods: [verificationMethods.SAS], }, ); - alice.client.crypto._deviceList.getRawStoredDevicesForUser = function() { + alice.client.crypto.deviceList.getRawStoredDevicesForUser = function() { return { Dynabook: { keys: { diff --git a/spec/unit/crypto/verification/sas.spec.js b/spec/unit/crypto/verification/sas.spec.js index 0a643d318ae..fcb73de29ba 100644 --- a/spec/unit/crypto/verification/sas.spec.js +++ b/spec/unit/crypto/verification/sas.spec.js @@ -87,8 +87,8 @@ describe("SAS verification", function() { }, ); - const aliceDevice = alice.client.crypto._olmDevice; - const bobDevice = bob.client.crypto._olmDevice; + const aliceDevice = alice.client.crypto.olmDevice; + const bobDevice = bob.client.crypto.olmDevice; ALICE_DEVICES = { Osborne2: { @@ -114,14 +114,14 @@ describe("SAS verification", function() { }, }; - alice.client.crypto._deviceList.storeDevicesForUser( + alice.client.crypto.deviceList.storeDevicesForUser( "@bob:example.com", BOB_DEVICES, ); alice.client.downloadKeys = () => { return Promise.resolve(); }; - bob.client.crypto._deviceList.storeDevicesForUser( + bob.client.crypto.deviceList.storeDevicesForUser( "@alice:example.com", ALICE_DEVICES, ); bob.client.downloadKeys = () => { @@ -296,9 +296,9 @@ describe("SAS verification", function() { await resetCrossSigningKeys(bob.client); - bob.client.crypto._deviceList.storeCrossSigningForUser( + bob.client.crypto.deviceList.storeCrossSigningForUser( "@alice:example.com", { - keys: alice.client.crypto._crossSigningInfo.keys, + keys: alice.client.crypto.crossSigningInfo.keys, }, ); diff --git a/src/client.ts b/src/client.ts index d6b4aecdaee..91beeef3943 100644 --- a/src/client.ts +++ b/src/client.ts @@ -48,7 +48,7 @@ import { retryNetworkOperation, } from "./http-api"; import { Crypto, fixBackupKey, IBootstrapCrossSigningOpts, isCryptoAvailable } from './crypto'; -import { DeviceInfo } from "./crypto/deviceinfo"; +import { DeviceInfo, IDevice } from "./crypto/deviceinfo"; import { decodeRecoveryKey } from './crypto/recoverykey'; import { keyFromAuthData } from './crypto/key_passphrase'; import { User } from "./models/user"; @@ -64,7 +64,7 @@ import { import { IIdentityServerProvider } from "./@types/IIdentityServerProvider"; import type Request from "request"; import { MatrixScheduler } from "./scheduler"; -import { ICryptoCallbacks, IDeviceTrustLevel, ISecretStorageKeyInfo, NotificationCountType } from "./matrix"; +import { ICryptoCallbacks, ISecretStorageKeyInfo, NotificationCountType } from "./matrix"; import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store"; import { LocalStorageCryptoStore } from "./crypto/store/localStorage-crypto-store"; import { IndexedDBCryptoStore } from "./crypto/store/indexeddb-crypto-store"; @@ -85,7 +85,7 @@ import { IRecoveryKey, ISecretStorageKey, } from "./crypto/api"; -import { CrossSigningInfo, UserTrustLevel } from "./crypto/CrossSigning"; +import { CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from "./crypto/CrossSigning"; import { Room } from "./models/room"; import { ICreateRoomOpts, @@ -1265,7 +1265,7 @@ export class MatrixClient extends EventEmitter { public downloadKeys( userIds: string[], forceDownload?: boolean, - ): Promise>> { + ): Promise>> { if (!this.crypto) { return Promise.reject(new Error("End-to-end encryption disabled")); } @@ -1571,9 +1571,9 @@ export class MatrixClient extends EventEmitter { * @param {string} userId The ID of the user whose devices is to be checked. * @param {string} deviceId The ID of the device to check * - * @returns {IDeviceTrustLevel} + * @returns {DeviceTrustLevel} */ - public checkDeviceTrust(userId: string, deviceId: string): IDeviceTrustLevel { + public checkDeviceTrust(userId: string, deviceId: string): DeviceTrustLevel { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -1948,7 +1948,7 @@ export class MatrixClient extends EventEmitter { * * @return {Promise} */ - public getEventSenderDeviceInfo(event: MatrixEvent): Promise { + public async getEventSenderDeviceInfo(event: MatrixEvent): Promise { if (!this.crypto) { return null; } @@ -2488,15 +2488,13 @@ export class MatrixClient extends EventEmitter { targetRoomId: string, targetSessionId: string, backupInfo: IKeyBackupVersion, - opts: IKeyBackupRestoreOpts, + opts?: IKeyBackupRestoreOpts, ): Promise { const privKey = await this.crypto.getSessionBackupPrivateKey(); if (!privKey) { throw new Error("Couldn't get key"); } - return this.restoreKeyBackup( - privKey, targetRoomId, targetSessionId, backupInfo, opts, - ); + return this.restoreKeyBackup(privKey, targetRoomId, targetSessionId, backupInfo, opts); } private async restoreKeyBackup( diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.ts similarity index 75% rename from src/crypto/CrossSigning.js rename to src/crypto/CrossSigning.ts index e2af6ce3b5b..2d983c2e51b 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.ts @@ -1,6 +1,5 @@ /* -Copyright 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. @@ -20,22 +19,43 @@ limitations under the License. * @module crypto/CrossSigning */ -import { decodeBase64, encodeBase64, pkSign, pkVerify } from './olmlib'; import { EventEmitter } from 'events'; + +import { decodeBase64, encodeBase64, pkSign, pkVerify } from './olmlib'; import { logger } from '../logger'; import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store'; import { decryptAES, encryptAES } from './aes'; +import { PkSigning } from "@matrix-org/olm"; +import { DeviceInfo } from "./deviceinfo"; +import { SecretStorage } from "./SecretStorage"; +import { CryptoStore, MatrixClient } from "../client"; +import { OlmDevice } from "./OlmDevice"; +import { ICryptoCallbacks } from "../matrix"; const KEY_REQUEST_TIMEOUT_MS = 1000 * 60; -function publicKeyFromKeyInfo(keyInfo) { +function publicKeyFromKeyInfo(keyInfo: any): any { // TODO types // `keys` is an object with { [`ed25519:${pubKey}`]: pubKey } // We assume only a single key, and we want the bare form without type // prefix, so we select the values. return Object.values(keyInfo.keys)[0]; } +interface ICacheCallbacks { + getCrossSigningKeyCache?(type: string, expectedPublicKey?: string): Promise; + storeCrossSigningKeyCache?(type: string, key: Uint8Array): Promise; +} + export class CrossSigningInfo extends EventEmitter { + public keys: Record = {}; // TODO types + public firstUse = true; + // This tracks whether we've ever verified this user with any identity. + // When you verify a user, any devices online at the time that receive + // the verifying signature via the homeserver will latch this to true + // and can use it in the future to detect cases where the user has + // become unverified later for any reason. + private crossSigningVerifiedBefore = false; + /** * Information about a user's cross-signing keys * @@ -46,27 +66,15 @@ export class CrossSigningInfo extends EventEmitter { * Requires getCrossSigningKey and saveCrossSigningKeys * @param {object} cacheCallbacks Callbacks used to interact with the cache */ - constructor(userId, callbacks, cacheCallbacks) { + constructor( + public readonly userId: string, + private callbacks: ICryptoCallbacks = {}, + private cacheCallbacks: ICacheCallbacks = {}, + ) { super(); - - // you can't change the userId - Object.defineProperty(this, 'userId', { - enumerable: true, - value: userId, - }); - this._callbacks = callbacks || {}; - this._cacheCallbacks = cacheCallbacks || {}; - this.keys = {}; - this.firstUse = true; - // This tracks whether we've ever verified this user with any identity. - // When you verify a user, any devices online at the time that receive - // the verifying signature via the homeserver will latch this to true - // and can use it in the future to detect cases where the user has - // become unverifed later for any reason. - this.crossSigningVerifiedBefore = false; } - static fromStorage(obj, userId) { + public static fromStorage(obj: object, userId: string): CrossSigningInfo { const res = new CrossSigningInfo(userId); for (const prop in obj) { if (obj.hasOwnProperty(prop)) { @@ -76,7 +84,7 @@ export class CrossSigningInfo extends EventEmitter { return res; } - toStorage() { + public toStorage(): object { return { keys: this.keys, firstUse: this.firstUse, @@ -92,10 +100,10 @@ export class CrossSigningInfo extends EventEmitter { * the stored public key for the given key type. * @returns {Array} An array with [ public key, Olm.PkSigning ] */ - async getCrossSigningKey(type, expectedPubkey) { + public async getCrossSigningKey(type: string, expectedPubkey?: string): Promise<[string, PkSigning]> { const shouldCache = ["master", "self_signing", "user_signing"].indexOf(type) >= 0; - if (!this._callbacks.getCrossSigningKey) { + if (!this.callbacks.getCrossSigningKey) { throw new Error("No getCrossSigningKey callback supplied"); } @@ -103,7 +111,7 @@ export class CrossSigningInfo extends EventEmitter { expectedPubkey = this.getId(type); } - function validateKey(key) { + function validateKey(key: Uint8Array): [string, PkSigning] { if (!key) return; const signing = new global.Olm.PkSigning(); const gotPubkey = signing.init_with_seed(key); @@ -114,9 +122,8 @@ export class CrossSigningInfo extends EventEmitter { } let privkey; - if (this._cacheCallbacks.getCrossSigningKeyCache && shouldCache) { - privkey = await this._cacheCallbacks - .getCrossSigningKeyCache(type, expectedPubkey); + if (this.cacheCallbacks.getCrossSigningKeyCache && shouldCache) { + privkey = await this.cacheCallbacks.getCrossSigningKeyCache(type, expectedPubkey); } const cacheresult = validateKey(privkey); @@ -124,11 +131,11 @@ export class CrossSigningInfo extends EventEmitter { return cacheresult; } - privkey = await this._callbacks.getCrossSigningKey(type, expectedPubkey); + privkey = await this.callbacks.getCrossSigningKey(type, expectedPubkey); const result = validateKey(privkey); if (result) { - if (this._cacheCallbacks.storeCrossSigningKeyCache && shouldCache) { - await this._cacheCallbacks.storeCrossSigningKeyCache(type, privkey); + if (this.cacheCallbacks.storeCrossSigningKeyCache && shouldCache) { + await this.cacheCallbacks.storeCrossSigningKeyCache(type, privkey); } return result; } @@ -156,10 +163,9 @@ export class CrossSigningInfo extends EventEmitter { * with, or null if it is not present or not encrypted with a trusted * key */ - async isStoredInSecretStorage(secretStorage) { + public async isStoredInSecretStorage(secretStorage: SecretStorage): Promise> { // check what SSSS keys have encrypted the master key (if any) - const stored = - await secretStorage.isStored("m.cross_signing.master", false) || {}; + const stored = await secretStorage.isStored("m.cross_signing.master", false) || {}; // then check which of those SSSS keys have also encrypted the SSK and USK function intersect(s) { for (const k of Object.keys(stored)) { @@ -169,9 +175,7 @@ export class CrossSigningInfo extends EventEmitter { } } for (const type of ["self_signing", "user_signing"]) { - intersect( - await secretStorage.isStored(`m.cross_signing.${type}`, false) || {}, - ); + intersect(await secretStorage.isStored(`m.cross_signing.${type}`, false) || {}); } return Object.keys(stored).length ? stored : null; } @@ -184,7 +188,10 @@ export class CrossSigningInfo extends EventEmitter { * @param {Map} keys The keys to store * @param {SecretStorage} secretStorage The secret store using account data */ - static async storeInSecretStorage(keys, secretStorage) { + public static async storeInSecretStorage( + keys: Map, + secretStorage: SecretStorage, + ): Promise { for (const [type, privateKey] of keys) { const encodedKey = encodeBase64(privateKey); await secretStorage.store(`m.cross_signing.${type}`, encodedKey); @@ -200,7 +207,7 @@ export class CrossSigningInfo extends EventEmitter { * @param {SecretStorage} secretStorage The secret store using account data * @return {Uint8Array} The private key */ - static async getFromSecretStorage(type, secretStorage) { + public static async getFromSecretStorage(type: string, secretStorage: SecretStorage): Promise { const encodedKey = await secretStorage.get(`m.cross_signing.${type}`); if (!encodedKey) { return null; @@ -215,8 +222,8 @@ export class CrossSigningInfo extends EventEmitter { * "self_signing", or "user_signing". Optional, will check all by default. * @returns {boolean} True if all keys are stored in the local cache. */ - async isStoredInKeyCache(type) { - const cacheCallbacks = this._cacheCallbacks; + public async isStoredInKeyCache(type?: string): Promise { + const cacheCallbacks = this.cacheCallbacks; if (!cacheCallbacks) return false; const types = type ? [type] : ["master", "self_signing", "user_signing"]; for (const t of types) { @@ -232,9 +239,9 @@ export class CrossSigningInfo extends EventEmitter { * * @returns {Map} A map from key type (string) to private key (Uint8Array) */ - async getCrossSigningKeysFromCache() { + public async getCrossSigningKeysFromCache(): Promise> { const keys = new Map(); - const cacheCallbacks = this._cacheCallbacks; + const cacheCallbacks = this.cacheCallbacks; if (!cacheCallbacks) return keys; for (const type of ["master", "self_signing", "user_signing"]) { const privKey = await cacheCallbacks.getCrossSigningKeyCache(type); @@ -255,8 +262,7 @@ export class CrossSigningInfo extends EventEmitter { * * @return {string} the ID */ - getId(type) { - type = type || "master"; + public getId(type = "master"): string { if (!this.keys[type]) return null; const keyInfo = this.keys[type]; return publicKeyFromKeyInfo(keyInfo); @@ -269,8 +275,8 @@ export class CrossSigningInfo extends EventEmitter { * * @param {CrossSigningLevel} level The key types to reset */ - async resetKeys(level) { - if (!this._callbacks.saveCrossSigningKeys) { + public async resetKeys(level?: CrossSigningLevel): Promise { + if (!this.callbacks.saveCrossSigningKeys) { throw new Error("No saveCrossSigningKeys callback supplied"); } @@ -289,8 +295,8 @@ export class CrossSigningInfo extends EventEmitter { return; } - const privateKeys = {}; - const keys = {}; + const privateKeys: Record = {}; + const keys: Record = {}; let masterSigning; let masterPub; @@ -347,7 +353,7 @@ export class CrossSigningInfo extends EventEmitter { } Object.assign(this.keys, keys); - this._callbacks.saveCrossSigningKeys(privateKeys); + this.callbacks.saveCrossSigningKeys(privateKeys); } finally { if (masterSigning) { masterSigning.free(); @@ -358,12 +364,12 @@ export class CrossSigningInfo extends EventEmitter { /** * unsets the keys, used when another session has reset the keys, to disable cross-signing */ - clearKeys() { + public clearKeys(): void { this.keys = {}; } - setKeys(keys) { - const signingKeys = {}; + public setKeys(keys: Record): void { + const signingKeys: Record = {}; if (keys.master) { if (keys.master.user_id !== this.userId) { const error = "Mismatched user ID " + keys.master.user_id + @@ -434,7 +440,7 @@ export class CrossSigningInfo extends EventEmitter { } } - updateCrossSigningVerifiedBefore(isCrossSigningVerified) { + public updateCrossSigningVerifiedBefore(isCrossSigningVerified: boolean): void { // It is critical that this value latches forward from false to true but // never back to false to avoid a downgrade attack. if (!this.crossSigningVerifiedBefore && isCrossSigningVerified) { @@ -442,7 +448,7 @@ export class CrossSigningInfo extends EventEmitter { } } - async signObject(data, type) { + public async signObject(data: T, type: string): Promise { if (!this.keys[type]) { throw new Error( "Attempted to sign with " + type + " key but no such key present", @@ -457,7 +463,7 @@ export class CrossSigningInfo extends EventEmitter { } } - async signUser(key) { + public async signUser(key: CrossSigningInfo): Promise { if (!this.keys.user_signing) { logger.info("No user signing key: not signing user"); return; @@ -465,7 +471,7 @@ export class CrossSigningInfo extends EventEmitter { return this.signObject(key.keys.master, "user_signing"); } - async signDevice(userId, device) { + public async signDevice(userId: string, device: DeviceInfo): Promise { if (userId !== this.userId) { throw new Error( `Trying to sign ${userId}'s device; can only sign our own device`, @@ -492,7 +498,7 @@ export class CrossSigningInfo extends EventEmitter { * * @returns {UserTrustLevel} */ - checkUserTrust(userCrossSigning) { + public checkUserTrust(userCrossSigning: CrossSigningInfo): UserTrustLevel { // if we're checking our own key, then it's trusted if the master key // and self-signing key match if (this.userId === userCrossSigning.userId @@ -530,12 +536,17 @@ export class CrossSigningInfo extends EventEmitter { * * @param {CrossSigningInfo} userCrossSigning Cross signing info for user * @param {module:crypto/deviceinfo} device The device to check - * @param {bool} localTrust Whether the device is trusted locally - * @param {bool} trustCrossSignedDevices Whether we trust cross signed devices + * @param {boolean} localTrust Whether the device is trusted locally + * @param {boolean} trustCrossSignedDevices Whether we trust cross signed devices * * @returns {DeviceTrustLevel} */ - checkDeviceTrust(userCrossSigning, device, localTrust, trustCrossSignedDevices) { + public checkDeviceTrust( + userCrossSigning: CrossSigningInfo, + device: DeviceInfo, + localTrust: boolean, + trustCrossSignedDevices: boolean, + ): DeviceTrustLevel { const userTrust = this.checkUserTrust(userCrossSigning); const userSSK = userCrossSigning.keys.self_signing; @@ -552,29 +563,23 @@ export class CrossSigningInfo extends EventEmitter { // if we can verify the user's SSK from their master key... pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId); // ...and this device's key from their SSK... - pkVerify( - deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId, - ); + pkVerify(deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId); // ...then we trust this device as much as far as we trust the user - return DeviceTrustLevel.fromUserTrustLevel( - userTrust, localTrust, trustCrossSignedDevices, - ); + return DeviceTrustLevel.fromUserTrustLevel(userTrust, localTrust, trustCrossSignedDevices); } catch (e) { - return new DeviceTrustLevel( - false, false, localTrust, trustCrossSignedDevices, - ); + return new DeviceTrustLevel(false, false, localTrust, trustCrossSignedDevices); } } /** * @returns {object} Cache callbacks */ - getCacheCallbacks() { - return this._cacheCallbacks; + public getCacheCallbacks(): ICacheCallbacks { + return this.cacheCallbacks; } } -function deviceToObject(device, userId) { +function deviceToObject(device: DeviceInfo, userId: string) { return { algorithms: device.algorithms, keys: device.keys, @@ -584,49 +589,49 @@ function deviceToObject(device, userId) { }; } -export const CrossSigningLevel = { - MASTER: 4, - USER_SIGNING: 2, - SELF_SIGNING: 1, -}; +export enum CrossSigningLevel { + MASTER = 4, + USER_SIGNING = 2, + SELF_SIGNING = 1, +} /** * Represents the ways in which we trust a user */ export class UserTrustLevel { - constructor(crossSigningVerified, crossSigningVerifiedBefore, tofu) { - this._crossSigningVerified = crossSigningVerified; - this._crossSigningVerifiedBefore = crossSigningVerifiedBefore; - this._tofu = tofu; - } + constructor( + private readonly crossSigningVerified: boolean, + private readonly crossSigningVerifiedBefore: boolean, + private readonly tofu: boolean, + ) {} /** - * @returns {bool} true if this user is verified via any means + * @returns {boolean} true if this user is verified via any means */ - isVerified() { + public isVerified(): boolean { return this.isCrossSigningVerified(); } /** - * @returns {bool} true if this user is verified via cross signing + * @returns {boolean} true if this user is verified via cross signing */ - isCrossSigningVerified() { - return this._crossSigningVerified; + public isCrossSigningVerified(): boolean { + return this.crossSigningVerified; } /** - * @returns {bool} true if we ever verified this user before (at least for + * @returns {boolean} true if we ever verified this user before (at least for * the history of verifications observed by this device). */ - wasCrossSigningVerified() { - return this._crossSigningVerifiedBefore; + public wasCrossSigningVerified(): boolean { + return this.crossSigningVerifiedBefore; } /** - * @returns {bool} true if this user's key is trusted on first use + * @returns {boolean} true if this user's key is trusted on first use */ - isTofu() { - return this._tofu; + public isTofu(): boolean { + return this.tofu; } } @@ -634,58 +639,62 @@ export class UserTrustLevel { * Represents the ways in which we trust a device */ export class DeviceTrustLevel { - constructor(crossSigningVerified, tofu, localVerified, trustCrossSignedDevices) { - this._crossSigningVerified = crossSigningVerified; - this._tofu = tofu; - this._localVerified = localVerified; - this._trustCrossSignedDevices = trustCrossSignedDevices; - } - - static fromUserTrustLevel(userTrustLevel, localVerified, trustCrossSignedDevices) { + constructor( + public readonly crossSigningVerified: boolean, + public readonly tofu: boolean, + private readonly localVerified: boolean, + private readonly trustCrossSignedDevices: boolean, + ) {} + + public static fromUserTrustLevel( + userTrustLevel: UserTrustLevel, + localVerified: boolean, + trustCrossSignedDevices: boolean, + ): DeviceTrustLevel { return new DeviceTrustLevel( - userTrustLevel._crossSigningVerified, - userTrustLevel._tofu, + userTrustLevel.isCrossSigningVerified(), + userTrustLevel.isTofu(), localVerified, trustCrossSignedDevices, ); } /** - * @returns {bool} true if this device is verified via any means + * @returns {boolean} true if this device is verified via any means */ - isVerified() { + public isVerified(): boolean { return Boolean(this.isLocallyVerified() || ( - this._trustCrossSignedDevices && this.isCrossSigningVerified() + this.trustCrossSignedDevices && this.isCrossSigningVerified() )); } /** - * @returns {bool} true if this device is verified via cross signing + * @returns {boolean} true if this device is verified via cross signing */ - isCrossSigningVerified() { - return this._crossSigningVerified; + public isCrossSigningVerified(): boolean { + return this.crossSigningVerified; } /** - * @returns {bool} true if this device is verified locally + * @returns {boolean} true if this device is verified locally */ - isLocallyVerified() { - return this._localVerified; + public isLocallyVerified(): boolean { + return this.localVerified; } /** - * @returns {bool} true if this device is trusted from a user's key + * @returns {boolean} true if this device is trusted from a user's key * that is trusted on first use */ - isTofu() { - return this._tofu; + public isTofu(): boolean { + return this.tofu; } } -export function createCryptoStoreCacheCallbacks(store, olmdevice) { +export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: OlmDevice): ICacheCallbacks { return { - getCrossSigningKeyCache: async function(type, _expectedPublicKey) { - const key = await new Promise((resolve) => { + getCrossSigningKeyCache: async function(type: string, _expectedPublicKey: string): Promise { + const key = await new Promise((resolve) => { return store.doTxn( 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], @@ -696,20 +705,20 @@ export function createCryptoStoreCacheCallbacks(store, olmdevice) { }); if (key && key.ciphertext) { - const pickleKey = Buffer.from(olmdevice._pickleKey); + const pickleKey = Buffer.from(olmDevice._pickleKey); const decrypted = await decryptAES(key, pickleKey, type); return decodeBase64(decrypted); } else { return key; } }, - storeCrossSigningKeyCache: async function(type, key) { + storeCrossSigningKeyCache: async function(type: string, key: Uint8Array): Promise { if (!(key instanceof Uint8Array)) { throw new Error( `storeCrossSigningKeyCache expects Uint8Array, got ${key}`, ); } - const pickleKey = Buffer.from(olmdevice._pickleKey); + const pickleKey = Buffer.from(olmDevice._pickleKey); key = await encryptAES(encodeBase64(key), pickleKey, type); return store.doTxn( 'readwrite', @@ -729,7 +738,7 @@ export function createCryptoStoreCacheCallbacks(store, olmdevice) { * @param {string} userId The user ID being verified * @param {string} deviceId The device ID being verified */ -export async function requestKeysDuringVerification(baseApis, userId, deviceId) { +export async function requestKeysDuringVerification(baseApis: MatrixClient, userId: string, deviceId: string) { // If this is a self-verification, ask the other party for keys if (baseApis.getUserId() !== userId) { return; @@ -739,7 +748,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId) // it. We return here in order to test. return new Promise((resolve, reject) => { const client = baseApis; - const original = client.crypto._crossSigningInfo; + const original = client.crypto.crossSigningInfo; // We already have all of the infrastructure we need to validate and // cache cross-signing keys, so instead of replicating that, here we set @@ -748,8 +757,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId) const crossSigning = new CrossSigningInfo( original.userId, { getCrossSigningKey: async (type) => { - logger.debug("Cross-signing: requesting secret", - type, deviceId); + logger.debug("Cross-signing: requesting secret", type, deviceId); const { promise } = client.requestSecret( `m.cross_signing.${type}`, [deviceId], ); @@ -757,7 +765,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId) const decoded = decodeBase64(result); return Uint8Array.from(decoded); } }, - original._cacheCallbacks, + original.getCacheCallbacks(), ); crossSigning.keys = original.keys; @@ -774,7 +782,8 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId) }); // also request and cache the key backup key - const backupKeyPromise = new Promise(async resolve => { + // eslint-disable-next-line no-async-promise-executor + const backupKeyPromise = new Promise(async resolve => { const cachedKey = await client.crypto.getSessionBackupPrivateKey(); if (!cachedKey) { logger.info("No cached backup key found. Requesting..."); @@ -791,9 +800,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId) logger.info("Backup key stored. Starting backup restore..."); const backupInfo = await client.getKeyBackupVersion(); // no need to await for this - just let it go in the bg - client.restoreKeyBackupWithCache( - undefined, undefined, backupInfo, - ).then(() => { + client.restoreKeyBackupWithCache(undefined, undefined, backupInfo).then(() => { logger.info("Backup restored."); }); } diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.ts similarity index 62% rename from src/crypto/DeviceList.js rename to src/crypto/DeviceList.ts index 9b74a54db0e..688e510a927 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.ts @@ -1,7 +1,5 @@ /* -Copyright 2017 Vector Creations Ltd -Copyright 2018, 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2017 - 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. @@ -23,12 +21,15 @@ limitations under the License. */ import { EventEmitter } from 'events'; + import { logger } from '../logger'; -import { DeviceInfo } from './deviceinfo'; +import { DeviceInfo, IDevice } from './deviceinfo'; import { CrossSigningInfo } from './CrossSigning'; import * as olmlib from './olmlib'; import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; -import { chunkPromises, defer, sleep } from '../utils'; +import { chunkPromises, defer, IDeferred, sleep } from '../utils'; +import { MatrixClient, CryptoStore } from "../client"; +import { OlmDevice } from "./OlmDevice"; /* State transition diagram for DeviceList._deviceTrackingStatus * @@ -51,91 +52,96 @@ import { chunkPromises, defer, sleep } from '../utils'; */ // constants for DeviceList._deviceTrackingStatus -const TRACKING_STATUS_NOT_TRACKED = 0; -const TRACKING_STATUS_PENDING_DOWNLOAD = 1; -const TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2; -const TRACKING_STATUS_UP_TO_DATE = 3; +enum TrackingStatus { + NotTracked, + PendingDownload, + DownloadInProgress, + UpToDate, +} + +type DeviceInfoMap = Record>; /** * @alias module:crypto/DeviceList */ export class DeviceList extends EventEmitter { - constructor(baseApis, cryptoStore, olmDevice, keyDownloadChunkSize = 250) { - super(); - - this._cryptoStore = cryptoStore; - - // userId -> { - // deviceId -> { - // [device info] - // } - // } - this._devices = {}; - - // userId -> { - // [key info] - // } - this._crossSigningInfo = {}; - - // map of identity keys to the user who owns it - this._userByIdentityKey = {}; - - // which users we are tracking device status for. - // userId -> TRACKING_STATUS_* - this._deviceTrackingStatus = {}; // loaded from storage in load() - - // The 'next_batch' sync token at the point the data was writen, - // ie. a token representing the point immediately after the - // moment represented by the snapshot in the db. - this._syncToken = null; - - this._serialiser = new DeviceListUpdateSerialiser( - baseApis, olmDevice, this, - ); - - // userId -> promise - this._keyDownloadsInProgressByUser = {}; - + // userId -> { + // deviceId -> { + // [device info] + // } + // } + private devices: DeviceInfoMap = {}; + + // userId -> { + // [key info] + // } + public crossSigningInfo: Record = {}; + + // map of identity keys to the user who owns it + private userByIdentityKey: Record = {}; + + // which users we are tracking device status for. + // userId -> TRACKING_STATUS_* + private deviceTrackingStatus: Record = {}; // loaded from storage in load() + + // The 'next_batch' sync token at the point the data was written, + // ie. a token representing the point immediately after the + // moment represented by the snapshot in the db. + private syncToken: string = null; + + // userId -> promise + private keyDownloadsInProgressByUser: Record> = {}; + + // Set whenever changes are made other than setting the sync token + private dirty = false; + + // Promise resolved when device data is saved + private savePromise: Promise = null; + // Function that resolves the save promise + private resolveSavePromise: (saved: boolean) => void = null; + // The time the save is scheduled for + private savePromiseTime: number = null; + // The timer used to delay the save + private saveTimer: NodeJS.Timeout = null; + // True if we have fetched data from the server or loaded a non-empty + // set of device data from the store + private hasFetched: boolean = null; + + private readonly serialiser: DeviceListUpdateSerialiser; + + constructor( + baseApis: MatrixClient, + private readonly cryptoStore: CryptoStore, + olmDevice: OlmDevice, // Maximum number of user IDs per request to prevent server overload (#1619) - this._keyDownloadChunkSize = keyDownloadChunkSize; - - // Set whenever changes are made other than setting the sync token - this._dirty = false; + public readonly keyDownloadChunkSize = 250, + ) { + super(); - // Promise resolved when device data is saved - this._savePromise = null; - // Function that resolves the save promise - this._resolveSavePromise = null; - // The time the save is scheduled for - this._savePromiseTime = null; - // The timer used to delay the save - this._saveTimer = null; - // True if we have fetched data from the server or loaded a non-empty - // set of device data from the store - this._hasFetched = null; + this.serialiser = new DeviceListUpdateSerialiser(baseApis, olmDevice, this); } /** * Load the device tracking state from storage */ - async load() { - await this._cryptoStore.doTxn( + public async load() { + await this.cryptoStore.doTxn( 'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => { - this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => { - this._hasFetched = Boolean(deviceData && deviceData.devices); - this._devices = deviceData ? deviceData.devices : {}, - this._crossSigningInfo = deviceData ? + this.cryptoStore.getEndToEndDeviceData(txn, (deviceData) => { + this.hasFetched = Boolean(deviceData && deviceData.devices); + this.devices = deviceData ? deviceData.devices : {}, + this.crossSigningInfo = deviceData ? deviceData.crossSigningInfo || {} : {}; - this._deviceTrackingStatus = deviceData ? + this.deviceTrackingStatus = deviceData ? deviceData.trackingStatus : {}; - this._syncToken = deviceData ? deviceData.syncToken : null; - this._userByIdentityKey = {}; - for (const user of Object.keys(this._devices)) { - const userDevices = this._devices[user]; + this.syncToken = deviceData ? deviceData.syncToken : null; + this.userByIdentityKey = {}; + for (const user of Object.keys(this.devices)) { + const userDevices = this.devices[user]; for (const device of Object.keys(userDevices)) { const idKey = userDevices[device].keys['curve25519:'+device]; if (idKey !== undefined) { - this._userByIdentityKey[idKey] = user; + this.userByIdentityKey[idKey] = user; } } } @@ -143,17 +149,17 @@ export class DeviceList extends EventEmitter { }, ); - for (const u of Object.keys(this._deviceTrackingStatus)) { + for (const u of Object.keys(this.deviceTrackingStatus)) { // if a download was in progress when we got shut down, it isn't any more. - if (this._deviceTrackingStatus[u] == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) { - this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD; + if (this.deviceTrackingStatus[u] == TrackingStatus.DownloadInProgress) { + this.deviceTrackingStatus[u] = TrackingStatus.PendingDownload; } } } - stop() { - if (this._saveTimer !== null) { - clearTimeout(this._saveTimer); + public stop() { + if (this.saveTimer !== null) { + clearTimeout(this.saveTimer); } } @@ -164,74 +170,73 @@ export class DeviceList extends EventEmitter { * The actual save will be delayed by a short amount of time to * aggregate multiple writes to the database. * - * @param {integer} delay Time in ms before which the save actually happens. + * @param {number} delay Time in ms before which the save actually happens. * By default, the save is delayed for a short period in order to batch * multiple writes, but this behaviour can be disabled by passing 0. * - * @return {Promise} true if the data was saved, false if + * @return {Promise} true if the data was saved, false if * it was not (eg. because no changes were pending). The promise * will only resolve once the data is saved, so may take some time * to resolve. */ - async saveIfDirty(delay) { - if (!this._dirty) return Promise.resolve(false); + public async saveIfDirty(delay = 500): Promise { + if (!this.dirty) return Promise.resolve(false); // Delay saves for a bit so we can aggregate multiple saves that happen // in quick succession (eg. when a whole room's devices are marked as known) - if (delay === undefined) delay = 500; const targetTime = Date.now + delay; - if (this._savePromiseTime && targetTime < this._savePromiseTime) { + if (this.savePromiseTime && targetTime < this.savePromiseTime) { // There's a save scheduled but for after we would like: cancel // it & schedule one for the time we want - clearTimeout(this._saveTimer); - this._saveTimer = null; - this._savePromiseTime = null; + clearTimeout(this.saveTimer); + this.saveTimer = null; + this.savePromiseTime = null; // (but keep the save promise since whatever called save before // will still want to know when the save is done) } - let savePromise = this._savePromise; + let savePromise = this.savePromise; if (savePromise === null) { savePromise = new Promise((resolve, reject) => { - this._resolveSavePromise = resolve; + this.resolveSavePromise = resolve; }); - this._savePromise = savePromise; + this.savePromise = savePromise; } - if (this._saveTimer === null) { - const resolveSavePromise = this._resolveSavePromise; - this._savePromiseTime = targetTime; - this._saveTimer = setTimeout(() => { - logger.log('Saving device tracking data', this._syncToken); + if (this.saveTimer === null) { + const resolveSavePromise = this.resolveSavePromise; + this.savePromiseTime = targetTime; + this.saveTimer = setTimeout(() => { + logger.log('Saving device tracking data', this.syncToken); // null out savePromise now (after the delay but before the write), // otherwise we could return the existing promise when the save has // actually already happened. - this._savePromiseTime = null; - this._saveTimer = null; - this._savePromise = null; - this._resolveSavePromise = null; + this.savePromiseTime = null; + this.saveTimer = null; + this.savePromise = null; + this.resolveSavePromise = null; - this._cryptoStore.doTxn( + this.cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => { - this._cryptoStore.storeEndToEndDeviceData({ - devices: this._devices, - crossSigningInfo: this._crossSigningInfo, - trackingStatus: this._deviceTrackingStatus, - syncToken: this._syncToken, + this.cryptoStore.storeEndToEndDeviceData({ + devices: this.devices, + crossSigningInfo: this.crossSigningInfo, + trackingStatus: this.deviceTrackingStatus, + syncToken: this.syncToken, }, txn); }, ).then(() => { - // The device list is considered dirty until the write - // completes. - this._dirty = false; - resolveSavePromise(); + // The device list is considered dirty until the write completes. + this.dirty = false; + resolveSavePromise(true); }, err => { - logger.error('Failed to save device tracking data', this._syncToken); + logger.error('Failed to save device tracking data', this.syncToken); logger.error(err); }); }, delay); } + return savePromise; } @@ -240,8 +245,8 @@ export class DeviceList extends EventEmitter { * * @return {string} The sync token */ - getSyncToken() { - return this._syncToken; + public getSyncToken(): string { + return this.syncToken; } /** @@ -254,8 +259,8 @@ export class DeviceList extends EventEmitter { * * @param {string} st The sync token */ - setSyncToken(st) { - this._syncToken = st; + public setSyncToken(st: string): void { + this.syncToken = st; } /** @@ -263,33 +268,33 @@ export class DeviceList extends EventEmitter { * downloading and storing them if they're not (or if forceDownload is * true). * @param {Array} userIds The users to fetch. - * @param {bool} forceDownload Always download the keys even if cached. + * @param {boolean} forceDownload Always download the keys even if cached. * * @return {Promise} A promise which resolves to a map userId->deviceId->{@link * module:crypto/deviceinfo|DeviceInfo}. */ - downloadKeys(userIds, forceDownload) { + public downloadKeys(userIds: string[], forceDownload: boolean): Promise { const usersToDownload = []; const promises = []; userIds.forEach((u) => { - const trackingStatus = this._deviceTrackingStatus[u]; - if (this._keyDownloadsInProgressByUser[u]) { + const trackingStatus = this.deviceTrackingStatus[u]; + if (this.keyDownloadsInProgressByUser[u]) { // already a key download in progress/queued for this user; its results // will be good enough for us. logger.log( `downloadKeys: already have a download in progress for ` + `${u}: awaiting its result`, ); - promises.push(this._keyDownloadsInProgressByUser[u]); - } else if (forceDownload || trackingStatus != TRACKING_STATUS_UP_TO_DATE) { + promises.push(this.keyDownloadsInProgressByUser[u]); + } else if (forceDownload || trackingStatus != TrackingStatus.UpToDate) { usersToDownload.push(u); } }); if (usersToDownload.length != 0) { logger.log("downloadKeys: downloading for", usersToDownload); - const downloadPromise = this._doKeyDownload(usersToDownload); + const downloadPromise = this.doKeyDownload(usersToDownload); promises.push(downloadPromise); } @@ -298,7 +303,7 @@ export class DeviceList extends EventEmitter { } return Promise.all(promises).then(() => { - return this._getDevicesFromStore(userIds); + return this.getDevicesFromStore(userIds); }); } @@ -309,12 +314,11 @@ export class DeviceList extends EventEmitter { * * @return {Object} userId->deviceId->{@link module:crypto/deviceinfo|DeviceInfo}. */ - _getDevicesFromStore(userIds) { + private getDevicesFromStore(userIds: string[]): DeviceInfoMap { const stored = {}; - const self = this; - userIds.map(function(u) { + userIds.map((u) => { stored[u] = {}; - const devices = self.getStoredDevicesForUser(u) || []; + const devices = this.getStoredDevicesForUser(u) || []; devices.map(function(dev) { stored[u][dev.deviceId] = dev; }); @@ -327,8 +331,8 @@ export class DeviceList extends EventEmitter { * * @return {array} All known user IDs */ - getKnownUserIds() { - return Object.keys(this._devices); + public getKnownUserIds(): string[] { + return Object.keys(this.devices); } /** @@ -339,8 +343,8 @@ export class DeviceList extends EventEmitter { * @return {module:crypto/deviceinfo[]|null} list of devices, or null if we haven't * managed to get a list of devices for this user yet. */ - getStoredDevicesForUser(userId) { - const devs = this._devices[userId]; + public getStoredDevicesForUser(userId: string): DeviceInfo[] | null { + const devs = this.devices[userId]; if (!devs) { return null; } @@ -361,19 +365,19 @@ export class DeviceList extends EventEmitter { * @return {Object} deviceId->{object} devices, or undefined if * there is no data for this user. */ - getRawStoredDevicesForUser(userId) { - return this._devices[userId]; + public getRawStoredDevicesForUser(userId: string): Record { + return this.devices[userId]; } - getStoredCrossSigningForUser(userId) { - if (!this._crossSigningInfo[userId]) return null; + public getStoredCrossSigningForUser(userId: string): CrossSigningInfo { + if (!this.crossSigningInfo[userId]) return null; - return CrossSigningInfo.fromStorage(this._crossSigningInfo[userId], userId); + return CrossSigningInfo.fromStorage(this.crossSigningInfo[userId], userId); } - storeCrossSigningForUser(userId, info) { - this._crossSigningInfo[userId] = info; - this._dirty = true; + public storeCrossSigningForUser(userId: string, info: CrossSigningInfo): void { + this.crossSigningInfo[userId] = info; + this.dirty = true; } /** @@ -385,8 +389,8 @@ export class DeviceList extends EventEmitter { * @return {module:crypto/deviceinfo?} device, or undefined * if we don't know about this device */ - getStoredDevice(userId, deviceId) { - const devs = this._devices[userId]; + public getStoredDevice(userId: string, deviceId: string): DeviceInfo { + const devs = this.devices[userId]; if (!devs || !devs[deviceId]) { return undefined; } @@ -401,7 +405,7 @@ export class DeviceList extends EventEmitter { * * @return {string} user ID */ - getUserByIdentityKey(algorithm, senderKey) { + public getUserByIdentityKey(algorithm: string, senderKey: string): string { if ( algorithm !== olmlib.OLM_ALGORITHM && algorithm !== olmlib.MEGOLM_ALGORITHM @@ -410,7 +414,7 @@ export class DeviceList extends EventEmitter { return null; } - return this._userByIdentityKey[senderKey]; + return this.userByIdentityKey[senderKey]; } /** @@ -421,13 +425,13 @@ export class DeviceList extends EventEmitter { * * @return {module:crypto/deviceinfo?} */ - getDeviceByIdentityKey(algorithm, senderKey) { + public getDeviceByIdentityKey(algorithm: string, senderKey: string): DeviceInfo | null { const userId = this.getUserByIdentityKey(algorithm, senderKey); if (!userId) { return null; } - const devices = this._devices[userId]; + const devices = this.devices[userId]; if (!devices) { return null; } @@ -462,25 +466,25 @@ export class DeviceList extends EventEmitter { * @param {string} u The user ID * @param {Object} devs New device info for user */ - storeDevicesForUser(u, devs) { - // remove previous devices from _userByIdentityKey - if (this._devices[u] !== undefined) { - for (const [deviceId, dev] of Object.entries(this._devices[u])) { + public storeDevicesForUser(u: string, devs: Record): void { + // remove previous devices from userByIdentityKey + if (this.devices[u] !== undefined) { + for (const [deviceId, dev] of Object.entries(this.devices[u])) { const identityKey = dev.keys['curve25519:'+deviceId]; - delete this._userByIdentityKey[identityKey]; + delete this.userByIdentityKey[identityKey]; } } - this._devices[u] = devs; + this.devices[u] = devs; // add new ones for (const [deviceId, dev] of Object.entries(devs)) { const identityKey = dev.keys['curve25519:'+deviceId]; - this._userByIdentityKey[identityKey] = u; + this.userByIdentityKey[identityKey] = u; } - this._dirty = true; + this.dirty = true; } /** @@ -492,7 +496,7 @@ export class DeviceList extends EventEmitter { * * @param {String} userId */ - startTrackingDeviceList(userId) { + public startTrackingDeviceList(userId: string): void { // sanity-check the userId. This is mostly paranoia, but if synapse // can't parse the userId we give it as an mxid, it 500s the whole // request and we can never update the device lists again (because @@ -503,12 +507,12 @@ export class DeviceList extends EventEmitter { if (typeof userId !== 'string') { throw new Error('userId must be a string; was '+userId); } - if (!this._deviceTrackingStatus[userId]) { + if (!this.deviceTrackingStatus[userId]) { logger.log('Now tracking device list for ' + userId); - this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD; + this.deviceTrackingStatus[userId] = TrackingStatus.PendingDownload; // we don't yet persist the tracking status, since there may be a lot // of calls; we save all data together once the sync is done - this._dirty = true; + this.dirty = true; } } @@ -521,14 +525,14 @@ export class DeviceList extends EventEmitter { * * @param {String} userId */ - stopTrackingDeviceList(userId) { - if (this._deviceTrackingStatus[userId]) { + public stopTrackingDeviceList(userId: string): void { + if (this.deviceTrackingStatus[userId]) { logger.log('No longer tracking device list for ' + userId); - this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED; + this.deviceTrackingStatus[userId] = TrackingStatus.NotTracked; // we don't yet persist the tracking status, since there may be a lot // of calls; we save all data together once the sync is done - this._dirty = true; + this.dirty = true; } } @@ -538,11 +542,11 @@ export class DeviceList extends EventEmitter { * This will flag each user whose devices we are tracking as in need of an * update. */ - stopTrackingAllDeviceLists() { - for (const userId of Object.keys(this._deviceTrackingStatus)) { - this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED; + public stopTrackingAllDeviceLists(): void { + for (const userId of Object.keys(this.deviceTrackingStatus)) { + this.deviceTrackingStatus[userId] = TrackingStatus.NotTracked; } - this._dirty = true; + this.dirty = true; } /** @@ -556,14 +560,14 @@ export class DeviceList extends EventEmitter { * * @param {String} userId */ - invalidateUserDeviceList(userId) { - if (this._deviceTrackingStatus[userId]) { + public invalidateUserDeviceList(userId: string): void { + if (this.deviceTrackingStatus[userId]) { logger.log("Marking device list outdated for", userId); - this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD; + this.deviceTrackingStatus[userId] = TrackingStatus.PendingDownload; // we don't yet persist the tracking status, since there may be a lot // of calls; we save all data together once the sync is done - this._dirty = true; + this.dirty = true; } } @@ -573,18 +577,18 @@ export class DeviceList extends EventEmitter { * @returns {Promise} which completes when the download completes; normally there * is no need to wait for this (it's mostly for the unit tests). */ - refreshOutdatedDeviceLists() { + public refreshOutdatedDeviceLists(): Promise { this.saveIfDirty(); const usersToDownload = []; - for (const userId of Object.keys(this._deviceTrackingStatus)) { - const stat = this._deviceTrackingStatus[userId]; - if (stat == TRACKING_STATUS_PENDING_DOWNLOAD) { + for (const userId of Object.keys(this.deviceTrackingStatus)) { + const stat = this.deviceTrackingStatus[userId]; + if (stat == TrackingStatus.PendingDownload) { usersToDownload.push(userId); } } - return this._doKeyDownload(usersToDownload); + return this.doKeyDownload(usersToDownload); } /** @@ -595,34 +599,34 @@ export class DeviceList extends EventEmitter { * * @param {Object} devices deviceId->{object} the new devices */ - _setRawStoredDevicesForUser(userId, devices) { - // remove old devices from _userByIdentityKey - if (this._devices[userId] !== undefined) { - for (const [deviceId, dev] of Object.entries(this._devices[userId])) { + public setRawStoredDevicesForUser(userId: string, devices: Record): void { + // remove old devices from userByIdentityKey + if (this.devices[userId] !== undefined) { + for (const [deviceId, dev] of Object.entries(this.devices[userId])) { const identityKey = dev.keys['curve25519:'+deviceId]; - delete this._userByIdentityKey[identityKey]; + delete this.userByIdentityKey[identityKey]; } } - this._devices[userId] = devices; + this.devices[userId] = devices; - // add new devices into _userByIdentityKey + // add new devices into userByIdentityKey for (const [deviceId, dev] of Object.entries(devices)) { const identityKey = dev.keys['curve25519:'+deviceId]; - this._userByIdentityKey[identityKey] = userId; + this.userByIdentityKey[identityKey] = userId; } } - setRawStoredCrossSigningForUser(userId, info) { - this._crossSigningInfo[userId] = info; + public setRawStoredCrossSigningForUser(userId: string, info: object): void { + this.crossSigningInfo[userId] = info; } /** * Fire off download update requests for the given users, and update the * device list tracking status for them, and the - * _keyDownloadsInProgressByUser map for them. + * keyDownloadsInProgressByUser map for them. * * @param {String[]} users list of userIds * @@ -630,15 +634,13 @@ export class DeviceList extends EventEmitter { * been updated. rejects if there was a problem updating any of the * users. */ - _doKeyDownload(users) { + private doKeyDownload(users: string[]): Promise { if (users.length === 0) { // nothing to do return Promise.resolve(); } - const prom = this._serialiser.updateDevicesForUsers( - users, this._syncToken, - ).then(() => { + const prom = this.serialiser.updateDevicesForUsers(users, this.syncToken).then(() => { finished(true); }, (e) => { logger.error( @@ -649,42 +651,41 @@ export class DeviceList extends EventEmitter { }); users.forEach((u) => { - this._keyDownloadsInProgressByUser[u] = prom; - const stat = this._deviceTrackingStatus[u]; - if (stat == TRACKING_STATUS_PENDING_DOWNLOAD) { - this._deviceTrackingStatus[u] = TRACKING_STATUS_DOWNLOAD_IN_PROGRESS; + this.keyDownloadsInProgressByUser[u] = prom; + const stat = this.deviceTrackingStatus[u]; + if (stat == TrackingStatus.PendingDownload) { + this.deviceTrackingStatus[u] = TrackingStatus.DownloadInProgress; } }); const finished = (success) => { - this.emit("crypto.willUpdateDevices", users, !this._hasFetched); + this.emit("crypto.willUpdateDevices", users, !this.hasFetched); users.forEach((u) => { - this._dirty = true; + this.dirty = true; // we may have queued up another download request for this user // since we started this request. If that happens, we should // ignore the completion of the first one. - if (this._keyDownloadsInProgressByUser[u] !== prom) { - logger.log('Another update in the queue for', u, - '- not marking up-to-date'); + if (this.keyDownloadsInProgressByUser[u] !== prom) { + logger.log('Another update in the queue for', u, '- not marking up-to-date'); return; } - delete this._keyDownloadsInProgressByUser[u]; - const stat = this._deviceTrackingStatus[u]; - if (stat == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) { + delete this.keyDownloadsInProgressByUser[u]; + const stat = this.deviceTrackingStatus[u]; + if (stat == TrackingStatus.DownloadInProgress) { if (success) { // we didn't get any new invalidations since this download started: // this user's device list is now up to date. - this._deviceTrackingStatus[u] = TRACKING_STATUS_UP_TO_DATE; + this.deviceTrackingStatus[u] = TrackingStatus.UpToDate; logger.log("Device list for", u, "now up to date"); } else { - this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD; + this.deviceTrackingStatus[u] = TrackingStatus.PendingDownload; } } }); this.saveIfDirty(); - this.emit("crypto.devicesUpdated", users, !this._hasFetched); - this._hasFetched = true; + this.emit("crypto.devicesUpdated", users, !this.hasFetched); + this.hasFetched = true; }; return prom; @@ -701,29 +702,28 @@ export class DeviceList extends EventEmitter { * time (and queuing other requests up). */ class DeviceListUpdateSerialiser { - /* - * @param {object} baseApis Base API object - * @param {object} olmDevice The Olm Device - * @param {object} deviceList The device list object - */ - constructor(baseApis, olmDevice, deviceList) { - this._baseApis = baseApis; - this._olmDevice = olmDevice; - this._deviceList = deviceList; // the device list to be updated + private downloadInProgress = false; - this._downloadInProgress = false; + // users which are queued for download + // userId -> true + private keyDownloadsQueuedByUser: Record = {}; - // users which are queued for download - // userId -> true - this._keyDownloadsQueuedByUser = {}; + // deferred which is resolved when the queued users are downloaded. + // non-null indicates that we have users queued for download. + private queuedQueryDeferred: IDeferred = null; - // deferred which is resolved when the queued users are downloaded. - // - // non-null indicates that we have users queued for download. - this._queuedQueryDeferred = null; + private syncToken: string = null; // The sync token we send with the requests - this._syncToken = null; // The sync token we send with the requests - } + /* + * @param {object} baseApis Base API object + * @param {object} olmDevice The Olm Device + * @param {object} deviceList The device list object, the device list to be updated + */ + constructor( + private readonly baseApis: MatrixClient, + private readonly olmDevice: OlmDevice, + private readonly deviceList: DeviceList, + ) {} /** * Make a key query request for the given users @@ -737,57 +737,57 @@ class DeviceListUpdateSerialiser { * been updated. rejects if there was a problem updating any of the * users. */ - updateDevicesForUsers(users, syncToken) { + public updateDevicesForUsers(users: string[], syncToken: string): Promise { users.forEach((u) => { - this._keyDownloadsQueuedByUser[u] = true; + this.keyDownloadsQueuedByUser[u] = true; }); - if (!this._queuedQueryDeferred) { - this._queuedQueryDeferred = defer(); + if (!this.queuedQueryDeferred) { + this.queuedQueryDeferred = defer(); } // We always take the new sync token and just use the latest one we've // been given, since it just needs to be at least as recent as the // sync response the device invalidation message arrived in - this._syncToken = syncToken; + this.syncToken = syncToken; - if (this._downloadInProgress) { + if (this.downloadInProgress) { // just queue up these users logger.log('Queued key download for', users); - return this._queuedQueryDeferred.promise; + return this.queuedQueryDeferred.promise; } // start a new download. - return this._doQueuedQueries(); + return this.doQueuedQueries(); } - _doQueuedQueries() { - if (this._downloadInProgress) { + private doQueuedQueries(): Promise { + if (this.downloadInProgress) { throw new Error( - "DeviceListUpdateSerialiser._doQueuedQueries called with request active", + "DeviceListUpdateSerialiser.doQueuedQueries called with request active", ); } - const downloadUsers = Object.keys(this._keyDownloadsQueuedByUser); - this._keyDownloadsQueuedByUser = {}; - const deferred = this._queuedQueryDeferred; - this._queuedQueryDeferred = null; + const downloadUsers = Object.keys(this.keyDownloadsQueuedByUser); + this.keyDownloadsQueuedByUser = {}; + const deferred = this.queuedQueryDeferred; + this.queuedQueryDeferred = null; logger.log('Starting key download for', downloadUsers); - this._downloadInProgress = true; + this.downloadInProgress = true; - const opts = {}; - if (this._syncToken) { - opts.token = this._syncToken; + const opts: Parameters[1] = {}; + if (this.syncToken) { + opts.token = this.syncToken; } const factories = []; - for (let i = 0; i < downloadUsers.length; i += this._deviceList._keyDownloadChunkSize) { - const userSlice = downloadUsers.slice(i, i + this._deviceList._keyDownloadChunkSize); - factories.push(() => this._baseApis.downloadKeysForUsers(userSlice, opts)); + for (let i = 0; i < downloadUsers.length; i += this.deviceList.keyDownloadChunkSize) { + const userSlice = downloadUsers.slice(i, i + this.deviceList.keyDownloadChunkSize); + factories.push(() => this.baseApis.downloadKeysForUsers(userSlice, opts)); } - chunkPromises(factories, 3).then(async (responses) => { + chunkPromises(factories, 3).then(async (responses: any[]) => { const dk = Object.assign({}, ...(responses.map(res => res.device_keys || {}))); const masterKeys = Object.assign({}, ...(responses.map(res => res.master_keys || {}))); const ssks = Object.assign({}, ...(responses.map(res => res.self_signing_keys || {}))); @@ -802,7 +802,7 @@ class DeviceListUpdateSerialiser { for (const userId of downloadUsers) { await sleep(5); try { - await this._processQueryResponseForUser( + await this.processQueryResponseForUser( userId, dk[userId], { master: masterKeys[userId], self_signing: ssks[userId], @@ -818,32 +818,34 @@ class DeviceListUpdateSerialiser { }).then(() => { logger.log('Completed key download for ' + downloadUsers); - this._downloadInProgress = false; + this.downloadInProgress = false; deferred.resolve(); // if we have queued users, fire off another request. - if (this._queuedQueryDeferred) { - this._doQueuedQueries(); + if (this.queuedQueryDeferred) { + this.doQueuedQueries(); } }, (e) => { logger.warn('Error downloading keys for ' + downloadUsers + ':', e); - this._downloadInProgress = false; + this.downloadInProgress = false; deferred.reject(e); }); return deferred.promise; } - async _processQueryResponseForUser( - userId, dkResponse, crossSigningResponse, - ) { + private async processQueryResponseForUser( + userId: string, + dkResponse: object, + crossSigningResponse: any, // TODO types + ): Promise { logger.log('got device keys for ' + userId + ':', dkResponse); logger.log('got cross-signing keys for ' + userId + ':', crossSigningResponse); { // map from deviceid -> deviceinfo for this user - const userStore = {}; - const devs = this._deviceList.getRawStoredDevicesForUser(userId); + const userStore: Record = {}; + const devs = this.deviceList.getRawStoredDevicesForUser(userId); if (devs) { Object.keys(devs).forEach((deviceId) => { const d = DeviceInfo.fromStorage(devs[deviceId], deviceId); @@ -851,9 +853,9 @@ class DeviceListUpdateSerialiser { }); } - await _updateStoredDeviceKeysForUser( - this._olmDevice, userId, userStore, dkResponse || {}, - this._baseApis.getUserId(), this._baseApis.deviceId, + await updateStoredDeviceKeysForUser( + this.olmDevice, userId, userStore, dkResponse || {}, + this.baseApis.getUserId(), this.baseApis.deviceId, ); // put the updates into the object that will be returned as our results @@ -862,7 +864,7 @@ class DeviceListUpdateSerialiser { storage[deviceId] = userStore[deviceId].toStorage(); }); - this._deviceList._setRawStoredDevicesForUser(userId, storage); + this.deviceList.setRawStoredDevicesForUser(userId, storage); } // now do the same for the cross-signing keys @@ -873,26 +875,31 @@ class DeviceListUpdateSerialiser { && (crossSigningResponse.master || crossSigningResponse.self_signing || crossSigningResponse.user_signing)) { const crossSigning - = this._deviceList.getStoredCrossSigningForUser(userId) + = this.deviceList.getStoredCrossSigningForUser(userId) || new CrossSigningInfo(userId); crossSigning.setKeys(crossSigningResponse); - this._deviceList.setRawStoredCrossSigningForUser( + this.deviceList.setRawStoredCrossSigningForUser( userId, crossSigning.toStorage(), ); // NB. Unlike most events in the js-sdk, this one is internal to the // js-sdk and is not re-emitted - this._deviceList.emit('userCrossSigningUpdated', userId); + this.deviceList.emit('userCrossSigningUpdated', userId); } } } } -async function _updateStoredDeviceKeysForUser( - _olmDevice, userId, userStore, userResult, localUserId, localDeviceId, -) { +async function updateStoredDeviceKeysForUser( + olmDevice: OlmDevice, + userId: string, + userStore: Record, + userResult: object, + localUserId: string, + localDeviceId: string, +): Promise { let updated = false; // remove any devices in the store which aren't in the response @@ -936,7 +943,7 @@ async function _updateStoredDeviceKeysForUser( continue; } - if (await _storeDeviceKeys(_olmDevice, userStore, deviceResult)) { + if (await storeDeviceKeys(olmDevice, userStore, deviceResult)) { updated = true; } } @@ -949,7 +956,11 @@ async function _updateStoredDeviceKeysForUser( * * returns (a promise for) true if a change was made, else false */ -async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) { +async function storeDeviceKeys( + olmDevice: OlmDevice, + userStore: Record, + deviceResult: any, // TODO types +): Promise { if (!deviceResult.keys) { // no keys? return false; @@ -961,8 +972,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) { const signKeyId = "ed25519:" + deviceId; const signKey = deviceResult.keys[signKeyId]; if (!signKey) { - logger.warn("Device " + userId + ":" + deviceId + - " has no ed25519 key"); + logger.warn("Device " + userId + ":" + deviceId + " has no ed25519 key"); return false; } @@ -970,10 +980,9 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) { const signatures = deviceResult.signatures || {}; try { - await olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey); + await olmlib.verifySignature(olmDevice, deviceResult, userId, deviceId, signKey); } catch (e) { - logger.warn("Unable to verify signature on device " + - userId + ":" + deviceId + ":" + e); + logger.warn("Unable to verify signature on device " + userId + ":" + deviceId + ":" + e); return false; } diff --git a/src/crypto/EncryptionSetup.js b/src/crypto/EncryptionSetup.js index d7f861a0335..8da30582489 100644 --- a/src/crypto/EncryptionSetup.js +++ b/src/crypto/EncryptionSetup.js @@ -184,7 +184,7 @@ export class EncryptionSetupOperation { }); // pass the new keys to the main instance of our own CrossSigningInfo. - crypto._crossSigningInfo.setKeys(this._crossSigningKeys.keys); + crypto.crossSigningInfo.setKeys(this._crossSigningKeys.keys); } // set account data if (this._accountData) { diff --git a/src/crypto/RoomList.js b/src/crypto/RoomList.ts similarity index 52% rename from src/crypto/RoomList.js rename to src/crypto/RoomList.ts index b00445247ac..ab653f45690 100644 --- a/src/crypto/RoomList.js +++ b/src/crypto/RoomList.ts @@ -1,5 +1,5 @@ /* -Copyright 2018, 2019 New Vector Ltd +Copyright 2018 - 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. @@ -21,44 +21,51 @@ limitations under the License. */ import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; +import { CryptoStore } from "../client"; + +/* eslint-disable camelcase */ +interface IRoomEncryption { + algorithm: string; + rotation_period_ms: number; + rotation_period_msgs: number; +} +/* eslint-enable camelcase */ /** * @alias module:crypto/RoomList */ export class RoomList { - constructor(cryptoStore) { - this._cryptoStore = cryptoStore; + // Object of roomId -> room e2e info object (body of the m.room.encryption event) + private roomEncryption: Record = {}; - // Object of roomId -> room e2e info object (body of the m.room.encryption event) - this._roomEncryption = {}; - } + constructor(private readonly cryptoStore: CryptoStore) {} - async init() { - await this._cryptoStore.doTxn( + public async init(): Promise { + await this.cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => { - this._cryptoStore.getEndToEndRooms(txn, (result) => { - this._roomEncryption = result; + this.cryptoStore.getEndToEndRooms(txn, (result) => { + this.roomEncryption = result; }); }, ); } - getRoomEncryption(roomId) { - return this._roomEncryption[roomId] || null; + public getRoomEncryption(roomId: string): IRoomEncryption { + return this.roomEncryption[roomId] || null; } - isRoomEncrypted(roomId) { + public isRoomEncrypted(roomId: string): boolean { return Boolean(this.getRoomEncryption(roomId)); } - async setRoomEncryption(roomId, roomInfo) { + public async setRoomEncryption(roomId: string, roomInfo: IRoomEncryption): Promise { // important that this happens before calling into the store // as it prevents the Crypto::setRoomEncryption from calling // this twice for consecutive m.room.encryption events - this._roomEncryption[roomId] = roomInfo; - await this._cryptoStore.doTxn( + this.roomEncryption[roomId] = roomInfo; + await this.cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => { - this._cryptoStore.storeEndToEndRoom(roomId, roomInfo, txn); + this.cryptoStore.storeEndToEndRoom(roomId, roomInfo, txn); }, ); } diff --git a/src/crypto/SecretStorage.js b/src/crypto/SecretStorage.js index a1ed647f06b..f57c61cb1c6 100644 --- a/src/crypto/SecretStorage.js +++ b/src/crypto/SecretStorage.js @@ -480,11 +480,11 @@ export class SecretStorage extends EventEmitter { }; const encryptedContent = { algorithm: olmlib.OLM_ALGORITHM, - sender_key: this._baseApis.crypto._olmDevice.deviceCurve25519Key, + sender_key: this._baseApis.crypto.olmDevice.deviceCurve25519Key, ciphertext: {}, }; await olmlib.ensureOlmSessionsForDevices( - this._baseApis.crypto._olmDevice, + this._baseApis.crypto.olmDevice, this._baseApis, { [sender]: [ @@ -496,7 +496,7 @@ export class SecretStorage extends EventEmitter { encryptedContent.ciphertext, this._baseApis.getUserId(), this._baseApis.deviceId, - this._baseApis.crypto._olmDevice, + this._baseApis.crypto.olmDevice, sender, this._baseApis.getStoredDevice(sender, deviceId), payload, @@ -527,7 +527,7 @@ export class SecretStorage extends EventEmitter { if (requestControl) { // make sure that the device that sent it is one of the devices that // we requested from - const deviceInfo = this._baseApis.crypto._deviceList.getDeviceByIdentityKey( + const deviceInfo = this._baseApis.crypto.deviceList.getDeviceByIdentityKey( olmlib.OLM_ALGORITHM, event.getSenderKey(), ); diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index eb003f75bdf..73c3cd5362c 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -64,16 +64,16 @@ export class DehydrationManager { this.getDehydrationKeyFromCache(); } async getDehydrationKeyFromCache(): Promise { - return await this.crypto._cryptoStore.doTxn( + return await this.crypto.cryptoStore.doTxn( 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { - this.crypto._cryptoStore.getSecretStorePrivateKey( + this.crypto.cryptoStore.getSecretStorePrivateKey( txn, async (result) => { if (result) { const { key, keyInfo, deviceDisplayName, time } = result; - const pickleKey = Buffer.from(this.crypto._olmDevice._pickleKey); + const pickleKey = Buffer.from(this.crypto.olmDevice._pickleKey); const decrypted = await decryptAES(key, pickleKey, DEHYDRATION_ALGORITHM); this.key = decodeBase64(decrypted); this.keyInfo = keyInfo; @@ -114,11 +114,11 @@ export class DehydrationManager { this.timeoutId = undefined; } // clear storage - await this.crypto._cryptoStore.doTxn( + await this.crypto.cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { - this.crypto._cryptoStore.storeSecretStorePrivateKey( + this.crypto.cryptoStore.storeSecretStorePrivateKey( txn, "dehydration", null, ); }, @@ -158,15 +158,15 @@ export class DehydrationManager { this.timeoutId = undefined; } try { - const pickleKey = Buffer.from(this.crypto._olmDevice._pickleKey); + const pickleKey = Buffer.from(this.crypto.olmDevice._pickleKey); // update the crypto store with the timestamp const key = await encryptAES(encodeBase64(this.key), pickleKey, DEHYDRATION_ALGORITHM); - await this.crypto._cryptoStore.doTxn( + await this.crypto.cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { - this.crypto._cryptoStore.storeSecretStorePrivateKey( + this.crypto.cryptoStore.storeSecretStorePrivateKey( txn, "dehydration", { keyInfo: this.keyInfo, @@ -205,7 +205,7 @@ export class DehydrationManager { } logger.log("Uploading account to server"); - const dehydrateResult = await this.crypto._baseApis.http.authedRequest( + const dehydrateResult = await this.crypto.baseApis.http.authedRequest( undefined, "PUT", "/dehydrated_device", @@ -223,9 +223,9 @@ export class DehydrationManager { const deviceId = dehydrateResult.device_id; logger.log("Preparing device keys", deviceId); const deviceKeys: DeviceKeys = { - algorithms: this.crypto._supportedAlgorithms, + algorithms: this.crypto.supportedAlgorithms, device_id: deviceId, - user_id: this.crypto._userId, + user_id: this.crypto.userId, keys: { [`ed25519:${deviceId}`]: e2eKeys.ed25519, [`curve25519:${deviceId}`]: e2eKeys.curve25519, @@ -233,12 +233,12 @@ export class DehydrationManager { }; const deviceSignature = account.sign(anotherjson.stringify(deviceKeys)); deviceKeys.signatures = { - [this.crypto._userId]: { + [this.crypto.userId]: { [`ed25519:${deviceId}`]: deviceSignature, }, }; - if (this.crypto._crossSigningInfo.getId("self_signing")) { - await this.crypto._crossSigningInfo.signObject(deviceKeys, "self_signing"); + if (this.crypto.crossSigningInfo.getId("self_signing")) { + await this.crypto.crossSigningInfo.signObject(deviceKeys, "self_signing"); } logger.log("Preparing one-time keys"); @@ -247,7 +247,7 @@ export class DehydrationManager { const k: OneTimeKey = { key }; const signature = account.sign(anotherjson.stringify(k)); k.signatures = { - [this.crypto._userId]: { + [this.crypto.userId]: { [`ed25519:${deviceId}`]: signature, }, }; @@ -260,7 +260,7 @@ export class DehydrationManager { const k: OneTimeKey = { key, fallback: true }; const signature = account.sign(anotherjson.stringify(k)); k.signatures = { - [this.crypto._userId]: { + [this.crypto.userId]: { [`ed25519:${deviceId}`]: signature, }, }; @@ -268,7 +268,7 @@ export class DehydrationManager { } logger.log("Uploading keys to server"); - await this.crypto._baseApis.http.authedRequest( + await this.crypto.baseApis.http.authedRequest( undefined, "POST", "/keys/upload/" + encodeURI(deviceId), diff --git a/src/crypto/deviceinfo.js b/src/crypto/deviceinfo.js deleted file mode 100644 index 379d72c6370..00000000000 --- a/src/crypto/deviceinfo.js +++ /dev/null @@ -1,168 +0,0 @@ -/* -Copyright 2016 OpenMarket Ltd -Copyright 2019 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/deviceinfo - */ - -/** - * Information about a user's device - * - * @constructor - * @alias module:crypto/deviceinfo - * - * @property {string} deviceId the ID of this device - * - * @property {string[]} algorithms list of algorithms supported by this device - * - * @property {Object.} keys a map from - * <key type>:<id> -> <base64-encoded key>> - * - * @property {module:crypto/deviceinfo.DeviceVerification} verified - * whether the device has been verified/blocked by the user - * - * @property {boolean} known - * whether the user knows of this device's existence (useful when warning - * the user that a user has added new devices) - * - * @property {Object} unsigned additional data from the homeserver - * - * @param {string} deviceId id of the device - */ -export function DeviceInfo(deviceId) { - // you can't change the deviceId - Object.defineProperty(this, 'deviceId', { - enumerable: true, - value: deviceId, - }); - - this.algorithms = []; - this.keys = {}; - this.verified = DeviceVerification.UNVERIFIED; - this.known = false; - this.unsigned = {}; - this.signatures = {}; -} - -/** - * rehydrate a DeviceInfo from the session store - * - * @param {object} obj raw object from session store - * @param {string} deviceId id of the device - * - * @return {module:crypto~DeviceInfo} new DeviceInfo - */ -DeviceInfo.fromStorage = function(obj, deviceId) { - const res = new DeviceInfo(deviceId); - for (const prop in obj) { - if (obj.hasOwnProperty(prop)) { - res[prop] = obj[prop]; - } - } - return res; -}; - -/** - * Prepare a DeviceInfo for JSON serialisation in the session store - * - * @return {object} deviceinfo with non-serialised members removed - */ -DeviceInfo.prototype.toStorage = function() { - return { - algorithms: this.algorithms, - keys: this.keys, - verified: this.verified, - known: this.known, - unsigned: this.unsigned, - signatures: this.signatures, - }; -}; - -/** - * Get the fingerprint for this device (ie, the Ed25519 key) - * - * @return {string} base64-encoded fingerprint of this device - */ -DeviceInfo.prototype.getFingerprint = function() { - return this.keys["ed25519:" + this.deviceId]; -}; - -/** - * Get the identity key for this device (ie, the Curve25519 key) - * - * @return {string} base64-encoded identity key of this device - */ -DeviceInfo.prototype.getIdentityKey = function() { - return this.keys["curve25519:" + this.deviceId]; -}; - -/** - * Get the configured display name for this device, if any - * - * @return {string?} displayname - */ -DeviceInfo.prototype.getDisplayName = function() { - return this.unsigned.device_display_name || null; -}; - -/** - * Returns true if this device is blocked - * - * @return {Boolean} true if blocked - */ -DeviceInfo.prototype.isBlocked = function() { - return this.verified == DeviceVerification.BLOCKED; -}; - -/** - * Returns true if this device is verified - * - * @return {Boolean} true if verified - */ -DeviceInfo.prototype.isVerified = function() { - return this.verified == DeviceVerification.VERIFIED; -}; - -/** - * Returns true if this device is unverified - * - * @return {Boolean} true if unverified - */ -DeviceInfo.prototype.isUnverified = function() { - return this.verified == DeviceVerification.UNVERIFIED; -}; - -/** - * Returns true if the user knows about this device's existence - * - * @return {Boolean} true if known - */ -DeviceInfo.prototype.isKnown = function() { - return this.known == true; -}; - -/** - * @enum - */ -DeviceInfo.DeviceVerification = { - VERIFIED: 1, - UNVERIFIED: 0, - BLOCKED: -1, -}; - -const DeviceVerification = DeviceInfo.DeviceVerification; - diff --git a/src/crypto/deviceinfo.ts b/src/crypto/deviceinfo.ts new file mode 100644 index 00000000000..d723eac4bd6 --- /dev/null +++ b/src/crypto/deviceinfo.ts @@ -0,0 +1,175 @@ +/* +Copyright 2016 - 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/deviceinfo + */ + +export interface IDevice { + keys: Record; + algorithms: string[]; + verified: DeviceVerification; + known: boolean; + unsigned?: Record; + signatures?: Record; +} + +enum DeviceVerification { + Blocked = -1, + Unverified = 0, + Verified = 1, +} + +/** + * Information about a user's device + * + * @constructor + * @alias module:crypto/deviceinfo + * + * @property {string} deviceId the ID of this device + * + * @property {string[]} algorithms list of algorithms supported by this device + * + * @property {Object.} keys a map from + * <key type>:<id> -> <base64-encoded key>> + * + * @property {module:crypto/deviceinfo.DeviceVerification} verified + * whether the device has been verified/blocked by the user + * + * @property {boolean} known + * whether the user knows of this device's existence (useful when warning + * the user that a user has added new devices) + * + * @property {Object} unsigned additional data from the homeserver + * + * @param {string} deviceId id of the device + */ +export class DeviceInfo { + /** + * rehydrate a DeviceInfo from the session store + * + * @param {object} obj raw object from session store + * @param {string} deviceId id of the device + * + * @return {module:crypto~DeviceInfo} new DeviceInfo + */ + public static fromStorage(obj: IDevice, deviceId: string): DeviceInfo { + const res = new DeviceInfo(deviceId); + for (const prop in obj) { + if (obj.hasOwnProperty(prop)) { + res[prop] = obj[prop]; + } + } + return res; + } + + /** + * @enum + */ + public static DeviceVerification = { + VERIFIED: DeviceVerification.Verified, + UNVERIFIED: DeviceVerification.Unverified, + BLOCKED: DeviceVerification.Blocked, + }; + + public algorithms: string[]; + public keys: Record = {}; + public verified = DeviceVerification.Unverified; + public known = false; + public unsigned: Record = {}; + public signatures: Record = {}; + + constructor(public readonly deviceId: string) {} + + /** + * Prepare a DeviceInfo for JSON serialisation in the session store + * + * @return {object} deviceinfo with non-serialised members removed + */ + public toStorage(): IDevice { + return { + algorithms: this.algorithms, + keys: this.keys, + verified: this.verified, + known: this.known, + unsigned: this.unsigned, + signatures: this.signatures, + }; + } + + /** + * Get the fingerprint for this device (ie, the Ed25519 key) + * + * @return {string} base64-encoded fingerprint of this device + */ + public getFingerprint(): string { + return this.keys["ed25519:" + this.deviceId]; + } + + /** + * Get the identity key for this device (ie, the Curve25519 key) + * + * @return {string} base64-encoded identity key of this device + */ + public getIdentityKey(): string { + return this.keys["curve25519:" + this.deviceId]; + } + + /** + * Get the configured display name for this device, if any + * + * @return {string?} displayname + */ + public getDisplayName(): string | null { + return this.unsigned.device_display_name || null; + } + + /** + * Returns true if this device is blocked + * + * @return {Boolean} true if blocked + */ + public isBlocked(): boolean { + return this.verified == DeviceVerification.Blocked; + } + + /** + * Returns true if this device is verified + * + * @return {Boolean} true if verified + */ + public isVerified(): boolean { + return this.verified == DeviceVerification.Verified; + } + + /** + * Returns true if this device is unverified + * + * @return {Boolean} true if unverified + */ + public isUnverified(): boolean { + return this.verified == DeviceVerification.Unverified; + } + + /** + * Returns true if the user knows about this device's existence + * + * @return {Boolean} true if known + */ + public isKnown(): boolean { + return this.known === true; + } +} diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 205a2195256..038708f8675 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -29,7 +29,7 @@ import { logger } from '../logger'; import { OlmDevice } from "./OlmDevice"; import * as olmlib from "./olmlib"; import { DeviceList } from "./DeviceList"; -import { DeviceInfo } from "./deviceinfo"; +import { DeviceInfo, IDevice } from "./deviceinfo"; import * as algorithms from "./algorithms"; import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from './CrossSigning'; import { EncryptionSetupBuilder } from "./EncryptionSetup"; @@ -420,9 +420,7 @@ export class Crypto extends EventEmitter { }; myDevices[this.deviceId] = deviceInfo; - this.deviceList.storeDevicesForUser( - this.userId, myDevices, - ); + this.deviceList.storeDevicesForUser(this.userId, myDevices); this.deviceList.saveIfDirty(); } @@ -922,10 +920,7 @@ export class Crypto extends EventEmitter { await this.crossSigningInfo.getCrossSigningKeysFromCache(); // This is writing to in-memory account data in // builder.accountDataClientAdapter so won't fail - await CrossSigningInfo.storeInSecretStorage( - crossSigningPrivateKeys, - secretStorage, - ); + await CrossSigningInfo.storeInSecretStorage(crossSigningPrivateKeys, secretStorage); } if (setupNewKeyBackup && !keyBackupInfo) { @@ -1172,7 +1167,7 @@ export class Crypto extends EventEmitter { // FIXME: do this in batches const users = {}; for (const [userId, crossSigningInfo] - of Object.entries(this.deviceList._crossSigningInfo)) { + of Object.entries(this.deviceList.crossSigningInfo)) { const upgradeInfo = await this.checkForDeviceVerificationUpgrade( userId, CrossSigningInfo.fromStorage(crossSigningInfo, userId), ); @@ -1248,7 +1243,7 @@ export class Crypto extends EventEmitter { private async checkForValidDeviceSignature( userId: string, key: any, // TODO types - devices: Record, + devices: Record, ): Promise { const deviceIds: string[] = []; if (devices && key.signatures && key.signatures[userId]) { @@ -1934,7 +1929,7 @@ export class Crypto extends EventEmitter { public downloadKeys( userIds: string[], forceDownload?: boolean, - ): Promise>> { + ): Promise>> { return this.deviceList.downloadKeys(userIds, forceDownload); } @@ -2003,7 +1998,7 @@ export class Crypto extends EventEmitter { verified?: boolean, blocked?: boolean, known?: boolean, - ): Promise { + ): Promise { // get rid of any `undefined`s here so we can just check // for null rather than null or undefined if (verified === undefined) verified = null; @@ -2068,7 +2063,7 @@ export class Crypto extends EventEmitter { // This will emit events when it comes back down the sync // (we could do local echo to speed things up) } - return device; + return device as any; // TODO types } else { return xsk; } diff --git a/src/crypto/key_passphrase.js b/src/crypto/key_passphrase.ts similarity index 76% rename from src/crypto/key_passphrase.js rename to src/crypto/key_passphrase.ts index 84384cc4e8c..3631fe30f7b 100644 --- a/src/crypto/key_passphrase.js +++ b/src/crypto/key_passphrase.ts @@ -1,6 +1,5 @@ /* -Copyright 2018 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2018 - 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. @@ -21,7 +20,21 @@ const DEFAULT_ITERATIONS = 500000; const DEFAULT_BITSIZE = 256; -export async function keyFromAuthData(authData, password) { +/* eslint-disable camelcase */ +interface IAuthData { + private_key_salt: string; + private_key_iterations: number; + private_key_bits?: number; +} +/* eslint-enable camelcase */ + +interface IKey { + key: Uint8Array; + salt: string; + iterations: number +} + +export async function keyFromAuthData(authData: IAuthData, password: string): Promise { if (!global.Olm) { throw new Error("Olm is not available"); } @@ -40,7 +53,7 @@ export async function keyFromAuthData(authData, password) { ); } -export async function keyFromPassphrase(password) { +export async function keyFromPassphrase(password: string): Promise { if (!global.Olm) { throw new Error("Olm is not available"); } @@ -52,7 +65,12 @@ export async function keyFromPassphrase(password) { return { key, salt, iterations: DEFAULT_ITERATIONS }; } -export async function deriveKey(password, salt, iterations, numBits = DEFAULT_BITSIZE) { +export async function deriveKey( + password: string, + salt: string, + iterations: number, + numBits = DEFAULT_BITSIZE, +): Promise { const subtleCrypto = global.crypto.subtle; const TextEncoder = global.TextEncoder; if (!subtleCrypto || !TextEncoder) { diff --git a/src/crypto/keybackup.ts b/src/crypto/keybackup.ts index c2fe3a1ce9c..8376245cd74 100644 --- a/src/crypto/keybackup.ts +++ b/src/crypto/keybackup.ts @@ -31,17 +31,22 @@ export interface IKeyBackupRoomSessions { [sessionId: string]: IKeyBackupSession; } +/* eslint-disable camelcase */ export interface IKeyBackupVersion { algorithm: string; - auth_data: { // eslint-disable-line camelcase - public_key: string; // eslint-disable-line camelcase + auth_data: { + public_key: string; signatures: ISignatures; + private_key_salt: string; + private_key_iterations: number; + private_key_bits?: number; }; count: number; etag: string; version: string; // number contained within - recovery_key: string; // eslint-disable-line camelcase + recovery_key: string; } +/* eslint-enable camelcase */ export interface IKeyBackupPrepareOpts { secureSecretStorage: boolean; diff --git a/src/crypto/verification/Base.js b/src/crypto/verification/Base.js index 2ac17cc88f2..4ca01bd112f 100644 --- a/src/crypto/verification/Base.js +++ b/src/crypto/verification/Base.js @@ -292,7 +292,7 @@ export class VerificationBase extends EventEmitter { await verifier(keyId, device, keyInfo); verifiedDevices.push(deviceId); } else { - const crossSigningInfo = this._baseApis.crypto._deviceList + const crossSigningInfo = this._baseApis.crypto.deviceList .getStoredCrossSigningForUser(userId); if (crossSigningInfo && crossSigningInfo.getId() === deviceId) { await verifier(keyId, DeviceInfo.fromStorage({ diff --git a/src/matrix.ts b/src/matrix.ts index c9192f2ea03..2cce6867a94 100644 --- a/src/matrix.ts +++ b/src/matrix.ts @@ -19,6 +19,7 @@ import { MemoryStore } from "./store/memory"; import { MatrixScheduler } from "./scheduler"; import { MatrixClient } from "./client"; import { ICreateClientOpts } from "./client"; +import { DeviceTrustLevel } from "./crypto/CrossSigning"; export * from "./client"; export * from "./http-api"; @@ -99,7 +100,7 @@ export function setCryptoStoreFactory(fac) { } export interface ICryptoCallbacks { - getCrossSigningKey?: (keyType: string, pubKey: Uint8Array) => Promise; + getCrossSigningKey?: (keyType: string, pubKey: string) => Promise; saveCrossSigningKeys?: (keys: Record) => void; shouldUpgradeDeviceVerifications?: ( users: Record @@ -112,7 +113,7 @@ export interface ICryptoCallbacks { ) => void; onSecretRequested?: ( userId: string, deviceId: string, - requestId: string, secretName: string, deviceTrust: IDeviceTrustLevel + requestId: string, secretName: string, deviceTrust: DeviceTrustLevel ) => Promise; getDehydrationKey?: ( keyInfo: ISecretStorageKeyInfo, @@ -132,14 +133,6 @@ export interface ISecretStorageKeyInfo { mac?: string; } -// TODO: Move this to `CrossSigning` once converted -export interface IDeviceTrustLevel { - isVerified(): boolean; - isCrossSigningVerified(): boolean; - isLocallyVerified(): boolean; - isTofu(): boolean; -} - /** * Construct a Matrix Client. Similar to {@link module:client.MatrixClient} * except that the 'request', 'store' and 'scheduler' dependencies are satisfied. diff --git a/src/utils.ts b/src/utils.ts index 14207ebca41..587d9a7f9e2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -435,12 +435,18 @@ export function isNullOrUndefined(val: any): boolean { return val === null || val === undefined; } +export interface IDeferred { + resolve: (value: T) => void; + reject: (any) => void; + promise: Promise; +} + // Returns a Deferred -export function defer() { +export function defer(): IDeferred { let resolve; let reject; - const promise = new Promise((_resolve, _reject) => { + const promise = new Promise((_resolve, _reject) => { resolve = _resolve; reject = _reject; }); From 664d920dd17554c530652aa68d316e85640b1c4e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Jun 2021 14:51:40 +0100 Subject: [PATCH 07/10] Fix issues identified by Typescriptification --- src/crypto/DeviceList.ts | 2 +- src/crypto/index.ts | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/crypto/DeviceList.ts b/src/crypto/DeviceList.ts index 688e510a927..3c330994fd5 100644 --- a/src/crypto/DeviceList.ts +++ b/src/crypto/DeviceList.ts @@ -184,7 +184,7 @@ export class DeviceList extends EventEmitter { // Delay saves for a bit so we can aggregate multiple saves that happen // in quick succession (eg. when a whole room's devices are marked as known) - const targetTime = Date.now + delay; + const targetTime = Date.now() + delay; if (this.savePromiseTime && targetTime < this.savePromiseTime) { // There's a save scheduled but for after we would like: cancel // it & schedule one for the time we want diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 038708f8675..14b37e322c7 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -1216,7 +1216,7 @@ export class Crypto extends EventEmitter { // only upgrade if this is the first cross-signing key that we've seen for // them, and if their cross-signing key isn't already verified const trustLevel = this.crossSigningInfo.checkUserTrust(crossSigningInfo); - if (crossSigningInfo.firstUse && !trustLevel.verified) { + if (crossSigningInfo.firstUse && !trustLevel.isVerified()) { const devices = this.deviceList.getRawStoredDevicesForUser(userId); const deviceIds = await this.checkForValidDeviceSignature( userId, crossSigningInfo.keys.master, devices, @@ -2908,9 +2908,6 @@ export class Crypto extends EventEmitter { this.deviceList.setSyncToken(syncData.nextSyncToken); this.deviceList.saveIfDirty(); - // catch up on any new devices we got told about during the sync. - this.deviceList.lastKnownSyncToken = nextSyncToken; - // we always track our own device list (for key backups etc) this.deviceList.startTrackingDeviceList(this.userId); From e9007429dd14c236017fd79300600e8b1a02d185 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Jun 2021 15:02:01 +0100 Subject: [PATCH 08/10] fix more underscored accesses --- spec/unit/crypto/algorithms/megolm.spec.js | 2 +- spec/unit/crypto/backup.spec.js | 6 +++--- spec/unit/crypto/crypto-utils.js | 10 +++++----- src/crypto/EncryptionSetup.js | 9 ++++----- src/crypto/algorithms/megolm.js | 10 +++++----- 5 files changed, 18 insertions(+), 19 deletions(-) diff --git a/spec/unit/crypto/algorithms/megolm.spec.js b/spec/unit/crypto/algorithms/megolm.spec.js index b3afc3e6c98..6c6034b72c5 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.js +++ b/spec/unit/crypto/algorithms/megolm.spec.js @@ -257,7 +257,7 @@ describe("MegolmDecryption", function() { }); it("re-uses sessions for sequential messages", async function() { - mockCrypto._backupManager = { + mockCrypto.backupManager = { backupGroupSession: () => {}, }; const mockStorage = new MockStorageApi(); diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index df65475d40f..ae6b25ddfa8 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -139,7 +139,7 @@ describe("MegolmBackup", function() { let megolmDecryption; beforeEach(async function() { mockCrypto = testUtils.mock(Crypto, 'Crypto'); - mockCrypto._backupManager = testUtils.mock(BackupManager, "BackupManager"); + mockCrypto.backupManager = testUtils.mock(BackupManager, "BackupManager"); mockCrypto.backupKey = new Olm.PkEncryption(); mockCrypto.backupKey.set_recipient_key( "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", @@ -217,14 +217,14 @@ describe("MegolmBackup", function() { }; mockCrypto.cancelRoomKeyRequest = function() {}; - mockCrypto._backupManager = { + mockCrypto.backupManager = { backupGroupSession: jest.fn(), }; return event.attemptDecryption(mockCrypto).then(() => { return megolmDecryption.onRoomKeyEvent(event); }).then(() => { - expect(mockCrypto._backupManager.backupGroupSession).toHaveBeenCalled(); + expect(mockCrypto.backupManager.backupGroupSession).toHaveBeenCalled(); }); }); diff --git a/spec/unit/crypto/crypto-utils.js b/spec/unit/crypto/crypto-utils.js index dbf6cec6546..b54b1a18ebe 100644 --- a/spec/unit/crypto/crypto-utils.js +++ b/spec/unit/crypto/crypto-utils.js @@ -11,12 +11,12 @@ export async function resetCrossSigningKeys(client, { const oldKeys = Object.assign({}, crypto.crossSigningInfo.keys); try { await crypto.crossSigningInfo.resetKeys(level); - await crypto._signObject(crypto.crossSigningInfo.keys.master); + await crypto.signObject(crypto.crossSigningInfo.keys.master); // write a copy locally so we know these are trusted keys - await crypto._cryptoStore.doTxn( + await crypto.cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { - crypto._cryptoStore.storeCrossSigningKeys( + crypto.cryptoStore.storeCrossSigningKeys( txn, crypto.crossSigningInfo.keys); }, ); @@ -26,8 +26,8 @@ export async function resetCrossSigningKeys(client, { crypto.crossSigningInfo.keys = oldKeys; throw e; } - crypto._baseApis.emit("crossSigning.keysChanged", {}); - await crypto._afterCrossSigningLocalKeyChange(); + crypto.baseApis.emit("crossSigning.keysChanged", {}); + await crypto.afterCrossSigningLocalKeyChange(); } export async function createSecretStorageKey() { diff --git a/src/crypto/EncryptionSetup.js b/src/crypto/EncryptionSetup.js index 8da30582489..1a7fcf36a14 100644 --- a/src/crypto/EncryptionSetup.js +++ b/src/crypto/EncryptionSetup.js @@ -121,18 +121,17 @@ export class EncryptionSetupBuilder { async persist(crypto) { // store private keys in cache if (this._crossSigningKeys) { - const cacheCallbacks = createCryptoStoreCacheCallbacks( - crypto._cryptoStore, crypto._olmDevice); + const cacheCallbacks = createCryptoStoreCacheCallbacks(crypto.cryptoStore, crypto.olmDevice); for (const type of ["master", "self_signing", "user_signing"]) { logger.log(`Cache ${type} cross-signing private key locally`); const privateKey = this.crossSigningCallbacks.privateKeys.get(type); await cacheCallbacks.storeCrossSigningKeyCache(type, privateKey); } // store own cross-sign pubkeys as trusted - await crypto._cryptoStore.doTxn( + await crypto.cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { - crypto._cryptoStore.storeCrossSigningKeys( + crypto.cryptoStore.storeCrossSigningKeys( txn, this._crossSigningKeys.keys); }, ); @@ -169,7 +168,7 @@ export class EncryptionSetupOperation { * @param {Crypto} crypto */ async apply(crypto) { - const baseApis = crypto._baseApis; + const baseApis = crypto.baseApis; // upload cross-signing keys if (this._crossSigningKeys) { const keys = {}; diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index e6d6e0b62e9..f457e6e6d3a 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -413,7 +413,7 @@ MegolmEncryption.prototype._prepareNewSession = async function(sharedHistory) { ); // don't wait for it to complete - this._crypto._backupManager.backupGroupSession( + this._crypto.backupManager.backupGroupSession( this._olmDevice.deviceCurve25519Key, sessionId, ); @@ -1424,7 +1424,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { }); }).then(() => { // don't wait for the keys to be backed up for the server - this._crypto._backupManager.backupGroupSession(senderKey, content.session_id); + this._crypto.backupManager.backupGroupSession(senderKey, content.session_id); }).catch((e) => { logger.error(`Error handling m.room_key_event: ${e}`); }); @@ -1460,14 +1460,14 @@ MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) { this.retryDecryptionFromSender(senderKey); return; } - let device = this._crypto._deviceList.getDeviceByIdentityKey( + let device = this._crypto.deviceList.getDeviceByIdentityKey( content.algorithm, senderKey, ); if (!device) { // if we don't know about the device, fetch the user's devices again // and retry before giving up await this._crypto.downloadKeys([sender], false); - device = this._crypto._deviceList.getDeviceByIdentityKey( + device = this._crypto.deviceList.getDeviceByIdentityKey( content.algorithm, senderKey, ); if (!device) { @@ -1640,7 +1640,7 @@ MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) { ).then(() => { if (opts.source !== "backup") { // don't wait for it to complete - this._crypto._backupManager.backupGroupSession( + this._crypto.backupManager.backupGroupSession( session.sender_key, session.session_id, ).catch((e) => { // This throws if the upload failed, but this is fine From 02afcc7d4b04461004c77076b44eca77eae39ad7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Jun 2021 15:48:21 +0100 Subject: [PATCH 09/10] delint --- src/crypto/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 14b37e322c7..930f5fbb635 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -2903,8 +2903,6 @@ export class Crypto extends EventEmitter { * @param {Object} syncData the data from the 'MatrixClient.sync' event */ public async onSyncCompleted(syncData: ISyncData): Promise { - const nextSyncToken = syncData.nextSyncToken; - this.deviceList.setSyncToken(syncData.nextSyncToken); this.deviceList.saveIfDirty(); From 3a5e4ffa91eb10974d2f6c17ebebe678e8b4cb27 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Jun 2021 15:54:48 +0100 Subject: [PATCH 10/10] Fix yet more underscored accesses --- spec/unit/crypto/algorithms/megolm.spec.js | 4 ++-- spec/unit/crypto/cross-signing.spec.js | 4 ++-- .../crypto/verification/secret_request.spec.js | 14 +++++++------- src/crypto/DeviceList.ts | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/spec/unit/crypto/algorithms/megolm.spec.js b/spec/unit/crypto/algorithms/megolm.spec.js index 6c6034b72c5..e7d63c6a201 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.js +++ b/spec/unit/crypto/algorithms/megolm.spec.js @@ -408,7 +408,7 @@ describe("MegolmDecryption", function() { "@bob:example.com", BOB_DEVICES, ); aliceClient.crypto.deviceList.downloadKeys = async function(userIds) { - return this._getDevicesFromStore(userIds); + return this.getDevicesFromStore(userIds); }; let run = false; @@ -512,7 +512,7 @@ describe("MegolmDecryption", function() { "@bob:example.com", BOB_DEVICES, ); aliceClient.crypto.deviceList.downloadKeys = async function(userIds) { - return this._getDevicesFromStore(userIds); + return this.getDevicesFromStore(userIds); }; aliceClient.claimOneTimeKeys = async () => { diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index 3118e636565..8638c1f4de9 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -222,7 +222,7 @@ describe("Cross Signing", function() { }); }); - const deviceInfo = alice.crypto.deviceList._devices["@alice:example.com"] + const deviceInfo = alice.crypto.deviceList.devices["@alice:example.com"] .Osborne2; const aliceDevice = { user_id: "@alice:example.com", @@ -444,7 +444,7 @@ describe("Cross Signing", function() { }); }); - const deviceInfo = alice.crypto.deviceList._devices["@alice:example.com"] + const deviceInfo = alice.crypto.deviceList.devices["@alice:example.com"] .Osborne2; const aliceDevice = { user_id: "@alice:example.com", diff --git a/spec/unit/crypto/verification/secret_request.spec.js b/spec/unit/crypto/verification/secret_request.spec.js index 8c957327313..f261afacffa 100644 --- a/spec/unit/crypto/verification/secret_request.spec.js +++ b/spec/unit/crypto/verification/secret_request.spec.js @@ -48,18 +48,18 @@ describe("self-verifications", () => { storeCrossSigningKeyCache: jest.fn(), }; - const _crossSigningInfo = new CrossSigningInfo( + const crossSigningInfo = new CrossSigningInfo( userId, {}, cacheCallbacks, ); - _crossSigningInfo.keys = { + crossSigningInfo.keys = { master: { keys: { X: testKeyPub } }, self_signing: { keys: { X: testKeyPub } }, user_signing: { keys: { X: testKeyPub } }, }; - const _secretStorage = { + const secretStorage = { request: jest.fn().mockReturnValue({ promise: Promise.resolve(encodeBase64(testKey)), }), @@ -70,12 +70,12 @@ describe("self-verifications", () => { const client = { crypto: { - _crossSigningInfo, - _secretStorage, + crossSigningInfo, + secretStorage, storeSessionBackupPrivateKey, getSessionBackupPrivateKey: () => null, }, - requestSecret: _secretStorage.request.bind(_secretStorage), + requestSecret: secretStorage.request.bind(secretStorage), getUserId: () => userId, getKeyBackupVersion: () => Promise.resolve({}), restoreKeyBackupWithCache, @@ -99,7 +99,7 @@ describe("self-verifications", () => { /* We should request, and store, 3 cross signing keys and the key backup key */ expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls.length).toBe(3); - expect(_secretStorage.request.mock.calls.length).toBe(4); + expect(secretStorage.request.mock.calls.length).toBe(4); expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[0][1]) .toEqual(testKey); diff --git a/src/crypto/DeviceList.ts b/src/crypto/DeviceList.ts index 3c330994fd5..f5ec71d1aa4 100644 --- a/src/crypto/DeviceList.ts +++ b/src/crypto/DeviceList.ts @@ -31,7 +31,7 @@ import { chunkPromises, defer, IDeferred, sleep } from '../utils'; import { MatrixClient, CryptoStore } from "../client"; import { OlmDevice } from "./OlmDevice"; -/* State transition diagram for DeviceList._deviceTrackingStatus +/* State transition diagram for DeviceList.deviceTrackingStatus * * | * stopTrackingDeviceList V @@ -51,7 +51,7 @@ import { OlmDevice } from "./OlmDevice"; * +----------------------- UP_TO_DATE ------------------------+ */ -// constants for DeviceList._deviceTrackingStatus +// constants for DeviceList.deviceTrackingStatus enum TrackingStatus { NotTracked, PendingDownload,