From fb1b554b862677848e94dc0e1cf74e6ea184419a Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 15 Jan 2018 01:50:24 +0000 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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) {