diff options
Diffstat (limited to 'includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/index.ts')
-rw-r--r-- | includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/index.ts | 3936 |
1 files changed, 0 insertions, 3936 deletions
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/index.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/index.ts deleted file mode 100644 index 68df6ca..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/index.ts +++ /dev/null @@ -1,3936 +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. -*/ - -import anotherjson from "another-json"; -import { v4 as uuidv4 } from "uuid"; - -import type { IDeviceKeys, IEventDecryptionResult, IMegolmSessionData, IOneTimeKey } from "../@types/crypto"; -import type { PkDecryption, PkSigning } from "@matrix-org/olm"; -import { EventType, ToDeviceMessageId } from "../@types/event"; -import { TypedReEmitter } from "../ReEmitter"; -import { logger } from "../logger"; -import { IExportedDevice, OlmDevice } from "./OlmDevice"; -import { IOlmDevice } from "./algorithms/megolm"; -import * as olmlib from "./olmlib"; -import { DeviceInfoMap, DeviceList } from "./DeviceList"; -import { DeviceInfo, IDevice } from "./deviceinfo"; -import type { DecryptionAlgorithm, EncryptionAlgorithm } from "./algorithms"; -import * as algorithms from "./algorithms"; -import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from "./CrossSigning"; -import { EncryptionSetupBuilder } from "./EncryptionSetup"; -import { - IAccountDataClient, - ISecretRequest, - SECRET_STORAGE_ALGORITHM_V1_AES, - SecretStorage, - SecretStorageKeyObject, - SecretStorageKeyTuple, -} from "./SecretStorage"; -import { - IAddSecretStorageKeyOpts, - ICreateSecretStorageOpts, - IEncryptedEventInfo, - IImportRoomKeysOpts, - IRecoveryKey, -} from "./api"; -import { OutgoingRoomKeyRequestManager } from "./OutgoingRoomKeyRequestManager"; -import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store"; -import { VerificationBase } from "./verification/Base"; -import { ReciprocateQRCode, SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD } from "./verification/QRCode"; -import { SAS as SASVerification } 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, Request } from "./verification/request/ToDeviceChannel"; -import { IllegalMethod } from "./verification/IllegalMethod"; -import { KeySignatureUploadError } from "../errors"; -import { calculateKeyCheck, decryptAES, encryptAES } from "./aes"; -import { DehydrationManager } from "./dehydration"; -import { BackupManager } from "./backup"; -import { IStore } from "../store"; -import { Room, RoomEvent } from "../models/room"; -import { RoomMember, RoomMemberEvent } from "../models/room-member"; -import { EventStatus, IEvent, MatrixEvent, MatrixEventEvent } from "../models/event"; -import { ToDeviceBatch } from "../models/ToDeviceMessage"; -import { - ClientEvent, - ICrossSigningKey, - IKeysUploadResponse, - ISignedKey, - IUploadKeySignaturesResponse, - MatrixClient, -} from "../client"; -import type { IRoomEncryption, RoomList } from "./RoomList"; -import { IKeyBackupInfo } from "./keybackup"; -import { ISyncStateData } from "../sync"; -import { CryptoStore } from "./store/base"; -import { IVerificationChannel } from "./verification/request/Channel"; -import { TypedEventEmitter } from "../models/typed-event-emitter"; -import { IContent } from "../models/event"; -import { ISyncResponse, IToDeviceEvent } from "../sync-accumulator"; -import { ISignatures } from "../@types/signed"; -import { IMessage } from "./algorithms/olm"; -import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend"; -import { RoomState, RoomStateEvent } from "../models/room-state"; -import { MapWithDefault, recursiveMapToObject } from "../utils"; -import { SecretStorageKeyDescription } from "../secret-storage"; - -const DeviceVerification = DeviceInfo.DeviceVerification; - -const defaultVerificationMethods = { - [ReciprocateQRCode.NAME]: ReciprocateQRCode, - [SASVerification.NAME]: SASVerification, - - // 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, -} as const; - -/** - * verification method names - */ -// legacy export identifier -export const verificationMethods = { - RECIPROCATE_QR_CODE: ReciprocateQRCode.NAME, - SAS: SASVerification.NAME, -} as const; - -export type VerificationMethod = keyof typeof verificationMethods | string; - -export function isCryptoAvailable(): boolean { - return Boolean(global.Olm); -} - -const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000; - -interface IInitOpts { - exportedOlmDevice?: IExportedDevice; - pickleKey?: string; -} - -export interface IBootstrapCrossSigningOpts { - /** Optional. Reset even if keys already exist. */ - setupNewCrossSigning?: boolean; - /** - * 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. - */ - authUploadDeviceSigningKeys?(makeRequest: (authData: any) => Promise<{}>): Promise<void>; -} - -export interface ICryptoCallbacks { - getCrossSigningKey?: (keyType: string, pubKey: string) => Promise<Uint8Array | null>; - saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void; - shouldUpgradeDeviceVerifications?: (users: Record<string, any>) => Promise<string[]>; - getSecretStorageKey?: ( - keys: { keys: Record<string, SecretStorageKeyDescription> }, - name: string, - ) => Promise<[string, Uint8Array] | null>; - cacheSecretStorageKey?: (keyId: string, keyInfo: SecretStorageKeyDescription, key: Uint8Array) => void; - onSecretRequested?: ( - userId: string, - deviceId: string, - requestId: string, - secretName: string, - deviceTrust: DeviceTrustLevel, - ) => Promise<string | undefined>; - getDehydrationKey?: ( - keyInfo: SecretStorageKeyDescription, - checkFunc: (key: Uint8Array) => void, - ) => Promise<Uint8Array>; - getBackupKey?: () => Promise<Uint8Array>; -} - -/* eslint-disable camelcase */ -interface IRoomKey { - room_id: string; - algorithm: string; -} - -/** - * 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. - */ -export interface IRoomKeyRequestBody extends IRoomKey { - session_id: string; - sender_key: string; -} - -/* eslint-enable camelcase */ - -interface IDeviceVerificationUpgrade { - devices: DeviceInfo[]; - crossSigningInfo: CrossSigningInfo; -} - -export interface ICheckOwnCrossSigningTrustOpts { - allowPrivateKeyRequests?: boolean; -} - -interface IUserOlmSession { - deviceIdKey: string; - sessions: { - sessionId: string; - hasReceivedMessage: boolean; - }[]; -} - -export interface IRoomKeyRequestRecipient { - userId: string; - deviceId: string; -} - -interface ISignableObject { - signatures?: ISignatures; - unsigned?: object; -} - -export interface IRequestsMap { - getRequest(event: MatrixEvent): VerificationRequest | undefined; - getRequestByChannel(channel: IVerificationChannel): VerificationRequest | undefined; - setRequest(event: MatrixEvent, request: VerificationRequest): void; - setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void; -} - -/* eslint-disable camelcase */ -export interface IOlmEncryptedContent { - algorithm: typeof olmlib.OLM_ALGORITHM; - sender_key: string; - ciphertext: Record<string, IMessage>; - [ToDeviceMessageId]?: string; -} - -export interface IMegolmEncryptedContent { - algorithm: typeof olmlib.MEGOLM_ALGORITHM; - sender_key: string; - session_id: string; - device_id: string; - ciphertext: string; - [ToDeviceMessageId]?: string; -} -/* eslint-enable camelcase */ - -export type IEncryptedContent = IOlmEncryptedContent | IMegolmEncryptedContent; - -export enum CryptoEvent { - DeviceVerificationChanged = "deviceVerificationChanged", - UserTrustStatusChanged = "userTrustStatusChanged", - UserCrossSigningUpdated = "userCrossSigningUpdated", - RoomKeyRequest = "crypto.roomKeyRequest", - RoomKeyRequestCancellation = "crypto.roomKeyRequestCancellation", - KeyBackupStatus = "crypto.keyBackupStatus", - KeyBackupFailed = "crypto.keyBackupFailed", - KeyBackupSessionsRemaining = "crypto.keyBackupSessionsRemaining", - KeySignatureUploadFailure = "crypto.keySignatureUploadFailure", - VerificationRequest = "crypto.verification.request", - Warning = "crypto.warning", - WillUpdateDevices = "crypto.willUpdateDevices", - DevicesUpdated = "crypto.devicesUpdated", - KeysChanged = "crossSigning.keysChanged", -} - -export type CryptoEventHandlerMap = { - /** - * Fires when a device is marked as verified/unverified/blocked/unblocked by - * {@link MatrixClient#setDeviceVerified|MatrixClient.setDeviceVerified} or - * {@link MatrixClient#setDeviceBlocked|MatrixClient.setDeviceBlocked}. - * - * @param userId - the owner of the verified device - * @param deviceId - the id of the verified device - * @param deviceInfo - updated device information - */ - [CryptoEvent.DeviceVerificationChanged]: (userId: string, deviceId: string, device: DeviceInfo) => void; - /** - * Fires when the trust status of a user changes - * If userId is the userId of the logged-in user, this indicated a change - * in the trust status of the cross-signing data on the account. - * - * The cross-signing API is currently UNSTABLE and may change without notice. - * @experimental - * - * @param userId - the userId of the user in question - * @param trustLevel - The new trust level of the user - */ - [CryptoEvent.UserTrustStatusChanged]: (userId: string, trustLevel: UserTrustLevel) => void; - /** - * Fires when we receive a room key request - * - * @param req - request details - */ - [CryptoEvent.RoomKeyRequest]: (request: IncomingRoomKeyRequest) => void; - /** - * Fires when we receive a room key request cancellation - */ - [CryptoEvent.RoomKeyRequestCancellation]: (request: IncomingRoomKeyRequestCancellation) => void; - /** - * Fires whenever the status of e2e key backup changes, as returned by getKeyBackupEnabled() - * @param enabled - true if key backup has been enabled, otherwise false - * @example - * ``` - * matrixClient.on("crypto.keyBackupStatus", function(enabled){ - * if (enabled) { - * [...] - * } - * }); - * ``` - */ - [CryptoEvent.KeyBackupStatus]: (enabled: boolean) => void; - [CryptoEvent.KeyBackupFailed]: (errcode: string) => void; - [CryptoEvent.KeyBackupSessionsRemaining]: (remaining: number) => void; - [CryptoEvent.KeySignatureUploadFailure]: ( - failures: IUploadKeySignaturesResponse["failures"], - source: "checkOwnCrossSigningTrust" | "afterCrossSigningLocalKeyChange" | "setDeviceVerification", - upload: (opts: { shouldEmit: boolean }) => Promise<void>, - ) => void; - /** - * Fires when a key verification is requested. - */ - [CryptoEvent.VerificationRequest]: (request: VerificationRequest<any>) => void; - /** - * Fires when the app may wish to warn the user about something related - * the end-to-end crypto. - * - * @param type - One of the strings listed above - */ - [CryptoEvent.Warning]: (type: string) => void; - /** - * Fires when the user's cross-signing keys have changed or cross-signing - * has been enabled/disabled. The client can use getStoredCrossSigningForUser - * with the user ID of the logged in user to check if cross-signing is - * enabled on the account. If enabled, it can test whether the current key - * is trusted using with checkUserTrust with the user ID of the logged - * in user. The checkOwnCrossSigningTrust function may be used to reconcile - * the trust in the account key. - * - * The cross-signing API is currently UNSTABLE and may change without notice. - * @experimental - */ - [CryptoEvent.KeysChanged]: (data: {}) => void; - /** - * Fires whenever the stored devices for a user will be updated - * @param users - A list of user IDs that will be updated - * @param initialFetch - If true, the store is empty (apart - * from our own device) and is being seeded. - */ - [CryptoEvent.WillUpdateDevices]: (users: string[], initialFetch: boolean) => void; - /** - * Fires whenever the stored devices for a user have changed - * @param users - A list of user IDs that were updated - * @param initialFetch - If true, the store was empty (apart - * from our own device) and has been seeded. - */ - [CryptoEvent.DevicesUpdated]: (users: string[], initialFetch: boolean) => void; - [CryptoEvent.UserCrossSigningUpdated]: (userId: string) => void; -}; - -export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap> implements CryptoBackend { - /** - * @returns The version of Olm. - */ - public static getOlmVersion(): [number, number, number] { - 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: TypedReEmitter<CryptoEvent, CryptoEventHandlerMap>; - private readonly verificationMethods: Map<VerificationMethod, typeof VerificationBase>; - public readonly supportedAlgorithms: string[]; - private readonly outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager; - private readonly toDeviceVerificationRequests: ToDeviceRequests; - public 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 = null; - private oneTimeKeyCheckInProgress = false; - - // EncryptionAlgorithm instance for each room - private roomEncryptors = new Map<string, EncryptionAlgorithm>(); - // map from algorithm to DecryptionAlgorithm instance, for each room - private roomDecryptors = new Map<string, Map<string, DecryptionAlgorithm>>(); - - private deviceKeys: Record<string, string> = {}; // type: key - - public globalBlacklistUnverifiedDevices = false; - public 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: { [roomId: string]: Promise<void> } = {}; - - // The timestamp of the last time we forced establishment - // of a new session for each device, in milliseconds. - // { - // userId: { - // deviceId: 1234567890000, - // }, - // } - // Map: user Id → device Id → timestamp - private lastNewSessionForced: MapWithDefault<string, MapWithDefault<string, number>> = new MapWithDefault( - () => new MapWithDefault(() => 0), - ); - - // 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; - private fallbackCleanup?: ReturnType<typeof setTimeout>; - - /** - * Cryptography bits - * - * This module is internal to the js-sdk; the public API is via MatrixClient. - * - * @internal - * - * @param baseApis - base matrix api interface - * - * @param userId - The user ID for the local user - * - * @param deviceId - The identifier for this device. - * - * @param clientStore - the MatrixClient data store. - * - * @param cryptoStore - storage for the crypto layer. - * - * @param roomList - An initialised RoomList object - * - * @param 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. - */ - public constructor( - public readonly baseApis: MatrixClient, - public readonly userId: string, - private readonly deviceId: string, - private readonly clientStore: IStore, - public readonly cryptoStore: CryptoStore, - private readonly roomList: RoomList, - verificationMethods: Array<VerificationMethod | (typeof VerificationBase & { NAME: string })>, - ) { - super(); - this.reEmitter = new TypedReEmitter(this); - - if (verificationMethods) { - this.verificationMethods = new Map(); - for (const method of verificationMethods) { - if (typeof method === "string") { - if (defaultVerificationMethods[method]) { - this.verificationMethods.set( - method, - <typeof VerificationBase>defaultVerificationMethods[method], - ); - } - } else if (method["NAME"]) { - this.verificationMethods.set(method["NAME"], method as typeof VerificationBase); - } else { - logger.warn(`Excluding unknown verification method ${method}`); - } - } - } else { - this.verificationMethods = new Map(Object.entries(defaultVerificationMethods)) as Map< - VerificationMethod, - typeof VerificationBase - >; - } - - 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 keys = await this.getSecretStorageKey(); - await this.storeSecret("m.megolm_backup.v1", fixedKey, [keys![0]]); - } - - return olmlib.decodeBase64(fixedKey || storedKey); - } - - // try to get key from app - if (this.baseApis.cryptoCallbacks && this.baseApis.cryptoCallbacks.getBackupKey) { - return 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(CryptoEvent.UserCrossSigningUpdated, this.onDeviceListUserCrossSigningUpdated); - this.reEmitter.reEmit(this.deviceList, [CryptoEvent.DevicesUpdated, CryptoEvent.WillUpdateDevices]); - - this.supportedAlgorithms = Array.from(algorithms.DECRYPTION_CLASSES.keys()); - - 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); - // Yes, we pass the client twice here: see SecretStorage - this.secretStorage = new SecretStorage(baseApis as IAccountDataClient, cryptoCallbacks, baseApis); - this.dehydrationManager = new DehydrationManager(this); - - // Assuming no app-supplied callback, default to getting from SSSS. - if (!cryptoCallbacks.getCrossSigningKey && cryptoCallbacks.getSecretStorageKey) { - cryptoCallbacks.getCrossSigningKey = async (type): Promise<Uint8Array | null> => { - 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 exportedOlmDevice - (Optional) data from exported device - * that must be re-created. - */ - public async init({ exportedOlmDevice, pickleKey }: IInitOpts = {}): Promise<void> { - 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 - * - * @returns 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 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(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj); - } - } - } - } - - /** - * Create a recovery key from a user-supplied passphrase. - * - * @param password - Passphrase string that can be entered by the user - * when restoring the backup as an alternative to entering the recovery key. - * Optional. - * @returns 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<IRecoveryKey> { - const decryption = new global.Olm.PkDecryption(); - try { - const keyInfo: Partial<IRecoveryKey["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: keyInfo as IRecoveryKey["keyInfo"], - encodedPrivateKey, - privateKey, - }; - } finally { - decryption?.free(); - } - } - - /** - * Checks if the user has previously published cross-signing keys - * - * This means downloading the devicelist for the user and checking if the list includes - * the cross-signing pseudo-device. - * - * @internal - */ - public async userHasCrossSigningKeys(): Promise<boolean> { - await this.downloadKeys([this.userId]); - return this.deviceList.getStoredCrossSigningForUser(this.userId) !== null; - } - - /** - * 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. - * - * @returns True if cross-signing is ready to be used on this device - */ - public async isCrossSigningReady(): Promise<boolean> { - 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. - * - * @returns True if secret storage is ready to be used on this device - */ - public async isSecretStorageReady(): Promise<boolean> { - const secretStorageKeyInAccount = await this.secretStorage.hasKey(); - const privateKeysInStorage = await this.crossSigningInfo.isStoredInSecretStorage(this.secretStorage); - const sessionBackupInStorage = - !this.backupManager.getKeyBackupEnabled() || (await 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 authUploadDeviceSigningKeys - Function - * called to await an interactive auth flow when uploading device signing keys. - * @param setupNewCrossSigning - Optional. Reset even if keys - * already exist. - * Args: - * 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<void> { - 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 (): Promise<void> => { - 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, - undefined, - ); - 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 createSecretStorageKey - Optional. Function - * called to await a secret storage key creation flow. - * Returns a Promise which resolves to an 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 keyBackupInfo - The current key backup object. If passed, - * the passphrase and recovery key from this backup will be used. - * @param 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 setupNewSecretStorage - Optional. Reset even if keys already exist. - * @param 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: - * 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 (): Promise<IRecoveryKey> => ({} as IRecoveryKey), - keyBackupInfo, - setupNewKeyBackup, - setupNewSecretStorage, - getKeyBackupPassphrase, - }: ICreateSecretStorageOpts = {}): Promise<void> { - 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, - undefined, - ); - - // the ID of the new SSSS key, if we create one - let newKeyId: string | null = null; - - // create a new SSSS key and set it as default - const createSSSS = async (opts: IAddSecretStorageKeyOpts, privateKey?: Uint8Array): Promise<string> => { - 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: string, keyInfo: SecretStorageKeyDescription): Promise<void> => { - 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 calculateKeyCheck(privateKey); - keyInfo.iv = iv; - keyInfo.mac = mac; - - await builder.setAccountData(`m.secret_storage.key.${keyId}`, keyInfo); - } - } - }; - - const signKeyBackupWithCrossSigning = async (keyBackupAuthData: IKeyBackupInfo["auth_data"]): Promise<void> => { - 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 = {} as IAddSecretStorageKeyOpts, 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 = {} as IAddSecretStorageKeyOpts; - - 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: IKeyBackupInfo = { - 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) { - const keyId = newKeyId || oldKeyId; - await secretStorage.store("m.megolm_backup.v1", fixedBackupKey, keyId ? [keyId] : null); - } - const decodedBackupKey = new Uint8Array(olmlib.decodeBase64(fixedBackupKey || sessionBackupKey)); - builder.addSessionBackupPrivateKeyToCache(decodedBackupKey); - } else if (this.backupManager.getKeyBackupEnabled()) { - // key backup is enabled but we don't have a session backup key in SSSS: see if we have one in - // the cache or the user can provide one, and if so, write it to SSSS - const backupKey = (await this.getSessionBackupPrivateKey()) || (await getKeyBackupPassphrase?.()); - if (!backupKey) { - // This will require user intervention to recover from since we don't have the key - // backup key anywhere. The user should probably just set up a new key backup and - // the key for the new backup will be stored. If we hit this scenario in the wild - // with any frequency, we should do more than just log an error. - logger.error("Key backup is enabled but couldn't get key backup key!"); - return; - } - logger.info("Got session backup key from cache/user that wasn't in SSSS: saving to SSSS"); - await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(backupKey)); - } - - 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: IAddSecretStorageKeyOpts, - keyID?: string, - ): Promise<SecretStorageKeyObject> { - return this.secretStorage.addKey(algorithm, opts, keyID); - } - - public hasSecretStorageKey(keyID?: string): Promise<boolean> { - return this.secretStorage.hasKey(keyID); - } - - public getSecretStorageKey(keyID?: string): Promise<SecretStorageKeyTuple | null> { - return this.secretStorage.getKey(keyID); - } - - public storeSecret(name: string, secret: string, keys?: string[]): Promise<void> { - return this.secretStorage.store(name, secret, keys); - } - - public getSecret(name: string): Promise<string | undefined> { - return this.secretStorage.get(name); - } - - public isSecretStored(name: string): Promise<Record<string, SecretStorageKeyDescription> | null> { - return this.secretStorage.isStored(name); - } - - public requestSecret(name: string, devices: string[]): ISecretRequest { - if (!devices) { - devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(this.userId)); - } - return this.secretStorage.request(name, devices); - } - - public getDefaultSecretStorageKeyId(): Promise<string | null> { - return this.secretStorage.getDefaultKeyId(); - } - - public setDefaultSecretStorageKeyId(k: string): Promise<void> { - return this.secretStorage.setDefaultKeyId(k); - } - - public checkSecretStorageKey(key: Uint8Array, info: SecretStorageKeyDescription): Promise<boolean> { - 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 privateKey - The private key - * @param expectedPublicKey - The public key - * @returns true if the key matches, otherwise false - */ - public checkSecretStoragePrivateKey(privateKey: Uint8Array, expectedPublicKey: string): boolean { - let decryption: PkDecryption | null = 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 { - decryption?.free(); - } - } - - /** - * Fetches the backup private key, if cached - * @returns the key, if any, or null - */ - public async getSessionBackupPrivateKey(): Promise<Uint8Array | null> { - let key = await new Promise<any>((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 key - the private key - * @returns a promise so you can catch failures - */ - public async storeSessionBackupPrivateKey(key: ArrayLike<number>): Promise<void> { - if (!(key instanceof Uint8Array)) { - // eslint-disable-next-line @typescript-eslint/no-base-to-string - throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`); - } - const pickleKey = Buffer.from(this.olmDevice.pickleKey); - const encryptedKey = 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", encryptedKey); - }); - } - - /** - * 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 privateKey - The private key - * @param expectedPublicKey - The public key - * @returns true if the key matches, otherwise false - */ - public checkCrossSigningPrivateKey(privateKey: Uint8Array, expectedPublicKey: string): boolean { - let signing: PkSigning | null = 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 { - 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<void> { - 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 = false }): Promise<void> => { - 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( - CryptoEvent.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: Record<string, IDeviceVerificationUpgrade> = {}; - 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 userId - the user whose cross-signing information is to be checked - * @param crossSigningInfo - the cross-signing information to check - */ - private async checkForDeviceVerificationUpgrade( - userId: string, - crossSigningInfo: CrossSigningInfo, - ): Promise<IDeviceVerificationUpgrade | undefined> { - // 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.isVerified()) { - 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 userId - the user ID whose key is being checked - * @param key - the key that is being checked - * @param devices - the user's devices. Should be a map from device ID - * to device info - */ - private async checkForValidDeviceSignature( - userId: string, - key: ICrossSigningKey, - devices: Record<string, IDevice>, - ): Promise<string[]> { - 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 type - The type of key to get the ID of. One of - * "master", "self_signing", or "user_signing". Defaults to "master". - * - * @returns the key ID - */ - public getCrossSigningId(type: string): string | null { - return this.crossSigningInfo.getId(type); - } - - /** - * Get the cross signing information for a given user. - * - * @param userId - the user ID to get the cross-signing info for. - * - * @returns the cross signing information for the user. - */ - public getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null { - return this.deviceList.getStoredCrossSigningForUser(userId); - } - - /** - * Check whether a given user is trusted. - * - * @param userId - The ID of the user to check. - * - * @returns - */ - 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 userId - The ID of the user whose devices is to be checked. - * @param deviceId - The ID of the device to check - * - * @returns - */ - 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 userId - The ID of the user whose devices is to be checked. - * @param device - The device info object to check - * - * @returns - */ - public checkDeviceInfoTrust(userId: string, device?: DeviceInfo): DeviceTrustLevel { - const trustedLocally = !!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); - } - } - - /** - * Check whether one of our own devices is cross-signed by our - * user's stored keys, regardless of whether we trust those keys yet. - * - * @param deviceId - The ID of the device to check - * - * @returns true if the device is cross-signed - */ - public checkIfOwnDeviceCrossSigned(deviceId: string): boolean { - const device = this.deviceList.getStoredDevice(this.userId, deviceId); - if (!device) return false; - const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(this.userId); - return ( - userCrossSigning?.checkDeviceTrust(userCrossSigning, device, false, true).isCrossSigningVerified() ?? false - ); - } - - /* - * Event handler for DeviceList's userNewDevices event - */ - private onDeviceListUserCrossSigningUpdated = async (userId: string): Promise<void> => { - 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(CryptoEvent.KeysChanged, {}); - // as the trust for our own user has changed, - // also emit an event for this - this.emit(CryptoEvent.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(CryptoEvent.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. - */ - public async checkOwnCrossSigningTrust({ - allowPrivateKeyRequests = false, - }: ICheckOwnCrossSigningTrustOpts = {}): Promise<void> { - 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: PkSigning | null = 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 { - 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: Record<string, ISignedKey> = {}; - - 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: PkSigning | null = 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 { - 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: PkSigning | null = 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 { - 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({} as ISignedKey, masterKey, { - signatures: { - [this.userId]: { - ["ed25519:" + this.deviceId]: deviceSig, - }, - }, - }); - } - - const keysToUpload = Object.keys(keySignatures); - if (keysToUpload.length) { - const upload = ({ shouldEmit = false }): Promise<void> => { - 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( - CryptoEvent.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(CryptoEvent.UserTrustStatusChanged, userId, this.checkUserTrust(userId)); - - if (masterChanged) { - this.emit(CryptoEvent.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 keys - The new trusted set of keys - */ - private async storeTrustedSelfKeys(keys: Record<string, ICrossSigningKey> | null): Promise<void> { - 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 userId - the user ID whose key should be checked - */ - private async checkDeviceVerifications(userId: string): Promise<void> { - 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 enableLazyLoading(): void { - this.lazyLoadMembers = true; - } - - /** - * Tell the crypto module to register for MatrixClient events which it needs to - * listen for - * - * @param eventEmitter - event source where we can register - * for event notifications - */ - public registerEventHandlers( - eventEmitter: TypedEventEmitter< - RoomMemberEvent.Membership | ClientEvent.ToDeviceEvent | RoomEvent.Timeline | MatrixEventEvent.Decrypted, - any - >, - ): void { - eventEmitter.on(RoomMemberEvent.Membership, this.onMembership); - eventEmitter.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent); - eventEmitter.on(RoomEvent.Timeline, this.onTimelineEvent); - eventEmitter.on(MatrixEventEvent.Decrypted, this.onTimelineEvent); - } - - /** - * @deprecated this does nothing and will be removed in a future version - */ - public start(): void { - logger.warn("MatrixClient.crypto.start() is deprecated"); - } - - /** 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 - * - * @returns base64-encoded ed25519 key. - */ - public getDeviceEd25519Key(): string | null { - return this.olmDevice.deviceEd25519Key; - } - - /** - * Get the Curve25519 key for this device - * - * @returns base64-encoded curve25519 key. - */ - public getDeviceCurve25519Key(): string | null { - 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 value - whether to blacklist all unverified devices by default - * - * @deprecated For external code, use {@link MatrixClient#setGlobalBlacklistUnverifiedDevices}. For - * internal code, set {@link MatrixClient#globalBlacklistUnverifiedDevices} directly. - */ - public setGlobalBlacklistUnverifiedDevices(value: boolean): void { - this.globalBlacklistUnverifiedDevices = value; - } - - /** - * @returns whether to blacklist all unverified devices by default - * - * @deprecated For external code, use {@link MatrixClient#getGlobalBlacklistUnverifiedDevices}. For - * internal code, reference {@link MatrixClient#globalBlacklistUnverifiedDevices} directly. - */ - public getGlobalBlacklistUnverifiedDevices(): boolean { - return this.globalBlacklistUnverifiedDevices; - } - - /** - * Upload the device keys to the homeserver. - * @returns A promise that will resolve when the keys are uploaded. - */ - public uploadDeviceKeys(): Promise<IKeysUploadResponse> { - 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 as Required<IDeviceKeys>, - }); - }); - } - - /** - * 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 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): void { - 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(): void { - // 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): Promise<void> => { - 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()) { - const fallbackKeys = await this.olmDevice.getFallbackKey(); - // if fallbackKeys is non-empty, we've already generated a - // fallback key, but it hasn't been published yet, so we - // can use that instead of generating a new one - if (!fallbackKeys.curve25519 || Object.keys(fallbackKeys.curve25519).length == 0) { - logger.info("generating fallback key"); - if (this.fallbackCleanup) { - // cancel any pending fallback cleanup because generating - // a new fallback key will already drop the old fallback - // that would have been dropped, and we don't want to kill - // the current key - clearTimeout(this.fallbackCleanup); - delete this.fallbackCleanup; - } - 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(): Promise<IKeysUploadResponse> { - const promises: Promise<unknown>[] = []; - - let fallbackJson: Record<string, IOneTimeKey> | undefined; - if (this.getNeedsNewFallback()) { - fallbackJson = {}; - 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: Record<string, { key: string }> = {}; - - 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 requestBody: Record<string, any> = { - one_time_keys: oneTimeJson, - }; - - if (fallbackJson) { - requestBody["org.matrix.msc2732.fallback_keys"] = fallbackJson; - requestBody["fallback_keys"] = fallbackJson; - } - - const res = await this.baseApis.uploadKeysRequest(requestBody); - - if (fallbackJson) { - this.fallbackCleanup = setTimeout(() => { - delete this.fallbackCleanup; - this.olmDevice.forgetOldFallbackKey(); - }, 60 * 60 * 1000); - } - - await this.olmDevice.markKeysAsPublished(); - return res; - } - - /** - * Download the keys for a list of users and stores the keys in the session - * store. - * @param userIds - The users to fetch. - * @param forceDownload - Always download the keys even if cached. - * - * @returns A promise which resolves to a map `userId->deviceId->{@link DeviceInfo}`. - */ - public downloadKeys(userIds: string[], forceDownload?: boolean): Promise<DeviceInfoMap> { - return this.deviceList.downloadKeys(userIds, !!forceDownload); - } - - /** - * Get the stored device keys for a user id - * - * @param userId - the user to list keys for. - * - * @returns 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<DeviceInfo> | null { - return this.deviceList.getStoredDevicesForUser(userId); - } - - /** - * Get the stored keys for a single device - * - * - * @returns 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 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. - * - * @returns 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<boolean> { - return this.deviceList.saveIfDirty(delay); - } - - /** - * Update the blocked/verified state of the given device - * - * @param userId - owner of the device - * @param deviceId - unique identifier for the device or user's - * cross-signing public key ID. - * - * @param verified - whether to mark the device as verified. Null to - * leave unchanged. - * - * @param blocked - whether to mark the device as blocked. Null to - * leave unchanged. - * - * @param known - whether to mark that the user has been made aware of - * the existence of this device. Null to leave unchanged - * - * @param keys - The list of keys that was present - * during the device verification. This will be double checked with the list - * of keys the given device has currently. - * - * @returns updated DeviceInfo - */ - public async setDeviceVerification( - userId: string, - deviceId: string, - verified: boolean | null = null, - blocked: boolean | null = null, - known: boolean | null = null, - keys?: Record<string, string>, - ): Promise<DeviceInfo | CrossSigningInfo> { - // 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"); - } - const gotKeyId = keys ? Object.values(keys)[0] : null; - if (keys && (Object.values(keys).length !== 1 || gotKeyId !== xsk.getId())) { - throw new Error(`Key did not match expected value: expected ${xsk.getId()}, got ${gotKeyId}`); - } - - 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(CryptoEvent.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 = false }): Promise<void> => { - 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( - CryptoEvent.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 as any; // TODO types - } 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) { - if (keys) { - for (const [keyId, key] of Object.entries(keys)) { - if (dev.keys[keyId] !== key) { - throw new Error(`Key did not match expected value: expected ${key}, got ${dev.keys[keyId]}`); - } - } - } - 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: ISignedKey | undefined; - 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 = false }): Promise<void> => { - 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( - CryptoEvent.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(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj); - return deviceObj; - } - - public findVerificationRequestDMInProgress(roomId: string): VerificationRequest | undefined { - return this.inRoomVerificationRequests.findRequestInProgress(roomId); - } - - public getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest[] { - return this.toDeviceVerificationRequests.getRequestsInProgress(userId); - } - - public requestVerificationDM(userId: string, roomId: string): Promise<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[]): Promise<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: IVerificationChannel, - requestsMap: IRequestsMap, - ): Promise<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 = null, - ): VerificationBase<any, any> { - let request: Request | undefined; - 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: VerificationMethod, - ): Promise<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 - * <p> - * 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 OlmDevice#getSessionInfoForDevice}). - * <p> - * This method is provided for debugging purposes. - * - * @param userId - id of user to inspect - */ - public async getOlmSessionsForUser(userId: string): Promise<Record<string, IUserOlmSession>> { - const devices = this.getStoredDevicesForUser(userId) || []; - const result: { [deviceId: string]: IUserOlmSession } = {}; - for (const device of devices) { - 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 event - event to be checked - */ - public getEventSenderDeviceInfo(event: MatrixEvent): DeviceInfo | null { - const senderKey = event.getSenderKey(); - const algorithm = event.getWireContent().algorithm; - - if (!senderKey || !algorithm) { - 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 event - event to be checked - * - * @returns 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<IEncryptedEventInfo> = {}; - - ret.senderKey = event.getSenderKey() ?? undefined; - ret.algorithm = event.getWireContent().algorithm; - - if (!ret.senderKey || !ret.algorithm) { - ret.encrypted = false; - return ret as IEncryptedEventInfo; - } - ret.encrypted = true; - - if (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) ?? undefined; - - // 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 roomId - The ID of the room to discard the session for - * - * This should not normally be necessary. - */ - public forceDiscardSession(roomId: string): Promise<void> { - const alg = this.roomEncryptors.get(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(); - return Promise.resolve(); - } - - /** - * Configure a room to use encryption (ie, save a flag in the cryptoStore). - * - * @param roomId - The room ID to enable encryption in. - * - * @param config - The encryption config for the room. - * - * @param 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. - * - * @deprecated It is normally incorrect to call this method directly. Encryption - * is enabled by receiving an `m.room.encryption` event (which we may have sent - * previously). - */ - public async setRoomEncryption( - roomId: string, - config: IRoomEncryption, - inhibitDeviceQuery?: boolean, - ): Promise<void> { - const room = this.clientStore.getRoom(roomId); - if (!room) { - throw new Error(`Unable to enable encryption tracking devices in unknown room ${roomId}`); - } - await this.setRoomEncryptionImpl(room, config); - if (!this.lazyLoadMembers && !inhibitDeviceQuery) { - this.deviceList.refreshOutdatedDeviceLists(); - } - } - - /** - * Set up encryption for a room. - * - * This is called when an <tt>m.room.encryption</tt> event is received. It saves a flag - * for the room in the cryptoStore (if it wasn't already set), sets up an "encryptor" for - * the room, and enables device-list tracking for the room. - * - * It does <em>not</em> initiate a device list query for the room. That is normally - * done once we finish processing the sync, in onSyncCompleted. - * - * @param room - The room to enable encryption in. - * @param config - The encryption config for the room. - */ - private async setRoomEncryptionImpl(room: Room, config: IRoomEncryption): Promise<void> { - const roomId = room.roomId; - - // 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.get(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: Promise<void> | null = null; - if (!existingConfig) { - storeConfigPromise = this.roomList.setRoomEncryption(roomId, config); - } - - const AlgClass = algorithms.ENCRYPTION_CLASSES.get(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, - config, - }); - this.roomEncryptors.set(roomId, alg); - - if (storeConfigPromise) { - await storeConfigPromise; - } - - logger.log(`Enabling encryption in ${roomId}`); - - // we don't want to force a download of the full membership list of this room, but as soon as we have that - // list we can start tracking the device list. - if (room.membersLoaded()) { - await this.trackRoomDevicesImpl(room); - } else { - // wait for the membership list to be loaded - const onState = (_state: RoomState): void => { - room.off(RoomStateEvent.Update, onState); - if (room.membersLoaded()) { - this.trackRoomDevicesImpl(room).catch((e) => { - logger.error(`Error enabling device tracking in ${roomId}`, e); - }); - } - }; - room.on(RoomStateEvent.Update, onState); - } - } - - /** - * Make sure we are tracking the device lists for all users in this room. - * - * @param roomId - The room ID to start tracking devices in. - * @returns when all devices for the room have been fetched and marked to track - * @deprecated there's normally no need to call this function: device list tracking - * will be enabled as soon as we have the full membership list. - */ - public trackRoomDevices(roomId: string): Promise<void> { - const room = this.clientStore.getRoom(roomId); - if (!room) { - throw new Error(`Unable to start tracking devices in unknown room ${roomId}`); - } - return this.trackRoomDevicesImpl(room); - } - - /** - * Make sure we are tracking the device lists for all users in this room. - * - * This is normally called when we are about to send an encrypted event, to make sure - * we have all the devices in the room; but it is also called when processing an - * m.room.encryption state event (if lazy-loading is disabled), or when members are - * loaded (if lazy-loading is enabled), to prepare the device list. - * - * @param room - Room to enable device-list tracking in - */ - private trackRoomDevicesImpl(room: Room): Promise<void> { - const roomId = room.roomId; - const trackMembers = async (): Promise<void> => { - // not an encrypted room - if (!this.roomEncryptors.has(roomId)) { - return; - } - 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) => { - delete this.roomDeviceTrackingState[roomId]; - throw err; - }); - } - return promise; - } - - /** - * Try to make sure we have established olm sessions for all known devices for - * the given users. - * - * @param users - list of user ids - * @param force - If true, force a new Olm session to be created. Default false. - * - * @returns resolves once the sessions are complete, to - * an Object mapping from userId to deviceId to - * {@link OlmSessionResult} - */ - public ensureOlmSessionsForUsers( - users: string[], - force?: boolean, - ): Promise<Map<string, Map<string, olmlib.IOlmSessionResult>>> { - // map user Id → DeviceInfo[] - const devicesByUser: Map<string, DeviceInfo[]> = new Map(); - - for (const userId of users) { - const userDevices: DeviceInfo[] = []; - devicesByUser.set(userId, userDevices); - - const devices = this.getStoredDevicesForUser(userId) || []; - for (const deviceInfo of devices) { - 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; - } - - userDevices.push(deviceInfo); - } - } - - return olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, force); - } - - /** - * Get a list containing all of the room keys - * - * @returns a list of session export objects - */ - public async exportRoomKeys(): Promise<IMegolmSessionData[]> { - const exportedSessions: IMegolmSessionData[] = []; - 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 keys - a list of session export objects - * @returns a promise which resolves once the keys have been imported - */ - public importRoomKeys(keys: IMegolmSessionData[], opts: IImportRoomKeysOpts = {}): Promise<void> { - let successes = 0; - let failures = 0; - const total = keys.length; - - function updateProgress(): void { - 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(() => { - successes++; - if (opts.progressCallback) { - updateProgress(); - } - }); - }), - ).then(); - } - - /** - * Counts the number of end to end session keys that are waiting to be backed up - * @returns Promise which resolves to the number of sessions requiring backup - */ - public countSessionsNeedingBackup(): Promise<number> { - 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 room - the room the event is in - */ - public prepareToEncrypt(room: Room): void { - const alg = this.roomEncryptors.get(room.roomId); - if (alg) { - alg.prepareToEncrypt(room); - } - } - - /** - * Encrypt an event according to the configuration of the room. - * - * @param event - event to be sent - * - * @param room - destination room. - * - * @returns Promise which resolves when the event has been - * encrypted, or null if nothing was needed - */ - public async encryptEvent(event: MatrixEvent, room: Room): Promise<void> { - const roomId = event.getRoomId()!; - - const alg = this.roomEncryptors.get(roomId); - if (!alg) { - // MatrixClient has already checked that this room should be encrypted, - // so this is an unexpected situation. - throw new Error( - "Room " + - roomId + - " was previously configured to use encryption, but is " + - "no longer. Perhaps the homeserver is hiding the " + - "configuration event.", - ); - } - - // wait for all the room devices to be loaded - await this.trackRoomDevicesImpl(room); - - 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"]; - } - - // Treat element's performance metrics the same as `m.relates_to` (when present) - const elementPerfMetrics = content["io.element.performance_metrics"]; - if (elementPerfMetrics) { - content = Object.assign({}, content); - delete content["io.element.performance_metrics"]; - } - - const encryptedContent = (await alg.encryptMessage(room, event.getType(), content)) as IContent; - - if (mRelatesTo) { - encryptedContent["m.relates_to"] = mRelatesTo; - } - if (elementPerfMetrics) { - encryptedContent["io.element.performance_metrics"] = elementPerfMetrics; - } - - event.makeEncrypted( - "m.room.encrypted", - encryptedContent, - this.olmDevice.deviceCurve25519Key!, - this.olmDevice.deviceEd25519Key!, - ); - } - - /** - * Decrypt a received event - * - * - * @returns 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<IEventDecryptionResult> { - if (event.isRedacted()) { - // Try to decrypt the redaction event, to support encrypted - // redaction reasons. If we can't decrypt, just fall back to using - // the original redacted_because. - const redactionEvent = new MatrixEvent({ - room_id: event.getRoomId(), - ...event.getUnsigned().redacted_because, - }); - let redactedBecause: IEvent = event.getUnsigned().redacted_because!; - if (redactionEvent.isEncrypted()) { - try { - const decryptedEvent = await this.decryptEvent(redactionEvent); - redactedBecause = decryptedEvent.clearEvent as IEvent; - } catch (e) { - logger.warn("Decryption of redaction failed. Falling back to unencrypted event.", e); - } - } - - return { - clearEvent: { - room_id: event.getRoomId(), - type: "m.room.message", - content: {}, - unsigned: { - redacted_because: redactedBecause, - }, - }, - }; - } else { - const content = event.getWireContent(); - const alg = this.getRoomDecryptor(event.getRoomId()!, content.algorithm); - return alg.decryptEvent(event); - } - } - - /** - * Handle the notification from /sync or /keys/changes that device lists have - * been changed. - * - * @param syncData - Object containing sync tokens associated with this sync - * @param syncDeviceLists - device_lists field from /sync, or response from - * /keys/changes - */ - public async handleDeviceListChanges( - syncData: ISyncStateData, - syncDeviceLists: Required<ISyncResponse>["device_lists"], - ): Promise<void> { - // 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 resend - whether to resend the key request if there is - * already one - * - * @returns a promise that resolves when the key request is queued - */ - public requestRoomKey( - requestBody: IRoomKeyRequestBody, - recipients: IRoomKeyRequestRecipient[], - resend = false, - ): Promise<void> { - 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 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 - */ - public async cancelAndResendAllOutgoingKeyRequests(): Promise<void> { - await this.outgoingRoomKeyRequestManager.cancelAndResendAllOutgoingRequests(); - } - - /** - * handle an m.room.encryption event - * - * @param room - in which the event was received - * @param event - encryption event to be processed - */ - public async onCryptoEvent(room: Room, event: MatrixEvent): Promise<void> { - const content = event.getContent<IRoomEncryption>(); - await this.setRoomEncryptionImpl(room, content); - } - - /** - * Called before the result of a sync is processed - * - * @param syncData - the data from the 'MatrixClient.sync' event - */ - public async onSyncWillProcess(syncData: ISyncStateData): Promise<void> { - 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 syncData - the data from the 'MatrixClient.sync' event - */ - public async onSyncCompleted(syncData: OnSyncCompletedData): Promise<void> { - this.deviceList.setSyncToken(syncData.nextSyncToken ?? null); - this.deviceList.saveIfDirty(); - - // 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 deviceLists - device_lists field from /sync, or response from - * /keys/changes - */ - private async evalDeviceListChanges(deviceLists: Required<ISyncResponse>["device_lists"]): Promise<void> { - if (Array.isArray(deviceLists?.changed)) { - deviceLists.changed.forEach((u) => { - this.deviceList.invalidateUserDeviceList(u); - }); - } - - if (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 List of user IDs - */ - private async getTrackedE2eUsers(): Promise<string[]> { - const e2eUserIds: string[] = []; - 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 - */ - private getTrackedE2eRooms(): Room[] { - return this.clientStore.getRooms().filter((room) => { - // check for rooms with encryption enabled - const alg = this.roomEncryptors.get(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"; - }); - } - - /** - * Encrypts and sends a given object via Olm to-device messages to a given - * set of devices. - * @param userDeviceInfoArr - the devices to send to - * @param payload - fields to include in the encrypted payload - * @returns Promise which - * resolves once the message has been encrypted and sent to the given - * userDeviceMap, and returns the `{ contentMap, deviceInfoByDeviceId }` - * of the successfully sent messages. - */ - public async encryptAndSendToDevices(userDeviceInfoArr: IOlmDevice<DeviceInfo>[], payload: object): Promise<void> { - const toDeviceBatch: ToDeviceBatch = { - eventType: EventType.RoomMessageEncrypted, - batch: [], - }; - - try { - await Promise.all( - userDeviceInfoArr.map(async ({ userId, deviceInfo }) => { - const deviceId = deviceInfo.deviceId; - const encryptedContent: IEncryptedContent = { - algorithm: olmlib.OLM_ALGORITHM, - sender_key: this.olmDevice.deviceCurve25519Key!, - ciphertext: {}, - [ToDeviceMessageId]: uuidv4(), - }; - - toDeviceBatch.batch.push({ - userId, - deviceId, - payload: encryptedContent, - }); - - await olmlib.ensureOlmSessionsForDevices( - this.olmDevice, - this.baseApis, - new Map([[userId, [deviceInfo]]]), - ); - await olmlib.encryptMessageForDevice( - encryptedContent.ciphertext, - this.userId, - this.deviceId, - this.olmDevice, - userId, - deviceInfo, - payload, - ); - }), - ); - - // prune out any devices that encryptMessageForDevice could not encrypt for, - // in which case it will have just not added anything to the ciphertext object. - // There's no point sending messages to devices if we couldn't encrypt to them, - // since that's effectively a blank message. - toDeviceBatch.batch = toDeviceBatch.batch.filter((msg) => { - if (Object.keys(msg.payload.ciphertext).length > 0) { - return true; - } else { - logger.log(`No ciphertext for device ${msg.userId}:${msg.deviceId}: pruning`); - return false; - } - }); - - try { - await this.baseApis.queueToDevice(toDeviceBatch); - } catch (e) { - logger.error("sendToDevice failed", e); - throw e; - } - } catch (e) { - logger.error("encryptAndSendToDevices promises failed", e); - throw e; - } - } - - private onMembership = (event: MatrixEvent, member: RoomMember, oldMembership?: string): void => { - try { - this.onRoomMembership(event, member, oldMembership); - } catch (e) { - logger.error("Error handling membership change:", e); - } - }; - - public async preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]> { - // all we do here is filter out encrypted to-device messages with the wrong algorithm. Decryption - // happens later in decryptEvent, via the EventMapper - return events.filter((toDevice) => { - if ( - toDevice.type === EventType.RoomMessageEncrypted && - !["m.olm.v1.curve25519-aes-sha2"].includes(toDevice.content?.algorithm) - ) { - logger.log("Ignoring invalid encrypted to-device event from " + toDevice.sender); - return false; - } - return true; - }); - } - - public preprocessOneTimeKeyCounts(oneTimeKeysCounts: Map<string, number>): Promise<void> { - const currentCount = oneTimeKeysCounts.get("signed_curve25519") || 0; - this.updateOneTimeKeyCount(currentCount); - return Promise.resolve(); - } - - public preprocessUnusedFallbackKeys(unusedFallbackKeys: Set<string>): Promise<void> { - this.setNeedsNewFallback(!unusedFallbackKeys.has("signed_curve25519")); - return Promise.resolve(); - } - - private onToDeviceEvent = (event: MatrixEvent): void => { - try { - logger.log( - `received to-device ${event.getType()} from: ` + - `${event.getSender()} id: ${event.getContent()[ToDeviceMessageId]}`, - ); - - 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() === "m.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(MatrixEventEvent.Decrypted, (ev) => { - this.onToDeviceEvent(ev); - }); - } - } catch (e) { - logger.error("Error handling toDeviceEvent:", e); - } - }; - - /** - * Handle a key event - * - * @internal - * @param 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 - * - * @internal - * @param 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()} ` + - `for ${content.algorithm} session ${content.sender_key}|${content.session_id} ` + - `in room ${content.room_id} with code ${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. - * - * @internal - * @param event - verification start event - */ - private onKeyVerificationMessage(event: MatrixEvent): void { - if (!ToDeviceChannel.validateEvent(event, this.baseApis)) { - return; - } - const createRequest = (event: MatrixEvent): VerificationRequest | undefined => { - 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 - * - * @internal - * @param event - the timeline event - * @param room - not used - * @param atStart - not used - * @param removed - not used - * @param 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: MatrixEvent): VerificationRequest => { - 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: IRequestsMap, - createRequest: (event: MatrixEvent) => VerificationRequest | undefined, - isLiveEvent = true, - ): Promise<void> { - // Wait for event to get its final ID with pendingEventOrdering: "chronological", since DM channels depend on it. - if (event.isSending() && event.status != EventStatus.SENT) { - let eventIdListener: () => void; - let statusListener: () => void; - try { - await new Promise<void>((resolve, reject) => { - eventIdListener = resolve; - statusListener = (): void => { - if (event.status == EventStatus.CANCELLED) { - reject(new Error("Event status set to CANCELLED.")); - } - }; - event.once(MatrixEventEvent.LocalEventIdReplaced, eventIdListener); - event.on(MatrixEventEvent.Status, statusListener); - }); - } catch (err) { - logger.error("error while waiting for the verification event to be sent: ", err); - return; - } finally { - event.removeListener(MatrixEventEvent.LocalEventIdReplaced, eventIdListener!); - event.removeListener(MatrixEventEvent.Status, statusListener!); - } - } - let request: VerificationRequest | undefined = 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); - } - const shouldEmit = - isNewRequest && - !request.initiatedByMe && - !request.invalid && // check it has enough events to pass the UNSENT stage - !request.observeOnly; - if (shouldEmit) { - this.baseApis.emit(CryptoEvent.VerificationRequest, request); - } - } - - /** - * Handle a toDevice event that couldn't be decrypted - * - * @internal - * @param event - undecryptable event - */ - private async onToDeviceBadEncrypted(event: MatrixEvent): Promise<void> { - const content = event.getWireContent(); - const sender = event.getSender(); - const algorithm = content.algorithm; - const deviceKey = content.sender_key; - - this.baseApis.emit(ClientEvent.UndecryptableToDeviceEvent, event); - - // 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 = (): void => { - 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. - const lastNewSessionDevices = this.lastNewSessionForced.getOrCreate(sender); - const lastNewSessionForced = lastNewSessionDevices.getOrCreate(deviceKey); - 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 = new Map([[sender, [device]]]); - await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, true); - - lastNewSessionDevices.set(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: IEncryptedContent = { - algorithm: olmlib.OLM_ALGORITHM, - sender_key: this.olmDevice.deviceCurve25519Key!, - ciphertext: {}, - [ToDeviceMessageId]: uuidv4(), - }; - 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", - new Map([[sender, new Map([[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 - * - * @internal - * @param event - event causing the change - * @param member - user whose membership changed - * @param 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.get(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 (roomId in this.roomDeviceTrackingState) { - 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. - * - * @internal - * @param 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. - * - * @internal - */ - private async processReceivedRoomKeyRequests(): Promise<void> { - 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 - * - */ - private async processReceivedRoomKeyRequest(req: IncomingRoomKeyRequest): Promise<void> { - 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.get(roomId)) { - logger.debug(`room key request for unencrypted room ${roomId}`); - return; - } - const encryptor = this.roomEncryptors.get(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.has(roomId)) { - logger.log(`room key request for unencrypted room ${roomId}`); - return; - } - - const decryptor = this.roomDecryptors.get(roomId)!.get(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 = (): void => { - 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(CryptoEvent.RoomKeyRequest, req); - } - - /** - * Helper for processReceivedRoomKeyRequests - * - */ - private async processReceivedRoomKeyRequestCancellation( - cancellation: IncomingRoomKeyRequestCancellation, - ): Promise<void> { - 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(CryptoEvent.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. - * - * @internal - * - * @param roomId - room id for decryptor. If undefined, a temporary - * decryptor is instantiated. - * - * @param algorithm - crypto algorithm - * - * @throws {@link DecryptionError} if the algorithm is unknown - */ - public getRoomDecryptor(roomId: string | null, algorithm: string): DecryptionAlgorithm { - let decryptors: Map<string, DecryptionAlgorithm> | undefined; - let alg: DecryptionAlgorithm | undefined; - - if (roomId) { - decryptors = this.roomDecryptors.get(roomId); - if (!decryptors) { - decryptors = new Map<string, DecryptionAlgorithm>(); - this.roomDecryptors.set(roomId, decryptors); - } - - alg = decryptors.get(algorithm); - if (alg) { - return alg; - } - } - - const AlgClass = algorithms.DECRYPTION_CLASSES.get(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 ?? undefined, - }); - - if (decryptors) { - decryptors.set(algorithm, alg); - } - return alg; - } - - /** - * Get all the room decryptors for a given encryption algorithm. - * - * @param algorithm - The encryption algorithm - * - * @returns An array of room decryptors - */ - private getRoomDecryptors(algorithm: string): DecryptionAlgorithm[] { - const decryptors: DecryptionAlgorithm[] = []; - for (const d of this.roomDecryptors.values()) { - if (d.has(algorithm)) { - decryptors.push(d.get(algorithm)!); - } - } - return decryptors; - } - - /** - * sign the given object with our ed25519 key - * - * @param obj - Object to which we will add a 'signatures' property - */ - public async signObject<T extends ISignableObject & object>(obj: T): Promise<void> { - const sigs = new Map(Object.entries(obj.signatures || {})); - const unsigned = obj.unsigned; - - delete obj.signatures; - delete obj.unsigned; - - const userSignatures = sigs.get(this.userId) || {}; - sigs.set(this.userId, userSignatures); - userSignatures["ed25519:" + this.deviceId] = await this.olmDevice.sign(anotherjson.stringify(obj)); - obj.signatures = recursiveMapToObject(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 key - the key to check - * @returns 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); -} - -/** - * Represents a received m.room_key_request event - */ -export class IncomingRoomKeyRequest { - /** user requesting the key */ - public readonly userId: string; - /** device requesting the key */ - public readonly deviceId: string; - /** unique id for the request */ - public readonly requestId: string; - public readonly requestBody: IRoomKeyRequestBody; - /** - * callback which, when called, will ask - * the relevant crypto algorithm implementation to share the keys for - * this request. - */ - public share: () => void; - - public 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 = (): void => { - throw new Error("don't know how to share keys for this request yet"); - }; - } -} - -/** - * Represents a received m.room_key_request cancellation - */ -class IncomingRoomKeyRequestCancellation { - /** user requesting the cancellation */ - public readonly userId: string; - /** device requesting the cancellation */ - public readonly deviceId: string; - /** unique id for the request to be cancelled */ - public readonly requestId: string; - - public constructor(event: MatrixEvent) { - const content = event.getContent(); - - this.userId = event.getSender()!; - this.deviceId = content.requesting_device_id; - this.requestId = content.request_id; - } -} - -// a number of types are re-exported for backwards compatibility, in case any applications are referencing it. -export type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto"; |