diff --git a/src/client.js b/src/client.js index 4ed0c6af1..4ccb9325a 100644 --- a/src/client.js +++ b/src/client.js @@ -53,6 +53,8 @@ import { keyForNewBackup, keyForExistingBackup } from './crypto/backup_password' import { randomString } from './randomstring'; import { pkSign } from './crypto/PkSigning'; +import IndexedDBCryptoStore from './crypto/store/indexeddb-crypto-store'; + // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. Promise.config({warnings: false}); @@ -982,24 +984,40 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(password) { algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, auth_data: authData, recovery_key: encodeRecoveryKey(decryption.get_private_key()), + accountKeys: null, }; if (signing) { - const ssk_seed = signing.generate_seed(); + await this._cryptoStore.doTxn('readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { + this._cryptoStore.getAccountKeys(txn, keys => { + returnInfo.accountKeys = keys; + }); + }); + + if (!returnInfo.accountKeys) { + const ssk_seed = signing.generate_seed(); + const usk_seed = signing.generate_seed(); + + returnInfo.accountKeys = { + self_signing_key_seed: Buffer.from(ssk_seed).toString('base64'), + user_signing_key_seed: Buffer.from(usk_seed).toString('base64'), + } + } + // put the encrypted version of the seed in the auth data to upload // XXX: our encryption really should support encrypting binary data. - authData.self_signing_key = encryption.encrypt(Buffer.from(ssk_seed).toString('base64')); - // and the unencrypted one in the returndata so we can use it later - returnInfo.ssk_seed = ssk_seed + authData.self_signing_key_seed = encryption.encrypt(returnInfo.accountKeys.self_signing_key_seed); // also keep the public part there - returnInfo.ssk_public = signing.init_with_seed(ssk_seed); + returnInfo.ssk_public = signing.init_with_seed(Buffer.from(returnInfo.accountKeys.self_signing_key_seed, 'base64')); signing.free(); - const usk_seed = signing.generate_seed(); - authData.user_signing_key = encryption.encrypt(Buffer.from(usk_seed).toString('base64')); - returnInfo.usk_seed = usk_seed; - returnInfo.usk_public = signing.init_with_seed(usk_seed); + // same for the USK + authData.user_signing_key_seed = encryption.encrypt(returnInfo.accountKeys.user_signing_key_seed); + returnInfo.usk_public = signing.init_with_seed(Buffer.from(returnInfo.accountKeys.user_signing_key_seed, 'base64')); signing.free(); + + // we don't save these keys back to the store yet: we'll do that when (if) we + // actually create the backup } return returnInfo; @@ -1035,7 +1053,8 @@ MatrixClient.prototype.createKeyBackupVersion = function(info, auth, replacesSsk }, }; - pkSign(uskInfo, info.ssk_seed, this.credentials.userId); + // sign the USK with the SSK + pkSign(uskInfo, Buffer.from(info.accountKeys.self_signing_key_seed, 'base64'), this.credentials.userId); const keys = { self_signing_key: { @@ -1050,7 +1069,11 @@ MatrixClient.prototype.createKeyBackupVersion = function(info, auth, replacesSsk auth, }; - return this.uploadDeviceSigningKeys(keys).then(() => { + return this._cryptoStore.doTxn('readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { + this._cryptoStore.storeAccountKeys(txn, info.accountKeys); + }).then(() => { + return this.uploadDeviceSigningKeys(keys); + }).then(() => { return this._crypto._signObject(data.auth_data); }).then(() => { return this._http.authedRequest( @@ -1150,27 +1173,25 @@ MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) { }; MatrixClient.prototype.restoreKeyBackupWithPassword = async function( - password, targetRoomId, targetSessionId, version, + password, targetRoomId, targetSessionId, backupInfo, ) { - const backupInfo = await this.getKeyBackupVersion(); - const privKey = await keyForExistingBackup(backupInfo, password); return this._restoreKeyBackup( - privKey, targetRoomId, targetSessionId, version, + privKey, targetRoomId, targetSessionId, backupInfo, ); }; MatrixClient.prototype.restoreKeyBackupWithRecoveryKey = function( - recoveryKey, targetRoomId, targetSessionId, version, + recoveryKey, targetRoomId, targetSessionId, backupInfo, ) { const privKey = decodeRecoveryKey(recoveryKey); return this._restoreKeyBackup( - privKey, targetRoomId, targetSessionId, version, + privKey, targetRoomId, targetSessionId, backupInfo, ); }; -MatrixClient.prototype._restoreKeyBackup = function( - privKey, targetRoomId, targetSessionId, version, +MatrixClient.prototype._restoreKeyBackup = async function( + privKey, targetRoomId, targetSessionId, backupInfo, ) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); @@ -1178,11 +1199,39 @@ MatrixClient.prototype._restoreKeyBackup = function( let totalKeyCount = 0; let keys = []; - const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, version); + const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, backupInfo.version); const decryption = new global.Olm.PkDecryption(); try { decryption.init_with_private_key(privKey); + + // decrypt the account keys from the backup info if there are any + // fetch the old ones first so we don't lose info if only one of them is in the backup + let accountKeys; + await this._cryptoStore.doTxn('readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { + this._cryptoStore.getAccountKeys(txn, keys => { + accountKeys = keys || {}; + }); + }); + + if (backupInfo.auth_data.self_signing_key_seed) { + accountKeys.self_signing_key_seed = decryption.decrypt( + backupInfo.auth_data.self_signing_key_seed.ephemeral, + backupInfo.auth_data.self_signing_key_seed.mac, + backupInfo.auth_data.self_signing_key_seed.ciphertext, + ); + } + if (backupInfo.auth_data.user_signing_key_seed) { + accountKeys.user_signing_key_seed = decryption.decrypt( + backupInfo.auth_data.user_signing_key_seed.ephemeral, + backupInfo.auth_data.user_signing_key_seed.mac, + backupInfo.auth_data.user_signing_key_seed.ciphertext, + ); + } + + await this._cryptoStore.doTxn('readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { + this._cryptoStore.storeAccountKeys(txn, accountKeys); + }); } catch(e) { decryption.free(); throw e; diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index f4b07ed5a..eac5049b9 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -332,6 +332,23 @@ export class Backend { objectStore.put(newData, "-"); } + getAccountKeys(txn, func) { + const objectStore = txn.objectStore("account"); + const getReq = objectStore.get("keys"); + getReq.onsuccess = function() { + try { + func(getReq.result || null); + } catch (e) { + abortWithException(txn, e); + } + }; + } + + storeAccountKeys(txn, keys) { + const objectStore = txn.objectStore("account"); + objectStore.put(keys, "keys"); + } + // Olm Sessions countEndToEndSessions(txn, func) { diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 5f9defd02..be6950e11 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -283,7 +283,7 @@ export default class IndexedDBCryptoStore { this._backendPromise.value().getAccount(txn, func); } - /* + /** * Write the account pickle to the store. * This requires an active transaction. See doTxn(). * @@ -294,6 +294,28 @@ export default class IndexedDBCryptoStore { this._backendPromise.value().storeAccount(txn, newData); } + /** + * Get the account keys fort cross-signing (eg. self-signing key, + * user signing key). + * + * @param {*} txn An active transaction. See doTxn(). + * @param {function(string)} func Called with the account keys object: + * { key_type: base64 encoded seed } where key type = user_signing_key_seed or self_signing_key_seed + */ + getAccountKeys(txn, func) { + this._backendPromise.value().getAccountKeys(txn, func); + } + + /** + * Write the account keys back to the store + * + * @param {*} txn An active transaction. See doTxn(). + * @param {string} keys Account keys object as getAccountKeys() + */ + storeAccountKeys(txn, keys) { + this._backendPromise.value().storeAccountKeys(txn, keys); + } + // Olm sessions /** diff --git a/src/crypto/store/localStorage-crypto-store.js b/src/crypto/store/localStorage-crypto-store.js index 4c30b6199..d5316132f 100644 --- a/src/crypto/store/localStorage-crypto-store.js +++ b/src/crypto/store/localStorage-crypto-store.js @@ -31,6 +31,7 @@ import MemoryCryptoStore from './memory-crypto-store.js'; const E2E_PREFIX = "crypto."; const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account"; +const KEY_END_TO_END_ACCOUNT_KEYS = E2E_PREFIX + "account_keys"; const KEY_DEVICE_DATA = E2E_PREFIX + "device_data"; const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/"; const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/"; @@ -274,6 +275,17 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { ); } + getAccountKeys(txn, func) { + const keys = getJsonItem(this.store, KEY_END_TO_END_ACCOUNT_KEYS); + func(keys); + } + + storeAccountKeys(txn, keys) { + setJsonItem( + this.store, KEY_END_TO_END_ACCOUNT_KEYS, keys, + ); + } + doTxn(mode, stores, func) { return Promise.resolve(func(null)); } diff --git a/src/crypto/store/memory-crypto-store.js b/src/crypto/store/memory-crypto-store.js index 49df5f238..fd2205c9f 100644 --- a/src/crypto/store/memory-crypto-store.js +++ b/src/crypto/store/memory-crypto-store.js @@ -33,6 +33,7 @@ export default class MemoryCryptoStore { constructor() { this._outgoingRoomKeyRequests = []; this._account = null; + this._accountKeys = null; // Map of {devicekey -> {sessionId -> session pickle}} this._sessions = {}; @@ -234,6 +235,14 @@ export default class MemoryCryptoStore { this._account = newData; } + getAccountKeys(txn, func) { + func(this._accountKeys); + } + + storeAccountKeys(txn, keys) { + this._accountKeys = keys; + } + // Olm Sessions countEndToEndSessions(txn, func) {