From fb1b554b862677848e94dc0e1cf74e6ea184419a Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 15 Jan 2018 01:50:24 +0000 Subject: [PATCH 01/38] initial pseudocode WIP for e2e online backups --- src/client.js | 1 + src/crypto/OlmDevice.js | 20 ++++++++++++++++++++ src/crypto/index.js | 14 ++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/src/client.js b/src/client.js index 0b00ab36b..293571185 100644 --- a/src/client.js +++ b/src/client.js @@ -393,6 +393,7 @@ MatrixClient.prototype.initCrypto = async function() { "crypto.roomKeyRequest", "crypto.roomKeyRequestCancellation", "crypto.warning", + "crypto.suggestKeyRestore", ]); await crypto.init(); diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index cda14779c..131419ea7 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -91,6 +91,21 @@ function OlmDevice(sessionStore, cryptoStore) { this.deviceEd25519Key = null; this._maxOneTimeKeys = null; + // 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; + + // track which of our other devices (if any) have cross-signed this device + // XXX: this should probably have a single source of truth in the /devices + // API store or whatever we use to track our self-signed devices. + this.crossSelfSigs = []; + + // track whether we have already suggested to the user that they should + // restore their keys from backup or by cross-signing the device. + // We use this to avoid repeatedly emitting the suggestion event. + this.suggestedKeyRestore = false; + // we don't bother stashing outboundgroupsessions in the sessionstore - // instead we keep them here. this._outboundGroupSessionStore = {}; @@ -921,6 +936,11 @@ OlmDevice.prototype.addInboundGroupSession = async function( this._cryptoStore.addEndToEndInboundGroupSession( senderKey, sessionId, sessionData, txn, ); + + if (this.backupKey) { + // get olm::Account::generate_backup_encryption_secret + // save sessionData (pickled with this secret) to the server + } } finally { session.free(); } diff --git a/src/crypto/index.js b/src/crypto/index.js index 6b1d8f477..31dcd76b6 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1015,6 +1015,13 @@ Crypto.prototype._onRoomKeyEvent = function(event) { return; } + if (!device.suggestedKeyRestore && + !device.backupKey && !device.selfCrossSigs.length) + { + this.emit("crypto.suggestKeyRestore"); + device.suggestKeyRestore = true; + } + const alg = this._getRoomDecryptor(content.room_id, content.algorithm); alg.onRoomKeyEvent(event); }; @@ -1355,6 +1362,13 @@ class IncomingRoomKeyRequestCancellation { * @param {module:crypto~IncomingRoomKeyRequestCancellation} req */ +/** + * Fires when we want to suggest to the user that they restore their megolm keys + * from backup or by cross-signing the device. + * + * @event module:client~MatrixClient#"crypto.suggestKeyRestore" + */ + /** * Fires when the app may wish to warn the user about something related * the end-to-end crypto. From e0c9b990e7db19bed24129b9307b37ed4600c765 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 18 Jan 2018 20:59:08 +0000 Subject: [PATCH 02/38] blindly move crypto.suggestKeyRestore over to /sync --- src/client.js | 7 ++++++- src/crypto/OlmDevice.js | 1 + src/crypto/index.js | 14 -------------- src/sync.js | 10 ++++++++++ 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/client.js b/src/client.js index 293571185..aa05462ac 100644 --- a/src/client.js +++ b/src/client.js @@ -393,7 +393,6 @@ MatrixClient.prototype.initCrypto = async function() { "crypto.roomKeyRequest", "crypto.roomKeyRequestCancellation", "crypto.warning", - "crypto.suggestKeyRestore", ]); await crypto.init(); @@ -3630,6 +3629,12 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * }); */ +/** + * Fires when we want to suggest to the user that they restore their megolm keys + * from backup or by cross-signing the device. + * + * @event module:client~MatrixClient#"crypto.suggestKeyRestore" + */ // EventEmitter JSDocs diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 131419ea7..656e4fddc 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -104,6 +104,7 @@ function OlmDevice(sessionStore, cryptoStore) { // track whether we have already suggested to the user that they should // restore their keys from backup or by cross-signing the device. // We use this to avoid repeatedly emitting the suggestion event. + // XXX: persist this somewhere! this.suggestedKeyRestore = false; // we don't bother stashing outboundgroupsessions in the sessionstore - diff --git a/src/crypto/index.js b/src/crypto/index.js index 31dcd76b6..6b1d8f477 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1015,13 +1015,6 @@ Crypto.prototype._onRoomKeyEvent = function(event) { return; } - if (!device.suggestedKeyRestore && - !device.backupKey && !device.selfCrossSigs.length) - { - this.emit("crypto.suggestKeyRestore"); - device.suggestKeyRestore = true; - } - const alg = this._getRoomDecryptor(content.room_id, content.algorithm); alg.onRoomKeyEvent(event); }; @@ -1362,13 +1355,6 @@ class IncomingRoomKeyRequestCancellation { * @param {module:crypto~IncomingRoomKeyRequestCancellation} req */ -/** - * Fires when we want to suggest to the user that they restore their megolm keys - * from backup or by cross-signing the device. - * - * @event module:client~MatrixClient#"crypto.suggestKeyRestore" - */ - /** * Fires when the app may wish to warn the user about something related * the end-to-end crypto. diff --git a/src/sync.js b/src/sync.js index 71fb866d5..74aff54bf 100644 --- a/src/sync.js +++ b/src/sync.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd +Copyright 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -983,6 +984,15 @@ SyncApi.prototype._processSyncResponse = async function( async function processRoomEvent(e) { client.emit("event", e); if (e.isState() && e.getType() == "m.room.encryption" && self.opts.crypto) { + + // XXX: get device + if (!device.getSuggestedKeyRestore() && + !device.backupKey && !device.selfCrossSigs.length) + { + client.emit("crypto.suggestKeyRestore"); + device.setSuggestedKeyRestore(true); + } + await self.opts.crypto.onCryptoEvent(e); } } From d55618921bb584164417297162ab22ba0df08564 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 7 Aug 2018 23:10:55 -0400 Subject: [PATCH 03/38] initial implementation of e2e key backup and restore --- spec/unit/crypto/backup.spec.js | 218 ++++++++++++++++++++++++++++++++ src/client.js | 84 ++++++++++++ src/crypto/OlmDevice.js | 10 -- src/crypto/algorithms/megolm.js | 56 +++++++- src/crypto/index.js | 5 + src/sync.js | 4 +- 6 files changed, 365 insertions(+), 12 deletions(-) create mode 100644 spec/unit/crypto/backup.spec.js diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js new file mode 100644 index 000000000..b6ec6dce5 --- /dev/null +++ b/spec/unit/crypto/backup.spec.js @@ -0,0 +1,218 @@ +try { + global.Olm = require('olm'); +} catch (e) { + console.warn("unable to run megolm backup tests: libolm not available"); +} + +import expect from 'expect'; +import Promise from 'bluebird'; + +import sdk from '../../..'; +import algorithms from '../../../lib/crypto/algorithms'; +import WebStorageSessionStore from '../../../lib/store/session/webstorage'; +import MemoryCryptoStore from '../../../lib/crypto/store/memory-crypto-store.js'; +import MockStorageApi from '../../MockStorageApi'; +import testUtils from '../../test-utils'; + +// Crypto and OlmDevice won't import unless we have global.Olm +let OlmDevice; +let Crypto; +if (global.Olm) { + OlmDevice = require('../../../lib/crypto/OlmDevice'); + Crypto = require('../../../lib/crypto'); +} + +const MatrixClient = sdk.MatrixClient; +const MatrixEvent = sdk.MatrixEvent; +const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; + +const ROOM_ID = '!ROOM:ID'; + +describe("MegolmBackup", function() { + if (!global.Olm) { + console.warn('Not running megolm backup unit tests: libolm not present'); + return; + } + + let olmDevice; + let mockOlmLib; + let mockCrypto; + let mockStorage; + let sessionStore; + let cryptoStore; + let megolmDecryption; + beforeEach(function () { + testUtils.beforeEach(this); // eslint-disable-line no-invalid-this + + mockCrypto = testUtils.mock(Crypto, 'Crypto'); + mockCrypto.backupKey = new Olm.PkEncryption(); + mockCrypto.backupKey.set_recipient_key("hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmoK"); + + mockStorage = new MockStorageApi(); + sessionStore = new WebStorageSessionStore(mockStorage); + cryptoStore = new MemoryCryptoStore(mockStorage); + + olmDevice = new OlmDevice(sessionStore, cryptoStore); + + // we stub out the olm encryption bits + mockOlmLib = {}; + mockOlmLib.ensureOlmSessionsForDevices = expect.createSpy(); + mockOlmLib.encryptMessageForDevice = + expect.createSpy().andReturn(Promise.resolve()); + }); + + describe("backup", function() { + let mockBaseApis; + + beforeEach(function() { + mockBaseApis = {}; + + megolmDecryption = new MegolmDecryption({ + userId: '@user:id', + crypto: mockCrypto, + olmDevice: olmDevice, + baseApis: mockBaseApis, + roomId: ROOM_ID, + }); + + megolmDecryption.olmlib = mockOlmLib; + }); + + it('automatically backs up keys', function() { + const groupSession = new global.Olm.OutboundGroupSession(); + groupSession.create(); + + // construct a fake decrypted key event via the use of a mocked + // 'crypto' implementation. + const event = new MatrixEvent({ + type: 'm.room.encrypted', + }); + const decryptedData = { + clearEvent: { + type: 'm.room_key', + content: { + algorithm: 'm.megolm.v1.aes-sha2', + room_id: ROOM_ID, + session_id: groupSession.session_id(), + session_key: groupSession.session_key(), + }, + }, + senderCurve25519Key: "SENDER_CURVE25519", + claimedEd25519Key: "SENDER_ED25519", + }; + + mockCrypto.decryptEvent = function() { + return Promise.resolve(decryptedData); + }; + + const sessionId = groupSession.session_id(); + const cipherText = groupSession.encrypt(JSON.stringify({ + room_id: ROOM_ID, + content: 'testytest', + })); + const msgevent = new MatrixEvent({ + type: 'm.room.encrypted', + room_id: ROOM_ID, + content: { + algorithm: 'm.megolm.v1.aes-sha2', + sender_key: "SENDER_CURVE25519", + session_id: sessionId, + ciphertext: cipherText, + }, + event_id: "$event1", + origin_server_ts: 1507753886000, + }); + + mockBaseApis.sendKeyBackup = expect.createSpy(); + + return event.attemptDecryption(mockCrypto).then(() => { + return megolmDecryption.onRoomKeyEvent(event); + }).then(() => { + expect(mockBaseApis.sendKeyBackup).toHaveBeenCalled(); + }); + }); + }); + + describe("restore", function () { + let client; + + beforeEach(function() { + const scheduler = [ + "getQueueForEvent", "queueEvent", "removeEventFromQueue", + "setProcessFunction", + ].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {}); + const store = [ + "getRoom", "getRooms", "getUser", "getSyncToken", "scrollback", + "save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", "storeUser", + "getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter", + "getSyncAccumulator", "startup", "deleteAllData", + ].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {}); + store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null)); + store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null)); + store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null)); + client = new MatrixClient({ + baseUrl: "https://my.home.server", + idBaseUrl: "https://identity.server", + accessToken: "my.access.token", + request: function() {}, // NOP + store: store, + scheduler: scheduler, + userId: "@alice:bar", + deviceId: "device", + sessionStore: sessionStore, + cryptoStore: cryptoStore, + }); + + megolmDecryption = new MegolmDecryption({ + userId: '@user:id', + crypto: mockCrypto, + olmDevice: olmDevice, + baseApis: client, + roomId: ROOM_ID, + }); + + megolmDecryption.olmlib = mockOlmLib; + + return client.initCrypto(); + }); + + it('can restore from backup', function () { + const event = new MatrixEvent({ + type: 'm.room.encrypted', + room_id: '!ROOM:ID', + content: { + algorithm: 'm.megolm.v1.aes-sha2', + sender_key: 'SENDER_CURVE25519', + session_id: 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc', + ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/NCiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBlmkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs' + }, + event_id: '$event1', + origin_server_ts: 1507753886000, + }); + client._http.authedRequest = function () { + return Promise.resolve({ + data: { + first_message_index: 0, + forwarded_count: 0, + is_verified: false, + session_data: { + ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZSlne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOySyw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGFru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxvC+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpeUg5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3NfQHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPyiie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg', + mac: '5lxYBHQU80M', + ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14', + } + }, + headers: {}, + code: 200 + }); + }; + const decryption = new Olm.PkDecryption(); + decryption.unpickle("secret_key", "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZDQWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA"); + return client.restoreKeyBackups(decryption, ROOM_ID, 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc') + .then(() => { + return megolmDecryption.decryptEvent(event); + }).then((res) => { + expect(res.clearEvent.content).toEqual('testytest'); + }); + }); + }); +}); diff --git a/src/client.js b/src/client.js index 2a0002df5..7aa40d09d 100644 --- a/src/client.js +++ b/src/client.js @@ -703,6 +703,90 @@ MatrixClient.prototype.importRoomKeys = function(keys) { return this._crypto.importRoomKeys(keys); }; +MatrixClient.prototype._makeKeyBackupPath = function(roomId, sessionId, version) { + let path; + if (sessionId !== undefined) { + path = utils.encodeUri("/room_keys/keys/$roomId/$sessionId", { + $roomId: roomId, + $sessionId: sessionId, + }); + } else if (roomId !== undefined) { + path = utils.encodeUri("/room_keys/keys/$roomId", { + $roomId: roomId, + }); + } else { + path = "/room_keys/keys"; + } + const queryData = version === undefined ? undefined : {version : version}; + return { + path: path, + queryData: queryData, + } +} + +/** + * Back up session keys to the homeserver. + * @param {string} roomId ID of the room that the keys are for Optional. + * @param {string} sessionId ID of the session that the keys are for Optional. + * @param {integer} version backup version Optional. + * @param {object} key data + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} a promise that will resolve when the keys + * are uploaded + */ +MatrixClient.prototype.sendKeyBackup = function(roomId, sessionId, version, data, callback) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + const path = this._makeKeyBackupPath(roomId, sessionId, version); + return this._http.authedRequest( + callback, "PUT", path.path, path.queryData, data, + ); +}; + +MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, roomId, sessionId, version, callback) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + const path = this._makeKeyBackupPath(roomId, sessionId, version); + return this._http.authedRequest( + undefined, "GET", path.path, path.queryData, + ).then((response) => { + if (response.code === 200) { + const keys = []; + // FIXME: for each room, session, if response has multiple + // decrypt response.data.session_data + const data = response.data; + const key = JSON.parse(decryptionKey.decrypt(data.session_data.ephemeral, data.session_data.mac, data.session_data.ciphertext)); + // set room_id and session_id + key.room_id = roomId; + key.session_id = sessionId; + keys.push(key); + return this.importRoomKeys(keys); + } else { + callback("aargh!"); + return Promise.reject("aaargh!"); + } + }).then(() => { + if (callback) { + callback(); + } + }) +}; + +MatrixClient.prototype.deleteKeyBackups = function(roomId, sessionId, version, callback) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + const path = this._makeKeyBackupPath(roomId, sessionId, version); + return this._http.authedRequest( + callback, "DELETE", path.path, path.queryData, + ) +}; + // Group ops // ========= // Operations on groups that come down the sync stream (ie. ones the diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 656e4fddc..950faa8e2 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -91,11 +91,6 @@ function OlmDevice(sessionStore, cryptoStore) { this.deviceEd25519Key = null; this._maxOneTimeKeys = null; - // 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; - // track which of our other devices (if any) have cross-signed this device // XXX: this should probably have a single source of truth in the /devices // API store or whatever we use to track our self-signed devices. @@ -937,11 +932,6 @@ OlmDevice.prototype.addInboundGroupSession = async function( this._cryptoStore.addEndToEndInboundGroupSession( senderKey, sessionId, sessionData, txn, ); - - if (this.backupKey) { - // get olm::Account::generate_backup_encryption_secret - // save sessionData (pickled with this secret) to the server - } } finally { session.free(); } diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index 005d157c5..b246fc141 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -804,7 +804,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { } console.log(`Adding key for megolm session ${senderKey}|${sessionId}`); - this._olmDevice.addInboundGroupSession( + return this._olmDevice.addInboundGroupSession( content.room_id, senderKey, forwardingKeyChain, sessionId, content.session_key, keysClaimed, exportFormat, @@ -819,6 +819,12 @@ 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, + ); }).catch((e) => { console.error(`Error handling m.room_key_event: ${e}`); }); @@ -941,6 +947,54 @@ 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 703bd0631..fe233f2cf 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -72,6 +72,11 @@ function Crypto(baseApis, sessionStore, userId, deviceId, this._cryptoStore = cryptoStore; this._roomList = roomList; + // 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._olmDevice = new OlmDevice(sessionStore, cryptoStore); this._deviceList = new DeviceList( baseApis, cryptoStore, sessionStore, this._olmDevice, diff --git a/src/sync.js b/src/sync.js index b85f838f6..7c769f20e 100644 --- a/src/sync.js +++ b/src/sync.js @@ -1060,13 +1060,15 @@ SyncApi.prototype._processSyncResponse = async function( client.emit("event", e); if (e.isState() && e.getType() == "m.room.encryption" && self.opts.crypto) { + /* // XXX: get device - if (!device.getSuggestedKeyRestore() && + if (!device.getSuggestedKeyRestore() && !device.backupKey && !device.selfCrossSigs.length) { client.emit("crypto.suggestKeyRestore"); device.setSuggestedKeyRestore(true); } + */ await self.opts.crypto.onCryptoEvent(e); } From 1faf4775373608ab67c5613a9ff58bda6179c93b Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 22 Aug 2018 23:58:59 -0400 Subject: [PATCH 04/38] fix formatting and fix authedRequest usage --- spec/unit/crypto/backup.spec.js | 63 +++++++++++++++++++++------------ src/client.js | 31 ++++++++-------- 2 files changed, 55 insertions(+), 39 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index b6ec6dce5..a4956647f 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -45,8 +45,10 @@ describe("MegolmBackup", function() { testUtils.beforeEach(this); // eslint-disable-line no-invalid-this mockCrypto = testUtils.mock(Crypto, 'Crypto'); - mockCrypto.backupKey = new Olm.PkEncryption(); - mockCrypto.backupKey.set_recipient_key("hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmoK"); + mockCrypto.backupKey = new global.Olm.PkEncryption(); + mockCrypto.backupKey.set_recipient_key( + "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmoK" + ); mockStorage = new MockStorageApi(); sessionStore = new WebStorageSessionStore(mockStorage); @@ -184,35 +186,50 @@ describe("MegolmBackup", function() { algorithm: 'm.megolm.v1.aes-sha2', sender_key: 'SENDER_CURVE25519', session_id: 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc', - ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/NCiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBlmkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs' + ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N' + + 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl' + + 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs' }, event_id: '$event1', origin_server_ts: 1507753886000, }); client._http.authedRequest = function () { return Promise.resolve({ - data: { - first_message_index: 0, - forwarded_count: 0, - is_verified: false, - session_data: { - ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZSlne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOySyw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGFru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxvC+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpeUg5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3NfQHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPyiie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg', - mac: '5lxYBHQU80M', - ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14', - } - }, - headers: {}, - code: 200 + first_message_index: 0, + forwarded_count: 0, + is_verified: false, + session_data: { + ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw' + + '6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ' + + 'Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9' + + 'SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy' + + 'Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF' + + 'ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV' + + '4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv' + + 'C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe' + + 'Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf' + + 'QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy' + + 'iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg', + mac: '5lxYBHQU80M', + ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14', + } }); }; - const decryption = new Olm.PkDecryption(); - decryption.unpickle("secret_key", "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZDQWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA"); - return client.restoreKeyBackups(decryption, ROOM_ID, 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc') - .then(() => { - return megolmDecryption.decryptEvent(event); - }).then((res) => { - expect(res.clearEvent.content).toEqual('testytest'); - }); + const decryption = new global.Olm.PkDecryption(); + decryption.unpickle( + "secret_key", + "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD" + + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA" + ); + return client.restoreKeyBackups( + decryption, + ROOM_ID, + 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc' + ).then(() => { + return megolmDecryption.decryptEvent(event); + }).then((res) => { + expect(res.clearEvent.content).toEqual('testytest'); + }); }); }); }); diff --git a/src/client.js b/src/client.js index 7aa40d09d..329a58498 100644 --- a/src/client.js +++ b/src/client.js @@ -753,22 +753,21 @@ MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, roomId, sessi const path = this._makeKeyBackupPath(roomId, sessionId, version); return this._http.authedRequest( undefined, "GET", path.path, path.queryData, - ).then((response) => { - if (response.code === 200) { - const keys = []; - // FIXME: for each room, session, if response has multiple - // decrypt response.data.session_data - const data = response.data; - const key = JSON.parse(decryptionKey.decrypt(data.session_data.ephemeral, data.session_data.mac, data.session_data.ciphertext)); - // set room_id and session_id - key.room_id = roomId; - key.session_id = sessionId; - keys.push(key); - return this.importRoomKeys(keys); - } else { - callback("aargh!"); - return Promise.reject("aaargh!"); - } + ).then((res) => { + const keys = []; + // FIXME: for each room, session, if response has multiple + // decrypt response.data.session_data + const session_data = res.session_data; + const key = JSON.parse(decryptionKey.decrypt( + session_data.ephemeral, + session_data.mac, + session_data.ciphertext + )); + // set room_id and session_id + key.room_id = roomId; + key.session_id = sessionId; + keys.push(key); + return this.importRoomKeys(keys); }).then(() => { if (callback) { callback(); From fb8efe368a2538d294af9aee3135bc6e9789a61c Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 23 Aug 2018 00:03:36 -0400 Subject: [PATCH 05/38] initial draft of API for working with backup versions --- src/client.js | 68 ++++++++++++++++++++++++++++++++++++++++++++ src/crypto/olmlib.js | 5 ++++ 2 files changed, 73 insertions(+) diff --git a/src/client.js b/src/client.js index 329a58498..e9f651d49 100644 --- a/src/client.js +++ b/src/client.js @@ -703,6 +703,74 @@ MatrixClient.prototype.importRoomKeys = function(keys) { return this._crypto.importRoomKeys(keys); }; +/** + * Get information about the current key backup. + */ +MatrixClient.prototype.getKeyBackupVersion = function(callback) { + return this._http.authedRequest( + undefined, "GET", "/room_keys/version", + ).then((res) => { + if (res.algorithm !== olmlib.MEGOLM_BACKUP_ALGORITHM) { + const err = "Unknown backup algorithm: " + res.algorithm; + callback(err); + return Promise.reject(err); + } else if (!(typeof res.auth_data === "object") + || !res.auth_data.public_key) { + const err = "Invalid backup data returned"; + callback(err); + return Promise.reject(err); + } else { + if (callback) { + callback(null, res); + } + return res; + } + }); +} + +/** + * Enable backing up of keys, using data previously returned from + * getKeyBackupVersion. + */ +MatrixClient.prototype.enableKeyBackup = function(info) { + this._crypto.backupKey = new global.Olm.PkEncryption(); + this._crypto.backupKey.set_recipient_key(info.auth_data.public_key); +} + +/** + * Disable backing up of keys. + */ +MatrixClient.prototype.disableKeyBackup = function() { + this._crypto.backupKey = undefined; +} + +/** + * Create a new key backup version and enable it. + */ +MatrixClient.prototype.createKeyBackupVersion = function(callback) { + const decryption = new global.Olm.PkDecryption(); + const public_key = decryption.generate_key(); + const encryption = new global.Olm.PkEncryption(); + encryption.set_recipient_key(public_key); + const data = { + algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, + auth_data: { + public_key: public_key, + } + }; + this._crypto._signObject(data.auth_data); + return this._http.authedRequest( + undefined, "POST", "/room_keys/version", undefined, data, + ).then((res) => { + this._crypto.backupKey = encryption; + // FIXME: pickle isn't the right thing to use, but we don't have + // anything else yet + const recovery_key = decryption.pickle(""); + callback(null, recovery_key); + return recovery_key; + }); +} + MatrixClient.prototype._makeKeyBackupPath = function(roomId, sessionId, version) { let path; if (sessionId !== undefined) { diff --git a/src/crypto/olmlib.js b/src/crypto/olmlib.js index 56799c513..f03714f16 100644 --- a/src/crypto/olmlib.js +++ b/src/crypto/olmlib.js @@ -35,6 +35,11 @@ module.exports.OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2"; */ module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2"; +/** + * matrix algorithm tag for megolm backups + */ +module.exports.MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1"; + /** * Encrypt an event payload for an Olm device From 75107f99b280899619b3de28f6ec1b5c3cdaaec9 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 23 Aug 2018 00:26:21 -0400 Subject: [PATCH 06/38] pass in key rather than decryption object to restoreKeyBackups --- spec/unit/crypto/backup.spec.js | 9 ++------- src/client.js | 8 ++++++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index a4956647f..d13b5b608 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -215,14 +215,9 @@ describe("MegolmBackup", function() { } }); }; - const decryption = new global.Olm.PkDecryption(); - decryption.unpickle( - "secret_key", - "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD" - + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA" - ); return client.restoreKeyBackups( - decryption, + "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD" + + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA", ROOM_ID, 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc' ).then(() => { diff --git a/src/client.js b/src/client.js index e9f651d49..56d546eb1 100644 --- a/src/client.js +++ b/src/client.js @@ -765,7 +765,7 @@ MatrixClient.prototype.createKeyBackupVersion = function(callback) { this._crypto.backupKey = encryption; // FIXME: pickle isn't the right thing to use, but we don't have // anything else yet - const recovery_key = decryption.pickle(""); + const recovery_key = decryption.pickle("secret_key"); callback(null, recovery_key); return recovery_key; }); @@ -818,6 +818,10 @@ MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, roomId, sessi throw new Error("End-to-end encryption disabled"); } + // FIXME: see the FIXME in createKeyBackupVersion + const decryption = new global.Olm.PkDecryption(); + decryption.unpickle("secret_key", decryptionKey); + const path = this._makeKeyBackupPath(roomId, sessionId, version); return this._http.authedRequest( undefined, "GET", path.path, path.queryData, @@ -826,7 +830,7 @@ MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, roomId, sessi // FIXME: for each room, session, if response has multiple // decrypt response.data.session_data const session_data = res.session_data; - const key = JSON.parse(decryptionKey.decrypt( + const key = JSON.parse(decryption.decrypt( session_data.ephemeral, session_data.mac, session_data.ciphertext From e5ec4799231b6618533c9e92d2f326920981bc42 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 23 Aug 2018 00:27:30 -0400 Subject: [PATCH 07/38] check that crypto is enabled --- src/client.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/client.js b/src/client.js index 56d546eb1..926b09355 100644 --- a/src/client.js +++ b/src/client.js @@ -707,6 +707,10 @@ MatrixClient.prototype.importRoomKeys = function(keys) { * Get information about the current key backup. */ MatrixClient.prototype.getKeyBackupVersion = function(callback) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + return this._http.authedRequest( undefined, "GET", "/room_keys/version", ).then((res) => { @@ -733,6 +737,10 @@ MatrixClient.prototype.getKeyBackupVersion = function(callback) { * getKeyBackupVersion. */ MatrixClient.prototype.enableKeyBackup = function(info) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + this._crypto.backupKey = new global.Olm.PkEncryption(); this._crypto.backupKey.set_recipient_key(info.auth_data.public_key); } @@ -741,6 +749,10 @@ MatrixClient.prototype.enableKeyBackup = function(info) { * Disable backing up of keys. */ MatrixClient.prototype.disableKeyBackup = function() { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + this._crypto.backupKey = undefined; } @@ -748,6 +760,10 @@ MatrixClient.prototype.disableKeyBackup = function() { * Create a new key backup version and enable it. */ MatrixClient.prototype.createKeyBackupVersion = function(callback) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + const decryption = new global.Olm.PkDecryption(); const public_key = decryption.generate_key(); const encryption = new global.Olm.PkEncryption(); From 73e294b1bd168e6e465406667987feba458618ef Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 23 Aug 2018 00:29:29 -0400 Subject: [PATCH 08/38] add copyright header to backup.spec --- spec/unit/crypto/backup.spec.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index d13b5b608..f832f0511 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -1,3 +1,18 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ try { global.Olm = require('olm'); } catch (e) { From 017f81e430cbb36f82ea78d6d1f4f7de24d5d6f9 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 24 Aug 2018 16:39:22 -0400 Subject: [PATCH 09/38] fix some bugs --- src/client.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 926b09355..a434b67da 100644 --- a/src/client.js +++ b/src/client.js @@ -41,6 +41,7 @@ const SyncApi = require("./sync"); const MatrixBaseApis = require("./base-apis"); const MatrixError = httpApi.MatrixError; const ContentHelpers = require("./content-helpers"); +const olmlib = require("./crypto/olmlib"); import ReEmitter from './ReEmitter'; import RoomList from './crypto/RoomList'; @@ -782,7 +783,9 @@ MatrixClient.prototype.createKeyBackupVersion = function(callback) { // FIXME: pickle isn't the right thing to use, but we don't have // anything else yet const recovery_key = decryption.pickle("secret_key"); - callback(null, recovery_key); + if (callback) { + callback(null, recovery_key); + } return recovery_key; }); } From bf873bde42b1c59af496e9590b03109b1c51ac5e Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 24 Aug 2018 22:13:13 -0400 Subject: [PATCH 10/38] split the backup version creation into two different methods --- src/client.js | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/client.js b/src/client.js index a434b67da..3e0f8a5cc 100644 --- a/src/client.js +++ b/src/client.js @@ -758,35 +758,49 @@ MatrixClient.prototype.disableKeyBackup = function() { } /** - * Create a new key backup version and enable it. + * Set up the data required to create a new backup version. The backup version + * will not be created and enabled until createKeyBackupVersion is called. */ -MatrixClient.prototype.createKeyBackupVersion = function(callback) { +MatrixClient.prototype.prepareKeyBackupVersion = function(callback) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } const decryption = new global.Olm.PkDecryption(); const public_key = decryption.generate_key(); - const encryption = new global.Olm.PkEncryption(); - encryption.set_recipient_key(public_key); - const data = { + return { algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, auth_data: { public_key: public_key, - } + }, + // FIXME: pickle isn't the right thing to use, but we don't have + // anything else yet, so use it for now + recovery_key: decryption.pickle("secret_key"), }; +} + +/** + * Create a new key backup version and enable it, using the information return + * from prepareKeyBackupVersion. + */ +MatrixClient.prototype.createKeyBackupVersion = function(info, callback) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + const data = { + 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._crypto.backupKey = encryption; - // FIXME: pickle isn't the right thing to use, but we don't have - // anything else yet - const recovery_key = decryption.pickle("secret_key"); + this.enableKeyBackup(info); if (callback) { - callback(null, recovery_key); + callback(null, res); } - return recovery_key; + return res; }); } From 3838fab7889cfcc897414710ef9b75be5c30f44e Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 13 Sep 2018 17:01:05 +0100 Subject: [PATCH 11/38] WIP e2e key backup support Continues from uhoreg's branch --- src/client.js | 68 +++++++++++++++++++++++++--- src/crypto/algorithms/megolm.js | 68 +++++++--------------------- src/crypto/index.js | 80 ++++++++++++++++++++++++++++++++- 3 files changed, 155 insertions(+), 61 deletions(-) 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. From e78974783416302578e0e90b2c64f3ef408c69c6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 14 Sep 2018 17:06:27 +0100 Subject: [PATCH 12/38] Check sigs on e2e backup & enable it if we can --- src/client.js | 44 ++++++++++-- src/crypto/algorithms/megolm.js | 2 + src/crypto/index.js | 114 ++++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 5 deletions(-) diff --git a/src/client.js b/src/client.js index 78a43f163..7997b4c29 100644 --- a/src/client.js +++ b/src/client.js @@ -544,7 +544,15 @@ MatrixClient.prototype.setDeviceVerified = function(userId, deviceId, verified) if (verified === undefined) { verified = true; } - return _setDeviceVerification(this, userId, deviceId, verified, null); + const prom = _setDeviceVerification(this, userId, deviceId, verified, null); + + // if one of the user's own devices is being marked as verified / unverified, + // check the key backup status, since whether or not we use this depends on + // whether it has a signature from a verified device + if (userId == this.credentials.userId) { + this._crypto.checkKeyBackup(); + } + return prom; }; /** @@ -752,10 +760,6 @@ MatrixClient.prototype.importRoomKeys = function(keys) { * Get information about the current key backup. */ MatrixClient.prototype.getKeyBackupVersion = function(callback) { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - return this._http.authedRequest( undefined, "GET", "/room_keys/version", ).then((res) => { @@ -784,6 +788,20 @@ MatrixClient.prototype.getKeyBackupVersion = function(callback) { }); } +/** + * @param {object} info key backup info dict from getKeyBackupVersion() + * @return {object} { + * usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device + * sigs: [ + * valid: [bool], + * device: [DeviceInfo], + * ] + * } + */ +MatrixClient.prototype.isKeyBackupTrusted = function(info) { + return this._crypto.isKeyBackupTrusted(info); +}; + /** * @returns {bool} true if the client is configured to back up keys to * the server, otherwise false. @@ -807,6 +825,8 @@ MatrixClient.prototype.enableKeyBackup = function(info) { this._crypto.backupInfo = info; this._crypto.backupKey = new global.Olm.PkEncryption(); this._crypto.backupKey.set_recipient_key(info.auth_data.public_key); + + this.emit('keyBackupStatus', true); } /** @@ -819,6 +839,8 @@ MatrixClient.prototype.disableKeyBackup = function() { this._crypto.backupInfo = null; this._crypto.backupKey = null; + + this.emit('keyBackupStatus', false); } /** @@ -3972,6 +3994,18 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * }); */ +/** + * Fires whenever the status of e2e key backup changes, as returned by getKeyBackupEnabled() + * @event module:client~MatrixClient#"keyBackupStatus" + * @param {bool} enabled true if key backup has been enabled, otherwise false + * @example + * matrixClient.on("keyBackupStatus", function(enabled){ + * if (enabled) { + * [...] + * } + * }); + */ + /** * Fires when we want to suggest to the user that they restore their megolm keys * from backup or by cross-signing the device. diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index af311e16b..f3cdbd17f 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -849,6 +849,8 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { this._retryDecryption(senderKey, sessionId); }).then(() => { if (this._crypto.backupInfo) { + // XXX: No retries on this at all: if this request dies for whatever + // reason, this key will never be uploaded. return this._crypto.backupGroupSession( content.room_id, senderKey, forwardingKeyChain, content.session_id, content.session_key, keysClaimed, diff --git a/src/crypto/index.js b/src/crypto/index.js index fb8f82614..357410b2e 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -77,6 +77,7 @@ function Crypto(baseApis, sessionStore, userId, deviceId, // XXX: this should probably have a single source of truth from OlmAccount this.backupInfo = null; // The info dict from /room_keys/version this.backupKey = null; // The encryption key object + this._checkedForBackup = false; // Have we checked the server for a backup we can use? this._olmDevice = new OlmDevice(sessionStore, cryptoStore); this._deviceList = new DeviceList( @@ -180,6 +181,113 @@ Crypto.prototype.init = async function() { ); this._deviceList.saveIfDirty(); } + + this._checkAndStartKeyBackup(); +}; + +/** + * Check the server for an active key backup and + * if one is present and has a valid signature from + * one of the user's verified devices, start backing up + * to it. + */ +Crypto.prototype._checkAndStartKeyBackup = async function() { + console.log("Checking key backup status..."); + let backupInfo; + try { + backupInfo = await this._baseApis.getKeyBackupVersion(); + } catch (e) { + console.log("Error checking for active key backup", e); + if (Number.isFinite(e.httpStatus) && e.httpStatus / 100 === 4) { + // well that's told us. we won't try again. + this._checkedForBackup = true; + } + return; + } + this._checkedForBackup = true; + + const trustInfo = await this.isKeyBackupTrusted(backupInfo); + + if (trustInfo.usable && !this.backupInfo) { + console.log("Found usable key backup: enabling key backups"); + this._baseApis.enableKeyBackup(backupInfo); + } else if (!trustInfo.usable && this.backupInfo) { + console.log("No usable key backup: disabling key backup"); + this._baseApis.disableKeyBackup(); + } else if (!trustInfo.usable && !this.backupInfo) { + console.log("No usable key backup: not enabling key backup"); + } +}; + +/** + * Forces a re-check of the key backup and enables/disables it + * as appropriate + */ +Crypto.prototype.checkKeyBackup = async function(backupInfo) { + this._checkedForBackup = false; + await this._checkAndStartKeyBackup(); +}; + +/** + * @param {object} backupInfo key backup info dict from /room_keys/version + * @return {object} { + * usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device + * sigs: [ + * valid: [bool], + * device: [DeviceInfo], + * ] + * } + */ +Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { + const ret = { + usable: false, + sigs: [], + }; + + if ( + !backupInfo || + !backupInfo.algorithm || + !backupInfo.auth_data || + !backupInfo.auth_data.public_key || + !backupInfo.auth_data.signatures + ) { + console.log("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; + } + + for (const keyId of Object.keys(mySigs)) { + const device = this._deviceList.getStoredDevice( + this._userId, keyId.split(':')[1], // XXX: is this how we're supposed to get the device ID? + ); + if (!device) { + console.log("Ignoring signature from unknown key " + keyId); + continue; + } + const sigInfo = { device }; + try { + await olmlib.verifySignature( + this._olmDevice, + backupInfo.auth_data, + this._userId, + device.deviceId, + device.getFingerprint(), + ); + sigInfo.valid = true; + } catch (e) { + console.log("Bad signature from device " + device.deviceId, e); + sigInfo.valid = false; + } + ret.sigs.push(sigInfo); + } + + ret.usable = ret.sigs.some(s => s.valid && s.device.isVerified()); + return ret; }; /** @@ -1233,6 +1341,12 @@ Crypto.prototype._onRoomKeyEvent = function(event) { return; } + if (!this._checkedForBackup) { + // don't bother awaiting on this - the important thing is that we retry if we + // haven't managed to check before + this._checkAndStartKeyBackup(); + } + const alg = this._getRoomDecryptor(content.room_id, content.algorithm); alg.onRoomKeyEvent(event); }; From 073fb73ff36aa7f56e810c8950ebe5b59fa26eb4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 17 Sep 2018 15:59:37 +0100 Subject: [PATCH 13/38] Make multi-room key restore work --- src/client.js | 69 ++++++++++++++++++++++++--------- src/crypto/algorithms/megolm.js | 2 + 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/src/client.js b/src/client.js index 7997b4c29..9d6d33eb8 100644 --- a/src/client.js +++ b/src/client.js @@ -68,6 +68,31 @@ try { console.warn("Unable to load crypto module: crypto will be disabled: " + e); } +function keysFromRecoverySession(sessions, decryptionKey, roomId, keys) { + for (const [sessionId, sessionData] of Object.entries(sessions)) { + try { + const decrypted = keyFromRecoverySession(sessionData, decryptionKey, keys); + decrypted.session_id = sessionId; + decrypted.room_id = roomId; + return decrypted; + } catch (e) { + console.log("Failed to decrypt session from backup"); + } + } +} + +function keyFromRecoverySession(session, decryptionKey, keys) { + try { + keys.push(JSON.parse(decryptionKey.decrypt( + session.session_data.ephemeral, + session.session_data.mac, + session.session_data.ciphertext + ))); + } catch (e) { + console.log("Failed to decrypt key from backup", e); + } +} + /** * Construct a Matrix Client. Only directly construct this if you want to use * custom modules. Normally, {@link createClient} should be used @@ -966,7 +991,7 @@ MatrixClient.prototype.backupAllGroupSessions = function(version) { return this._crypto.backupAllGroupSessions(version); }; -MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, roomId, sessionId, version, callback) { +MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, targetRoomId, targetSessionId, version) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } @@ -975,28 +1000,36 @@ MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, roomId, sessi const decryption = new global.Olm.PkDecryption(); decryption.unpickle("secret_key", decryptionKey); - const path = this._makeKeyBackupPath(roomId, sessionId, version); + let totalKeyCount = 0; + const keys = []; + + const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, version); return this._http.authedRequest( undefined, "GET", path.path, path.queryData, ).then((res) => { - const keys = []; - // FIXME: for each room, session, if response has multiple - // decrypt response.data.session_data - const session_data = res.session_data; - const key = JSON.parse(decryption.decrypt( - session_data.ephemeral, - session_data.mac, - session_data.ciphertext - )); - // set room_id and session_id - key.room_id = roomId; - key.session_id = sessionId; - keys.push(key); + if (res.rooms) { + for (const [roomId, roomData] of Object.entries(res.rooms)) { + if (!roomData.sessions) continue; + + totalKeyCount += Object.keys(roomData.sessions).length; + const roomKeys = []; + keysFromRecoverySession(roomData.sessions, decryption, roomId, roomKeys); + for (const k of roomKeys) { + k.room_id = roomId; + keys.push(k); + } + } + } else if (res.sessions) { + totalKeyCount = Object.keys(res.sessions).length; + keys.push(...keysFromRecoverySession(res.sessions, decryption, roomId, keys)); + } else { + totalKeyCount = 1; + keys.push(keyFromRecoverySession(res, decryption, keys)); + } + return this.importRoomKeys(keys); }).then(() => { - if (callback) { - callback(); - } + return {total: totalKeyCount, imported: keys.length}; }) }; diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index f3cdbd17f..1e1de101b 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -851,6 +851,8 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { if (this._crypto.backupInfo) { // XXX: No retries on this at all: if this request dies for whatever // reason, this key will never be uploaded. + // More XXX: If this fails it'll cause the message send to fail, + // and this will happen if the backup is deleted from another client. return this._crypto.backupGroupSession( content.room_id, senderKey, forwardingKeyChain, content.session_id, content.session_key, keysClaimed, From 009430e829c3c5112c4b0d5179425c66f742ab03 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 17 Sep 2018 17:04:29 +0100 Subject: [PATCH 14/38] Add isValidRecoveryKey Add method to check if a given string is a valid recovery key --- src/client.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/client.js b/src/client.js index 9d6d33eb8..1c991c7da 100644 --- a/src/client.js +++ b/src/client.js @@ -991,6 +991,24 @@ MatrixClient.prototype.backupAllGroupSessions = function(version) { return this._crypto.backupAllGroupSessions(version); }; +MatrixClient.prototype.isValidRecoveryKey = function(decryptionKey) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + const decryption = new global.Olm.PkDecryption(); + try { + // FIXME: see the FIXME in createKeyBackupVersion + decryption.unpickle("secret_key", decryptionKey); + return true; + } catch (e) { + console.log(e); + return false; + } finally { + decryption.free(); + } +}; + MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, targetRoomId, targetSessionId, version) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); From f75d188131bb40d30ea2e5d401b6227fb367d437 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 17 Sep 2018 19:25:42 +0100 Subject: [PATCH 15/38] Soe progress on linting --- src/client.js | 80 +++++++++++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/src/client.js b/src/client.js index 1c991c7da..c6d7e9f9e 100644 --- a/src/client.js +++ b/src/client.js @@ -86,7 +86,7 @@ function keyFromRecoverySession(session, decryptionKey, keys) { keys.push(JSON.parse(decryptionKey.decrypt( session.session_data.ephemeral, session.session_data.mac, - session.session_data.ciphertext + session.session_data.ciphertext, ))); } catch (e) { console.log("Failed to decrypt key from backup", e); @@ -783,35 +783,30 @@ MatrixClient.prototype.importRoomKeys = function(keys) { /** * Get information about the current key backup. + * @returns {Promise} Information object from API or null */ -MatrixClient.prototype.getKeyBackupVersion = function(callback) { +MatrixClient.prototype.getKeyBackupVersion = function() { return this._http.authedRequest( undefined, "GET", "/room_keys/version", ).then((res) => { if (res.algorithm !== olmlib.MEGOLM_BACKUP_ALGORITHM) { const err = "Unknown backup algorithm: " + res.algorithm; - callback(err); return Promise.reject(err); } else if (!(typeof res.auth_data === "object") || !res.auth_data.public_key) { const err = "Invalid backup data returned"; - callback(err); return Promise.reject(err); } else { - if (callback) { - callback(null, res); - } return res; } - }).catch(e => { + }).catch((e) => { if (e.errcode === 'M_NOT_FOUND') { - if (callback) callback(null); return null; } else { throw e; } }); -} +}; /** * @param {object} info key backup info dict from getKeyBackupVersion() @@ -836,11 +831,13 @@ MatrixClient.prototype.getKeyBackupEnabled = function() { throw new Error("End-to-end encryption disabled"); } return Boolean(this._crypto.backupKey); -} +}; /** * Enable backing up of keys, using data previously returned from * getKeyBackupVersion. + * + * @param {object} info Backup information object as returned by getKeyBackupVersion */ MatrixClient.prototype.enableKeyBackup = function(info) { if (this._crypto === null) { @@ -852,7 +849,7 @@ MatrixClient.prototype.enableKeyBackup = function(info) { this._crypto.backupKey.set_recipient_key(info.auth_data.public_key); this.emit('keyBackupStatus', true); -} +}; /** * Disable backing up of keys. @@ -866,35 +863,41 @@ MatrixClient.prototype.disableKeyBackup = function() { this._crypto.backupKey = null; this.emit('keyBackupStatus', false); -} +}; /** * Set up the data required to create a new backup version. The backup version * will not be created and enabled until createKeyBackupVersion is called. + * + * @returns {object} Object that can be passed to createKeyBackupVersion and + * additionally has a 'recovery_key' member with the user-facing recovery key string. */ -MatrixClient.prototype.prepareKeyBackupVersion = function(callback) { +MatrixClient.prototype.prepareKeyBackupVersion = function() { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } const decryption = new global.Olm.PkDecryption(); - const public_key = decryption.generate_key(); + const publicKey = decryption.generate_key(); return { algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, auth_data: { - public_key: public_key, + public_key: publicKey, }, // FIXME: pickle isn't the right thing to use, but we don't have // anything else yet, so use it for now recovery_key: decryption.pickle("secret_key"), }; -} +}; /** * Create a new key backup version and enable it, using the information return * from prepareKeyBackupVersion. + * + * @param {object} info Info object from prepareKeyBackupVersion + * @returns {Promise} Object with 'version' param indicating the version created */ -MatrixClient.prototype.createKeyBackupVersion = function(info, callback) { +MatrixClient.prototype.createKeyBackupVersion = function(info) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } @@ -902,7 +905,7 @@ MatrixClient.prototype.createKeyBackupVersion = function(info, callback) { const data = { algorithm: info.algorithm, auth_data: info.auth_data, // FIXME: should this be cloned? - } + }; return this._crypto._signObject(data.auth_data).then(() => { return this._http.authedRequest( undefined, "POST", "/room_keys/version", undefined, data, @@ -913,12 +916,9 @@ MatrixClient.prototype.createKeyBackupVersion = function(info, callback) { auth_data: info.auth_data, version: res.version, }); - if (callback) { - callback(null, res); - } return res; }); -} +}; MatrixClient.prototype.deleteKeyBackupVersion = function(version) { if (this._crypto === null) { @@ -955,31 +955,30 @@ MatrixClient.prototype._makeKeyBackupPath = function(roomId, sessionId, version) } else { path = "/room_keys/keys"; } - const queryData = version === undefined ? undefined : {version : version}; + const queryData = version === undefined ? undefined : { version: version }; return { path: path, queryData: queryData, - } -} + }; +}; /** * Back up session keys to the homeserver. * @param {string} roomId ID of the room that the keys are for Optional. * @param {string} sessionId ID of the session that the keys are for Optional. * @param {integer} version backup version Optional. - * @param {object} key data - * @param {module:client.callback} callback Optional. + * @param {object} data Object keys to send * @return {module:client.Promise} a promise that will resolve when the keys * are uploaded */ -MatrixClient.prototype.sendKeyBackup = function(roomId, sessionId, version, data, callback) { +MatrixClient.prototype.sendKeyBackup = function(roomId, sessionId, version, data) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } const path = this._makeKeyBackupPath(roomId, sessionId, version); return this._http.authedRequest( - callback, "PUT", path.path, path.queryData, data, + undefined, "PUT", path.path, path.queryData, data, ); }; @@ -1009,7 +1008,9 @@ MatrixClient.prototype.isValidRecoveryKey = function(decryptionKey) { } }; -MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, targetRoomId, targetSessionId, version) { +MatrixClient.prototype.restoreKeyBackups = function( + decryptionKey, targetRoomId, targetSessionId, version, +) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } @@ -1039,27 +1040,32 @@ MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, targetRoomId, } } else if (res.sessions) { totalKeyCount = Object.keys(res.sessions).length; - keys.push(...keysFromRecoverySession(res.sessions, decryption, roomId, keys)); + keys.push(...keysFromRecoverySession( + res.sessions, decryption, targetRoomId, keys, + )); } else { totalKeyCount = 1; - keys.push(keyFromRecoverySession(res, decryption, keys)); + const key = keyFromRecoverySession(res, decryption, keys); + key.room_id = targetRoomId; + key.session_id = targetSessionId; + keys.push(key); } return this.importRoomKeys(keys); }).then(() => { return {total: totalKeyCount, imported: keys.length}; - }) + }); }; -MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, version, callback) { +MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, version) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } const path = this._makeKeyBackupPath(roomId, sessionId, version); return this._http.authedRequest( - callback, "DELETE", path.path, path.queryData, - ) + undefined, "DELETE", path.path, path.queryData, + ); }; // Group ops From 3af9af96ea2ded3f75c4333f5c56f478fc56f815 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 17 Sep 2018 19:31:37 +0100 Subject: [PATCH 16/38] More linting --- src/crypto/index.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 357410b2e..d45797fcb 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -36,6 +36,11 @@ const DeviceList = require('./DeviceList').default; import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; +const Olm = global.Olm; +if (!Olm) { + throw new Error("global.Olm is not defined"); +} + /** * Cryptography bits * @@ -222,6 +227,8 @@ Crypto.prototype._checkAndStartKeyBackup = async function() { /** * Forces a re-check of the key backup and enables/disables it * as appropriate + * + * @param {object} backupInfo Backup info from /room_keys/version endpoint */ Crypto.prototype.checkKeyBackup = async function(backupInfo) { this._checkedForBackup = false; @@ -286,7 +293,7 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { 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()); return ret; }; @@ -965,7 +972,6 @@ Crypto.prototype._backupPayloadForSession = function( ) { // new session. const session = new Olm.InboundGroupSession(); - let first_known_index; try { if (exportFormat) { session.import_session(sessionKey); @@ -982,7 +988,7 @@ Crypto.prototype._backupPayloadForSession = function( if (!exportFormat) { sessionKey = session.export_session(); } - const first_known_index = session.first_known_index(); + const firstKnownIndex = session.first_known_index(); const sessionData = { algorithm: olmlib.MEGOLM_ALGORITHM, @@ -993,7 +999,7 @@ Crypto.prototype._backupPayloadForSession = function( }; const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData)); return { - first_message_index: first_known_index, + first_message_index: firstKnownIndex, forwarded_count: forwardingCurve25519KeyChain.length, is_verified: false, // FIXME: how do we determine this? session_data: encrypted, From 54c443ac68a2b386f52bd03e1df2fbd93aed1d73 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 18 Sep 2018 14:48:02 +0100 Subject: [PATCH 17/38] Make tests pass --- spec/unit/crypto/backup.spec.js | 5 ++++- src/client.js | 9 +++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index f832f0511..8763d2d8b 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -64,6 +64,9 @@ describe("MegolmBackup", function() { mockCrypto.backupKey.set_recipient_key( "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmoK" ); + mockCrypto.backupInfo = { + version: 1, + }; mockStorage = new MockStorageApi(); sessionStore = new WebStorageSessionStore(mockStorage); @@ -145,7 +148,7 @@ describe("MegolmBackup", function() { return event.attemptDecryption(mockCrypto).then(() => { return megolmDecryption.onRoomKeyEvent(event); }).then(() => { - expect(mockBaseApis.sendKeyBackup).toHaveBeenCalled(); + expect(mockCrypto.backupGroupSession).toHaveBeenCalled(); }); }); }); diff --git a/src/client.js b/src/client.js index c6d7e9f9e..4838493e6 100644 --- a/src/client.js +++ b/src/client.js @@ -1045,10 +1045,11 @@ MatrixClient.prototype.restoreKeyBackups = function( )); } else { totalKeyCount = 1; - const key = keyFromRecoverySession(res, decryption, keys); - key.room_id = targetRoomId; - key.session_id = targetSessionId; - keys.push(key); + keyFromRecoverySession(res, decryption, keys); + if (keys.length) { + keys[0].room_id = targetRoomId; + keys[0].session_id = targetSessionId; + } } return this.importRoomKeys(keys); From e4bb37b1a80090ef406016759ea30a90927985b1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 18 Sep 2018 14:53:59 +0100 Subject: [PATCH 18/38] Fix lint mostly --- spec/unit/crypto/backup.spec.js | 40 +++++++++------------------------ 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index 8763d2d8b..ad8bc9c6d 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -56,13 +56,13 @@ describe("MegolmBackup", function() { let sessionStore; let cryptoStore; let megolmDecryption; - beforeEach(function () { + beforeEach(function() { testUtils.beforeEach(this); // eslint-disable-line no-invalid-this mockCrypto = testUtils.mock(Crypto, 'Crypto'); mockCrypto.backupKey = new global.Olm.PkEncryption(); mockCrypto.backupKey.set_recipient_key( - "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmoK" + "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmoK", ); mockCrypto.backupInfo = { version: 1, @@ -125,24 +125,6 @@ describe("MegolmBackup", function() { return Promise.resolve(decryptedData); }; - const sessionId = groupSession.session_id(); - const cipherText = groupSession.encrypt(JSON.stringify({ - room_id: ROOM_ID, - content: 'testytest', - })); - const msgevent = new MatrixEvent({ - type: 'm.room.encrypted', - room_id: ROOM_ID, - content: { - algorithm: 'm.megolm.v1.aes-sha2', - sender_key: "SENDER_CURVE25519", - session_id: sessionId, - ciphertext: cipherText, - }, - event_id: "$event1", - origin_server_ts: 1507753886000, - }); - mockBaseApis.sendKeyBackup = expect.createSpy(); return event.attemptDecryption(mockCrypto).then(() => { @@ -153,7 +135,7 @@ describe("MegolmBackup", function() { }); }); - describe("restore", function () { + describe("restore", function() { let client; beforeEach(function() { @@ -163,9 +145,9 @@ describe("MegolmBackup", function() { ].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {}); const store = [ "getRoom", "getRooms", "getUser", "getSyncToken", "scrollback", - "save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", "storeUser", - "getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter", - "getSyncAccumulator", "startup", "deleteAllData", + "save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", + "storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter", + "storeFilter", "getSyncAccumulator", "startup", "deleteAllData", ].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {}); store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null)); store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null)); @@ -196,7 +178,7 @@ describe("MegolmBackup", function() { return client.initCrypto(); }); - it('can restore from backup', function () { + it('can restore from backup', function() { const event = new MatrixEvent({ type: 'm.room.encrypted', room_id: '!ROOM:ID', @@ -206,12 +188,12 @@ describe("MegolmBackup", function() { session_id: 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc', ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N' + 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl' - + 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs' + + 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs', }, event_id: '$event1', origin_server_ts: 1507753886000, }); - client._http.authedRequest = function () { + client._http.authedRequest = function() { return Promise.resolve({ first_message_index: 0, forwarded_count: 0, @@ -230,14 +212,14 @@ describe("MegolmBackup", function() { + 'iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg', mac: '5lxYBHQU80M', ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14', - } + }, }); }; return client.restoreKeyBackups( "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD" + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA", ROOM_ID, - 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc' + 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc', ).then(() => { return megolmDecryption.decryptEvent(event); }).then((res) => { From 0bad7b213e705857b38e85912726b84abe7d4790 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 18 Sep 2018 14:56:11 +0100 Subject: [PATCH 19/38] Fix lint Remove commented code block as it's not immediately obvious it makes sense or is the right way of suggesting a key restore. --- src/sync.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/sync.js b/src/sync.js index 2baf78215..bcb131c59 100644 --- a/src/sync.js +++ b/src/sync.js @@ -1099,17 +1099,6 @@ SyncApi.prototype._processSyncResponse = async function( async function processRoomEvent(e) { client.emit("event", e); if (e.isState() && e.getType() == "m.room.encryption" && self.opts.crypto) { - - /* - // XXX: get device - if (!device.getSuggestedKeyRestore() && - !device.backupKey && !device.selfCrossSigs.length) - { - client.emit("crypto.suggestKeyRestore"); - device.setSuggestedKeyRestore(true); - } - */ - await self.opts.crypto.onCryptoEvent(e); } } From a78825eff9fa5b7d119bd93ce7a1408656263d55 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 18 Sep 2018 15:06:28 +0100 Subject: [PATCH 20/38] Bump to Olm 2.3.0 for PkEncryption --- travis.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/travis.sh b/travis.sh index 68d915def..4c47f00e7 100755 --- a/travis.sh +++ b/travis.sh @@ -5,7 +5,7 @@ set -ex npm run lint # install Olm so that we can run the crypto tests. -npm install https://matrix.org/packages/npm/olm/olm-2.2.2.tgz +npm install https://matrix.org/packages/npm/olm/olm-2.3.0.tgz npm run test From 1b62a21dbd37c9d4b46bddb4006e30368b8c0c2f Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 18 Sep 2018 16:12:37 +0100 Subject: [PATCH 21/38] Free PkEncryption/Decryption objects --- src/client.js | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/client.js b/src/client.js index 4838493e6..7a1a0748e 100644 --- a/src/client.js +++ b/src/client.js @@ -845,6 +845,7 @@ MatrixClient.prototype.enableKeyBackup = function(info) { } this._crypto.backupInfo = info; + if (this._crypto.backupKey) this._crypto.backupKey.free(); this._crypto.backupKey = new global.Olm.PkEncryption(); this._crypto.backupKey.set_recipient_key(info.auth_data.public_key); @@ -860,6 +861,7 @@ MatrixClient.prototype.disableKeyBackup = function() { } this._crypto.backupInfo = null; + if (this._crypto.backupKey) this._crypto.backupKey.free(); this._crypto.backupKey = null; this.emit('keyBackupStatus', false); @@ -878,16 +880,20 @@ MatrixClient.prototype.prepareKeyBackupVersion = function() { } const decryption = new global.Olm.PkDecryption(); - const publicKey = decryption.generate_key(); - return { - algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, - auth_data: { - public_key: publicKey, - }, - // FIXME: pickle isn't the right thing to use, but we don't have - // anything else yet, so use it for now - recovery_key: decryption.pickle("secret_key"), - }; + try { + const publicKey = decryption.generate_key(); + return { + algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, + auth_data: { + public_key: publicKey, + }, + // FIXME: pickle isn't the right thing to use, but we don't have + // anything else yet, so use it for now + recovery_key: decryption.pickle("secret_key"), + }; + } finally { + decryption.free(); + } }; /** @@ -1014,15 +1020,20 @@ MatrixClient.prototype.restoreKeyBackups = function( if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } - - // FIXME: see the FIXME in createKeyBackupVersion - const decryption = new global.Olm.PkDecryption(); - decryption.unpickle("secret_key", decryptionKey); - let totalKeyCount = 0; const keys = []; const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, version); + + // FIXME: see the FIXME in createKeyBackupVersion + const decryption = new global.Olm.PkDecryption(); + try { + decryption.unpickle("secret_key", decryptionKey); + } catch(e) { + decryption.free(); + throw e; + } + return this._http.authedRequest( undefined, "GET", path.path, path.queryData, ).then((res) => { @@ -1055,6 +1066,8 @@ MatrixClient.prototype.restoreKeyBackups = function( return this.importRoomKeys(keys); }).then(() => { return {total: totalKeyCount, imported: keys.length}; + }).finally(() => { + decryption.free(); }); }; From 2f4c1dfcc4d3b5cd4ff5765e757cfdb6a9de026a Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 18 Sep 2018 17:33:47 +0100 Subject: [PATCH 22/38] Test all 3 code paths on backup restore --- spec/unit/crypto/backup.spec.js | 95 +++++++++++++++++++++------------ src/client.js | 44 +++++++-------- 2 files changed, 82 insertions(+), 57 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index ad8bc9c6d..25f6c112a 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -43,6 +43,42 @@ const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; const ROOM_ID = '!ROOM:ID'; +const ENCRYPTED_EVENT = new MatrixEvent({ + type: 'm.room.encrypted', + room_id: '!ROOM:ID', + content: { + algorithm: 'm.megolm.v1.aes-sha2', + sender_key: 'SENDER_CURVE25519', + session_id: 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc', + ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N' + + 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl' + + 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs', + }, + event_id: '$event1', + origin_server_ts: 1507753886000, +}); + +const KEY_BACKUP_DATA = { + first_message_index: 0, + forwarded_count: 0, + is_verified: false, + session_data: { + ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw' + + '6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ' + + 'Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9' + + 'SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy' + + 'Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF' + + 'ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV' + + '4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv' + + 'C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe' + + 'Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf' + + 'QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy' + + 'iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg', + mac: '5lxYBHQU80M', + ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14', + }, +}; + describe("MegolmBackup", function() { if (!global.Olm) { console.warn('Not running megolm backup unit tests: libolm not present'); @@ -179,41 +215,8 @@ describe("MegolmBackup", function() { }); it('can restore from backup', function() { - const event = new MatrixEvent({ - type: 'm.room.encrypted', - room_id: '!ROOM:ID', - content: { - algorithm: 'm.megolm.v1.aes-sha2', - sender_key: 'SENDER_CURVE25519', - session_id: 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc', - ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N' - + 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl' - + 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs', - }, - event_id: '$event1', - origin_server_ts: 1507753886000, - }); client._http.authedRequest = function() { - return Promise.resolve({ - first_message_index: 0, - forwarded_count: 0, - is_verified: false, - session_data: { - ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw' - + '6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ' - + 'Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9' - + 'SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy' - + 'Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF' - + 'ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV' - + '4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv' - + 'C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe' - + 'Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf' - + 'QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy' - + 'iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg', - mac: '5lxYBHQU80M', - ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14', - }, - }); + return Promise.resolve(KEY_BACKUP_DATA); }; return client.restoreKeyBackups( "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD" @@ -221,7 +224,29 @@ describe("MegolmBackup", function() { ROOM_ID, 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc', ).then(() => { - return megolmDecryption.decryptEvent(event); + return megolmDecryption.decryptEvent(ENCRYPTED_EVENT); + }).then((res) => { + expect(res.clearEvent.content).toEqual('testytest'); + }); + }); + + it('can restore backup by room', function() { + client._http.authedRequest = function() { + return Promise.resolve({ + rooms: { + [ROOM_ID]: { + sessions: { + 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc': KEY_BACKUP_DATA, + }, + }, + } + }); + }; + return client.restoreKeyBackups( + "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD" + + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA", + ).then(() => { + return megolmDecryption.decryptEvent(ENCRYPTED_EVENT); }).then((res) => { expect(res.clearEvent.content).toEqual('testytest'); }); diff --git a/src/client.js b/src/client.js index 7a1a0748e..c864bf490 100644 --- a/src/client.js +++ b/src/client.js @@ -68,29 +68,27 @@ try { console.warn("Unable to load crypto module: crypto will be disabled: " + e); } -function keysFromRecoverySession(sessions, decryptionKey, roomId, keys) { +function keysFromRecoverySession(sessions, decryptionKey, roomId) { + const keys = []; for (const [sessionId, sessionData] of Object.entries(sessions)) { try { - const decrypted = keyFromRecoverySession(sessionData, decryptionKey, keys); + const decrypted = keyFromRecoverySession(sessionData, decryptionKey); decrypted.session_id = sessionId; decrypted.room_id = roomId; - return decrypted; + keys.push(decrypted); } catch (e) { console.log("Failed to decrypt session from backup"); } } + return keys; } -function keyFromRecoverySession(session, decryptionKey, keys) { - try { - keys.push(JSON.parse(decryptionKey.decrypt( - session.session_data.ephemeral, - session.session_data.mac, - session.session_data.ciphertext, - ))); - } catch (e) { - console.log("Failed to decrypt key from backup", e); - } +function keyFromRecoverySession(session, decryptionKey) { + return JSON.parse(decryptionKey.decrypt( + session.session_data.ephemeral, + session.session_data.mac, + session.session_data.ciphertext, + )); } /** @@ -1021,7 +1019,7 @@ MatrixClient.prototype.restoreKeyBackups = function( throw new Error("End-to-end encryption disabled"); } let totalKeyCount = 0; - const keys = []; + let keys = []; const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, version); @@ -1042,8 +1040,7 @@ MatrixClient.prototype.restoreKeyBackups = function( if (!roomData.sessions) continue; totalKeyCount += Object.keys(roomData.sessions).length; - const roomKeys = []; - keysFromRecoverySession(roomData.sessions, decryption, roomId, roomKeys); + const roomKeys = keysFromRecoverySession(roomData.sessions, decryption, roomId, roomKeys); for (const k of roomKeys) { k.room_id = roomId; keys.push(k); @@ -1051,15 +1048,18 @@ MatrixClient.prototype.restoreKeyBackups = function( } } else if (res.sessions) { totalKeyCount = Object.keys(res.sessions).length; - keys.push(...keysFromRecoverySession( + keys = keysFromRecoverySession( res.sessions, decryption, targetRoomId, keys, - )); + ); } else { totalKeyCount = 1; - keyFromRecoverySession(res, decryption, keys); - if (keys.length) { - keys[0].room_id = targetRoomId; - keys[0].session_id = targetSessionId; + try { + const key = keyFromRecoverySession(res, decryption); + key.room_id = targetRoomId; + key.session_id = targetSessionId; + keys.push(key); + } catch (e) { + console.log("Failed to decrypt session from backup"); } } From c556ca40b159e141b90e542cd8f82329fdce6043 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 25 Sep 2018 17:49:54 +0100 Subject: [PATCH 23/38] Support Olm with WebAssembly wasm Olm has a new interface: it now has an init method that needs to be called and the promise it returns waited on before the Olm module is used. Support that, and allow Crypto etc to be imported whether Olm is enabled or not. Change whether olm is enabled to be async since now it will be unavailable if the async module init fails. Don't call getOlmVersion() until the Olm.init() is done. --- spec/unit/crypto.spec.js | 13 ++++---- spec/unit/crypto/algorithms/megolm.spec.js | 15 ++++------ src/client.js | 35 ++++++++++------------ src/crypto/OlmDevice.js | 3 -- src/crypto/index.js | 15 +++++++--- 5 files changed, 40 insertions(+), 41 deletions(-) diff --git a/spec/unit/crypto.spec.js b/spec/unit/crypto.spec.js index 4d949e05e..734941cb5 100644 --- a/spec/unit/crypto.spec.js +++ b/spec/unit/crypto.spec.js @@ -1,20 +1,23 @@ "use strict"; import 'source-map-support/register'; +import Crypto from '../../lib/crypto'; +import expect from 'expect'; const sdk = require("../.."); -let Crypto; -if (sdk.CRYPTO_ENABLED) { - Crypto = require("../../lib/crypto"); -} -import expect from 'expect'; describe("Crypto", function() { if (!sdk.CRYPTO_ENABLED) { return; } + + beforeEach(function(done) { + Olm.init().then(done); + }); + it("Crypto exposes the correct olm library version", function() { + console.log(Crypto); expect(Crypto.getOlmVersion()[0]).toEqual(2); }); }); diff --git a/spec/unit/crypto/algorithms/megolm.spec.js b/spec/unit/crypto/algorithms/megolm.spec.js index cf8e58f2e..db28e3a51 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.js +++ b/spec/unit/crypto/algorithms/megolm.spec.js @@ -13,14 +13,8 @@ import WebStorageSessionStore from '../../../../lib/store/session/webstorage'; import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js'; import MockStorageApi from '../../../MockStorageApi'; import testUtils from '../../../test-utils'; - -// Crypto and OlmDevice won't import unless we have global.Olm -let OlmDevice; -let Crypto; -if (global.Olm) { - OlmDevice = require('../../../../lib/crypto/OlmDevice'); - Crypto = require('../../../../lib/crypto'); -} +import OlmDevice from '../../../../lib/crypto/OlmDevice'; +import Crypto from '../../../../lib/crypto'; const MatrixEvent = sdk.MatrixEvent; const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; @@ -69,7 +63,8 @@ describe("MegolmDecryption", function() { describe('receives some keys:', function() { let groupSession; - beforeEach(function() { + beforeEach(async function() { + await Olm.init(); groupSession = new global.Olm.OutboundGroupSession(); groupSession.create(); @@ -98,7 +93,7 @@ describe("MegolmDecryption", function() { }, }; - return event.attemptDecryption(mockCrypto).then(() => { + await event.attemptDecryption(mockCrypto).then(() => { megolmDecryption.onRoomKeyEvent(event); }); }); diff --git a/src/client.js b/src/client.js index 8f0ca893d..8ca115a38 100644 --- a/src/client.js +++ b/src/client.js @@ -45,6 +45,8 @@ const ContentHelpers = require("./content-helpers"); import ReEmitter from './ReEmitter'; import RoomList from './crypto/RoomList'; +import Crypto from './crypto'; +import { isCryptoAvailable } from './crypto'; const LAZY_LOADING_MESSAGES_FILTER = { lazy_load_members: true, @@ -58,14 +60,7 @@ const LAZY_LOADING_SYNC_FILTER = { const SCROLLBACK_DELAY_MS = 3000; -let CRYPTO_ENABLED = false; - -try { - var Crypto = require("./crypto"); - CRYPTO_ENABLED = true; -} catch (e) { - console.warn("Unable to load crypto module: crypto will be disabled: " + e); -} +let CRYPTO_ENABLED = isCryptoAvailable(); /** * Construct a Matrix Client. Only directly construct this if you want to use @@ -140,6 +135,8 @@ function MatrixClient(opts) { MatrixBaseApis.call(this, opts); + this.olmVersion = null; // Populated after initCrypto is done + this.reEmitter = new ReEmitter(this); this.store = opts.store || new StubStore(); @@ -192,10 +189,6 @@ function MatrixClient(opts) { this._forceTURN = opts.forceTURN || false; - if (CRYPTO_ENABLED) { - this.olmVersion = Crypto.getOlmVersion(); - } - // List of which rooms have encryption enabled: separate from crypto because // we still want to know which rooms are encrypted even if crypto is disabled: // we don't want to start sending unencrypted events to them. @@ -385,6 +378,14 @@ MatrixClient.prototype.setNotifTimelineSet = function(notifTimelineSet) { * successfully initialised. */ MatrixClient.prototype.initCrypto = async function() { + if (!isCryptoAvailable()) { + throw new Error( + `End-to-end encryption not supported in this js-sdk build: did ` + + `you remember to load the olm library?`, + ); + return; + } + if (this._crypto) { console.warn("Attempt to re-initialise e2e encryption on MatrixClient"); return; @@ -402,13 +403,6 @@ MatrixClient.prototype.initCrypto = async function() { // initialise the list of encrypted rooms (whether or not crypto is enabled) await this._roomList.init(); - if (!CRYPTO_ENABLED) { - throw new Error( - `End-to-end encryption not supported in this js-sdk build: did ` + - `you remember to load the olm library?`, - ); - } - const userId = this.getUserId(); if (userId === null) { throw new Error( @@ -440,6 +434,9 @@ MatrixClient.prototype.initCrypto = async function() { await crypto.init(); + this.olmVersion = Crypto.getOlmVersion(); + + // if crypto initialisation was successful, tell it to attach its event // handlers. crypto.registerEventHandlers(this); diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index cda14779c..b8c70cd0c 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -23,9 +23,6 @@ import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; * @module crypto/OlmDevice */ const Olm = global.Olm; -if (!Olm) { - throw new Error("global.Olm is not defined"); -} // The maximum size of an event is 65K, and we base64 the content, so this is a diff --git a/src/crypto/index.js b/src/crypto/index.js index f00477d4b..0fe33663f 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -36,6 +36,12 @@ const DeviceList = require('./DeviceList').default; import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; +const Olm = global.Olm; + +export function isCryptoAvailable() { + return Boolean(Olm); +} + /** * Cryptography bits * @@ -62,7 +68,7 @@ import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; * * @param {RoomList} roomList An initialised RoomList object */ -function Crypto(baseApis, sessionStore, userId, deviceId, +export default function Crypto(baseApis, sessionStore, userId, deviceId, clientStore, cryptoStore, roomList) { this._baseApis = baseApis; this._sessionStore = sessionStore; @@ -124,6 +130,10 @@ utils.inherits(Crypto, EventEmitter); * Returns a promise which resolves once the crypto module is ready for use. */ Crypto.prototype.init = async function() { + // Olm is just an object with a .then, not a fully-fledged promise, so + // pass it into bluebird to make it a proper promise. + await Olm.init(); + const sessionStoreHasAccount = Boolean(this._sessionStore.getEndToEndAccount()); let cryptoStoreHasAccount; await this._cryptoStore.doTxn( @@ -1518,6 +1528,3 @@ class IncomingRoomKeyRequestCancellation { * @event module:client~MatrixClient#"crypto.warning" * @param {string} type One of the strings listed above */ - -/** */ -module.exports = Crypto; From 63cc3fd890b9927c489d65e50895a0ba74196ca4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 25 Sep 2018 18:14:11 +0100 Subject: [PATCH 24/38] lint --- spec/unit/crypto.spec.js | 1 + spec/unit/crypto/algorithms/megolm.spec.js | 2 ++ src/client.js | 3 +-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/unit/crypto.spec.js b/spec/unit/crypto.spec.js index 734941cb5..1b28ad683 100644 --- a/spec/unit/crypto.spec.js +++ b/spec/unit/crypto.spec.js @@ -6,6 +6,7 @@ import expect from 'expect'; const sdk = require("../.."); +const Olm = global.Olm; describe("Crypto", function() { if (!sdk.CRYPTO_ENABLED) { diff --git a/spec/unit/crypto/algorithms/megolm.spec.js b/spec/unit/crypto/algorithms/megolm.spec.js index db28e3a51..6c777859e 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.js +++ b/spec/unit/crypto/algorithms/megolm.spec.js @@ -21,6 +21,8 @@ const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; const ROOM_ID = '!ROOM:ID'; +const Olm = global.Olm; + describe("MegolmDecryption", function() { if (!global.Olm) { console.warn('Not running megolm unit tests: libolm not present'); diff --git a/src/client.js b/src/client.js index 8ca115a38..1a05998eb 100644 --- a/src/client.js +++ b/src/client.js @@ -60,7 +60,7 @@ const LAZY_LOADING_SYNC_FILTER = { const SCROLLBACK_DELAY_MS = 3000; -let CRYPTO_ENABLED = isCryptoAvailable(); +const CRYPTO_ENABLED = isCryptoAvailable(); /** * Construct a Matrix Client. Only directly construct this if you want to use @@ -383,7 +383,6 @@ MatrixClient.prototype.initCrypto = async function() { `End-to-end encryption not supported in this js-sdk build: did ` + `you remember to load the olm library?`, ); - return; } if (this._crypto) { From 33ad65a105d379b6208a950679e3c5bb80525698 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 26 Sep 2018 16:39:22 +0100 Subject: [PATCH 25/38] Don't assume Olm will be available from start By doing `Olm = global.Olm` on script load, we require that Olm is available right from the start, which isn't really necessary. As long as it appears some time before we actually want to use it, this is fine (we can probably assume it's not going to go away again..?) This means Riot doesn't need to faff about making sure olm is loaded before starting anything else. --- src/crypto/OlmDevice.js | 30 +++++++++++------------------- src/crypto/index.js | 6 ++---- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index b8c70cd0c..043905c09 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -17,14 +17,6 @@ limitations under the License. import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; -/** - * olm.js wrapper - * - * @module crypto/OlmDevice - */ -const Olm = global.Olm; - - // The maximum size of an event is 65K, and we base64 the content, so this is a // reasonable approximation to the biggest plaintext we can encrypt. const MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4; @@ -124,7 +116,7 @@ OlmDevice.prototype.init = async function() { await this._migrateFromSessionStore(); let e2eKeys; - const account = new Olm.Account(); + const account = new global.Olm.Account(); try { await _initialiseAccount( this._sessionStore, this._cryptoStore, this._pickleKey, account, @@ -158,7 +150,7 @@ async function _initialiseAccount(sessionStore, cryptoStore, pickleKey, account) * @return {array} The version of Olm. */ OlmDevice.getOlmVersion = function() { - return Olm.get_library_version(); + return global.Olm.get_library_version(); }; OlmDevice.prototype._migrateFromSessionStore = async function() { @@ -265,7 +257,7 @@ OlmDevice.prototype._migrateFromSessionStore = async function() { */ OlmDevice.prototype._getAccount = function(txn, func) { this._cryptoStore.getAccount(txn, (pickledAccount) => { - const account = new Olm.Account(); + const account = new global.Olm.Account(); try { account.unpickle(this._pickleKey, pickledAccount); func(account); @@ -318,7 +310,7 @@ OlmDevice.prototype._getSession = function(deviceKey, sessionId, txn, func) { * @private */ OlmDevice.prototype._unpickleSession = function(pickledSession, func) { - const session = new Olm.Session(); + const session = new global.Olm.Session(); try { session.unpickle(this._pickleKey, pickledSession); func(session); @@ -351,7 +343,7 @@ OlmDevice.prototype._saveSession = function(deviceKey, session, txn) { * @private */ OlmDevice.prototype._getUtility = function(func) { - const utility = new Olm.Utility(); + const utility = new global.Olm.Utility(); try { return func(utility); } finally { @@ -463,7 +455,7 @@ OlmDevice.prototype.createOutboundSession = async function( ], (txn) => { this._getAccount(txn, (account) => { - const session = new Olm.Session(); + const session = new global.Olm.Session(); try { session.create_outbound(account, theirIdentityKey, theirOneTimeKey); newSessionId = session.session_id(); @@ -507,7 +499,7 @@ OlmDevice.prototype.createInboundSession = async function( ], (txn) => { this._getAccount(txn, (account) => { - const session = new Olm.Session(); + const session = new global.Olm.Session(); try { session.create_inbound_from( account, theirDeviceIdentityKey, ciphertext, @@ -725,7 +717,7 @@ OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) { throw new Error("Unknown outbound group session " + sessionId); } - const session = new Olm.OutboundGroupSession(); + const session = new global.Olm.OutboundGroupSession(); try { session.unpickle(this._pickleKey, pickled); return func(session); @@ -741,7 +733,7 @@ OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) { * @return {string} sessionId for the outbound session. */ OlmDevice.prototype.createOutboundGroupSession = function() { - const session = new Olm.OutboundGroupSession(); + const session = new global.Olm.OutboundGroupSession(); try { session.create(); this._saveOutboundGroupSession(session); @@ -813,7 +805,7 @@ OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) { * @return {*} result of func */ OlmDevice.prototype._unpickleInboundGroupSession = function(sessionData, func) { - const session = new Olm.InboundGroupSession(); + const session = new global.Olm.InboundGroupSession(); try { session.unpickle(this._pickleKey, sessionData.session); return func(session); @@ -894,7 +886,7 @@ OlmDevice.prototype.addInboundGroupSession = async function( } // new session. - const session = new Olm.InboundGroupSession(); + const session = new global.Olm.InboundGroupSession(); try { if (exportFormat) { session.import_session(sessionKey); diff --git a/src/crypto/index.js b/src/crypto/index.js index 0fe33663f..9ed3c7113 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -36,10 +36,8 @@ const DeviceList = require('./DeviceList').default; import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; -const Olm = global.Olm; - export function isCryptoAvailable() { - return Boolean(Olm); + return Boolean(global.Olm); } /** @@ -132,7 +130,7 @@ utils.inherits(Crypto, EventEmitter); Crypto.prototype.init = async function() { // Olm is just an object with a .then, not a fully-fledged promise, so // pass it into bluebird to make it a proper promise. - await Olm.init(); + await global.Olm.init(); const sessionStoreHasAccount = Boolean(this._sessionStore.getEndToEndAccount()); let cryptoStoreHasAccount; From 7cd101d8cb06a036b5c67737c47e01249955f621 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 2 Oct 2018 19:22:10 +0100 Subject: [PATCH 26/38] Fix recovery key format --- package.json | 1 + src/client.js | 23 +++++++---------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index db0b14bb1..4ae0b36a7 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "dependencies": { "another-json": "^0.2.0", "babel-runtime": "^6.26.0", + "base58check": "^2.0.0", "bluebird": "^3.5.0", "browser-request": "^0.3.3", "content-type": "^1.0.2", diff --git a/src/client.js b/src/client.js index ceeeff426..951dbcdbb 100644 --- a/src/client.js +++ b/src/client.js @@ -49,6 +49,7 @@ import {InvalidStoreError} from './errors'; import Crypto from './crypto'; import { isCryptoAvailable } from './crypto'; +import { encodeRecoveryKey, decodeRecoveryKey } from './crypto/recoverykey'; const LAZY_LOADING_MESSAGES_FILTER = { lazy_load_members: true, @@ -882,9 +883,7 @@ MatrixClient.prototype.prepareKeyBackupVersion = function() { auth_data: { public_key: publicKey, }, - // FIXME: pickle isn't the right thing to use, but we don't have - // anything else yet, so use it for now - recovery_key: decryption.pickle("secret_key"), + recovery_key: encodeRecoveryKey(decryption.get_private_key()), }; } finally { decryption.free(); @@ -991,26 +990,17 @@ MatrixClient.prototype.backupAllGroupSessions = function(version) { return this._crypto.backupAllGroupSessions(version); }; -MatrixClient.prototype.isValidRecoveryKey = function(decryptionKey) { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - - const decryption = new global.Olm.PkDecryption(); +MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) { try { - // FIXME: see the FIXME in createKeyBackupVersion - decryption.unpickle("secret_key", decryptionKey); + decodeRecoveryKey(recoveryKey); return true; } catch (e) { - console.log(e); return false; - } finally { - decryption.free(); } }; MatrixClient.prototype.restoreKeyBackups = function( - decryptionKey, targetRoomId, targetSessionId, version, + recoveryKey, targetRoomId, targetSessionId, version, ) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); @@ -1021,9 +1011,10 @@ MatrixClient.prototype.restoreKeyBackups = function( const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, version); // FIXME: see the FIXME in createKeyBackupVersion + const privkey = decodeRecoveryKey(recoveryKey); const decryption = new global.Olm.PkDecryption(); try { - decryption.unpickle("secret_key", decryptionKey); + decryption.init_with_private_key(privkey); } catch(e) { decryption.free(); throw e; From 262ace1773f22bb917078e74fb72c6e1dabe38da Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 3 Oct 2018 10:20:57 +0100 Subject: [PATCH 27/38] commit the recovery key util file --- src/crypto/recoverykey.js | 44 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/crypto/recoverykey.js diff --git a/src/crypto/recoverykey.js b/src/crypto/recoverykey.js new file mode 100644 index 000000000..82de3a158 --- /dev/null +++ b/src/crypto/recoverykey.js @@ -0,0 +1,44 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import base58check from 'base58check'; + +// picked arbitrarily but to try & avoid clashing with any bitcoin ones +const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01]; + +export function encodeRecoveryKey(key) { + const base58key = base58check.encode(Buffer.from(OLM_RECOVERY_KEY_PREFIX), Buffer.from(key)); + return base58key.match(/.{1,4}/g).join(" "); +} + +export function decodeRecoveryKey(recoverykey) { + const result = base58check.decode(recoverykey.replace(/ /, '')); + // the encoding doesn't include the length of the prefix, so the + // decoder assumes it's 1 byte. sigh. + const prefix = Buffer.concat([result.prefix, result.data.slice(0, OLM_RECOVERY_KEY_PREFIX.length - 1)]); + + if (!prefix.equals(Buffer.from(OLM_RECOVERY_KEY_PREFIX))) { + throw new Error("Incorrect prefix"); + } + + const key = result.data.slice(OLM_RECOVERY_KEY_PREFIX.length - 1); + + if (key.length !== global.Olm.PRIVATE_KEY_LENGTH) { + throw new Error("Incorrect length"); + } + + return key; +} From 258adda67c0b7d43a053249f564260e10de8908d Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 4 Oct 2018 15:19:20 -0400 Subject: [PATCH 28/38] retry key backups when they fail --- spec/unit/crypto/backup.spec.js | 192 +++++++++++++++++- src/client.js | 2 + src/crypto/index.js | 126 ++++++------ .../store/indexeddb-crypto-store-backend.js | 70 +++++++ src/crypto/store/indexeddb-crypto-store.js | 33 +++ src/crypto/store/memory-crypto-store.js | 35 ++++ 6 files changed, 399 insertions(+), 59 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index 25f6c112a..7a7f0b414 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -98,7 +98,7 @@ describe("MegolmBackup", function() { mockCrypto = testUtils.mock(Crypto, 'Crypto'); mockCrypto.backupKey = new global.Olm.PkEncryption(); mockCrypto.backupKey.set_recipient_key( - "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmoK", + "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", ); mockCrypto.backupInfo = { version: 1, @@ -134,7 +134,7 @@ describe("MegolmBackup", function() { megolmDecryption.olmlib = mockOlmLib; }); - it('automatically backs up keys', function() { + it('automatically calls the key back up', function() { const groupSession = new global.Olm.OutboundGroupSession(); groupSession.create(); @@ -169,6 +169,194 @@ describe("MegolmBackup", function() { expect(mockCrypto.backupGroupSession).toHaveBeenCalled(); }); }); + + it('sends backups to the server', function () { + const groupSession = new global.Olm.OutboundGroupSession(); + groupSession.create(); + const ibGroupSession = new global.Olm.InboundGroupSession(); + ibGroupSession.create(groupSession.session_key()); + + const scheduler = [ + "getQueueForEvent", "queueEvent", "removeEventFromQueue", + "setProcessFunction", + ].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {}); + const store = [ + "getRoom", "getRooms", "getUser", "getSyncToken", "scrollback", + "save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", + "storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter", + "storeFilter", "getSyncAccumulator", "startup", "deleteAllData", + ].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {}); + store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null)); + store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null)); + store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null)); + const client = new MatrixClient({ + baseUrl: "https://my.home.server", + idBaseUrl: "https://identity.server", + accessToken: "my.access.token", + request: function() {}, // NOP + store: store, + scheduler: scheduler, + userId: "@alice:bar", + deviceId: "device", + sessionStore: sessionStore, + cryptoStore: cryptoStore, + }); + + megolmDecryption = new MegolmDecryption({ + userId: '@user:id', + crypto: mockCrypto, + olmDevice: olmDevice, + baseApis: client, + roomId: ROOM_ID, + }); + + megolmDecryption.olmlib = mockOlmLib; + + return client.initCrypto() + .then(() => { + return cryptoStore.doTxn("readwrite", [cryptoStore.STORE_SESSION], (txn) => { + cryptoStore.addEndToEndInboundGroupSession( + "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", + groupSession.session_id(), + { + forwardingCurve25519KeyChain: undefined, + keysClaimed: { + ed25519: "SENDER_ED25519", + }, + room_id: ROOM_ID, + session: ibGroupSession.pickle(olmDevice._pickleKey), + }, + txn); + }); + }) + .then(() => { + client.enableKeyBackup({ + algorithm: "foobar", + version: 1, + auth_data: { + public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmoK" + }, + }); + let numCalls = 0; + return new Promise((resolve, reject) => { + client._http.authedRequest = function(callback, method, path, queryParams, data, opts) { + expect(++numCalls <= 1); + if (numCalls >= 2) { + // exit out of retry loop if there's something wrong + reject(new Error("authedRequest called too many timmes")); + return Promise.resolve({}); + } + expect(method).toBe("PUT"); + expect(path).toBe("/room_keys/keys"); + expect(queryParams.version).toBe(1); + expect(data.rooms[ROOM_ID].sessions).toExist(); + expect(data.rooms[ROOM_ID].sessions).toIncludeKey(groupSession.session_id()); + resolve(); + return Promise.resolve({}); + }; + client._crypto.backupGroupSession("roomId", "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", [], groupSession.session_id(), groupSession.session_key()); + }) + .then(() => { + expect(numCalls).toBe(1); + }); + }); + }); + + it('retries when a backup fails', function () { + const groupSession = new global.Olm.OutboundGroupSession(); + groupSession.create(); + const ibGroupSession = new global.Olm.InboundGroupSession(); + ibGroupSession.create(groupSession.session_key()); + + const scheduler = [ + "getQueueForEvent", "queueEvent", "removeEventFromQueue", + "setProcessFunction", + ].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {}); + const store = [ + "getRoom", "getRooms", "getUser", "getSyncToken", "scrollback", + "save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", + "storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter", + "storeFilter", "getSyncAccumulator", "startup", "deleteAllData", + ].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {}); + store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null)); + store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null)); + store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null)); + const client = new MatrixClient({ + baseUrl: "https://my.home.server", + idBaseUrl: "https://identity.server", + accessToken: "my.access.token", + request: function() {}, // NOP + store: store, + scheduler: scheduler, + userId: "@alice:bar", + deviceId: "device", + sessionStore: sessionStore, + cryptoStore: cryptoStore, + }); + + megolmDecryption = new MegolmDecryption({ + userId: '@user:id', + crypto: mockCrypto, + olmDevice: olmDevice, + baseApis: client, + roomId: ROOM_ID, + }); + + megolmDecryption.olmlib = mockOlmLib; + + return client.initCrypto() + .then(() => { + return cryptoStore.doTxn("readwrite", [cryptoStore.STORE_SESSION], (txn) => { + cryptoStore.addEndToEndInboundGroupSession( + "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", + groupSession.session_id(), + { + forwardingCurve25519KeyChain: undefined, + keysClaimed: { + ed25519: "SENDER_ED25519", + }, + room_id: ROOM_ID, + session: ibGroupSession.pickle(olmDevice._pickleKey), + }, + txn); + }); + }) + .then(() => { + client.enableKeyBackup({ + algorithm: "foobar", + version: 1, + auth_data: { + public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmoK" + }, + }); + let numCalls = 0; + return new Promise((resolve, reject) => { + client._http.authedRequest = function(callback, method, path, queryParams, data, opts) { + expect(++numCalls <= 2); + if (numCalls >= 3) { + // exit out of retry loop if there's something wrong + reject(new Error("authedRequest called too many timmes")); + return Promise.resolve({}); + } + expect(method).toBe("PUT"); + expect(path).toBe("/room_keys/keys"); + expect(queryParams.version).toBe(1); + expect(data.rooms[ROOM_ID].sessions).toExist(); + expect(data.rooms[ROOM_ID].sessions).toIncludeKey(groupSession.session_id()); + if (numCalls > 1) { + resolve(); + return Promise.resolve({}); + } else { + return Promise.reject(new Error("this is an expected failure")); + } + }; + client._crypto.backupGroupSession("roomId", "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", [], groupSession.session_id(), groupSession.session_key()); + }) + .then(() => { + expect(numCalls).toBe(2); + }); + }); + }); }); describe("restore", function() { diff --git a/src/client.js b/src/client.js index c864bf490..7080fbcf6 100644 --- a/src/client.js +++ b/src/client.js @@ -848,6 +848,8 @@ MatrixClient.prototype.enableKeyBackup = function(info) { this._crypto.backupKey.set_recipient_key(info.auth_data.public_key); this.emit('keyBackupStatus', true); + + this._crypto._maybeSendKeyBackup(); }; /** diff --git a/src/crypto/index.js b/src/crypto/index.js index d45797fcb..41dfdff73 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -83,6 +83,7 @@ function Crypto(baseApis, sessionStore, userId, deviceId, this.backupInfo = null; // The info dict from /room_keys/version this.backupKey = null; // The encryption key object this._checkedForBackup = false; // Have we checked the server for a backup we can use? + this._sendingBackups = false; // Are we currently sending backups? this._olmDevice = new OlmDevice(sessionStore, cryptoStore); this._deviceList = new DeviceList( @@ -965,51 +966,62 @@ Crypto.prototype.importRoomKeys = function(keys) { ); }; -Crypto.prototype._backupPayloadForSession = function( - senderKey, forwardingCurve25519KeyChain, - sessionId, sessionKey, keysClaimed, - exportFormat, -) { - // new session. - const session = new Olm.InboundGroupSession(); - 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, - ); - } +Crypto.prototype._maybeSendKeyBackup = async function() { + if (!this._sendingBackups) { + this._sendingBackups = true; + while (1) { + if (!this.backupKey) { + this._sendingBackups = false; + return; + } + // FIXME: figure out what limit is reasonable + const sessions = await this._cryptoStore.getSessionsNeedingBackup(10); + if (!sessions.length) { + this._sendingBackups = false; + return; + } + const data = {}; + for (const session of sessions) { + const room_id = session.sessionData.room_id; + if (data[room_id] === undefined) + data[room_id] = {sessions: {}}; - if (!exportFormat) { - sessionKey = session.export_session(); - } - const firstKnownIndex = session.first_known_index(); + const sessionData = await this._olmDevice.exportInboundGroupSession(session.senderKey, session.sessionId, session.sessionData); + sessionData.algorithm = olmlib.MEGOLM_ALGORITHM; + delete sessionData.session_id; + delete sessionData.room_id; + const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData)); - 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: firstKnownIndex, - forwarded_count: forwardingCurve25519KeyChain.length, - is_verified: false, // FIXME: how do we determine this? - session_data: encrypted, - }; - } finally { - session.free(); + data[room_id]['sessions'][session.sessionId] = { + first_message_index: 1, // FIXME + forwarded_count: (sessionData.forwardingCurve25519KeyChain || []).length, + is_verified: false, // FIXME: how do we determine this? + session_data: encrypted, + }; + } + + let successful = false; + do { + if (!this.backupKey) { + this._sendingBackups = false; + return; + } + try { + await this._baseApis.sendKeyBackup(undefined, undefined, this.backupInfo.version, {rooms: data}); + successful = true; + await this._cryptoStore.unmarkSessionsNeedingBackup(sessions); + } + catch (e) { + console.log("send failed", e); + // FIXME: pause + } + } while (!successful); + // FIXME: pause between iterations? + } } -}; +} -Crypto.prototype.backupGroupSession = function( +Crypto.prototype.backupGroupSession = async function( roomId, senderKey, forwardingCurve25519KeyChain, sessionId, sessionKey, keysClaimed, exportFormat, @@ -1018,26 +1030,26 @@ Crypto.prototype.backupGroupSession = function( 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); + await this._cryptoStore.markSessionsNeedingBackup([{ + senderKey: senderKey, + sessionId: sessionId, + }]); + + this._maybeSendKeyBackup(); }; 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: {}}; + await this._cryptoStore.doTxn( + 'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS, IndexedDBCryptoStore.STORE_BACKUP], (txn) => { + this._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => { + if (session !== null) { + this._cryptoStore.markSessionsNeedingBackup([session], txn); + } + }); + } + ); - 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}); + this._maybeSendKeyBackup(); }; /* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307 diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index 4a7f48789..9935dbd38 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -460,6 +460,71 @@ export class Backend { }; } + // session backups + + getSessionsNeedingBackup(limit) { + return new Promise((resolve, reject) => { + const sessions = []; + + const txn = this._db.transaction(["sessions_needing_backup", "inbound_group_sessions"], "readonly"); + txn.onerror = reject; + txn.oncomplete = function() { + resolve(sessions); + } + const objectStore = txn.objectStore("sessions_needing_backup"); + const sessionStore = txn.objectStore("inbound_group_sessions"); + const getReq = objectStore.openCursor(); + getReq.onsuccess = function() { + const cursor = getReq.result; + if (cursor) { + const sessionGetReq = sessionStore.get(cursor.key) + sessionGetReq.onsuccess = function() { + sessions.push({ + senderKey: sessionGetReq.result.senderCurve25519Key, + sessionId: sessionGetReq.result.sessionId, + sessionData: sessionGetReq.result.session + }); + } + //sessions.push(cursor.value); + if (!limit || sessions.length < limit) { + cursor.continue(); + } + } + } + }); + } + + unmarkSessionsNeedingBackup(sessions) { + const txn = this._db.transaction("sessions_needing_backup", "readwrite"); + const objectStore = txn.objectStore("sessions_needing_backup"); + return Promise.all(sessions.map((session) => { + return new Promise((resolve, reject) => { + console.log(session); + const req = objectStore.delete([session.senderKey, session.sessionId]); + req.onsuccess = resolve; + req.onerror = reject; + }); + })); + } + + markSessionsNeedingBackup(sessions, txn) { + if (!txn) { + txn = this._db.transaction("sessions_needing_backup", "readwrite"); + } + const objectStore = txn.objectStore("sessions_needing_backup"); + return Promise.all(sessions.map((session) => { + return new Promise((resolve, reject) => { + const req = objectStore.put({ + senderCurve25519Key: session.senderKey, + sessionId: session.sessionId + }); + req.onsuccess = resolve; + req.onerror = reject; + }); + })); + + } + doTxn(mode, stores, func) { const txn = this._db.transaction(stores, mode); const promise = promiseifyTxn(txn); @@ -498,6 +563,11 @@ export function upgradeDatabase(db, oldVersion) { if (oldVersion < 6) { db.createObjectStore("rooms"); } + if (oldVersion < 7) { + db.createObjectStore("sessions_needing_backup", { + keyPath: ["senderCurve25519Key", "sessionId"], + }); + } // Expand as needed. } diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 249b29b63..0eca4c373 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -407,6 +407,38 @@ export default class IndexedDBCryptoStore { this._backendPromise.value().getEndToEndRooms(txn, func); } + /** + * Get the inbound group sessions that need to be backed up. + * @param {integer} limit The maximum number of sessions to retrieve. 0 + * for no limit. + */ + getSessionsNeedingBackup(limit) { + return this._connect().then((backend) => { + return backend.getSessionsNeedingBackup(limit); + }); + } + + /** + * Unmark sessions as needing to be backed up. + * @param {[object]} The sessions that need to be backed up. + */ + unmarkSessionsNeedingBackup(sessions) { + return this._connect().then((backend) => { + return backend.unmarkSessionsNeedingBackup(sessions); + }); + } + + /** + * Mark sessions as needing to be backed up. + * @param {[object]} The sessions that need to be backed up. + * @param {*} txn An active transaction. See doTxn(). (optional) + */ + markSessionsNeedingBackup(sessions, txn) { + return this._connect().then((backend) => { + return backend.markSessionsNeedingBackup(sessions, txn); + }); + } + /** * Perform a transaction on the crypto store. Any store methods * that require a transaction (txn) object to be passed in may @@ -440,3 +472,4 @@ IndexedDBCryptoStore.STORE_SESSIONS = 'sessions'; IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions'; IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data'; IndexedDBCryptoStore.STORE_ROOMS = 'rooms'; +IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup'; diff --git a/src/crypto/store/memory-crypto-store.js b/src/crypto/store/memory-crypto-store.js index 469cdb49b..cd5ec5be4 100644 --- a/src/crypto/store/memory-crypto-store.js +++ b/src/crypto/store/memory-crypto-store.js @@ -41,6 +41,8 @@ export default class MemoryCryptoStore { this._deviceData = null; // roomId -> Opaque roomInfo object this._rooms = {}; + // Set of {senderCurve25519Key+'/'+sessionId} + this._sessionsNeedingBackup = {}; } /** @@ -295,6 +297,39 @@ export default class MemoryCryptoStore { func(this._rooms); } + getSessionsNeedingBackup(limit) { + const sessions = []; + for (const session in this._sessionsNeedingBackup) { + if (this._inboundGroupSessions[session]) { + sessions.push({ + senderKey: session.substr(0, 43), + sessionId: session.substr(44), + sessionData: this._inboundGroupSessions[session], + }); + if (limit && session.length >= limit) { + break; + } + } + } + return Promise.resolve(sessions); + } + + unmarkSessionsNeedingBackup(sessions) { + for(const session of sessions) { + delete this._sessionsNeedingBackup[session.senderKey + '/' + session.sessionId]; + } + return Promise.resolve(); + } + + markSessionsNeedingBackup(sessions) { + for(const session of sessions) { + this._sessionsNeedingBackup[session.senderKey + '/' + session.sessionId] = true; + } + return Promise.resolve(); + } + + // Session key backups + doTxn(mode, stores, func) { return Promise.resolve(func(null)); } From 59e60665795ecbd3684d1ada12ced5c9341c7f11 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 9 Oct 2018 14:15:03 +0100 Subject: [PATCH 29/38] Replace base58check with a simple parity check base58check seems way overcomplicated for this purpose (plus the module was exporting an es6 file, breaking the js-sdk build). A parity check empirically detects single substitution and transposition errors. Another option would be Luhn's algorithm. --- package.json | 2 +- src/crypto/recoverykey.js | 42 +++++++++++++++++++++++++++++---------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 4ae0b36a7..17c3ec5ce 100644 --- a/package.json +++ b/package.json @@ -53,9 +53,9 @@ "dependencies": { "another-json": "^0.2.0", "babel-runtime": "^6.26.0", - "base58check": "^2.0.0", "bluebird": "^3.5.0", "browser-request": "^0.3.3", + "bs58": "^4.0.1", "content-type": "^1.0.2", "request": "^2.53.0" }, diff --git a/src/crypto/recoverykey.js b/src/crypto/recoverykey.js index 82de3a158..69dc8ae6e 100644 --- a/src/crypto/recoverykey.js +++ b/src/crypto/recoverykey.js @@ -14,31 +14,51 @@ See the License for the specific language governing permissions and limitations under the License. */ -import base58check from 'base58check'; +import bs58 from 'bs58'; // picked arbitrarily but to try & avoid clashing with any bitcoin ones +// (also base58 encoded, albeit with a lot of hashing) const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01]; export function encodeRecoveryKey(key) { - const base58key = base58check.encode(Buffer.from(OLM_RECOVERY_KEY_PREFIX), Buffer.from(key)); + const buf = new Uint8Array(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1); + buf.set(OLM_RECOVERY_KEY_PREFIX, 0); + buf.set(key, OLM_RECOVERY_KEY_PREFIX.length); + + let parity = 0; + for (let i = 0; i < buf.length - 1; ++i) { + parity ^= buf[i]; + } + buf[buf.length - 1] = parity; + const base58key = bs58.encode(buf); + + return base58key.match(/.{1,4}/g).join(" "); } export function decodeRecoveryKey(recoverykey) { - const result = base58check.decode(recoverykey.replace(/ /, '')); - // the encoding doesn't include the length of the prefix, so the - // decoder assumes it's 1 byte. sigh. - const prefix = Buffer.concat([result.prefix, result.data.slice(0, OLM_RECOVERY_KEY_PREFIX.length - 1)]); + const result = bs58.decode(recoverykey.replace(/ /g, '')); - if (!prefix.equals(Buffer.from(OLM_RECOVERY_KEY_PREFIX))) { - throw new Error("Incorrect prefix"); + let parity = 0; + for (const b of result) { + parity ^= b; + } + if (parity !== 0) { + throw new Error("Incorrect parity"); } - const key = result.data.slice(OLM_RECOVERY_KEY_PREFIX.length - 1); + for (let i = 0; i < OLM_RECOVERY_KEY_PREFIX.length; ++i) { + if (result[i] !== OLM_RECOVERY_KEY_PREFIX[i]) { + throw new Error("Incorrect prefix"); + } + } - if (key.length !== global.Olm.PRIVATE_KEY_LENGTH) { + if (result.length !== OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH + 1) { throw new Error("Incorrect length"); } - return key; + return result.slice( + OLM_RECOVERY_KEY_PREFIX.length, + OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH, + ); } From ada4b6ef16a3557e24b54e494d5b5a6f644c64f3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 9 Oct 2018 15:46:12 +0100 Subject: [PATCH 30/38] Lint --- spec/unit/crypto/backup.spec.js | 9 +++++---- src/client.js | 4 +++- src/crypto/index.js | 2 +- src/crypto/recoverykey.js | 5 ++++- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index 25f6c112a..f1da584f4 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -43,13 +43,14 @@ const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; const ROOM_ID = '!ROOM:ID'; +const SESSION_ID = 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc'; const ENCRYPTED_EVENT = new MatrixEvent({ type: 'm.room.encrypted', room_id: '!ROOM:ID', content: { algorithm: 'm.megolm.v1.aes-sha2', sender_key: 'SENDER_CURVE25519', - session_id: 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc', + session_id: SESSION_ID, ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N' + 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl' + 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs', @@ -222,7 +223,7 @@ describe("MegolmBackup", function() { "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD" + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA", ROOM_ID, - 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc', + SESSION_ID, ).then(() => { return megolmDecryption.decryptEvent(ENCRYPTED_EVENT); }).then((res) => { @@ -236,10 +237,10 @@ describe("MegolmBackup", function() { rooms: { [ROOM_ID]: { sessions: { - 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc': KEY_BACKUP_DATA, + SESSION_ID: KEY_BACKUP_DATA, }, }, - } + }, }); }; return client.restoreKeyBackups( diff --git a/src/client.js b/src/client.js index 5a9481122..9c3a86911 100644 --- a/src/client.js +++ b/src/client.js @@ -1032,7 +1032,9 @@ MatrixClient.prototype.restoreKeyBackups = function( if (!roomData.sessions) continue; totalKeyCount += Object.keys(roomData.sessions).length; - const roomKeys = keysFromRecoverySession(roomData.sessions, decryption, roomId, roomKeys); + const roomKeys = keysFromRecoverySession( + roomData.sessions, decryption, roomId, roomKeys, + ); for (const k of roomKeys) { k.room_id = roomId; keys.push(k); diff --git a/src/crypto/index.js b/src/crypto/index.js index 7aa76e37e..b6926b837 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -974,7 +974,7 @@ Crypto.prototype._backupPayloadForSession = function( exportFormat, ) { // new session. - const session = new Olm.InboundGroupSession(); + const session = new global.Olm.InboundGroupSession(); try { if (exportFormat) { session.import_session(sessionKey); diff --git a/src/crypto/recoverykey.js b/src/crypto/recoverykey.js index 69dc8ae6e..bb85697e8 100644 --- a/src/crypto/recoverykey.js +++ b/src/crypto/recoverykey.js @@ -53,7 +53,10 @@ export function decodeRecoveryKey(recoverykey) { } } - if (result.length !== OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH + 1) { + if ( + result.length !== + OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH + 1 + ) { throw new Error("Incorrect length"); } From da65f43983af1e5504ef0b1ba2d4a38c6d9acc81 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 10 Oct 2018 19:31:28 -0400 Subject: [PATCH 31/38] wrap backup sending in a try, and add delays --- spec/unit/crypto/backup.spec.js | 2 + src/crypto/index.js | 94 +++++++++++++++++++-------------- 2 files changed, 56 insertions(+), 40 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index 7a7f0b414..de910592b 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -171,6 +171,7 @@ describe("MegolmBackup", function() { }); it('sends backups to the server', function () { + this.timeout(12000); const groupSession = new global.Olm.OutboundGroupSession(); groupSession.create(); const ibGroupSession = new global.Olm.InboundGroupSession(); @@ -263,6 +264,7 @@ describe("MegolmBackup", function() { }); it('retries when a backup fails', function () { + this.timeout(12000); const groupSession = new global.Olm.OutboundGroupSession(); groupSession.create(); const ibGroupSession = new global.Olm.InboundGroupSession(); diff --git a/src/crypto/index.js b/src/crypto/index.js index 41dfdff73..7104320ee 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -969,54 +969,68 @@ Crypto.prototype.importRoomKeys = function(keys) { Crypto.prototype._maybeSendKeyBackup = async function() { if (!this._sendingBackups) { this._sendingBackups = true; - while (1) { - if (!this.backupKey) { - this._sendingBackups = false; - return; - } - // FIXME: figure out what limit is reasonable - const sessions = await this._cryptoStore.getSessionsNeedingBackup(10); - if (!sessions.length) { - this._sendingBackups = false; - return; - } - const data = {}; - for (const session of sessions) { - const room_id = session.sessionData.room_id; - if (data[room_id] === undefined) - data[room_id] = {sessions: {}}; - - const sessionData = await this._olmDevice.exportInboundGroupSession(session.senderKey, session.sessionId, session.sessionData); - sessionData.algorithm = olmlib.MEGOLM_ALGORITHM; - delete sessionData.session_id; - delete sessionData.room_id; - const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData)); - - data[room_id]['sessions'][session.sessionId] = { - first_message_index: 1, // FIXME - forwarded_count: (sessionData.forwardingCurve25519KeyChain || []).length, - is_verified: false, // FIXME: how do we determine this? - session_data: encrypted, - }; - } - - let successful = false; - do { + try { + // wait between 0 and 10 seconds, to avoid backup requests from + // different clients hitting the server all at the same time when a + // new key is sent + await new Promise((resolve, reject) => { + setTimeout(resolve, Math.random() * 10000); + }); + let numFailures = 0; // number of consecutive failures + while (1) { if (!this.backupKey) { - this._sendingBackups = false; return; } + // FIXME: figure out what limit is reasonable + const sessions = await this._cryptoStore.getSessionsNeedingBackup(10); + if (!sessions.length) { + return; + } + const data = {}; + for (const session of sessions) { + const room_id = session.sessionData.room_id; + if (data[room_id] === undefined) + data[room_id] = {sessions: {}}; + + const sessionData = await this._olmDevice.exportInboundGroupSession(session.senderKey, session.sessionId, session.sessionData); + sessionData.algorithm = olmlib.MEGOLM_ALGORITHM; + delete sessionData.session_id; + delete sessionData.room_id; + const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData)); + + data[room_id]['sessions'][session.sessionId] = { + first_message_index: 1, // FIXME + forwarded_count: (sessionData.forwardingCurve25519KeyChain || []).length, + is_verified: false, // FIXME: how do we determine this? + session_data: encrypted, + }; + } + try { await this._baseApis.sendKeyBackup(undefined, undefined, this.backupInfo.version, {rooms: data}); - successful = true; + numFailures = 0; await this._cryptoStore.unmarkSessionsNeedingBackup(sessions); } - catch (e) { - console.log("send failed", e); - // FIXME: pause + catch (err) { + numFailures++; + console.log("send failed", err); + if (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401) { + // retrying probably won't help much, so we should give up + // FIXME: disable backups? + return; + } } - } while (!successful); - // FIXME: pause between iterations? + if (numFailures) { + // exponential backoff if we have failures + await new Promise((resolve, reject) => { + setTimeout(resolve, 1000 * Math.pow(2, Math.min(numFailures - 1, 4))); + }); + } + } + } + finally + { + this._sendingBackups = false; } } } From fc59bc2992d9543be987aaac60a22aa567aa81eb Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 10 Oct 2018 19:32:07 -0400 Subject: [PATCH 32/38] add localstorage support for key backups --- src/crypto/store/localStorage-crypto-store.js | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/crypto/store/localStorage-crypto-store.js b/src/crypto/store/localStorage-crypto-store.js index 3f2f0d09a..71a904fd8 100644 --- a/src/crypto/store/localStorage-crypto-store.js +++ b/src/crypto/store/localStorage-crypto-store.js @@ -32,6 +32,7 @@ const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account"; const KEY_DEVICE_DATA = E2E_PREFIX + "device_data"; const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/"; const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/"; +const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup"; function keyEndToEndSessions(deviceKey) { return E2E_PREFIX + "sessions/" + deviceKey; @@ -165,6 +166,48 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { func(result); } + getSessionsNeedingBackup(limit) { + const sessions = []; + + for (const session in getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP)) { + const senderKey = session.substr(0, 43); + const sessionId = session.substr(44); + getEndToEndInboundGroupSession(senderKey, sessionId, null, (sessionData) => { + sessions.push({ + senderKey: senderKey, + sessionId: sessionId, + sessionData: sessionData, + }); + }) + if (limit && session.length >= limit) { + break; + } + } + return Promise.resolve(sessions); + } + + unmarkSessionsNeedingBackup(sessions) { + const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; + for(const session of sessions) { + delete sessionsNeedingBackup[session.senderKey + '/' + session.sessionId]; + } + setJsonItem( + this.store, KEY_SESSION_NEEDING_BACKUP, sessionsNeedinBackup, + ); + return Promise.resolve(); + } + + markSessionsNeedingBackup(sessions) { + const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; + for(const session of sessions) { + sessionsNeedingBackup[session.senderKey + '/' + session.sessionId] = true; + } + setJsonItem( + this.store, KEY_SESSION_NEEDING_BACKUP, sessionsNeedinBackup, + ); + return Promise.resolve(); + } + /** * Delete all data from this store. * From 9b12c228235e5f13dfe9664f00f827e094b49adf Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 12 Oct 2018 10:38:10 -0400 Subject: [PATCH 33/38] de-lint plus some minor fixes --- .eslintrc.js | 1 + spec/unit/crypto/backup.spec.js | 148 +++++++++++------- src/crypto/algorithms/megolm.js | 4 - src/crypto/index.js | 45 ++++-- .../store/indexeddb-crypto-store-backend.js | 19 +-- src/crypto/store/indexeddb-crypto-store.js | 9 +- 6 files changed, 138 insertions(+), 88 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index fec2d7b5a..ae1826de5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,6 +16,7 @@ module.exports = { }, extends: ["eslint:recommended", "google"], rules: { + "indent": ["error", 4], // rules we've always adhered to or now do "max-len": ["error", { code: 90, diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index b1948c8a2..7217f3226 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -37,6 +37,8 @@ if (global.Olm) { Crypto = require('../../../lib/crypto'); } +const Olm = global.Olm; + const MatrixClient = sdk.MatrixClient; const MatrixEvent = sdk.MatrixEvent; const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; @@ -93,16 +95,21 @@ describe("MegolmBackup", function() { let sessionStore; let cryptoStore; let megolmDecryption; - beforeEach(function() { + beforeEach(async function() { + await Olm.init(); testUtils.beforeEach(this); // eslint-disable-line no-invalid-this mockCrypto = testUtils.mock(Crypto, 'Crypto'); - mockCrypto.backupKey = new global.Olm.PkEncryption(); + mockCrypto.backupKey = new Olm.PkEncryption(); mockCrypto.backupKey.set_recipient_key( "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", ); mockCrypto.backupInfo = { + algorithm: "m.megolm_backup.v1", version: 1, + auth_data: { + public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", + }, }; mockStorage = new MockStorageApi(); @@ -136,7 +143,7 @@ describe("MegolmBackup", function() { }); it('automatically calls the key back up', function() { - const groupSession = new global.Olm.OutboundGroupSession(); + const groupSession = new Olm.OutboundGroupSession(); groupSession.create(); // construct a fake decrypted key event via the use of a mocked @@ -161,8 +168,9 @@ describe("MegolmBackup", function() { mockCrypto.decryptEvent = function() { return Promise.resolve(decryptedData); }; + mockCrypto.cancelRoomKeyRequest = function() {}; - mockBaseApis.sendKeyBackup = expect.createSpy(); + mockCrypto.backupGroupSession = expect.createSpy(); return event.attemptDecryption(mockCrypto).then(() => { return megolmDecryption.onRoomKeyEvent(event); @@ -171,23 +179,23 @@ describe("MegolmBackup", function() { }); }); - it('sends backups to the server', function () { - this.timeout(12000); - const groupSession = new global.Olm.OutboundGroupSession(); + it('sends backups to the server', function() { + this.timeout(12000); // eslint-disable-line no-invalid-this + const groupSession = new Olm.OutboundGroupSession(); groupSession.create(); - const ibGroupSession = new global.Olm.InboundGroupSession(); + const ibGroupSession = new Olm.InboundGroupSession(); ibGroupSession.create(groupSession.session_key()); const scheduler = [ "getQueueForEvent", "queueEvent", "removeEventFromQueue", "setProcessFunction", - ].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {}); + ].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {}); const store = [ "getRoom", "getRooms", "getUser", "getSyncToken", "scrollback", "save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", "storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter", "getSyncAccumulator", "startup", "deleteAllData", - ].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {}); + ].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {}); store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null)); store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null)); store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null)); @@ -216,32 +224,37 @@ describe("MegolmBackup", function() { return client.initCrypto() .then(() => { - return cryptoStore.doTxn("readwrite", [cryptoStore.STORE_SESSION], (txn) => { - cryptoStore.addEndToEndInboundGroupSession( - "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", - groupSession.session_id(), - { - forwardingCurve25519KeyChain: undefined, - keysClaimed: { - ed25519: "SENDER_ED25519", + return cryptoStore.doTxn( + "readwrite", + [cryptoStore.STORE_SESSION], + (txn) => { + cryptoStore.addEndToEndInboundGroupSession( + "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", + groupSession.session_id(), + { + forwardingCurve25519KeyChain: undefined, + keysClaimed: { + ed25519: "SENDER_ED25519", + }, + room_id: ROOM_ID, + session: ibGroupSession.pickle(olmDevice._pickleKey), }, - room_id: ROOM_ID, - session: ibGroupSession.pickle(olmDevice._pickleKey), - }, - txn); - }); + txn); + }); }) .then(() => { client.enableKeyBackup({ - algorithm: "foobar", + algorithm: "m.megolm_backup.v1", version: 1, auth_data: { - public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmoK" + public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", }, }); let numCalls = 0; return new Promise((resolve, reject) => { - client._http.authedRequest = function(callback, method, path, queryParams, data, opts) { + client._http.authedRequest = function( + callback, method, path, queryParams, data, opts, + ) { expect(++numCalls <= 1); if (numCalls >= 2) { // exit out of retry loop if there's something wrong @@ -252,11 +265,19 @@ describe("MegolmBackup", function() { expect(path).toBe("/room_keys/keys"); expect(queryParams.version).toBe(1); expect(data.rooms[ROOM_ID].sessions).toExist(); - expect(data.rooms[ROOM_ID].sessions).toIncludeKey(groupSession.session_id()); + expect(data.rooms[ROOM_ID].sessions).toIncludeKey( + groupSession.session_id(), + ); resolve(); return Promise.resolve({}); }; - client._crypto.backupGroupSession("roomId", "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", [], groupSession.session_id(), groupSession.session_key()); + client._crypto.backupGroupSession( + "roomId", + "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", + [], + groupSession.session_id(), + groupSession.session_key(), + ); }) .then(() => { expect(numCalls).toBe(1); @@ -264,23 +285,23 @@ describe("MegolmBackup", function() { }); }); - it('retries when a backup fails', function () { - this.timeout(12000); - const groupSession = new global.Olm.OutboundGroupSession(); + it('retries when a backup fails', function() { + this.timeout(12000); // eslint-disable-line no-invalid-this + const groupSession = new Olm.OutboundGroupSession(); groupSession.create(); - const ibGroupSession = new global.Olm.InboundGroupSession(); + const ibGroupSession = new Olm.InboundGroupSession(); ibGroupSession.create(groupSession.session_key()); const scheduler = [ "getQueueForEvent", "queueEvent", "removeEventFromQueue", "setProcessFunction", - ].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {}); + ].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {}); const store = [ "getRoom", "getRooms", "getUser", "getSyncToken", "scrollback", "save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", "storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter", "getSyncAccumulator", "startup", "deleteAllData", - ].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {}); + ].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {}); store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null)); store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null)); store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null)); @@ -309,32 +330,37 @@ describe("MegolmBackup", function() { return client.initCrypto() .then(() => { - return cryptoStore.doTxn("readwrite", [cryptoStore.STORE_SESSION], (txn) => { - cryptoStore.addEndToEndInboundGroupSession( - "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", - groupSession.session_id(), - { - forwardingCurve25519KeyChain: undefined, - keysClaimed: { - ed25519: "SENDER_ED25519", + return cryptoStore.doTxn( + "readwrite", + [cryptoStore.STORE_SESSION], + (txn) => { + cryptoStore.addEndToEndInboundGroupSession( + "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", + groupSession.session_id(), + { + forwardingCurve25519KeyChain: undefined, + keysClaimed: { + ed25519: "SENDER_ED25519", + }, + room_id: ROOM_ID, + session: ibGroupSession.pickle(olmDevice._pickleKey), }, - room_id: ROOM_ID, - session: ibGroupSession.pickle(olmDevice._pickleKey), - }, - txn); - }); + txn); + }); }) .then(() => { client.enableKeyBackup({ algorithm: "foobar", version: 1, auth_data: { - public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmoK" + public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", }, }); let numCalls = 0; return new Promise((resolve, reject) => { - client._http.authedRequest = function(callback, method, path, queryParams, data, opts) { + client._http.authedRequest = function( + callback, method, path, queryParams, data, opts, + ) { expect(++numCalls <= 2); if (numCalls >= 3) { // exit out of retry loop if there's something wrong @@ -345,15 +371,25 @@ describe("MegolmBackup", function() { expect(path).toBe("/room_keys/keys"); expect(queryParams.version).toBe(1); expect(data.rooms[ROOM_ID].sessions).toExist(); - expect(data.rooms[ROOM_ID].sessions).toIncludeKey(groupSession.session_id()); + expect(data.rooms[ROOM_ID].sessions).toIncludeKey( + groupSession.session_id(), + ); if (numCalls > 1) { resolve(); return Promise.resolve({}); } else { - return Promise.reject(new Error("this is an expected failure")); + return Promise.reject( + new Error("this is an expected failure"), + ); } }; - client._crypto.backupGroupSession("roomId", "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", [], groupSession.session_id(), groupSession.session_key()); + client._crypto.backupGroupSession( + "roomId", + "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", + [], + groupSession.session_id(), + groupSession.session_key(), + ); }) .then(() => { expect(numCalls).toBe(2); @@ -369,13 +405,13 @@ describe("MegolmBackup", function() { const scheduler = [ "getQueueForEvent", "queueEvent", "removeEventFromQueue", "setProcessFunction", - ].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {}); + ].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {}); const store = [ "getRoom", "getRooms", "getUser", "getSyncToken", "scrollback", "save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", "storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter", "getSyncAccumulator", "startup", "deleteAllData", - ].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {}); + ].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {}); store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null)); store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null)); store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null)); @@ -411,7 +447,7 @@ describe("MegolmBackup", function() { }; return client.restoreKeyBackups( "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD" - + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA", + + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA", ROOM_ID, SESSION_ID, ).then(() => { @@ -435,7 +471,7 @@ describe("MegolmBackup", function() { }; return client.restoreKeyBackups( "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD" - + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA", + + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA", ).then(() => { return megolmDecryption.decryptEvent(ENCRYPTED_EVENT); }).then((res) => { diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index 1e1de101b..af311e16b 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -849,10 +849,6 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { this._retryDecryption(senderKey, sessionId); }).then(() => { if (this._crypto.backupInfo) { - // XXX: No retries on this at all: if this request dies for whatever - // reason, this key will never be uploaded. - // More XXX: If this fails it'll cause the message send to fail, - // and this will happen if the backup is deleted from another client. return this._crypto.backupGroupSession( content.room_id, senderKey, forwardingKeyChain, content.session_id, content.session_key, keysClaimed, diff --git a/src/crypto/index.js b/src/crypto/index.js index 0ca0d8506..d45caabe3 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -991,33 +991,41 @@ Crypto.prototype._maybeSendKeyBackup = async function() { } const data = {}; for (const session of sessions) { - const room_id = session.sessionData.room_id; - if (data[room_id] === undefined) - data[room_id] = {sessions: {}}; + const roomId = session.sessionData.room_id; + if (data[roomId] === undefined) { + data[roomId] = {sessions: {}}; + } - const sessionData = await this._olmDevice.exportInboundGroupSession(session.senderKey, session.sessionId, session.sessionData); + const sessionData = await this._olmDevice.exportInboundGroupSession( + session.senderKey, session.sessionId, session.sessionData, + ); sessionData.algorithm = olmlib.MEGOLM_ALGORITHM; delete sessionData.session_id; delete sessionData.room_id; const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData)); - data[room_id]['sessions'][session.sessionId] = { + data[roomId]['sessions'][session.sessionId] = { first_message_index: 1, // FIXME - forwarded_count: (sessionData.forwardingCurve25519KeyChain || []).length, + forwarded_count: + (sessionData.forwardingCurve25519KeyChain || []).length, is_verified: false, // FIXME: how do we determine this? session_data: encrypted, }; } try { - await this._baseApis.sendKeyBackup(undefined, undefined, this.backupInfo.version, {rooms: data}); + await this._baseApis.sendKeyBackup( + undefined, undefined, this.backupInfo.version, + {rooms: data}, + ); numFailures = 0; await this._cryptoStore.unmarkSessionsNeedingBackup(sessions); - } - catch (err) { + } catch (err) { numFailures++; console.log("send failed", err); - if (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401) { + if (err.httpStatus === 400 + || err.httpStatus === 403 + || err.httpStatus === 401) { // retrying probably won't help much, so we should give up // FIXME: disable backups? return; @@ -1026,17 +1034,18 @@ Crypto.prototype._maybeSendKeyBackup = async function() { if (numFailures) { // exponential backoff if we have failures await new Promise((resolve, reject) => { - setTimeout(resolve, 1000 * Math.pow(2, Math.min(numFailures - 1, 4))); + setTimeout( + resolve, + 1000 * Math.pow(2, Math.min(numFailures - 1, 4)), + ); }); } } - } - finally - { + } finally { this._sendingBackups = false; } } -} +}; Crypto.prototype.backupGroupSession = async function( roomId, senderKey, forwardingCurve25519KeyChain, @@ -1057,13 +1066,15 @@ Crypto.prototype.backupGroupSession = async function( Crypto.prototype.backupAllGroupSessions = async function(version) { await this._cryptoStore.doTxn( - 'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS, IndexedDBCryptoStore.STORE_BACKUP], (txn) => { + 'readwrite', + [IndexedDBCryptoStore.STORE_SESSIONS, IndexedDBCryptoStore.STORE_BACKUP], + (txn) => { this._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => { if (session !== null) { this._cryptoStore.markSessionsNeedingBackup([session], txn); } }); - } + }, ); this._maybeSendKeyBackup(); diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index 9935dbd38..d0bb9f1b7 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -466,31 +466,33 @@ export class Backend { return new Promise((resolve, reject) => { const sessions = []; - const txn = this._db.transaction(["sessions_needing_backup", "inbound_group_sessions"], "readonly"); + const txn = this._db.transaction( + ["sessions_needing_backup", "inbound_group_sessions"], + "readonly", + ); txn.onerror = reject; txn.oncomplete = function() { resolve(sessions); - } + }; const objectStore = txn.objectStore("sessions_needing_backup"); const sessionStore = txn.objectStore("inbound_group_sessions"); const getReq = objectStore.openCursor(); getReq.onsuccess = function() { const cursor = getReq.result; if (cursor) { - const sessionGetReq = sessionStore.get(cursor.key) + const sessionGetReq = sessionStore.get(cursor.key); sessionGetReq.onsuccess = function() { sessions.push({ senderKey: sessionGetReq.result.senderCurve25519Key, sessionId: sessionGetReq.result.sessionId, - sessionData: sessionGetReq.result.session + sessionData: sessionGetReq.result.session, }); - } - //sessions.push(cursor.value); + }; if (!limit || sessions.length < limit) { cursor.continue(); } } - } + }; }); } @@ -516,13 +518,12 @@ export class Backend { return new Promise((resolve, reject) => { const req = objectStore.put({ senderCurve25519Key: session.senderKey, - sessionId: session.sessionId + sessionId: session.sessionId, }); req.onsuccess = resolve; req.onerror = reject; }); })); - } doTxn(mode, stores, func) { diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 0eca4c373..59d68f8fc 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -407,10 +407,13 @@ export default class IndexedDBCryptoStore { this._backendPromise.value().getEndToEndRooms(txn, func); } + // session backups + /** * Get the inbound group sessions that need to be backed up. * @param {integer} limit The maximum number of sessions to retrieve. 0 * for no limit. + * @returns {Promise} resolves to an array of inbound group sessions */ getSessionsNeedingBackup(limit) { return this._connect().then((backend) => { @@ -420,7 +423,8 @@ export default class IndexedDBCryptoStore { /** * Unmark sessions as needing to be backed up. - * @param {[object]} The sessions that need to be backed up. + * @param {[object]} sessions The sessions that need to be backed up. + * @returns {Promise} resolves when the sessions are unmarked */ unmarkSessionsNeedingBackup(sessions) { return this._connect().then((backend) => { @@ -430,8 +434,9 @@ export default class IndexedDBCryptoStore { /** * Mark sessions as needing to be backed up. - * @param {[object]} The sessions that need to be backed up. + * @param {[object]} sessions The sessions that need to be backed up. * @param {*} txn An active transaction. See doTxn(). (optional) + * @returns {Promise} resolves when the sessions are marked */ markSessionsNeedingBackup(sessions, txn) { return this._connect().then((backend) => { From 91fb7b0a7c4d530c33f2e47a769fc741302fa0a8 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 12 Oct 2018 12:03:51 -0400 Subject: [PATCH 34/38] fix unit tests for backup recovery --- spec/unit/crypto/backup.spec.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index 7217f3226..3a3ed3a97 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -446,8 +446,7 @@ describe("MegolmBackup", function() { return Promise.resolve(KEY_BACKUP_DATA); }; return client.restoreKeyBackups( - "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD" - + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA", + "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", ROOM_ID, SESSION_ID, ).then(() => { @@ -463,15 +462,14 @@ describe("MegolmBackup", function() { rooms: { [ROOM_ID]: { sessions: { - SESSION_ID: KEY_BACKUP_DATA, + [SESSION_ID]: KEY_BACKUP_DATA, }, }, }, }); }; return client.restoreKeyBackups( - "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD" - + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA", + "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", ).then(() => { return megolmDecryption.decryptEvent(ENCRYPTED_EVENT); }).then((res) => { From d49c0a1bcb5a53b5cf90bfbef968ce6e2ac12ccc Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 12 Oct 2018 14:28:31 -0400 Subject: [PATCH 35/38] more de-linting and fixing --- src/crypto/algorithms/megolm.js | 19 ++++++-- src/crypto/index.js | 4 +- .../store/indexeddb-crypto-store-backend.js | 1 - src/crypto/store/localStorage-crypto-store.js | 45 +++++++++++-------- src/crypto/store/memory-crypto-store.js | 10 +++-- 5 files changed, 51 insertions(+), 28 deletions(-) diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index af311e16b..d1115f00e 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -264,8 +264,8 @@ MegolmEncryption.prototype._prepareNewSession = async function() { ); if (this._crypto.backupInfo) { - // Not strictly necessary to wait for this - await this._crypto.backupGroupSession( + // don't wait for it to complete + this._crypto.backupGroupSession( this._roomId, this._olmDevice.deviceCurve25519Key, [], sessionId, key.key, ); @@ -849,7 +849,8 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { this._retryDecryption(senderKey, sessionId); }).then(() => { if (this._crypto.backupInfo) { - return this._crypto.backupGroupSession( + // don't wait for it to complete + this._crypto.backupGroupSession( content.room_id, senderKey, forwardingKeyChain, content.session_id, content.session_key, keysClaimed, exportFormat, @@ -972,6 +973,18 @@ MegolmDecryption.prototype.importRoomKey = function(session) { session.sender_claimed_keys, true, ).then(() => { + if (this._crypto.backupInfo) { + // don't wait for it to complete + this._crypto.backupGroupSession( + session.room_id, + session.sender_key, + session.forwarding_curve25519_key_chain, + session.session_id, + session.session_key, + session.sender_claimed_keys, + true, + ); + } // have another go at decrypting events sent with this session. this._retryDecryption(session.sender_key, session.session_id); }); diff --git a/src/crypto/index.js b/src/crypto/index.js index d45caabe3..20341c4b2 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1061,7 +1061,7 @@ Crypto.prototype.backupGroupSession = async function( sessionId: sessionId, }]); - this._maybeSendKeyBackup(); + await this._maybeSendKeyBackup(); }; Crypto.prototype.backupAllGroupSessions = async function(version) { @@ -1077,7 +1077,7 @@ Crypto.prototype.backupAllGroupSessions = async function(version) { }, ); - this._maybeSendKeyBackup(); + await this._maybeSendKeyBackup(); }; /* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307 diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index d0bb9f1b7..d5b66c30f 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -501,7 +501,6 @@ export class Backend { const objectStore = txn.objectStore("sessions_needing_backup"); return Promise.all(sessions.map((session) => { return new Promise((resolve, reject) => { - console.log(session); const req = objectStore.delete([session.senderKey, session.sessionId]); req.onsuccess = resolve; req.onerror = reject; diff --git a/src/crypto/store/localStorage-crypto-store.js b/src/crypto/store/localStorage-crypto-store.js index 71a904fd8..cad6a7d64 100644 --- a/src/crypto/store/localStorage-crypto-store.js +++ b/src/crypto/store/localStorage-crypto-store.js @@ -167,43 +167,52 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { } getSessionsNeedingBackup(limit) { + const sessionsNeedingBackup + = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; const sessions = []; - for (const session in getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP)) { - const senderKey = session.substr(0, 43); - const sessionId = session.substr(44); - getEndToEndInboundGroupSession(senderKey, sessionId, null, (sessionData) => { - sessions.push({ - senderKey: senderKey, - sessionId: sessionId, - sessionData: sessionData, - }); - }) - if (limit && session.length >= limit) { - break; + for (const session in sessionsNeedingBackup) { + if (Object.prototype.hasOwnProperty.call(sessionsNeedingBackup, session)) { + const senderKey = session.substr(0, 43); + const sessionId = session.substr(44); + this.getEndToEndInboundGroupSession( + senderKey, sessionId, null, + (sessionData) => { + sessions.push({ + senderKey: senderKey, + sessionId: sessionId, + sessionData: sessionData, + }); + }, + ); + if (limit && session.length >= limit) { + break; + } } } return Promise.resolve(sessions); } unmarkSessionsNeedingBackup(sessions) { - const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; - for(const session of sessions) { + const sessionsNeedingBackup + = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; + for (const session of sessions) { delete sessionsNeedingBackup[session.senderKey + '/' + session.sessionId]; } setJsonItem( - this.store, KEY_SESSION_NEEDING_BACKUP, sessionsNeedinBackup, + this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup, ); return Promise.resolve(); } markSessionsNeedingBackup(sessions) { - const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; - for(const session of sessions) { + const sessionsNeedingBackup + = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; + for (const session of sessions) { sessionsNeedingBackup[session.senderKey + '/' + session.sessionId] = true; } setJsonItem( - this.store, KEY_SESSION_NEEDING_BACKUP, sessionsNeedinBackup, + this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup, ); return Promise.resolve(); } diff --git a/src/crypto/store/memory-crypto-store.js b/src/crypto/store/memory-crypto-store.js index cd5ec5be4..6af312219 100644 --- a/src/crypto/store/memory-crypto-store.js +++ b/src/crypto/store/memory-crypto-store.js @@ -315,15 +315,17 @@ export default class MemoryCryptoStore { } unmarkSessionsNeedingBackup(sessions) { - for(const session of sessions) { - delete this._sessionsNeedingBackup[session.senderKey + '/' + session.sessionId]; + for (const session of sessions) { + const sessionKey = session.senderKey + '/' + session.sessionId; + delete this._sessionsNeedingBackup[sessionKey]; } return Promise.resolve(); } markSessionsNeedingBackup(sessions) { - for(const session of sessions) { - this._sessionsNeedingBackup[session.senderKey + '/' + session.sessionId] = true; + for (const session of sessions) { + const sessionKey = session.senderKey + '/' + session.sessionId; + this._sessionsNeedingBackup[sessionKey] = true; } return Promise.resolve(); } From 40d0a823428e331063670b4978f07cc5fc7ca774 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 12 Oct 2018 15:45:48 -0400 Subject: [PATCH 36/38] remove accidental change to eslintrc --- .eslintrc.js | 1 - 1 file changed, 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index ae1826de5..fec2d7b5a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,7 +16,6 @@ module.exports = { }, extends: ["eslint:recommended", "google"], rules: { - "indent": ["error", 4], // rules we've always adhered to or now do "max-len": ["error", { code: 90, From 434ac86090836b1c1b2e8820d30e505aacd6f19b Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 19 Oct 2018 10:51:19 -0400 Subject: [PATCH 37/38] properly fill out the is_verified and first_message_index fields --- src/crypto/DeviceList.js | 41 +++++++++++++++++++++++++++++++++++++++- src/crypto/OlmDevice.js | 1 + src/crypto/index.js | 17 +++++++++++++---- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index fa55f2fa6..c3a86ae1e 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -71,6 +71,9 @@ export default class DeviceList { // } this._devices = {}; + // map of identity keys to the user who owns it + this._userByIdentityKey = {}; + // which users we are tracking device status for. // userId -> TRACKING_STATUS_* this._deviceTrackingStatus = {}; // loaded from storage in load() @@ -128,6 +131,19 @@ export default class DeviceList { deviceData.trackingStatus : {}; this._syncToken = deviceData ? deviceData.syncToken : null; } + this._userByIdentityKey = {}; + for (const user in this._devices) { + if (!this._devices.hasOwnProperty(user)) { + continue; + } + const userDevices = this._devices[user]; + for (const device in userDevices) { + if (!userDevices.hasOwnProperty(device)) { + continue; + } + this._userByIdentityKey[userDevices[device].senderKey] = user; + } + } }); }, ); @@ -357,13 +373,24 @@ export default class DeviceList { /** * Find a device by curve25519 identity key * - * @param {string} userId owner of the device + * @param {string} userId owner of the device (optional) * @param {string} algorithm encryption algorithm * @param {string} senderKey curve25519 key to match * * @return {module:crypto/deviceinfo?} */ getDeviceByIdentityKey(userId, algorithm, senderKey) { + if (arguments.length === 2) { + // if userId is omitted, shift the other arguments, and look up the + // userid + senderKey = algorithm; + algorithm = userId; + userId = this._userByIdentityKey[senderKey]; + if (!userId) { + return null; + } + } + if ( algorithm !== olmlib.OLM_ALGORITHM && algorithm !== olmlib.MEGOLM_ALGORITHM @@ -409,6 +436,12 @@ export default class DeviceList { */ storeDevicesForUser(u, devs) { this._devices[u] = devs; + for (const device in devs) { + if (!devs.hasOwnProperty(device)) { + continue; + } + this._userByIdentityKey[devs[device].senderKey] = u; + } this._dirty = true; } @@ -526,6 +559,12 @@ export default class DeviceList { */ _setRawStoredDevicesForUser(userId, devices) { this._devices[userId] = devices; + for (const device in devices) { + if (!devices.hasOwnProperty(device)) { + continue; + } + this._userByIdentityKey[devices[device].senderKey] = userId; + } } /** diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 818840054..74e46e2a4 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -1119,6 +1119,7 @@ OlmDevice.prototype.exportInboundGroupSession = function( "session_id": sessionId, "session_key": session.export_session(messageIndex), "forwarding_curve25519_key_chain": session.forwardingCurve25519KeyChain || [], + "first_known_index": session.first_known_index(), }; }); }; diff --git a/src/crypto/index.js b/src/crypto/index.js index 20341c4b2..2f1c39bd9 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -940,6 +940,7 @@ Crypto.prototype.exportRoomKeys = async function() { const sess = this._olmDevice.exportInboundGroupSession( s.senderKey, s.sessionId, s.sessionData, ); + delete sess.first_known_index; sess.algorithm = olmlib.MEGOLM_ALGORITHM; exportedSessions.push(sess); }); @@ -1002,13 +1003,21 @@ Crypto.prototype._maybeSendKeyBackup = async function() { sessionData.algorithm = olmlib.MEGOLM_ALGORITHM; delete sessionData.session_id; delete sessionData.room_id; + const firstKnownIndex = sessionData.first_known_index; + delete sessionData.first_known_index; const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData)); + const forwardedCount = + (sessionData.forwardingCurve25519KeyChain || []).length; + + const device = this._deviceList.getDeviceByIdentityKey( + olmlib.MEGOLM_ALGORITHM, session.senderKey, + ); + data[roomId]['sessions'][session.sessionId] = { - first_message_index: 1, // FIXME - forwarded_count: - (sessionData.forwardingCurve25519KeyChain || []).length, - is_verified: false, // FIXME: how do we determine this? + first_message_index: firstKnownIndex, + forwarded_count: forwardedCount, + is_verified: !!(device && device.isVerified()), session_data: encrypted, }; } From 322ef1fd637003889fe3825957c66d3e8c39abf2 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 22 Oct 2018 11:28:16 -0400 Subject: [PATCH 38/38] update backup algorithm name to agree with the proposal --- src/crypto/olmlib.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/olmlib.js b/src/crypto/olmlib.js index f03714f16..bbe942036 100644 --- a/src/crypto/olmlib.js +++ b/src/crypto/olmlib.js @@ -38,7 +38,7 @@ module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2"; /** * matrix algorithm tag for megolm backups */ -module.exports.MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1"; +module.exports.MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1.curve25519-aes-sha2"; /**