"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.IncomingRoomKeyRequest = exports.CryptoEvent = exports.Crypto = void 0; exports.fixBackupKey = fixBackupKey; exports.isCryptoAvailable = isCryptoAvailable; exports.verificationMethods = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _anotherJson = _interopRequireDefault(require("another-json")); var _uuid = require("uuid"); var _event = require("../@types/event"); var _ReEmitter = require("../ReEmitter"); var _logger = require("../logger"); var _OlmDevice = require("./OlmDevice"); var olmlib = _interopRequireWildcard(require("./olmlib")); var _DeviceList = require("./DeviceList"); var _deviceinfo = require("./deviceinfo"); var algorithms = _interopRequireWildcard(require("./algorithms")); var _CrossSigning = require("./CrossSigning"); var _EncryptionSetup = require("./EncryptionSetup"); var _SecretStorage = require("./SecretStorage"); var _OutgoingRoomKeyRequestManager = require("./OutgoingRoomKeyRequestManager"); var _indexeddbCryptoStore = require("./store/indexeddb-crypto-store"); var _QRCode = require("./verification/QRCode"); var _SAS = require("./verification/SAS"); var _key_passphrase = require("./key_passphrase"); var _recoverykey = require("./recoverykey"); var _VerificationRequest = require("./verification/request/VerificationRequest"); var _InRoomChannel = require("./verification/request/InRoomChannel"); var _ToDeviceChannel = require("./verification/request/ToDeviceChannel"); var _IllegalMethod = require("./verification/IllegalMethod"); var _errors = require("../errors"); var _aes = require("./aes"); var _dehydration = require("./dehydration"); var _backup = require("./backup"); var _room = require("../models/room"); var _roomMember = require("../models/room-member"); var _event2 = require("../models/event"); var _client = require("../client"); var _typedEventEmitter = require("../models/typed-event-emitter"); var _roomState = require("../models/room-state"); var _utils = require("../utils"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } const DeviceVerification = _deviceinfo.DeviceInfo.DeviceVerification; const defaultVerificationMethods = { [_QRCode.ReciprocateQRCode.NAME]: _QRCode.ReciprocateQRCode, [_SAS.SAS.NAME]: _SAS.SAS, // These two can't be used for actual verification, but we do // need to be able to define them here for the verification flows // to start. [_QRCode.SHOW_QR_CODE_METHOD]: _IllegalMethod.IllegalMethod, [_QRCode.SCAN_QR_CODE_METHOD]: _IllegalMethod.IllegalMethod }; /** * verification method names */ // legacy export identifier const verificationMethods = { RECIPROCATE_QR_CODE: _QRCode.ReciprocateQRCode.NAME, SAS: _SAS.SAS.NAME }; exports.verificationMethods = verificationMethods; function isCryptoAvailable() { return Boolean(global.Olm); } const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000; let CryptoEvent; exports.CryptoEvent = CryptoEvent; (function (CryptoEvent) { CryptoEvent["DeviceVerificationChanged"] = "deviceVerificationChanged"; CryptoEvent["UserTrustStatusChanged"] = "userTrustStatusChanged"; CryptoEvent["UserCrossSigningUpdated"] = "userCrossSigningUpdated"; CryptoEvent["RoomKeyRequest"] = "crypto.roomKeyRequest"; CryptoEvent["RoomKeyRequestCancellation"] = "crypto.roomKeyRequestCancellation"; CryptoEvent["KeyBackupStatus"] = "crypto.keyBackupStatus"; CryptoEvent["KeyBackupFailed"] = "crypto.keyBackupFailed"; CryptoEvent["KeyBackupSessionsRemaining"] = "crypto.keyBackupSessionsRemaining"; CryptoEvent["KeySignatureUploadFailure"] = "crypto.keySignatureUploadFailure"; CryptoEvent["VerificationRequest"] = "crypto.verification.request"; CryptoEvent["Warning"] = "crypto.warning"; CryptoEvent["WillUpdateDevices"] = "crypto.willUpdateDevices"; CryptoEvent["DevicesUpdated"] = "crypto.devicesUpdated"; CryptoEvent["KeysChanged"] = "crossSigning.keysChanged"; })(CryptoEvent || (exports.CryptoEvent = CryptoEvent = {})); class Crypto extends _typedEventEmitter.TypedEventEmitter { /** * @returns The version of Olm. */ static getOlmVersion() { return _OlmDevice.OlmDevice.getOlmVersion(); } /** * 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. */ constructor(baseApis, userId, deviceId, clientStore, cryptoStore, roomList, verificationMethods) { super(); this.baseApis = baseApis; this.userId = userId; this.deviceId = deviceId; this.clientStore = clientStore; this.cryptoStore = cryptoStore; this.roomList = roomList; (0, _defineProperty2.default)(this, "backupManager", void 0); (0, _defineProperty2.default)(this, "crossSigningInfo", void 0); (0, _defineProperty2.default)(this, "olmDevice", void 0); (0, _defineProperty2.default)(this, "deviceList", void 0); (0, _defineProperty2.default)(this, "dehydrationManager", void 0); (0, _defineProperty2.default)(this, "secretStorage", void 0); (0, _defineProperty2.default)(this, "reEmitter", void 0); (0, _defineProperty2.default)(this, "verificationMethods", void 0); (0, _defineProperty2.default)(this, "supportedAlgorithms", void 0); (0, _defineProperty2.default)(this, "outgoingRoomKeyRequestManager", void 0); (0, _defineProperty2.default)(this, "toDeviceVerificationRequests", void 0); (0, _defineProperty2.default)(this, "inRoomVerificationRequests", void 0); (0, _defineProperty2.default)(this, "trustCrossSignedDevices", true); (0, _defineProperty2.default)(this, "lastOneTimeKeyCheck", null); (0, _defineProperty2.default)(this, "oneTimeKeyCheckInProgress", false); (0, _defineProperty2.default)(this, "roomEncryptors", new Map()); (0, _defineProperty2.default)(this, "roomDecryptors", new Map()); (0, _defineProperty2.default)(this, "deviceKeys", {}); (0, _defineProperty2.default)(this, "globalBlacklistUnverifiedDevices", false); (0, _defineProperty2.default)(this, "globalErrorOnUnknownDevices", true); (0, _defineProperty2.default)(this, "receivedRoomKeyRequests", []); (0, _defineProperty2.default)(this, "receivedRoomKeyRequestCancellations", []); (0, _defineProperty2.default)(this, "processingRoomKeyRequests", false); (0, _defineProperty2.default)(this, "lazyLoadMembers", false); (0, _defineProperty2.default)(this, "roomDeviceTrackingState", {}); (0, _defineProperty2.default)(this, "lastNewSessionForced", new _utils.MapWithDefault(() => new _utils.MapWithDefault(() => 0))); (0, _defineProperty2.default)(this, "sendKeyRequestsImmediately", false); (0, _defineProperty2.default)(this, "oneTimeKeyCount", void 0); (0, _defineProperty2.default)(this, "needsNewFallback", void 0); (0, _defineProperty2.default)(this, "fallbackCleanup", void 0); (0, _defineProperty2.default)(this, "onDeviceListUserCrossSigningUpdated", async userId => { if (userId === this.userId) { // An update to our own cross-signing key. // Get the new key first: const newCrossSigning = this.deviceList.getStoredCrossSigningForUser(userId); const seenPubkey = newCrossSigning ? newCrossSigning.getId() : null; const currentPubkey = this.crossSigningInfo.getId(); const changed = currentPubkey !== seenPubkey; if (currentPubkey && seenPubkey && !changed) { // If it's not changed, just make sure everything is up to date await this.checkOwnCrossSigningTrust(); } else { // We'll now be in a state where cross-signing on the account is not trusted // because our locally stored cross-signing keys will not match the ones // on the server for our account. So we clear our own stored cross-signing keys, // effectively disabling cross-signing until the user gets verified by the device // that reset the keys this.storeTrustedSelfKeys(null); // emit cross-signing has been disabled this.emit(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)); } }); (0, _defineProperty2.default)(this, "onMembership", (event, member, oldMembership) => { try { this.onRoomMembership(event, member, oldMembership); } catch (e) { _logger.logger.error("Error handling membership change:", e); } }); (0, _defineProperty2.default)(this, "onToDeviceEvent", event => { try { _logger.logger.log(`received to-device ${event.getType()} from: ` + `${event.getSender()} id: ${event.getContent()[_event.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(_event2.MatrixEventEvent.Decrypted, ev => { this.onToDeviceEvent(ev); }); } } catch (e) { _logger.logger.error("Error handling toDeviceEvent:", e); } }); (0, _defineProperty2.default)(this, "onTimelineEvent", (event, room, atStart, removed, { liveEvent = true } = {}) => { if (!_InRoomChannel.InRoomChannel.validateEvent(event, this.baseApis)) { return; } const createRequest = event => { const channel = new _InRoomChannel.InRoomChannel(this.baseApis, event.getRoomId()); return new _VerificationRequest.VerificationRequest(channel, this.verificationMethods, this.baseApis); }; this.handleVerificationEvent(event, this.inRoomVerificationRequests, createRequest, liveEvent); }); this.reEmitter = new _ReEmitter.TypedReEmitter(this); if (verificationMethods) { this.verificationMethods = new Map(); for (const method of verificationMethods) { if (typeof method === "string") { if (defaultVerificationMethods[method]) { this.verificationMethods.set(method, defaultVerificationMethods[method]); } } else if (method["NAME"]) { this.verificationMethods.set(method["NAME"], method); } else { _logger.logger.warn(`Excluding unknown verification method ${method}`); } } } else { this.verificationMethods = new Map(Object.entries(defaultVerificationMethods)); } this.backupManager = new _backup.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.OlmDevice(cryptoStore); this.deviceList = new _DeviceList.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.OutgoingRoomKeyRequestManager(baseApis, this.deviceId, this.cryptoStore); this.toDeviceVerificationRequests = new _ToDeviceChannel.ToDeviceRequests(); this.inRoomVerificationRequests = new _InRoomChannel.InRoomRequests(); const cryptoCallbacks = this.baseApis.cryptoCallbacks || {}; const cacheCallbacks = (0, _CrossSigning.createCryptoStoreCacheCallbacks)(cryptoStore, this.olmDevice); this.crossSigningInfo = new _CrossSigning.CrossSigningInfo(userId, cryptoCallbacks, cacheCallbacks); // Yes, we pass the client twice here: see SecretStorage this.secretStorage = new _SecretStorage.SecretStorage(baseApis, cryptoCallbacks, baseApis); this.dehydrationManager = new _dehydration.DehydrationManager(this); // Assuming no app-supplied callback, default to getting from SSSS. if (!cryptoCallbacks.getCrossSigningKey && cryptoCallbacks.getSecretStorageKey) { cryptoCallbacks.getCrossSigningKey = async type => { return _CrossSigning.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. */ async init({ exportedOlmDevice, pickleKey } = {}) { _logger.logger.log("Crypto: initialising Olm..."); await global.Olm.init(); _logger.logger.log(exportedOlmDevice ? "Crypto: initialising Olm device from exported device..." : "Crypto: initialising Olm device..."); await this.olmDevice.init({ fromExportedDevice: exportedOlmDevice, pickleKey }); _logger.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.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.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.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.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.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 */ getCryptoTrustCrossSignedDevices() { 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 */ setCryptoTrustCrossSignedDevices(val) { this.trustCrossSignedDevices = val; for (const userId of this.deviceList.getKnownUserIds()) { const devices = this.deviceList.getRawStoredDevicesForUser(userId); for (const deviceId of Object.keys(devices)) { const deviceTrust = this.checkDeviceTrust(userId, deviceId); // If the device is locally verified then isVerified() is always true, // so this will only have caused the value to change if the device is // cross-signing verified but not locally verified if (!deviceTrust.isLocallyVerified() && deviceTrust.isCrossSigningVerified()) { const deviceObj = this.deviceList.getStoredDevice(userId, deviceId); this.emit(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. */ async createRecoveryKeyFromPassphrase(password) { const decryption = new global.Olm.PkDecryption(); try { const keyInfo = {}; if (password) { const derivation = await (0, _key_passphrase.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 = (0, _recoverykey.encodeRecoveryKey)(privateKey); return { keyInfo: keyInfo, encodedPrivateKey, privateKey }; } finally { decryption === null || decryption === void 0 ? void 0 : 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 */ async userHasCrossSigningKeys() { 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 */ async isCrossSigningReady() { 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 */ async isSecretStorageReady() { 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. */ async bootstrapCrossSigning({ authUploadDeviceSigningKeys, setupNewCrossSigning } = {}) { _logger.logger.log("Bootstrapping cross-signing"); const delegateCryptoCallbacks = this.baseApis.cryptoCallbacks; const builder = new _EncryptionSetup.EncryptionSetupBuilder(this.baseApis.store.accountData, delegateCryptoCallbacks); const crossSigningInfo = new _CrossSigning.CrossSigningInfo(this.userId, builder.crossSigningCallbacks, builder.crossSigningCallbacks); // Reset the cross-signing keys const resetCrossSigning = async () => { crossSigningInfo.resetKeys(); // Sign master key with device key await this.signObject(crossSigningInfo.keys.master); // Store auth flow helper function, as we need to call it when uploading // to ensure we handle auth errors properly. builder.addCrossSigningKeys(authUploadDeviceSigningKeys, crossSigningInfo.keys); // Cross-sign own device const device = this.deviceList.getStoredDevice(this.userId, this.deviceId); const deviceSignature = await crossSigningInfo.signDevice(this.userId, device); builder.addKeySignature(this.userId, this.deviceId, deviceSignature); // Sign message key backup with cross-signing master key if (this.backupManager.backupInfo) { await crossSigningInfo.signObject(this.backupManager.backupInfo.auth_data, "master"); builder.addSessionBackup(this.backupManager.backupInfo); } }; const publicKeysOnDevice = this.crossSigningInfo.getId(); const privateKeysInCache = await this.crossSigningInfo.isStoredInKeyCache(); const privateKeysInStorage = await this.crossSigningInfo.isStoredInSecretStorage(this.secretStorage); const privateKeysExistSomewhere = privateKeysInCache || privateKeysInStorage; // Log all relevant state for easier parsing of debug logs. _logger.logger.log({ setupNewCrossSigning, publicKeysOnDevice, privateKeysInCache, privateKeysInStorage, privateKeysExistSomewhere }); if (!privateKeysExistSomewhere || setupNewCrossSigning) { _logger.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.logger.log("Cross-signing public keys trusted and private keys found locally"); } else if (privateKeysInStorage) { _logger.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.SecretStorage(builder.accountDataClientAdapter, builder.ssssCryptoCallbacks, undefined); if (await secretStorage.hasKey()) { _logger.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 _CrossSigning.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.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 async bootstrapSecretStorage({ createSecretStorageKey = async () => ({}), keyBackupInfo, setupNewKeyBackup, setupNewSecretStorage, getKeyBackupPassphrase } = {}) { _logger.logger.log("Bootstrapping Secure Secret Storage"); const delegateCryptoCallbacks = this.baseApis.cryptoCallbacks; const builder = new _EncryptionSetup.EncryptionSetupBuilder(this.baseApis.store.accountData, delegateCryptoCallbacks); const secretStorage = new _SecretStorage.SecretStorage(builder.accountDataClientAdapter, builder.ssssCryptoCallbacks, undefined); // the ID of the new SSSS key, if we create one let newKeyId = null; // create a new SSSS key and set it as default const createSSSS = async (opts, privateKey) => { if (privateKey) { opts.key = privateKey; } const { keyId, keyInfo } = await secretStorage.addKey(_SecretStorage.SECRET_STORAGE_ALGORITHM_V1_AES, opts); if (privateKey) { // make the private key available to encrypt 4S secrets builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey); } await secretStorage.setDefaultKeyId(keyId); return keyId; }; const ensureCanCheckPassphrase = async (keyId, keyInfo) => { if (!keyInfo.mac) { var _this$baseApis$crypto, _this$baseApis$crypto2; const key = await ((_this$baseApis$crypto = (_this$baseApis$crypto2 = this.baseApis.cryptoCallbacks).getSecretStorageKey) === null || _this$baseApis$crypto === void 0 ? void 0 : _this$baseApis$crypto.call(_this$baseApis$crypto2, { keys: { [keyId]: keyInfo } }, "")); if (key) { const privateKey = key[1]; builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey); const { iv, mac } = await (0, _aes.calculateKeyCheck)(privateKey); keyInfo.iv = iv; keyInfo.mac = mac; await builder.setAccountData(`m.secret_storage.key.${keyId}`, keyInfo); } } }; const signKeyBackupWithCrossSigning = async keyBackupAuthData => { if (this.crossSigningInfo.getId() && (await this.crossSigningInfo.isStoredInKeyCache("master"))) { try { _logger.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.logger.error("Signing key backup with cross-signing keys failed", e); } } else { _logger.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 === _SecretStorage.SECRET_STORAGE_ALGORITHM_V1_AES; // Log all relevant state for easier parsing of debug logs. _logger.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.logger.log("Secret storage does not exist, creating new storage key"); // if we already have a usable default SSSS key and aren't resetting // SSSS just use it. otherwise, create a new one // Note: we leave the old SSSS key in place: there could be other // secrets using it, in theory. We could move them to the new key but a) // that would mean we'd need to prompt for the old passphrase, and b) // it's not clear that would be the right thing to do anyway. const { keyInfo = {}, privateKey } = await createSecretStorageKey(); newKeyId = await createSSSS(keyInfo, privateKey); } else if (!storageExists && keyBackupInfo) { // we have an existing backup, but no SSSS _logger.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 === null || getKeyBackupPassphrase === void 0 ? void 0 : getKeyBackupPassphrase())); // create a new SSSS key and use the backup key as the new SSSS key const opts = {}; if (keyBackupInfo.auth_data.private_key_salt && keyBackupInfo.auth_data.private_key_iterations) { // FIXME: ??? opts.passphrase = { algorithm: "m.pbkdf2", iterations: keyBackupInfo.auth_data.private_key_iterations, salt: keyBackupInfo.auth_data.private_key_salt, bits: 256 }; } newKeyId = await createSSSS(opts, backupKey); // store the backup key in secret storage await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId]); // The backup is trusted because the user provided the private key. // Sign the backup with the cross-signing key so the key backup can // be trusted via cross-signing. await signKeyBackupWithCrossSigning(keyBackupInfo.auth_data); builder.addSessionBackup(keyBackupInfo); } else { // 4S is already set up _logger.logger.log("Secret storage exists"); if (oldKeyInfo && oldKeyInfo.algorithm === _SecretStorage.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.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 _CrossSigning.CrossSigningInfo.storeInSecretStorage(crossSigningPrivateKeys, secretStorage); } if (setupNewKeyBackup && !keyBackupInfo) { _logger.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 = (0, _recoverykey.decodeRecoveryKey)(info.recovery_key); await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(privateKey)); // create keyBackupInfo object to add to builder const data = { algorithm: info.algorithm, auth_data: info.auth_data }; // Sign with cross-signing master key await signKeyBackupWithCrossSigning(data.auth_data); // sign with the device fingerprint await this.signObject(data.auth_data); builder.addSessionBackup(data); } // Cache the session backup key const sessionBackupKey = await secretStorage.get("m.megolm_backup.v1"); if (sessionBackupKey) { _logger.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 === null || getKeyBackupPassphrase === void 0 ? void 0 : 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.logger.error("Key backup is enabled but couldn't get key backup key!"); return; } _logger.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.logger.log("Secure Secret Storage ready"); } addSecretStorageKey(algorithm, opts, keyID) { return this.secretStorage.addKey(algorithm, opts, keyID); } hasSecretStorageKey(keyID) { return this.secretStorage.hasKey(keyID); } getSecretStorageKey(keyID) { return this.secretStorage.getKey(keyID); } storeSecret(name, secret, keys) { return this.secretStorage.store(name, secret, keys); } getSecret(name) { return this.secretStorage.get(name); } isSecretStored(name) { return this.secretStorage.isStored(name); } requestSecret(name, devices) { if (!devices) { devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(this.userId)); } return this.secretStorage.request(name, devices); } getDefaultSecretStorageKeyId() { return this.secretStorage.getDefaultKeyId(); } setDefaultSecretStorageKeyId(k) { return this.secretStorage.setDefaultKeyId(k); } checkSecretStorageKey(key, info) { return this.secretStorage.checkKey(key, info); } /** * Checks that a given secret storage private key matches a given public key. * This can be used by the getSecretStorageKey callback to verify that the * private key it is about to supply is the one that was requested. * * @param privateKey - The private key * @param expectedPublicKey - The public key * @returns true if the key matches, otherwise false */ checkSecretStoragePrivateKey(privateKey, expectedPublicKey) { let decryption = null; try { decryption = new global.Olm.PkDecryption(); const gotPubkey = decryption.init_with_private_key(privateKey); // make sure it agrees with the given pubkey return gotPubkey === expectedPublicKey; } finally { var _decryption; (_decryption = decryption) === null || _decryption === void 0 ? void 0 : _decryption.free(); } } /** * Fetches the backup private key, if cached * @returns the key, if any, or null */ async getSessionBackupPrivateKey() { let key = await new Promise(resolve => { // TODO types this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.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 (0, _aes.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 */ async storeSessionBackupPrivateKey(key) { 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 (0, _aes.encryptAES)(olmlib.encodeBase64(key), pickleKey, "m.megolm_backup.v1"); return this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.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 */ checkCrossSigningPrivateKey(privateKey, expectedPublicKey) { let signing = null; try { signing = new global.Olm.PkSigning(); const gotPubkey = signing.init_with_seed(privateKey); // make sure it agrees with the given pubkey return gotPubkey === expectedPublicKey; } finally { var _signing; (_signing = signing) === null || _signing === void 0 ? void 0 : _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. */ async afterCrossSigningLocalKeyChange() { _logger.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.logger.info(`Starting background key sig upload for ${this.deviceId}`); const upload = ({ shouldEmit = false }) => { 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 _errors.KeySignatureUploadError("Key upload failed", { failures }); } _logger.logger.info(`Finished background key sig upload for ${this.deviceId}`); }).catch(e => { _logger.logger.error(`Error during background key sig upload for ${this.deviceId}`, e); }); }; upload({ shouldEmit: true }); const shouldUpgradeCb = this.baseApis.cryptoCallbacks.shouldUpgradeDeviceVerifications; if (shouldUpgradeCb) { _logger.logger.info("Starting device verification upgrade"); // Check all users for signatures if upgrade callback present // FIXME: do this in batches const users = {}; for (const [userId, crossSigningInfo] of Object.entries(this.deviceList.crossSigningInfo)) { const upgradeInfo = await this.checkForDeviceVerificationUpgrade(userId, _CrossSigning.CrossSigningInfo.fromStorage(crossSigningInfo, userId)); if (upgradeInfo) { users[userId] = upgradeInfo; } } if (Object.keys(users).length > 0) { _logger.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.logger.log("shouldUpgradeDeviceVerifications threw an error: not upgrading", e); } } _logger.logger.info("Finished device verification upgrade"); } _logger.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 */ async checkForDeviceVerificationUpgrade(userId, crossSigningInfo) { // only upgrade if this is the first cross-signing key that we've seen for // them, and if their cross-signing key isn't already verified const trustLevel = this.crossSigningInfo.checkUserTrust(crossSigningInfo); if (crossSigningInfo.firstUse && !trustLevel.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.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 */ async checkForValidDeviceSignature(userId, key, devices) { const deviceIds = []; if (devices && key.signatures && key.signatures[userId]) { for (const signame of Object.keys(key.signatures[userId])) { const [, deviceId] = signame.split(":", 2); if (deviceId in devices && devices[deviceId].verified === DeviceVerification.VERIFIED) { try { await olmlib.verifySignature(this.olmDevice, key, userId, deviceId, devices[deviceId].keys[signame]); deviceIds.push(deviceId); } catch (e) {} } } } return deviceIds; } /** * Get the user's cross-signing key ID. * * @param 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 */ getCrossSigningId(type) { 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. */ getStoredCrossSigningForUser(userId) { return this.deviceList.getStoredCrossSigningForUser(userId); } /** * Check whether a given user is trusted. * * @param userId - The ID of the user to check. * * @returns */ checkUserTrust(userId) { const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(userId); if (!userCrossSigning) { return new _CrossSigning.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 */ checkDeviceTrust(userId, deviceId) { 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 */ checkDeviceInfoTrust(userId, device) { const trustedLocally = !!(device !== null && device !== void 0 && 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 _CrossSigning.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 */ checkIfOwnDeviceCrossSigned(deviceId) { var _userCrossSigning$che; const device = this.deviceList.getStoredDevice(this.userId, deviceId); if (!device) return false; const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(this.userId); return (_userCrossSigning$che = userCrossSigning === null || userCrossSigning === void 0 ? void 0 : userCrossSigning.checkDeviceTrust(userCrossSigning, device, false, true).isCrossSigningVerified()) !== null && _userCrossSigning$che !== void 0 ? _userCrossSigning$che : false; } /* * Event handler for DeviceList's userNewDevices event */ /** * Check the copy of our cross-signing key that we have in the device list and * see if we can get the private key. If so, mark it as trusted. */ async checkOwnCrossSigningTrust({ allowPrivateKeyRequests = false } = {}) { const userId = this.userId; // Before proceeding, ensure our cross-signing public keys have been // downloaded via the device list. await this.downloadKeys([this.userId]); // Also check which private keys are locally cached. const crossSigningPrivateKeys = await this.crossSigningInfo.getCrossSigningKeysFromCache(); // If we see an update to our own master key, check it against the master // key we have and, if it matches, mark it as verified // First, get the new cross-signing info const newCrossSigning = this.deviceList.getStoredCrossSigningForUser(userId); if (!newCrossSigning) { _logger.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.logger.info("Got new master public key", seenPubkey); } if (allowPrivateKeyRequests && (masterChanged || masterExistsNotLocallyCached)) { _logger.logger.info("Attempting to retrieve cross-signing master private key"); let signing = null; // It's important for control flow that we leave any errors alone for // higher levels to handle so that e.g. cancelling access properly // aborts any larger operation as well. try { const ret = await this.crossSigningInfo.getCrossSigningKey("master", seenPubkey); signing = ret[1]; _logger.logger.info("Got cross-signing master private key"); } finally { var _signing2; (_signing2 = signing) === null || _signing2 === void 0 ? void 0 : _signing2.free(); } } const oldSelfSigningId = this.crossSigningInfo.getId("self_signing"); const oldUserSigningId = this.crossSigningInfo.getId("user_signing"); // Update the version of our keys in our cross-signing object and the local store this.storeTrustedSelfKeys(newCrossSigning.keys); const selfSigningChanged = oldSelfSigningId !== newCrossSigning.getId("self_signing"); const userSigningChanged = oldUserSigningId !== newCrossSigning.getId("user_signing"); const selfSigningExistsNotLocallyCached = newCrossSigning.getId("self_signing") && !crossSigningPrivateKeys.has("self_signing"); const userSigningExistsNotLocallyCached = newCrossSigning.getId("user_signing") && !crossSigningPrivateKeys.has("user_signing"); const keySignatures = {}; if (selfSigningChanged) { _logger.logger.info("Got new self-signing key", newCrossSigning.getId("self_signing")); } if (allowPrivateKeyRequests && (selfSigningChanged || selfSigningExistsNotLocallyCached)) { _logger.logger.info("Attempting to retrieve cross-signing self-signing private key"); let signing = null; try { const ret = await this.crossSigningInfo.getCrossSigningKey("self_signing", newCrossSigning.getId("self_signing")); signing = ret[1]; _logger.logger.info("Got cross-signing self-signing private key"); } finally { var _signing3; (_signing3 = signing) === null || _signing3 === void 0 ? void 0 : _signing3.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.logger.info("Got new user-signing key", newCrossSigning.getId("user_signing")); } if (allowPrivateKeyRequests && (userSigningChanged || userSigningExistsNotLocallyCached)) { _logger.logger.info("Attempting to retrieve cross-signing user-signing private key"); let signing = null; try { const ret = await this.crossSigningInfo.getCrossSigningKey("user_signing", newCrossSigning.getId("user_signing")); signing = ret[1]; _logger.logger.info("Got cross-signing user-signing private key"); } finally { var _signing4; (_signing4 = signing) === null || _signing4 === void 0 ? void 0 : _signing4.free(); } } if (masterChanged) { const masterKey = this.crossSigningInfo.keys.master; await this.signObject(masterKey); const deviceSig = masterKey.signatures[this.userId]["ed25519:" + this.deviceId]; // Include only the _new_ device signature in the upload. // We may have existing signatures from deleted devices, which will cause // the entire upload to fail. keySignatures[this.crossSigningInfo.getId()] = Object.assign({}, masterKey, { signatures: { [this.userId]: { ["ed25519:" + this.deviceId]: deviceSig } } }); } const keysToUpload = Object.keys(keySignatures); if (keysToUpload.length) { const upload = ({ shouldEmit = false }) => { _logger.logger.info(`Starting background key sig upload for ${keysToUpload}`); return this.baseApis.uploadKeySignatures({ [this.userId]: keySignatures }).then(response => { const { failures } = response || {}; _logger.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 _errors.KeySignatureUploadError("Key upload failed", { failures }); } }).catch(e => { _logger.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 */ async storeTrustedSelfKeys(keys) { if (keys) { this.crossSigningInfo.setKeys(keys); } else { this.crossSigningInfo.clearKeys(); } await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.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 */ async checkDeviceVerifications(userId) { const shouldUpgradeCb = this.baseApis.cryptoCallbacks.shouldUpgradeDeviceVerifications; if (!shouldUpgradeCb) { // Upgrading skipped when callback is not present. return; } _logger.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.logger.info(`Finished device verification upgrade for ${userId}`); } /** */ enableLazyLoading() { 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 */ registerEventHandlers(eventEmitter) { eventEmitter.on(_roomMember.RoomMemberEvent.Membership, this.onMembership); eventEmitter.on(_client.ClientEvent.ToDeviceEvent, this.onToDeviceEvent); eventEmitter.on(_room.RoomEvent.Timeline, this.onTimelineEvent); eventEmitter.on(_event2.MatrixEventEvent.Decrypted, this.onTimelineEvent); } /** * @deprecated this does nothing and will be removed in a future version */ start() { _logger.logger.warn("MatrixClient.crypto.start() is deprecated"); } /** Stop background processes related to crypto */ stop() { this.outgoingRoomKeyRequestManager.stop(); this.deviceList.stop(); this.dehydrationManager.stop(); } /** * Get the Ed25519 key for this device * * @returns base64-encoded ed25519 key. */ getDeviceEd25519Key() { return this.olmDevice.deviceEd25519Key; } /** * Get the Curve25519 key for this device * * @returns base64-encoded curve25519 key. */ getDeviceCurve25519Key() { 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. */ setGlobalBlacklistUnverifiedDevices(value) { 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. */ getGlobalBlacklistUnverifiedDevices() { return this.globalBlacklistUnverifiedDevices; } /** * Upload the device keys to the homeserver. * @returns A promise that will resolve when the keys are uploaded. */ uploadDeviceKeys() { const deviceKeys = { algorithms: this.supportedAlgorithms, device_id: this.deviceId, keys: this.deviceKeys, user_id: this.userId }; return this.signObject(deviceKeys).then(() => { return this.baseApis.uploadKeysRequest({ device_keys: deviceKeys }); }); } /** * Stores the current one_time_key count which will be handled later (in a call of * onSyncCompleted). The count is e.g. coming from a /sync response. * * @param currentCount - The current count of one_time_keys to be stored */ updateOneTimeKeyCount(currentCount) { if (isFinite(currentCount)) { this.oneTimeKeyCount = currentCount; } else { throw new TypeError("Parameter for updateOneTimeKeyCount has to be a number"); } } setNeedsNewFallback(needsNewFallback) { this.needsNewFallback = needsNewFallback; } getNeedsNewFallback() { return !!this.needsNewFallback; } // check if it's time to upload one-time keys, and do so if so. maybeUploadOneTimeKeys() { // frequency with which to check & upload one-time keys const uploadPeriod = 1000 * 60; // one minute // max number of keys to upload at once // Creating keys can be an expensive operation so we limit the // number we generate in one go to avoid blocking the application // for too long. const maxKeysPerCycle = 5; if (this.oneTimeKeyCheckInProgress) { return; } const now = Date.now(); if (this.lastOneTimeKeyCheck !== null && now - this.lastOneTimeKeyCheck < uploadPeriod) { // we've done a key upload recently. return; } this.lastOneTimeKeyCheck = now; // We need to keep a pool of one time public keys on the server so that // other devices can start conversations with us. But we can only store // a finite number of private keys in the olm Account object. // To complicate things further then can be a delay between a device // claiming a public one time key from the server and it sending us a // message. We need to keep the corresponding private key locally until // we receive the message. // But that message might never arrive leaving us stuck with duff // private keys clogging up our local storage. // So we need some kind of engineering compromise to balance all of // these factors. // Check how many keys we can store in the Account object. const maxOneTimeKeys = this.olmDevice.maxNumberOfOneTimeKeys(); // Try to keep at most half that number on the server. This leaves the // rest of the slots free to hold keys that have been claimed from the // server but we haven't received a message for. // If we run out of slots when generating new keys then olm will // discard the oldest private keys first. This will eventually clean // out stale private keys that won't receive a message. const keyLimit = Math.floor(maxOneTimeKeys / 2); const uploadLoop = async keyCount => { while (keyLimit > keyCount || this.getNeedsNewFallback()) { // Ask olm to generate new one time keys, then upload them to synapse. if (keyLimit > keyCount) { _logger.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.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.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.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 async uploadOneTimeKeys() { const promises = []; let fallbackJson; 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 = {}; 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 = { 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}`. */ downloadKeys(userIds, forceDownload) { 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. */ getStoredDevicesForUser(userId) { 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 */ getStoredDevice(userId, deviceId) { 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. */ saveDeviceList(delay) { 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 */ async setDeviceVerification(userId, deviceId, verified = null, blocked = null, known = null, keys) { // 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.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 }) => { _logger.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 _errors.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; // 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.logger.info("Own device " + deviceId + " marked verified: signing"); // Signing only needed if other device not already signed let device; const deviceTrust = this.checkDeviceTrust(userId, deviceId); if (deviceTrust.isCrossSigningVerified()) { _logger.logger.log(`Own device ${deviceId} already cross-signing verified`); } else { device = await this.crossSigningInfo.signDevice(userId, _deviceinfo.DeviceInfo.fromStorage(dev, deviceId)); } if (device) { const upload = async ({ shouldEmit = false }) => { _logger.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 _errors.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.DeviceInfo.fromStorage(dev, deviceId); this.emit(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj); return deviceObj; } findVerificationRequestDMInProgress(roomId) { return this.inRoomVerificationRequests.findRequestInProgress(roomId); } getVerificationRequestsToDeviceInProgress(userId) { return this.toDeviceVerificationRequests.getRequestsInProgress(userId); } requestVerificationDM(userId, roomId) { const existingRequest = this.inRoomVerificationRequests.findRequestInProgress(roomId); if (existingRequest) { return Promise.resolve(existingRequest); } const channel = new _InRoomChannel.InRoomChannel(this.baseApis, roomId, userId); return this.requestVerificationWithChannel(userId, channel, this.inRoomVerificationRequests); } requestVerification(userId, devices) { if (!devices) { devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(userId)); } const existingRequest = this.toDeviceVerificationRequests.findRequestInProgress(userId, devices); if (existingRequest) { return Promise.resolve(existingRequest); } const channel = new _ToDeviceChannel.ToDeviceChannel(this.baseApis, userId, devices, _ToDeviceChannel.ToDeviceChannel.makeTransactionId()); return this.requestVerificationWithChannel(userId, channel, this.toDeviceVerificationRequests); } async requestVerificationWithChannel(userId, channel, requestsMap) { let request = new _VerificationRequest.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.logger.log(`Crypto: adding new request to ` + `requestsByTxnId with id ${channel.transactionId} ${channel.roomId}`); requestsMap.setRequestByChannel(channel, request); } return request; } beginKeyVerification(method, userId, deviceId, transactionId = null) { let request; if (transactionId) { request = this.toDeviceVerificationRequests.getRequestBySenderAndTxnId(userId, transactionId); if (!request) { throw new Error(`No request found for user ${userId} with ` + `transactionId ${transactionId}`); } } else { transactionId = _ToDeviceChannel.ToDeviceChannel.makeTransactionId(); const channel = new _ToDeviceChannel.ToDeviceChannel(this.baseApis, userId, [deviceId], transactionId, deviceId); request = new _VerificationRequest.VerificationRequest(channel, this.verificationMethods, this.baseApis); this.toDeviceVerificationRequests.setRequestBySenderAndTxnId(userId, transactionId, request); } return request.beginKeyVerification(method, { userId, deviceId }); } async legacyDeviceVerification(userId, deviceId, method) { const transactionId = _ToDeviceChannel.ToDeviceChannel.makeTransactionId(); const channel = new _ToDeviceChannel.ToDeviceChannel(this.baseApis, userId, [deviceId], transactionId, deviceId); const request = new _VerificationRequest.VerificationRequest(channel, this.verificationMethods, this.baseApis); this.toDeviceVerificationRequests.setRequestBySenderAndTxnId(userId, transactionId, request); const verifier = request.beginKeyVerification(method, { userId, deviceId }); // either reject by an error from verify() while sending .start // or resolve when the request receives the // local (fake remote) echo for sending the .start event await Promise.race([verifier.verify(), request.waitFor(r => r.started)]); return request; } /** * Get information on the active olm sessions with a user *

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

