import { logger } from "../logger"; import { MatrixEvent } from "../models/event"; import { EventEmitter } from "events"; import { createCryptoStoreCacheCallbacks } from "./CrossSigning"; import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; import { PREFIX_UNSTABLE, } from "../http-api"; /** * Builds an EncryptionSetupOperation by calling any of the add.. methods. * Once done, `buildOperation()` can be called which allows to apply to operation. * * This is used as a helper by Crypto to keep track of all the network requests * and other side-effects of bootstrapping, so it can be applied in one go (and retried in the future) * Also keeps track of all the private keys created during bootstrapping, so we don't need to prompt for them * more than once. */ export class EncryptionSetupBuilder { /** * @param {Object.} accountData pre-existing account data, will only be read, not written. * @param {CryptoCallbacks} delegateCryptoCallbacks crypto callbacks to delegate to if the key isn't in cache yet */ constructor(accountData, delegateCryptoCallbacks) { this.accountDataClientAdapter = new AccountDataClientAdapter(accountData); this.crossSigningCallbacks = new CrossSigningCallbacks(); this.ssssCryptoCallbacks = new SSSSCryptoCallbacks(delegateCryptoCallbacks); this._crossSigningKeys = null; this._keySignatures = null; this._keyBackupInfo = null; } /** * Adds new cross-signing public keys * * @param {function} authUpload 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 {Object} keys the new keys */ addCrossSigningKeys(authUpload, keys) { this._crossSigningKeys = { authUpload, keys }; } /** * Adds the key backup info to be updated on the server * * Used either to create a new key backup, or add signatures * from the new MSK. * * @param {Object} keyBackupInfo as received from/sent to the server */ addSessionBackup(keyBackupInfo) { this._keyBackupInfo = keyBackupInfo; } /** * Adds the session backup private key to be updated in the local cache * * Used after fixing the format of the key * * @param {Uint8Array} privateKey */ addSessionBackupPrivateKeyToCache(privateKey) { this._sessionBackupPrivateKey = privateKey; } /** * Add signatures from a given user and device/x-sign key * Used to sign the new cross-signing key with the device key * * @param {String} userId * @param {String} deviceId * @param {String} signature */ addKeySignature(userId, deviceId, signature) { if (!this._keySignatures) { this._keySignatures = {}; } const userSignatures = this._keySignatures[userId] || {}; this._keySignatures[userId] = userSignatures; userSignatures[deviceId] = signature; } /** * @param {String} type * @param {Object} content * @return {Promise} */ setAccountData(type, content) { return this.accountDataClientAdapter.setAccountData(type, content); } /** * builds the operation containing all the parts that have been added to the builder * @return {EncryptionSetupOperation} */ buildOperation() { const accountData = this.accountDataClientAdapter._values; return new EncryptionSetupOperation( accountData, this._crossSigningKeys, this._keyBackupInfo, this._keySignatures, ); } /** * Stores the created keys locally. * * This does not yet store the operation in a way that it can be restored, * but that is the idea in the future. * * @param {Crypto} crypto * @return {Promise} */ async persist(crypto) { // store private keys in cache if (this._crossSigningKeys) { const cacheCallbacks = createCryptoStoreCacheCallbacks( crypto._cryptoStore, crypto._olmDevice); for (const type of ["master", "self_signing", "user_signing"]) { logger.log(`Cache ${type} cross-signing private key locally`); const privateKey = this.crossSigningCallbacks.privateKeys.get(type); await cacheCallbacks.storeCrossSigningKeyCache(type, privateKey); } // store own cross-sign pubkeys as trusted await crypto._cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { crypto._cryptoStore.storeCrossSigningKeys( txn, this._crossSigningKeys.keys); }, ); } // store session backup key in cache if (this._sessionBackupPrivateKey) { await crypto.storeSessionBackupPrivateKey(this._sessionBackupPrivateKey); } } } /** * Can be created from EncryptionSetupBuilder, or * (in a follow-up PR, not implemented yet) restored from storage, to retry. * * It does not have knowledge of any private keys, unlike the builder. */ export class EncryptionSetupOperation { /** * @param {Map} accountData * @param {Object} crossSigningKeys * @param {Object} keyBackupInfo * @param {Object} keySignatures */ constructor(accountData, crossSigningKeys, keyBackupInfo, keySignatures) { this._accountData = accountData; this._crossSigningKeys = crossSigningKeys; this._keyBackupInfo = keyBackupInfo; this._keySignatures = keySignatures; } /** * Runs the (remaining part of, in the future) operation by sending requests to the server. * @param {Crypto} crypto */ async apply(crypto) { const baseApis = crypto._baseApis; // upload cross-signing keys if (this._crossSigningKeys) { const keys = {}; for (const [name, key] of Object.entries(this._crossSigningKeys.keys)) { keys[name + "_key"] = key; } // We must only call `uploadDeviceSigningKeys` from inside this auth // helper to ensure we properly handle auth errors. await this._crossSigningKeys.authUpload(authDict => { return baseApis.uploadDeviceSigningKeys(authDict, keys); }); // pass the new keys to the main instance of our own CrossSigningInfo. crypto.crossSigningInfo.setKeys(this._crossSigningKeys.keys); } // set account data if (this._accountData) { for (const [type, content] of this._accountData) { await baseApis.setAccountData(type, content); } } // upload first cross-signing signatures with the new key // (e.g. signing our own device) if (this._keySignatures) { await baseApis.uploadKeySignatures(this._keySignatures); } // need to create/update key backup info if (this._keyBackupInfo) { if (this._keyBackupInfo.version) { // session backup signature // 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 baseApis.http.authedRequest( undefined, "PUT", "/room_keys/version/" + this._keyBackupInfo.version, undefined, { algorithm: this._keyBackupInfo.algorithm, auth_data: this._keyBackupInfo.auth_data, }, { prefix: PREFIX_UNSTABLE }, ); } else { // add new key backup await baseApis.http.authedRequest( undefined, "POST", "/room_keys/version", undefined, this._keyBackupInfo, { prefix: PREFIX_UNSTABLE }, ); } } } } /** * Catches account data set by SecretStorage during bootstrapping by * implementing the methods related to account data in MatrixClient */ class AccountDataClientAdapter extends EventEmitter { /** * @param {Object.} accountData existing account data */ constructor(accountData) { super(); this._existingValues = accountData; this._values = new Map(); } /** * @param {String} type * @return {Promise} the content of the account data */ getAccountDataFromServer(type) { return Promise.resolve(this.getAccountData(type)); } /** * @param {String} type * @return {Object} the content of the account data */ getAccountData(type) { const modifiedValue = this._values.get(type); if (modifiedValue) { return modifiedValue; } const existingValue = this._existingValues[type]; if (existingValue) { return existingValue.getContent(); } return null; } /** * @param {String} type * @param {Object} content * @return {Promise} */ setAccountData(type, content) { const lastEvent = this._values.get(type); this._values.set(type, content); // ensure accountData is emitted on the next tick, // as SecretStorage listens for it while calling this method // and it seems to rely on this. return Promise.resolve().then(() => { const event = new MatrixEvent({ type, content }); this.emit("accountData", event, lastEvent); }); } } /** * Catches the private cross-signing keys set during bootstrapping * by both cache callbacks (see createCryptoStoreCacheCallbacks) as non-cache callbacks. * See CrossSigningInfo constructor */ class CrossSigningCallbacks { constructor() { this.privateKeys = new Map(); } // cache callbacks getCrossSigningKeyCache(type, expectedPublicKey) { return this.getCrossSigningKey(type, expectedPublicKey); } storeCrossSigningKeyCache(type, key) { this.privateKeys.set(type, key); return Promise.resolve(); } // non-cache callbacks getCrossSigningKey(type, _expectedPubkey) { return Promise.resolve(this.privateKeys.get(type)); } saveCrossSigningKeys(privateKeys) { for (const [type, privateKey] of Object.entries(privateKeys)) { this.privateKeys.set(type, privateKey); } } } /** * Catches the 4S private key set during bootstrapping by implementing * the SecretStorage crypto callbacks */ class SSSSCryptoCallbacks { constructor(delegateCryptoCallbacks) { this._privateKeys = new Map(); this._delegateCryptoCallbacks = delegateCryptoCallbacks; } async getSecretStorageKey({ keys }, name) { for (const keyId of Object.keys(keys)) { const privateKey = this._privateKeys.get(keyId); if (privateKey) { return [keyId, privateKey]; } } // if we don't have the key cached yet, ask // for it to the general crypto callbacks and cache it if (this._delegateCryptoCallbacks) { const result = await this._delegateCryptoCallbacks. getSecretStorageKey({ keys }, name); if (result) { const [keyId, privateKey] = result; this._privateKeys.set(keyId, privateKey); } return result; } } addPrivateKey(keyId, keyInfo, privKey) { this._privateKeys.set(keyId, privKey); // Also pass along to application to cache if it wishes if ( this._delegateCryptoCallbacks && this._delegateCryptoCallbacks.cacheSecretStorageKey ) { this._delegateCryptoCallbacks.cacheSecretStorageKey(keyId, keyInfo, privKey); } } }