From 5e4f10a80c267ec5b383748a495cf794d469fd64 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 7 Feb 2019 14:37:25 +0000 Subject: [PATCH] Trust on decrypt Trust backups that we've restored by saving the matching pubkey locally. NB. Contains technically breaking API changes to the backup restore (takes backupInfo rather than version). --- src/client.js | 32 ++++++++++++++++++++++---------- src/crypto/index.js | 27 +++++++++++++++++++-------- 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/client.js b/src/client.js index 9efdf6d53..314e31b26 100644 --- a/src/client.js +++ b/src/client.js @@ -1093,28 +1093,28 @@ MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) { } }; -MatrixClient.prototype.restoreKeyBackupWithPassword = async function( - password, targetRoomId, targetSessionId, version, -) { - const backupInfo = await this.getKeyBackupVersion(); +MatrixClient.prototype.RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY'; +MatrixClient.prototype.restoreKeyBackupWithPassword = async function( + password, targetRoomId, targetSessionId, backupInfo, +) { 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, + privKey, targetRoomId, targetSessionId, backupInfo, ) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); @@ -1122,16 +1122,26 @@ 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(); + let backupPubKey; try { - decryption.init_with_private_key(privKey); + backupPubKey = decryption.init_with_private_key(privKey); } catch(e) { decryption.free(); throw e; } + // If the pubkey computed from the private data we've been given + // doesn't match the one in the auth_data, the user has enetered + // a different recovery key / the wrong passphrase. + if (backupPubKey !== backupInfo.auth_data.public_key) { + return Promise.reject({errcode: this.RESTORE_BACKUP_ERROR_BAD_KEY}); + } + return this._http.authedRequest( undefined, "GET", path.path, path.queryData, ).then((res) => { @@ -1166,6 +1176,8 @@ MatrixClient.prototype._restoreKeyBackup = function( } return this.importRoomKeys(keys); + }).then(() => { + return this._crypto.setTrustedBackupPubKey(backupPubKey); }).then(() => { return {total: totalKeyCount, imported: keys.length}; }).finally(() => { diff --git a/src/crypto/index.js b/src/crypto/index.js index 7c4d60f32..5dd6bd570 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -292,6 +292,13 @@ Crypto.prototype._checkAndStartKeyBackup = async function() { } }; +Crypto.prototype.setTrustedBackupPubKey = async function(trustedPubKey) { + // This should be redundant post cross-signing is a thing, so just + // plonk it in localStorage for now. + global.localStorage.setItem("mx_trusted_backup_pubkey", trustedPubKey); + await this.checkKeyBackup(); +}; + /** * Forces a re-check of the key backup and enables/disables it * as appropriate. @@ -315,6 +322,7 @@ Crypto.prototype.checkKeyBackup = async function() { Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { const ret = { usable: false, + trusted_locally: false, sigs: [], }; @@ -325,16 +333,19 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { !backupInfo.auth_data.public_key || !backupInfo.auth_data.signatures ) { - console.log("Key backup is absent or missing required data"); + logger.info("Key backup is absent or missing required data"); return ret; } - const mySigs = backupInfo.auth_data.signatures[this._userId]; - if (!mySigs || mySigs.length === 0) { - console.log("Ignoring key backup because it lacks any signatures from this user"); - return ret; + const trustedPubkey = global.localStorage.getItem("mx_trusted_backup_pubkey"); + + if (backupInfo.auth_data.public_key === trustedPubkey) { + logger.info("Backup public key " + trustedPubkey + " is trusted locally"); + ret.trusted_locally = true; } + const mySigs = backupInfo.auth_data.signatures[this._userId] || []; + for (const keyId of Object.keys(mySigs)) { const sigInfo = { deviceId: keyId.split(':')[1] }; // XXX: is this how we're supposed to get the device ID? const device = this._deviceList.getStoredDevice( @@ -352,17 +363,17 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { ); sigInfo.valid = true; } catch (e) { - console.log("Bad signature from device " + device.deviceId, e); + logger.info("Bad signature from device " + device.deviceId, e); sigInfo.valid = false; } } else { sigInfo.valid = null; // Can't determine validity because we don't have the signing device - console.log("Ignoring signature from unknown key " + keyId); + logger.info("Ignoring signature from unknown key " + keyId); } ret.sigs.push(sigInfo); } - ret.usable = ret.sigs.some((s) => s.valid && s.device.isVerified()); + ret.usable = ret.sigs.some((s) => s.valid && s.device.isVerified()) || ret.trusted_locally; return ret; };