diff --git a/src/client.js b/src/client.js index 1dbe4bfe9..78a43f163 100644 --- a/src/client.js +++ b/src/client.js @@ -774,9 +774,27 @@ MatrixClient.prototype.getKeyBackupVersion = function(callback) { } return res; } + }).catch(e => { + if (e.errcode === 'M_NOT_FOUND') { + if (callback) callback(null); + return null; + } else { + throw e; + } }); } +/** + * @returns {bool} true if the client is configured to back up keys to + * the server, otherwise false. + */ +MatrixClient.prototype.getKeyBackupEnabled = function() { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + return Boolean(this._crypto.backupKey); +} + /** * Enable backing up of keys, using data previously returned from * getKeyBackupVersion. @@ -786,6 +804,7 @@ MatrixClient.prototype.enableKeyBackup = function(info) { throw new Error("End-to-end encryption disabled"); } + this._crypto.backupInfo = info; this._crypto.backupKey = new global.Olm.PkEncryption(); this._crypto.backupKey.set_recipient_key(info.auth_data.public_key); } @@ -798,7 +817,8 @@ MatrixClient.prototype.disableKeyBackup = function() { throw new Error("End-to-end encryption disabled"); } - this._crypto.backupKey = undefined; + this._crypto.backupInfo = null; + this._crypto.backupKey = null; } /** @@ -836,11 +856,16 @@ MatrixClient.prototype.createKeyBackupVersion = function(info, callback) { algorithm: info.algorithm, auth_data: info.auth_data, // FIXME: should this be cloned? } - this._crypto._signObject(data.auth_data); - return this._http.authedRequest( - undefined, "POST", "/room_keys/version", undefined, data, - ).then((res) => { - this.enableKeyBackup(info); + return this._crypto._signObject(data.auth_data).then(() => { + return this._http.authedRequest( + undefined, "POST", "/room_keys/version", undefined, data, + ); + }).then((res) => { + this.enableKeyBackup({ + algorithm: info.algorithm, + auth_data: info.auth_data, + version: res.version, + }); if (callback) { callback(null, res); } @@ -848,6 +873,27 @@ MatrixClient.prototype.createKeyBackupVersion = function(info, callback) { }); } +MatrixClient.prototype.deleteKeyBackupVersion = function(version) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + // If we're currently backing up to this backup... stop. + // (We start using it automatically in createKeyBackupVersion + // so this is symmetrical). + if (this._crypto.backupInfo && this._crypto.backupInfo.version === version) { + this.disableKeyBackup(); + } + + const path = utils.encodeUri("/room_keys/version/$version", { + $version: version, + }); + + return this._http.authedRequest( + undefined, "DELETE", path, undefined, undefined, + ); +}; + MatrixClient.prototype._makeKeyBackupPath = function(roomId, sessionId, version) { let path; if (sessionId !== undefined) { @@ -890,6 +936,14 @@ MatrixClient.prototype.sendKeyBackup = function(roomId, sessionId, version, data ); }; +MatrixClient.prototype.backupAllGroupSessions = function(version) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + return this._crypto.backupAllGroupSessions(version); +}; + MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, roomId, sessionId, version, callback) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); @@ -924,7 +978,7 @@ MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, roomId, sessi }) }; -MatrixClient.prototype.deleteKeyBackups = function(roomId, sessionId, version, callback) { +MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, version, callback) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index ac9c72f68..af311e16b 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -263,6 +263,14 @@ MegolmEncryption.prototype._prepareNewSession = async function() { key.key, {ed25519: this._olmDevice.deviceEd25519Key}, ); + if (this._crypto.backupInfo) { + // Not strictly necessary to wait for this + await this._crypto.backupGroupSession( + this._roomId, this._olmDevice.deviceCurve25519Key, [], + sessionId, key.key, + ); + } + return new OutboundSessionInfo(sessionId); }; @@ -840,11 +848,13 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { // have another go at decrypting events sent with this session. this._retryDecryption(senderKey, sessionId); }).then(() => { - return this.backupGroupSession( - content.room_id, senderKey, forwardingKeyChain, - content.session_id, content.session_key, keysClaimed, - exportFormat, - ); + if (this._crypto.backupInfo) { + return this._crypto.backupGroupSession( + content.room_id, senderKey, forwardingKeyChain, + content.session_id, content.session_key, keysClaimed, + exportFormat, + ); + } }).catch((e) => { console.error(`Error handling m.room_key_event: ${e}`); }); @@ -967,54 +977,6 @@ MegolmDecryption.prototype.importRoomKey = function(session) { }); }; -MegolmDecryption.prototype.backupGroupSession = async function( - roomId, senderKey, forwardingCurve25519KeyChain, - sessionId, sessionKey, keysClaimed, - exportFormat, -) { - // new session. - const session = new Olm.InboundGroupSession(); - let first_known_index; - try { - if (exportFormat) { - session.import_session(sessionKey); - } else { - session.create(sessionKey); - } - if (sessionId != session.session_id()) { - throw new Error( - "Mismatched group session ID from senderKey: " + - senderKey, - ); - } - - if (!exportFormat) { - sessionKey = session.export_session(); - } - const first_known_index = session.first_known_index(); - - const sessionData = { - algorithm: olmlib.MEGOLM_ALGORITHM, - sender_key: senderKey, - sender_claimed_keys: keysClaimed, - forwardingCurve25519KeyChain: forwardingCurve25519KeyChain, - session_key: sessionKey - }; - const encrypted = this._crypto.backupKey.encrypt(JSON.stringify(sessionData)); - const data = { - first_message_index: first_known_index, - forwarded_count: forwardingCurve25519KeyChain.length, - is_verified: false, // FIXME: how do we determine this? - session_data: encrypted - }; - return this._baseApis.sendKeyBackup(roomId, sessionId, data); - } catch (e) { - return Promise.reject(e); - } finally { - session.free(); - } -} - /** * Have another go at decrypting events after we receive a key * diff --git a/src/crypto/index.js b/src/crypto/index.js index 54bb0d738..fb8f82614 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -75,7 +75,8 @@ function Crypto(baseApis, sessionStore, userId, deviceId, // track whether this device's megolm keys are being backed up incrementally // to the server or not. // XXX: this should probably have a single source of truth from OlmAccount - this.backupKey = null; + this.backupInfo = null; // The info dict from /room_keys/version + this.backupKey = null; // The encryption key object this._olmDevice = new OlmDevice(sessionStore, cryptoStore); this._deviceList = new DeviceList( @@ -848,6 +849,83 @@ Crypto.prototype.importRoomKeys = function(keys) { }, ); }; + +Crypto.prototype._backupPayloadForSession = function( + senderKey, forwardingCurve25519KeyChain, + sessionId, sessionKey, keysClaimed, + exportFormat, +) { + // new session. + const session = new Olm.InboundGroupSession(); + let first_known_index; + try { + if (exportFormat) { + session.import_session(sessionKey); + } else { + session.create(sessionKey); + } + if (sessionId != session.session_id()) { + throw new Error( + "Mismatched group session ID from senderKey: " + + senderKey, + ); + } + + if (!exportFormat) { + sessionKey = session.export_session(); + } + const first_known_index = session.first_known_index(); + + const sessionData = { + algorithm: olmlib.MEGOLM_ALGORITHM, + sender_key: senderKey, + sender_claimed_keys: keysClaimed, + session_key: sessionKey, + forwarding_curve25519_key_chain: forwardingCurve25519KeyChain, + }; + const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData)); + return { + first_message_index: first_known_index, + forwarded_count: forwardingCurve25519KeyChain.length, + is_verified: false, // FIXME: how do we determine this? + session_data: encrypted, + }; + } finally { + session.free(); + } +}; + +Crypto.prototype.backupGroupSession = function( + roomId, senderKey, forwardingCurve25519KeyChain, + sessionId, sessionKey, keysClaimed, + exportFormat, +) { + if (!this.backupInfo) { + throw new Error("Key backups are not enabled"); + } + + const data = this._backupPayloadForSession( + senderKey, forwardingCurve25519KeyChain, + sessionId, sessionKey, keysClaimed, + exportFormat, + ); + return this._baseApis.sendKeyBackup(roomId, sessionId, this.backupInfo.version, data); +}; + +Crypto.prototype.backupAllGroupSessions = async function(version) { + const keys = await this.exportRoomKeys(); + const data = {}; + for (const key of keys) { + if (data[key.room_id] === undefined) data[key.room_id] = {sessions: {}}; + + data[key.room_id]['sessions'][key.session_id] = this._backupPayloadForSession( + key.sender_key, key.forwarding_curve25519_key_chain, + key.session_id, key.session_key, key.sender_claimed_keys, true, + ); + } + return this._baseApis.sendKeyBackup(undefined, undefined, version, {rooms: data}); +}; + /* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307 /** * Encrypt an event according to the configuration of the room.