diff --git a/spec/integ/megolm.spec.js b/spec/integ/megolm.spec.js index c4531765c..5f1152d1b 100644 --- a/spec/integ/megolm.spec.js +++ b/spec/integ/megolm.spec.js @@ -894,4 +894,89 @@ describe("megolm", function() { return q.all([downloadPromise, sendPromise]); }).nodeify(done); }); + + + it("Alice exports megolm keys and imports them to a new device", function(done) { + let messageEncrypted; + + return aliceTestClient.start().then(() => { + const p2pSession = createOlmSession( + testOlmAccount, aliceTestClient + ); + + const groupSession = new Olm.OutboundGroupSession(); + groupSession.create(); + + // make the room_key event + const roomKeyEncrypted = encryptGroupSessionKey({ + senderKey: testSenderKey, + recipient: aliceTestClient, + p2pSession: p2pSession, + groupSession: groupSession, + room_id: ROOM_ID, + }); + + // encrypt a message with the group session + messageEncrypted = encryptMegolmEvent({ + senderKey: testSenderKey, + groupSession: groupSession, + room_id: ROOM_ID, + }); + + // Alice gets both the events in a single sync + const syncResponse = { + next_batch: 1, + to_device: { + events: [roomKeyEncrypted], + }, + rooms: { + join: {}, + }, + }; + syncResponse.rooms.join[ROOM_ID] = { + timeline: { + events: [messageEncrypted], + }, + }; + + aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse); + return aliceTestClient.httpBackend.flush("/sync", 1); + }).then(function() { + const room = aliceTestClient.client.getRoom(ROOM_ID); + const event = room.getLiveTimeline().getEvents()[0]; + expect(event.getContent().body).toEqual('42'); + + return aliceTestClient.client.exportRoomKeys(); + }).then(function(exported) { + // start a new client + aliceTestClient.stop(); + + aliceTestClient = new TestClient( + "@alice:localhost", "device2", "access_token2" + ); + + aliceTestClient.client.importRoomKeys(exported); + + return aliceTestClient.start(); + }).then(function() { + const syncResponse = { + next_batch: 1, + rooms: { + join: {}, + }, + }; + syncResponse.rooms.join[ROOM_ID] = { + timeline: { + events: [messageEncrypted], + }, + }; + + aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse); + return aliceTestClient.httpBackend.flush("/sync", 1); + }).then(function() { + const room = aliceTestClient.client.getRoom(ROOM_ID); + const event = room.getLiveTimeline().getEvents()[0]; + expect(event.getContent().body).toEqual('42'); + }).nodeify(done); + }); }); diff --git a/src/client.js b/src/client.js index 06b4b4fa3..13a1e1cc7 100644 --- a/src/client.js +++ b/src/client.js @@ -476,6 +476,18 @@ MatrixClient.prototype.exportRoomKeys = function() { return this._crypto.exportRoomKeys(); }; +/** + * Import a list of room keys previously exported by exportRoomKeys + * + * @param {Object[]} keys a list of session export objects + */ +MatrixClient.prototype.importRoomKeys = function(keys) { + if (!this._crypto) { + throw new Error("End-to-end encryption disabled"); + } + this._crypto.importRoomKeys(keys); +}; + /** * Decrypt a received event according to the algorithm specified in the event. * diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 9dbc2683c..72199c940 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -47,6 +47,18 @@ function checkPayloadLength(payloadString) { } +/** + * The type of object we use for importing and exporting megolm session data. + * + * @typedef {Object} module:crypto/OlmDevice.MegolmSessionData + * @property {String} sender_key Sender's Curve25519 device key + * @property {Object} sender_claimed_keys Other keys the sender claims. + * @property {String} room_id Room this session is used in + * @property {String} session_id Unique id for the session + * @property {String} session_key Base64'ed key data + */ + + /** * Manages the olm cryptography functions. Each OlmDevice has a single * OlmAccount and a number of OlmSessions. @@ -683,6 +695,48 @@ OlmDevice.prototype.addInboundGroupSession = function( } }; + +/** + * Add a previously-exported inbound group session to the session store + * + * @param {module:crypto/OlmDevice.MegolmSessionData} data session data + */ +OlmDevice.prototype.importInboundGroupSession = function(data) { + /* if we already have this session, consider updating it */ + function updateSession(session) { + console.log("Update for megolm session " + data.sender_key + "|" + + data.session_id); + // for now we just ignore updates. TODO: implement something here + + return true; + } + + const r = this._getInboundGroupSession( + data.room_id, data.sender_key, data.session_id, updateSession + ); + + if (r !== null) { + return; + } + + // new session. + const session = new Olm.InboundGroupSession(); + try { + session.import_session(data.session_key); + if (data.session_id != session.session_id()) { + throw new Error( + "Mismatched group session ID from senderKey: " + data.sender_key + ); + } + this._saveInboundGroupSession( + data.room_id, data.sender_key, data.session_id, session, + data.sender_claimed_keys + ); + } finally { + session.free(); + } +}; + /** * Decrypt a received message with an inbound group session * @@ -744,7 +798,7 @@ OlmDevice.prototype.decryptGroupMessage = function( * * @param {string} senderKey base64-encoded curve25519 key of the sender * @param {string} sessionId session identifier - * @return {object} exported session data + * @return {module:crypto/OlmDevice.MegolmSessionData} exported session data */ OlmDevice.prototype.exportInboundGroupSession = function(senderKey, sessionId) { const s = this._sessionStore.getEndToEndInboundGroupSession( diff --git a/src/crypto/algorithms/base.js b/src/crypto/algorithms/base.js index df24b2aa2..7973231a7 100644 --- a/src/crypto/algorithms/base.js +++ b/src/crypto/algorithms/base.js @@ -136,6 +136,15 @@ DecryptionAlgorithm.prototype.onRoomKeyEvent = function(params) { // ignore by default }; +/** + * Import a room key + * + * @param {module:crypto/OlmDevice.MegolmSessionData} session + */ +DecryptionAlgorithm.prototype.importRoomKey = function(session) { + // ignore by default +}; + /** * Exception thrown when decryption fails * diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index 7962bdeb1..5462e1c29 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -564,19 +564,45 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { content.session_key, event.getKeysClaimed() ); - let k = event.getSenderKey() + "|" + content.session_id; - let pending = this._pendingEvents[k]; - if (pending) { - // have another go at decrypting events sent with this session. - delete this._pendingEvents[k]; + // have another go at decrypting events sent with this session. + this._retryDecryption(event.getSenderKey, content.session_id); +}; - for (let i = 0; i < pending.length; i++) { - try { - this.decryptEvent(pending[i]); - console.log("successful re-decryption of", pending[i]); - } catch (e) { - console.log("Still can't decrypt", pending[i], e.stack || e); - } + +/** + * @inheritdoc + * + * @param {module:crypto/OlmDevice.MegolmSessionData} session + */ +MegolmDecryption.prototype.importRoomKey = function(session) { + this._olmDevice.importInboundGroupSession(session); + + // have another go at decrypting events sent with this session. + this._retryDecryption(session.sender_key, session.session_id); +}; + +/** + * Have another go at decrypting events after we receive a key + * + * @private + * @param {String} senderKey + * @param {String} sessionId + */ +MegolmDecryption.prototype._retryDecryption = function(senderKey, sessionId) { + const k = senderKey + "|" + sessionId; + const pending = this._pendingEvents[k]; + if (!pending) { + return; + } + + delete this._pendingEvents[k]; + + for (let i = 0; i < pending.length; i++) { + try { + this.decryptEvent(pending[i]); + console.log("successful re-decryption of", pending[i]); + } catch (e) { + console.log("Still can't decrypt", pending[i], e.stack || e); } } }; diff --git a/src/crypto/index.js b/src/crypto/index.js index 871d1bf95..d22cf8868 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -919,6 +919,23 @@ Crypto.prototype.exportRoomKeys = function() { ); }; +/** + * Import a list of room keys previously exported by exportRoomKeys + * + * @param {Object[]} keys a list of session export objects + */ +Crypto.prototype.importRoomKeys = function(keys) { + keys.map((session) => { + if (!session.room_id || !session.algorithm) { + console.warn("ignoring session entry with missing fields", session); + return; + } + + const alg = this._getRoomDecryptor(session.room_id, session.algorithm); + alg.importRoomKey(session); + }); +}; + /** * Encrypt an event according to the configuration of the room, if necessary. *