diff --git a/spec/unit/crypto/algorithms/megolm.spec.js b/spec/unit/crypto/algorithms/megolm.spec.js index d712cc7ba..1383cecbd 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.js +++ b/spec/unit/crypto/algorithms/megolm.spec.js @@ -5,6 +5,7 @@ try { } import expect from 'expect'; +import q from 'q'; import sdk from '../../../..'; import algorithms from '../../../../lib/crypto/algorithms'; @@ -105,5 +106,55 @@ describe("MegolmDecryption", function() { megolmDecryption.decryptEvent(event); expect(event.getContent()).toEqual('testytest'); }); + + it('can respond to a key request event', function() { + const keyRequest = { + userId: '@alice:foo', + deviceId: 'alidevice', + requestBody: { + room_id: ROOM_ID, + sender_key: "SENDER_CURVE25519", + session_id: groupSession.session_id(), + }, + }; + + expect(megolmDecryption.hasKeysForKeyRequest(keyRequest)) + .toBe(true); + + // set up some pre-conditions for the share call + const deviceInfo = {}; + mockCrypto.getStoredDevice.andReturn(deviceInfo); + mockOlmLib.ensureOlmSessionsForDevices.andReturn( + q({'@alice:foo': {'alidevice': { + sessionId: 'alisession', + }}}), + ); + mockBaseApis.sendToDevice = expect.createSpy(); + + + // do the share + megolmDecryption.shareKeysWithDevice(keyRequest); + + // it's asynchronous, so we have to wait a bit + return q.delay(1).then(() => { + // check that it called encryptMessageForDevice with + // appropriate args. + expect(mockOlmLib.encryptMessageForDevice.calls.length) + .toEqual(1); + + const call = mockOlmLib.encryptMessageForDevice.calls[0]; + const payload = call.arguments[6]; + + expect(payload.type).toEqual("m.forwarded_room_key"); + expect(payload.content).toInclude({ + sender_key: "SENDER_CURVE25519", + sender_claimed_ed25519_key: "SENDER_ED25519", + session_id: groupSession.session_id(), + chain_index: 0, + forwarding_curve25519_key_chain: [], + }); + expect(payload.content.session_key).toExist(); + }); + }); }); }); diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index eb6d28610..0f4615958 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -55,6 +55,8 @@ function checkPayloadLength(payloadString) { * * @typedef {Object} module:crypto/OlmDevice.MegolmSessionData * @property {String} sender_key Sender's Curve25519 device key + * @property {String[]} forwarding_curve25519_key_chain Devices which forwarded + * this session to us (normally empty). * @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 @@ -587,6 +589,8 @@ OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) { * @property {string} room_Id * @property {string} session pickled Olm.InboundGroupSession * @property {Object} keysClaimed + * @property {[string]} forwardingCurve25519KeyChain Devices involved in forwarding + * this session to us (normally empty). */ /** @@ -657,12 +661,18 @@ OlmDevice.prototype._getInboundGroupSession = function( * * @param {string} roomId room in which this session will be used * @param {string} senderKey base64-encoded curve25519 key of the sender + * @param {string[]} forwardingCurve25519KeyChain Devices involved in forwarding + * this session to us. * @param {string} sessionId session identifier * @param {string} sessionKey base64-encoded secret key * @param {Object} keysClaimed Other keys the sender claims. + * @param {boolean} exportFormat true if the megolm keys are in export format + * (ie, they lack an ed25519 signature) */ OlmDevice.prototype.addInboundGroupSession = function( - roomId, senderKey, sessionId, sessionKey, keysClaimed, + roomId, senderKey, forwardingCurve25519KeyChain, + sessionId, sessionKey, keysClaimed, + exportFormat, ) { const self = this; @@ -685,7 +695,11 @@ OlmDevice.prototype.addInboundGroupSession = function( // new session. const session = new Olm.InboundGroupSession(); try { - session.create(sessionKey); + 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, @@ -696,6 +710,7 @@ OlmDevice.prototype.addInboundGroupSession = function( room_id: roomId, session: session.pickle(this._pickleKey), keysClaimed: keysClaimed, + forwardingCurve25519KeyChain: forwardingCurve25519KeyChain, }; self._saveInboundGroupSession( @@ -765,8 +780,9 @@ OlmDevice.prototype.importInboundGroupSession = function(data) { * * @return {null} the sessionId is unknown * - * @return {{result: string, senderKey: string, keysClaimed: - * Object}} result + * @return {{result: string, senderKey: string, + * forwardingCurve25519KeyChain: [string], + * keysClaimed: Object}} */ OlmDevice.prototype.decryptGroupMessage = function( roomId, senderKey, sessionId, body, @@ -800,6 +816,7 @@ OlmDevice.prototype.decryptGroupMessage = function( result: plaintext, keysClaimed: sessionData.keysClaimed || {}, senderKey: senderKey, + forwardingCurve25519KeyChain: sessionData.forwardingCurve25519KeyChain || [], }; } @@ -846,16 +863,26 @@ OlmDevice.prototype.hasInboundSessionKeys = function(roomId, senderKey, sessionI * @param {string} senderKey base64-encoded curve25519 key of the sender * @param {string} sessionId session identifier * - * @returns {{chain_index: number, key: string}} details of the session key. The - * key is a base64-encoded megolm key in export format. + * @returns {{chain_index: number, key: string, + * forwarding_curve25519_key_chain: [string], + * sender_claimed_ed25519_key: string, + * }} + * details of the session key. The key is a base64-encoded megolm key in + * export format. */ OlmDevice.prototype.getInboundGroupSessionKey = function(roomId, senderKey, sessionId) { function getKey(session, sessionData) { const messageIndex = session.first_known_index(); + const claimedKeys = sessionData.keysClaimed || {}; + const senderEd25519Key = claimedKeys.ed25519 || null; + return { "chain_index": messageIndex, "key": session.export_session(messageIndex), + "forwarding_curve25519_key_chain": + sessionData.forwardingCurve25519KeyChain || [], + "sender_claimed_ed25519_key": senderEd25519Key, }; } @@ -894,6 +921,8 @@ OlmDevice.prototype.exportInboundGroupSession = function(senderKey, sessionId) { "room_id": r.room_id, "session_id": sessionId, "session_key": session.export_session(messageIndex), + "forwarding_curve25519_key_chain": + session.forwardingCurve25519KeyChain || [], }; } finally { session.free(); diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index eedccfe02..61c44e5e0 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -250,7 +250,7 @@ MegolmEncryption.prototype._prepareNewSession = function() { const key = this._olmDevice.getOutboundGroupSessionKey(sessionId); this._olmDevice.addInboundGroupSession( - this._roomId, this._olmDevice.deviceCurve25519Key, sessionId, + this._roomId, this._olmDevice.deviceCurve25519Key, [], sessionId, key.key, {ed25519: this._olmDevice.deviceEd25519Key}, ); @@ -595,7 +595,8 @@ MegolmDecryption.prototype._decryptEvent = function(event, requestKeysOnFail) { ); } - event.setClearData(payload, res.senderKey, res.keysClaimed.ed25519); + event.setClearData(payload, res.senderKey, res.keysClaimed.ed25519, + res.forwardingCurve25519KeyChain); }; MegolmDecryption.prototype._requestKeysForEvent = function(event) { @@ -645,8 +646,11 @@ MegolmDecryption.prototype._addEventToPendingList = function(event) { */ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { const content = event.getContent(); - const senderKey = event.getSenderKey(); const sessionId = content.session_id; + let senderKey = event.getSenderKey(); + let forwardingKeyChain = []; + let exportFormat = false; + let keysClaimed; if (!content.room_id || !sessionId || @@ -655,15 +659,49 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { console.error("key event is missing fields"); return; } + if (!senderKey) { console.error("key event has no sender key (not encrypted?)"); return; } + if (event.getType() == "m.forwarded_room_key") { + exportFormat = true; + forwardingKeyChain = content.forwarding_curve25519_key_chain; + if (!utils.isArray(forwardingKeyChain)) { + forwardingKeyChain = []; + } + + // copy content before we modify it + forwardingKeyChain = forwardingKeyChain.slice(); + forwardingKeyChain.push(senderKey); + + senderKey = content.sender_key; + if (!senderKey) { + console.error("forwarded_room_key event is missing sender_key field"); + return; + } + + const ed25519Key = content.sender_claimed_ed25519_key; + if (!ed25519Key) { + console.error( + `forwarded_room_key_event is missing sender_claimed_ed25519_key field`, + ); + return; + } + + keysClaimed = { + ed25519: ed25519Key, + }; + } else { + keysClaimed = event.getKeysClaimed(); + } + console.log(`Adding key for megolm session ${senderKey}|${sessionId}`); this._olmDevice.addInboundGroupSession( - content.room_id, senderKey, sessionId, - content.session_key, event.getKeysClaimed(), + content.room_id, senderKey, forwardingKeyChain, sessionId, + content.session_key, keysClaimed, + exportFormat, ); // cancel any outstanding room key requests for this session @@ -722,22 +760,10 @@ MegolmDecryption.prototype.shareKeysWithDevice = function(keyRequest) { + userId + ":" + deviceId, ); - const key = this._olmDevice.getInboundGroupSessionKey( + const payload = this._buildKeyForwardingMessage( body.room_id, body.sender_key, body.session_id, ); - const payload = { - type: "m.forwarded_room_key", - content: { - algorithm: olmlib.MEGOLM_ALGORITHM, - room_id: body.room_id, - sender_key: body.sender_key, - session_id: body.session_id, - session_key: key.key, - chain_index: key.chain_index, - }, - }; - const encryptedContent = { algorithm: olmlib.OLM_ALGORITHM, sender_key: this._olmDevice.deviceCurve25519Key, @@ -765,6 +791,27 @@ MegolmDecryption.prototype.shareKeysWithDevice = function(keyRequest) { }).done(); }; +MegolmDecryption.prototype._buildKeyForwardingMessage = function( + roomId, senderKey, sessionId, +) { + const key = this._olmDevice.getInboundGroupSessionKey( + roomId, senderKey, sessionId, + ); + + return { + type: "m.forwarded_room_key", + content: { + algorithm: olmlib.MEGOLM_ALGORITHM, + room_id: roomId, + sender_key: senderKey, + sender_claimed_ed25519_key: key.sender_claimed_ed25519_key, + session_id: sessionId, + session_key: key.key, + chain_index: key.chain_index, + forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain, + }, + }; +}; /** * @inheritdoc diff --git a/src/crypto/index.js b/src/crypto/index.js index be46655a1..89d8e56a4 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -161,7 +161,8 @@ function _registerEventHandlers(crypto, eventEmitter) { eventEmitter.on("toDeviceEvent", function(event) { try { - if (event.getType() == "m.room_key") { + if (event.getType() == "m.room_key" + || event.getType() == "m.forwarded_room_key") { crypto._onRoomKeyEvent(event); } else if (event.getType() == "m.new_device") { crypto._onNewDeviceEvent(event);