UNPKG

matrix-js-sdk

Version:
1,054 lines (1,008 loc) 136 kB
"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"); 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", {}); (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)) { 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)), cros