* This method is provided for debugging purposes. * * @param userId - id of user to inspect */ async getOlmSessionsForUser(userId) { const devices = this.getStoredDevicesForUser(userId) || []; const result = {}; 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 */ getEventSenderDeviceInfo(event) { 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.logger.warn("Event " + event.getId() + " claims no ed25519 key: " + "cannot verify sending device"); return null; } if (claimedKey !== device.getFingerprint()) { _logger.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) */ getEventEncryptionInfo(event) { var _event$getSenderKey, _this$deviceList$getD; const ret = {}; ret.senderKey = (_event$getSenderKey = event.getSenderKey()) !== null && _event$getSenderKey !== void 0 ? _event$getSenderKey : undefined; ret.algorithm = event.getWireContent().algorithm; if (!ret.senderKey || !ret.algorithm) { ret.encrypted = false; return ret; } 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$getD = this.deviceList.getDeviceByIdentityKey(ret.algorithm, ret.senderKey)) !== null && _this$deviceList$getD !== void 0 ? _this$deviceList$getD : 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.logger.warn("Event " + event.getId() + " claims no ed25519 key: " + "cannot verify sending device"); ret.mismatchedSender = true; } if (ret.sender && claimedKey !== ret.sender.getFingerprint()) { _logger.logger.warn("Event " + event.getId() + " claims ed25519 key " + claimedKey + "but sender device has key " + ret.sender.getFingerprint()); ret.mismatchedSender = true; } return ret; } /** * Forces the current outbound group session to be discarded such * that another one will be created next time an event is sent. * * @param roomId - The ID of the room to discard the session for * * This should not normally be necessary. */ forceDiscardSession(roomId) { 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). */ async setRoomEncryption(roomId, config, inhibitDeviceQuery) { 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 m.room.encryption 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 not 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. */ async setRoomEncryptionImpl(room, config) { 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.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.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 = 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.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 => { room.off(_roomState.RoomStateEvent.Update, onState); if (room.membersLoaded()) { this.trackRoomDevicesImpl(room).catch(e => { _logger.logger.error(`Error enabling device tracking in ${roomId}`, e); }); } }; room.on(_roomState.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. */ trackRoomDevices(roomId) { 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 */ trackRoomDevicesImpl(room) { const roomId = room.roomId; const trackMembers = async () => { // not an encrypted room if (!this.roomEncryptors.has(roomId)) { return; } _logger.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} */ ensureOlmSessionsForUsers(users, force) { // map user Id → DeviceInfo[] const devicesByUser = new Map(); for (const userId of users) { const userDevices = []; 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 */ async exportRoomKeys() { const exportedSessions = []; await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.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 */ importRoomKeys(keys, opts = {}) { let successes = 0; let failures = 0; const total = keys.length; function updateProgress() { var _opts$progressCallbac; (_opts$progressCallbac = opts.progressCallback) === null || _opts$progressCallbac === void 0 ? void 0 : _opts$progressCallbac.call(opts, { stage: "load_keys", successes, failures, total }); } return Promise.all(keys.map(key => { if (!key.room_id || !key.algorithm) { _logger.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 */ countSessionsNeedingBackup() { 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 */ prepareToEncrypt(room) { 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 */ async encryptEvent(event, room) { 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); 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. */ async decryptEvent(event) { 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 _event2.MatrixEvent(_objectSpread({ room_id: event.getRoomId() }, event.getUnsigned().redacted_because)); let redactedBecause = event.getUnsigned().redacted_because; if (redactionEvent.isEncrypted()) { try { const decryptedEvent = await this.decryptEvent(redactionEvent); redactedBecause = decryptedEvent.clearEvent; } catch (e) { _logger.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 */ async handleDeviceListChanges(syncData, syncDeviceLists) { // Initial syncs don't have device change lists. We'll either get the complete list // of changes for the interval or will have invalidated everything in willProcessSync if (!syncData.oldSyncToken) return; // Here, we're relying on the fact that we only ever save the sync data after // sucessfully saving the device list data, so we're guaranteed that the device // list store is at least as fresh as the sync token from the sync store, ie. // any device changes received in sync tokens prior to the 'next' token here // have been processed and are reflected in the current device list. // If we didn't make this assumption, we'd have to use the /keys/changes API // to get key changes between the sync token in the device list and the 'old' // sync token used here to make sure we didn't miss any. await this.evalDeviceListChanges(syncDeviceLists); } /** * Send a request for some room keys, if we have not already done so * * @param resend - whether to resend the key request if there is * already one * * @returns a promise that resolves when the key request is queued */ requestRoomKey(requestBody, recipients, resend = false) { return this.outgoingRoomKeyRequestManager.queueRoomKeyRequest(requestBody, recipients, resend).then(() => { if (this.sendKeyRequestsImmediately) { this.outgoingRoomKeyRequestManager.sendQueuedRequests(); } }).catch(e => { // this normally means we couldn't talk to the store _logger.logger.error("Error requesting key for event", e); }); } /** * Cancel any earlier room key request * * @param requestBody - parameters to match for cancellation */ cancelRoomKeyRequest(requestBody) { this.outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody).catch(e => { _logger.logger.warn("Error clearing pending room key requests", e); }); } /** * Re-send any outgoing key requests, eg after verification * @returns */ async cancelAndResendAllOutgoingKeyRequests() { 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 */ async onCryptoEvent(room, event) { const content = event.getContent(); await this.setRoomEncryptionImpl(room, content); } /** * Called before the result of a sync is processed * * @param syncData - the data from the 'MatrixClient.sync' event */ async onSyncWillProcess(syncData) { if (!syncData.oldSyncToken) { // If there is no old sync token, we start all our tracking from // scratch, so mark everything as untracked. onCryptoEvent will // be called for all e2e rooms during the processing of the sync, // at which point we'll start tracking all the users of that room. _logger.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 */ async onSyncCompleted(syncData) { var _syncData$nextSyncTok; this.deviceList.setSyncToken((_syncData$nextSyncTok = syncData.nextSyncToken) !== null && _syncData$nextSyncTok !== void 0 ? _syncData$nextSyncTok : 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 */ async evalDeviceListChanges(deviceLists) { if (Array.isArray(deviceLists === null || deviceLists === void 0 ? void 0 : deviceLists.changed)) { deviceLists.changed.forEach(u => { this.deviceList.invalidateUserDeviceList(u); }); } if (Array.isArray(deviceLists === null || deviceLists === void 0 ? void 0 : 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 */ async getTrackedE2eUsers() { const e2eUserIds = []; for (const room of this.getTrackedE2eRooms()) { const members = await room.getEncryptionTargetMembers(); for (const member of members) { e2eUserIds.push(member.userId); } } return e2eUserIds; } /** * Get a list of the e2e-enabled rooms we are members of, * and for which we are already tracking the devices * * @returns */ getTrackedE2eRooms() { 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. */ async encryptAndSendToDevices(userDeviceInfoArr, payload) { const toDeviceBatch = { eventType: _event.EventType.RoomMessageEncrypted, batch: [] }; try { await Promise.all(userDeviceInfoArr.map(async ({ userId, deviceInfo }) => { const deviceId = deviceInfo.deviceId; const encryptedContent = { algorithm: olmlib.OLM_ALGORITHM, sender_key: this.olmDevice.deviceCurve25519Key, ciphertext: {}, [_event.ToDeviceMessageId]: (0, _uuid.v4)() }; 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.logger.log(`No ciphertext for device ${msg.userId}:${msg.deviceId}: pruning`); return false; } }); try { await this.baseApis.queueToDevice(toDeviceBatch); } catch (e) { _logger.logger.error("sendToDevice failed", e); throw e; } } catch (e) { _logger.logger.error("encryptAndSendToDevices promises failed", e); throw e; } } async preprocessToDeviceMessages(events) { // 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 => { var _toDevice$content; if (toDevice.type === _event.EventType.RoomMessageEncrypted && !["m.olm.v1.curve25519-aes-sha2"].includes((_toDevice$content = toDevice.content) === null || _toDevice$content === void 0 ? void 0 : _toDevice$content.algorithm)) { _logger.logger.log("Ignoring invalid encrypted to-device event from " + toDevice.sender); return false; } return true; }); } preprocessOneTimeKeyCounts(oneTimeKeysCounts) { const currentCount = oneTimeKeysCounts.get("signed_curve25519") || 0; this.updateOneTimeKeyCount(currentCount); return Promise.resolve(); } preprocessUnusedFallbackKeys(unusedFallbackKeys) { this.setNeedsNewFallback(!unusedFallbackKeys.has("signed_curve25519")); return Promise.resolve(); } /** * Handle a key event * * @internal * @param event - key event */ onRoomKeyEvent(event) { const content = event.getContent(); if (!content.room_id || !content.algorithm) { _logger.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 */ onRoomKeyWithheldEvent(event) { const content = event.getContent(); if (content.code !== "m.no_olm" && (!content.room_id || !content.session_id) || !content.algorithm || !content.sender_key) { _logger.logger.error("key withheld event is missing fields"); return; } _logger.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 */ onKeyVerificationMessage(event) { if (!_ToDeviceChannel.ToDeviceChannel.validateEvent(event, this.baseApis)) { return; } const createRequest = event => { if (!_ToDeviceChannel.ToDeviceChannel.canCreateRequest(_ToDeviceChannel.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.ToDeviceChannel(this.baseApis, userId, [deviceId]); return new _VerificationRequest.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 */ async handleVerificationEvent(event, requestsMap, createRequest, isLiveEvent = true) { // Wait for event to get its final ID with pendingEventOrdering: "chronological", since DM channels depend on it. if (event.isSending() && event.status != _event2.EventStatus.SENT) { let eventIdListener; let statusListener; try { await new Promise((resolve, reject) => { eventIdListener = resolve; statusListener = () => { if (event.status == _event2.EventStatus.CANCELLED) { reject(new Error("Event status set to CANCELLED.")); } }; event.once(_event2.MatrixEventEvent.LocalEventIdReplaced, eventIdListener); event.on(_event2.MatrixEventEvent.Status, statusListener); }); } catch (err) { _logger.logger.error("error while waiting for the verification event to be sent: ", err); return; } finally { event.removeListener(_event2.MatrixEventEvent.LocalEventIdReplaced, eventIdListener); event.removeListener(_event2.MatrixEventEvent.Status, statusListener); } } let request = requestsMap.getRequest(event); let isNewRequest = false; if (!request) { request = createRequest(event); // a request could not be made from this event, so ignore event if (!request) { _logger.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.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 */ async onToDeviceBadEncrypted(event) { const content = event.getWireContent(); const sender = event.getSender(); const algorithm = content.algorithm; const deviceKey = content.sender_key; this.baseApis.emit(_client.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 = () => { 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.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.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 = { algorithm: olmlib.OLM_ALGORITHM, sender_key: this.olmDevice.deviceCurve25519Key, ciphertext: {}, [_event.ToDeviceMessageId]: (0, _uuid.v4)() }; 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 */ onRoomMembership(event, member, oldMembership) { // this event handler is registered on the *client* (as opposed to the room // member itself), which means it is only called on changes to the *live* // membership state (ie, it is not called when we back-paginate, nor when // we load the state in the initialsync). // // Further, it is automatically registered and called when new members // arrive in the room. const roomId = member.roomId; const alg = this.roomEncryptors.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) { var _this$clientStore$get; if (member.membership == "join") { _logger.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$get = this.clientStore.getRoom(roomId)) !== null && _this$clientStore$get !== void 0 && _this$clientStore$get.shouldEncryptForInvitedMembers()) { _logger.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 */ onRoomKeyRequestEvent(event) { const content = event.getContent(); if (content.action === "request") { // Queue it up for now, because they tend to arrive before the room state // events at initial sync, and we want to see if we know anything about the // room before passing them on to the app. const req = new IncomingRoomKeyRequest(event); this.receivedRoomKeyRequests.push(req); } else if (content.action === "request_cancellation") { const req = new IncomingRoomKeyRequestCancellation(event); this.receivedRoomKeyRequestCancellations.push(req); } } /** * Process any m.room_key_request events which were queued up during the * current sync. * * @internal */ async processReceivedRoomKeyRequests() { 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.logger.error(`Error processing room key requsts: ${e}`); } finally { this.processingRoomKeyRequests = false; } } /** * Helper for processReceivedRoomKeyRequests * */ async processReceivedRoomKeyRequest(req) { const userId = req.userId; const deviceId = req.deviceId; const body = req.requestBody; const roomId = body.room_id; const alg = body.algorithm; _logger.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.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.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.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.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.logger.log(`room key request for unencrypted room ${roomId}`); return; } const decryptor = this.roomDecryptors.get(roomId).get(alg); if (!decryptor) { _logger.logger.log(`room key request for unknown alg ${alg} in room ${roomId}`); return; } if (!(await decryptor.hasKeysForKeyRequest(req))) { _logger.logger.log(`room key request for unknown session ${roomId} / ` + body.session_id); return; } req.share = () => { decryptor.shareKeysWithDevice(req); }; // if the device is verified already, share the keys if (this.checkDeviceTrust(userId, deviceId).isVerified()) { _logger.logger.log("device is already verified: sharing keys"); req.share(); return; } this.emit(CryptoEvent.RoomKeyRequest, req); } /** * Helper for processReceivedRoomKeyRequests * */ async processReceivedRoomKeyRequestCancellation(cancellation) { _logger.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 */ getRoomDecryptor(roomId, algorithm) { let decryptors; let alg; if (roomId) { decryptors = this.roomDecryptors.get(roomId); if (!decryptors) { decryptors = new Map(); 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 !== null && roomId !== void 0 ? 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 */ getRoomDecryptors(algorithm) { const decryptors = []; 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 */ async signObject(obj) { 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.default.stringify(obj)); obj.signatures = (0, _utils.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. * */ exports.Crypto = Crypto; function fixBackupKey(key) { if (typeof key !== "string" || key.indexOf(",") < 0) { return null; } const fixedKey = Uint8Array.from(key.split(","), x => parseInt(x)); return olmlib.encodeBase64(fixedKey); } /** * Represents a received m.room_key_request event */ class IncomingRoomKeyRequest { /** user requesting the key */ /** device requesting the key */ /** unique id for the request */ /** * callback which, when called, will ask * the relevant crypto algorithm implementation to share the keys for * this request. */ constructor(event) { (0, _defineProperty2.default)(this, "userId", void 0); (0, _defineProperty2.default)(this, "deviceId", void 0); (0, _defineProperty2.default)(this, "requestId", void 0); (0, _defineProperty2.default)(this, "requestBody", void 0); (0, _defineProperty2.default)(this, "share", void 0); const content = event.getContent(); this.userId = event.getSender(); this.deviceId = content.requesting_device_id; this.requestId = content.request_id; this.requestBody = content.body || {}; this.share = () => { throw new Error("don't know how to share keys for this request yet"); }; } } /** * Represents a received m.room_key_request cancellation */ exports.IncomingRoomKeyRequest = IncomingRoomKeyRequest; class IncomingRoomKeyRequestCancellation { /** user requesting the cancellation */ /** device requesting the cancellation */ /** unique id for the request to be cancelled */ constructor(event) { (0, _defineProperty2.default)(this, "userId", void 0); (0, _defineProperty2.default)(this, "deviceId", void 0); (0, _defineProperty2.default)(this, "requestId", void 0); 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. //# sourceMappingURL=index.js.map