diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index aa53993dd..e84625eca 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -20,7 +20,7 @@ import anotherjson from 'another-json'; import * as olmlib from "../../../src/crypto/olmlib"; import {TestClient} from '../../TestClient'; import {HttpResponse, setHttpResponses} from '../../test-utils'; -import {resetCrossSigningKeys, createSecretStorageKey} from "./crypto-utils"; +import { resetCrossSigningKeys } from "./crypto-utils"; import { MatrixError } from '../../../src/http-api'; async function makeTestClient(userInfo, options, keys) { @@ -71,8 +71,7 @@ describe("Cross Signing", function() { alice.setAccountData = async () => {}; alice.getAccountDataFromServer = async () => {}; // set Alice's cross-signing key - await alice.bootstrapSecretStorage({ - createSecretStorageKey, + await alice.bootstrapCrossSigning({ authUploadDeviceSigningKeys: async func => await func({}), }); expect(alice.uploadDeviceSigningKeys).toHaveBeenCalled(); @@ -118,8 +117,7 @@ describe("Cross Signing", function() { // through failure, stopping before actually applying changes. let bootstrapDidThrow = false; try { - await alice.bootstrapSecretStorage({ - createSecretStorageKey, + await alice.bootstrapCrossSigning({ authUploadDeviceSigningKeys, }); } catch (e) { diff --git a/spec/unit/crypto/secrets.spec.js b/spec/unit/crypto/secrets.spec.js index ceb01e716..b8ae95b52 100644 --- a/spec/unit/crypto/secrets.spec.js +++ b/spec/unit/crypto/secrets.spec.js @@ -326,9 +326,11 @@ describe("Secrets", function() { this.emit("accountData", event); }; + await bob.bootstrapCrossSigning({ + authUploadDeviceSigningKeys: async func => await func({}), + }); await bob.bootstrapSecretStorage({ createSecretStorageKey, - authUploadDeviceSigningKeys: async func => await func({}), }); const crossSigning = bob._crypto._crossSigningInfo; @@ -379,13 +381,15 @@ describe("Secrets", function() { const secretStorage = bob._crypto._secretStorage; // Set up cross-signing keys from scratch with specific storage key + await bob.bootstrapCrossSigning({ + authUploadDeviceSigningKeys: async func => await func({}), + }); await bob.bootstrapSecretStorage({ createSecretStorageKey: async () => ({ // `pubkey` not used anymore with symmetric 4S keyInfo: { pubkey: storagePublicKey }, privateKey: storagePrivateKey, }), - authUploadDeviceSigningKeys: async func => await func({}), }); // Clear local cross-signing keys and read from secret storage @@ -394,7 +398,7 @@ describe("Secrets", function() { crossSigning.toStorage(), ); crossSigning.keys = {}; - await bob.bootstrapSecretStorage({ + await bob.bootstrapCrossSigning({ authUploadDeviceSigningKeys: async func => await func({}), }); @@ -517,9 +521,7 @@ describe("Secrets", function() { this.emit("accountData", event); }; - await alice.bootstrapSecretStorage({ - authUploadDeviceSigningKeys: async func => await func({}), - }); + await alice.bootstrapSecretStorage(); expect(alice.getAccountData("m.secret_storage.default_key").getContent()) .toEqual({key: "key_id"}); @@ -659,9 +661,7 @@ describe("Secrets", function() { this.emit("accountData", event); }; - await alice.bootstrapSecretStorage({ - authUploadDeviceSigningKeys: async func => await func({}), - }); + await alice.bootstrapSecretStorage(); const backupKey = alice.getAccountData("m.megolm_backup.v1") .getContent(); diff --git a/src/client.js b/src/client.js index ab3bb405e..56017be85 100644 --- a/src/client.js +++ b/src/client.js @@ -1176,6 +1176,7 @@ wrapCryptoFuncs(MatrixClient, [ "legacyDeviceVerification", "prepareToEncrypt", "isCrossSigningReady", + "bootstrapCrossSigning", "getCryptoTrustCrossSignedDevices", "setCryptoTrustCrossSignedDevices", "countSessionsNeedingBackup", @@ -1360,6 +1361,7 @@ wrapCryptoFuncs(MatrixClient, [ wrapCryptoFuncs(MatrixClient, [ "getEventEncryptionInfo", "createRecoveryKeyFromPassphrase", + "isSecretStorageReady", "bootstrapSecretStorage", "addSecretStorageKey", "hasSecretStorageKey", diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index 9e3133b81..d3ecaf647 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -86,6 +86,7 @@ export class CrossSigningInfo extends EventEmitter { /** * Calls the app callback to ask for a private key + * * @param {string} type The key type ("master", "self_signing", or "user_signing") * @param {string} expectedPubkey The matching public key or undefined to use * the stored public key for the given key type. @@ -204,6 +205,38 @@ export class CrossSigningInfo extends EventEmitter { return decodeBase64(encodedKey); } + /** + * Check whether the private keys exist in the local key cache. + * + * @returns {boolean} True if all keys are stored in the local cache. + */ + async isStoredInKeyCache() { + const cacheCallbacks = this._cacheCallbacks; + if (!cacheCallbacks) return false; + for (const type of ["master", "self_signing", "user_signing"]) { + if (!await cacheCallbacks.getCrossSigningKeyCache(type)) { + return false; + } + } + return true; + } + + /** + * Get cross-signing private keys from the local cache. + * + * @returns {Map} A map from key type (string) to private key (Uint8Array) + */ + async getCrossSigningKeysFromCache() { + const keys = new Map(); + const cacheCallbacks = this._cacheCallbacks; + if (!cacheCallbacks) return keys; + for (const type of ["master", "self_signing", "user_signing"]) { + const privKey = await cacheCallbacks.getCrossSigningKeyCache(type); + keys.set(type, privKey); + } + return keys; + } + /** * Get the ID used to identify the user. This can also be used to test for * the existence of a given key type. diff --git a/src/crypto/index.js b/src/crypto/index.js index c89efea7f..de6f1149e 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -406,14 +406,12 @@ Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) { /** * Checks whether cross signing: - * - is enabled on this account - * - is trusted by this device - * - has private keys stored in secret storage - * and that the account has a secret storage key + * - 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, bootstrapSecretStorage() can be used + * If this function returns false, bootstrapCrossSigning() can be used * to fix things such that it returns true. That is to say, after - * bootstrapSecretStorage() completes successfully, this function should + * bootstrapCrossSigning() completes successfully, this function should * return true. * * The cross-signing API is currently UNSTABLE and may change without notice. @@ -422,18 +420,166 @@ Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) { */ Crypto.prototype.isCrossSigningReady = async function() { const publicKeysOnDevice = this._crossSigningInfo.getId(); - const privateKeysInStorage = await this._crossSigningInfo.isStoredInSecretStorage( - this._secretStorage, + const privateKeysExistSomewhere = ( + await this._crossSigningInfo.isStoredInKeyCache() || + await this._crossSigningInfo.isStoredInSecretStorage( + this._secretStorage, + ) ); - const secretStorageKeyInAccount = await this._secretStorage.hasKey(); return ( publicKeysOnDevice && - privateKeysInStorage && - secretStorageKeyInAccount + 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 secret storage API is currently UNSTABLE and may change without notice. + * + * @return {bool} True if secret storage is ready to be used on this device + */ +Crypto.prototype.isSecretStorageReady = async function() { + const secretStorageKeyInAccount = await this._secretStorage.hasKey(); + const privateKeysInStorage = await this._crossSigningInfo.isStoredInSecretStorage( + this._secretStorage, + ); + const sessionBackupInStorage = ( + !this._baseApis.getKeyBackupEnabled() || + this._baseApis.isKeyBackupKeyStored() + ); + + return ( + secretStorageKeyInAccount && + privateKeysInStorage && + sessionBackupInStorage + ); +}; + +/** + * Bootstrap cross-signing by creating keys if needed. If everything is already + * set up, then no changes are made, so this is safe to run to ensure + * cross-signing is ready for use. + * + * This function: + * - creates new cross-signing keys if they are not found locally cached nor in + * secret storage (if it has been setup) + * + * @param {function} opts.authUploadDeviceSigningKeys Function + * called to await an interactive auth flow when uploading device signing keys. + * @param {bool} [opts.setupNewCrossSigning] Optional. Reset even if keys + * already exist. + * Args: + * {function} A function that makes the request requiring auth. Receives the + * auth data as an object. Can be called multiple times, first with an empty + * authDict, to obtain the flows. + */ +Crypto.prototype.bootstrapCrossSigning = async function({ + authUploadDeviceSigningKeys, + setupNewCrossSigning, +} = {}) { + logger.log("Bootstrapping cross-signing"); + + const delegateCryptoCallbacks = this._baseApis._cryptoCallbacks; + const builder = new EncryptionSetupBuilder( + this._baseApis.store.accountData, + delegateCryptoCallbacks, + ); + + const crossSigningInfo = new CrossSigningInfo( + this._userId, + builder.crossSigningCallbacks, + builder.crossSigningCallbacks); + + // Reset the cross-signing keys + const resetCrossSigning = async () => { + crossSigningInfo.resetKeys(); + // Sign master key with device key + await this._signObject(crossSigningInfo.keys.master); + + // Store auth flow helper function, as we need to call it when uploading + // to ensure we handle auth errors properly. + builder.addCrossSigningKeys(authUploadDeviceSigningKeys, crossSigningInfo.keys); + + // Cross-sign own device + const device = this._deviceList.getStoredDevice(this._userId, this._deviceId); + const deviceSignature = await crossSigningInfo.signDevice(this._userId, device); + builder.addKeySignature(this._userId, this._deviceId, deviceSignature); + + // Sign message key backup with cross-signing master key + if (this.backupInfo) { + await crossSigningInfo.signObject(this.backupInfo.auth_data, "master"); + builder.addSessionBackup(this.backupInfo); + } + }; + + const publicKeysOnDevice = this._crossSigningInfo.getId(); + const privateKeysInCache = await this._crossSigningInfo.isStoredInKeyCache(); + const privateKeysInStorage = await this._crossSigningInfo.isStoredInSecretStorage( + this._secretStorage, + ); + const privateKeysExistSomewhere = ( + privateKeysInCache || + privateKeysInStorage + ); + + if (publicKeysOnDevice && privateKeysInCache) { + logger.log( + "Cross-signing public keys trusted and private keys found locally", + ); + } else if (!privateKeysExistSomewhere || setupNewCrossSigning) { + logger.log( + "Cross-signing private keys not found locally or in secret storage, " + + "creating new keys", + ); + await resetCrossSigning(); + } else if (privateKeysInStorage) { + logger.log( + "Cross-signing private keys not found locally, but they are available " + + "in secret storage, reading storage and caching locally", + ); + await this.checkOwnCrossSigningTrust(); + } + + // Assuming no app-supplied callback, default to storing new private keys in + // secret storage if it exists. If it does not, it is assumed this will be + // done as part of setting up secret storage later. + const crossSigningPrivateKeys = builder.crossSigningCallbacks.privateKeys; + if ( + crossSigningPrivateKeys.size && + !this._baseApis._cryptoCallbacks.saveCrossSigningKeys + ) { + const secretStorage = new SecretStorage( + builder.accountDataClientAdapter, + builder.ssssCryptoCallbacks); + if (await secretStorage.hasKey()) { + logger.log("Storing cross-signing private keys in secret storage"); + // This is writing to in-memory account data in + // builder.accountDataClientAdapter so won't fail + await CrossSigningInfo.storeInSecretStorage( + crossSigningPrivateKeys, + secretStorage, + ); + } + } + + const operation = builder.buildOperation(); + await operation.apply(this); + // This persists private keys and public keys as trusted, + // only do this if apply succeeded for now as retry isn't in place yet + await builder.persist(this); + + logger.log("Cross-signing ready"); +}; /** * Bootstrap Secure Secret Storage if needed by creating a default key. If everything is @@ -448,12 +594,6 @@ Crypto.prototype.isCrossSigningReady = async function() { * - migrates Secure Secret Storage to use the latest algorithm, if an outdated * algorithm is found * - * @param {function} opts.authUploadDeviceSigningKeys Function - * called to await an interactive auth flow when uploading device signing keys. - * Args: - * {function} A function that makes the request requiring auth. Receives the - * auth data as an object. Can be called multiple times, first with an empty - * authDict, to obtain the flows. * @param {function} [opts.createSecretStorageKey] Optional. Function * called to await a secret storage key creation flow. * Returns: @@ -473,9 +613,7 @@ Crypto.prototype.isCrossSigningReady = async function() { * {Promise} A promise which resolves to key creation data for * SecretStorage#addKey: an object with `passphrase` and/or `pubkey` fields. */ - Crypto.prototype.bootstrapSecretStorage = async function({ - authUploadDeviceSigningKeys, createSecretStorageKey = async () => ({ }), keyBackupInfo, setupNewKeyBackup, @@ -491,10 +629,6 @@ Crypto.prototype.bootstrapSecretStorage = async function({ const secretStorage = new SecretStorage( builder.accountDataClientAdapter, builder.ssssCryptoCallbacks); - const crossSigningInfo = new CrossSigningInfo( - this._userId, - builder.crossSigningCallbacks, - builder.crossSigningCallbacks); // the ID of the new SSSS key, if we create one let newKeyId = null; @@ -519,27 +653,6 @@ Crypto.prototype.bootstrapSecretStorage = async function({ return keyId; }; - // 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); - - if (keyBackupInfo) { - await crossSigningInfo.signObject(keyBackupInfo.auth_data, "master"); - builder.addSessionBackup(keyBackupInfo); - } - }; - const ensureCanCheckPassphrase = async (keyId, keyInfo) => { if (!keyInfo.mac) { const key = await this._baseApis._cryptoCallbacks.getSecretStorageKey( @@ -561,46 +674,36 @@ Crypto.prototype.bootstrapSecretStorage = async function({ const oldSSSSKey = await this.getSecretStorageKey(); const [oldKeyId, oldKeyInfo] = oldSSSSKey || [null, null]; - const decryptionKeys = - await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage); - const inStorage = !setupNewSecretStorage && decryptionKeys; + const storageExists = ( + !setupNewSecretStorage && + oldKeyInfo && + oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES + ); - if (!inStorage && !keyBackupInfo) { + if (!storageExists && !keyBackupInfo) { // either we don't have anything, or we've been asked to restart // from scratch logger.log( - "Cross-signing private keys not found in secret storage, " + - "creating new keys", + "Secret storage does not exist, creating new storage key", ); - await resetCrossSigning(); - - if ( - setupNewSecretStorage || - !oldKeyInfo || - oldKeyInfo.algorithm !== SECRET_STORAGE_ALGORITHM_V1_AES - ) { - // 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 (!inStorage && keyBackupInfo) { + // if we already have a usable default SSSS key and aren't resetting + // SSSS just use it. otherwise, create a new one + // Note: we leave the old SSSS key in place: there could be other + // secrets using it, in theory. We could move them to the new key but a) + // that would mean we'd need to prompt for the old passphrase, and b) + // it's not clear that would be the right thing to do anyway. + const { keyInfo, privateKey } = await createSecretStorageKey(); + newKeyId = await createSSSS(keyInfo, privateKey); + } else if (!storageExists && keyBackupInfo) { // we have an existing backup, but no SSSS - - logger.log("Secret storage default key not found, using key backup key"); + logger.log("Secret storage does not exist, using key backup key"); // if we have the backup key already cached, use it; otherwise use the // callback to prompt for the key const backupKey = await this.getSessionBackupPrivateKey() || await getKeyBackupPassphrase(); - // create new cross-signing keys - await resetCrossSigning(); - // create a new SSSS key and use the backup key as the new SSSS key const opts = {}; @@ -624,36 +727,16 @@ Crypto.prototype.bootstrapSecretStorage = async function({ ); // The backup is trusted because the user provided the private key. - // Sign the backup with the cross signing key so the key backup can + // Sign the backup with the cross-signing key so the key backup can // be trusted via cross-signing. logger.log("Adding cross signing signature to key backup"); - await crossSigningInfo.signObject( + await this._crossSigningInfo.signObject( keyBackupInfo.auth_data, "master", ); builder.addSessionBackup(keyBackupInfo); - } else if (!this._crossSigningInfo.getId()) { - // we have SSSS, but we don't know if the server's cross-signing - // keys should be trusted - logger.log("Cross-signing private keys found in secret storage"); - - // TODO: take this use case out of bootstrapping - // fetch the private keys and set up our local copy of the keys for - // use - // - // so if some other device resets the cross-signing keys, - // we mark them as untrusted from _onDeviceListUserCrossSigningUpdated - // you can either fix this by hitting the verify this session which (might?) call this method, - // or the reset button in the settings - await this.checkOwnCrossSigningTrust(); - - if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { - // make sure that the default key has the information needed to - // check the passphrase - await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo); - } } else { - // we have SSSS and we cross-signing is already set up - logger.log("Cross signing keys are present in secret storage"); + // 4S is already set up + logger.log("Secret storage exists"); if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { // make sure that the default key has the information needed to @@ -662,21 +745,26 @@ Crypto.prototype.bootstrapSecretStorage = async function({ } } - const crossSigningPrivateKeys = builder.crossSigningCallbacks.privateKeys; - if (crossSigningPrivateKeys.size) { + // 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() && + !await this._crossSigningInfo.isStoredInSecretStorage(secretStorage) + ) { logger.log("Storing cross-signing private keys in secret storage"); - // Assuming no app-supplied callback, default to storing in SSSS. - if (!this._baseApis._cryptoCallbacks.saveCrossSigningKeys) { - // this is writing to in-memory account data in builder.accountDataClientAdapter - // so won't fail - await CrossSigningInfo.storeInSecretStorage( - crossSigningPrivateKeys, - secretStorage, - ); - } + const crossSigningPrivateKeys = + await this._crossSigningInfo.getCrossSigningKeysFromCache(); + // This is writing to in-memory account data in + // builder.accountDataClientAdapter so won't fail + await CrossSigningInfo.storeInSecretStorage( + crossSigningPrivateKeys, + secretStorage, + ); } if (setupNewKeyBackup && !keyBackupInfo) { + logger.log("Creating new message key backup version"); const info = await this._baseApis.prepareKeyBackupVersion( null /* random key */, // don't write to secret storage, as it will write to this._secretStorage. @@ -694,11 +782,10 @@ Crypto.prototype.bootstrapSecretStorage = async function({ auth_data: info.auth_data, }; // sign with cross-sign master key - await crossSigningInfo.signObject(data.auth_data, "master"); + await this._crossSigningInfo.signObject(data.auth_data, "master"); // sign with the device fingerprint await this._signObject(data.auth_data); - builder.addSessionBackup(data); }