From 19c257703c43d4be59516ac72f30a7afd96f3618 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 20 Oct 2016 15:42:06 +0100 Subject: [PATCH] Rotate megolm sessions In order to mitigate backward-secrecy concerns, make sure that we rotate the outbound megolm session at regular intervals (every week/100 msgs by default). --- lib/crypto/algorithms/base.js | 1 + lib/crypto/algorithms/megolm.js | 155 ++++++++++++++++++++------------ lib/crypto/index.js | 1 + 3 files changed, 102 insertions(+), 55 deletions(-) diff --git a/lib/crypto/algorithms/base.js b/lib/crypto/algorithms/base.js index 2e1c8e72d..6d25bc2bd 100644 --- a/lib/crypto/algorithms/base.js +++ b/lib/crypto/algorithms/base.js @@ -51,6 +51,7 @@ module.exports.DECRYPTION_CLASSES = {}; * @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper * @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface * @param {string} params.roomId The ID of the room we will be sending to + * @param {object} params.config The body of the m.room.encryption event */ var EncryptionAlgorithm = function(params) { this._userId = params.userId; diff --git a/lib/crypto/algorithms/megolm.js b/lib/crypto/algorithms/megolm.js index 0452b0dc5..076158012 100644 --- a/lib/crypto/algorithms/megolm.js +++ b/lib/crypto/algorithms/megolm.js @@ -27,6 +27,52 @@ var utils = require("../../utils"); var olmlib = require("../olmlib"); var base = require("./base"); +/** + * @private + * @constructor + * + * @param {string} sessionId + * + * @property {string} sessionId + * @property {Number} useCount number of times this session has been used + * @property {Number} creationTime when the session was created (ms since the epoch) + * @property {module:client.Promise?} sharePromise If a share operation is in progress, + * a promise which resolves when it is complete. + */ +function OutboundSessionInfo(sessionId) { + this.sessionId = sessionId; + this.useCount = 0; + this.creationTime = new Date().getTime(); + this.sharePromise = null; +} + + +/** + * Check if it's time to rotate the session + * + * @param {Number} rotationPeriodMsgs + * @param {Number} rotationPeriodMs + * @return {Boolean} + */ +OutboundSessionInfo.prototype.needsRotation = function( + rotationPeriodMsgs, rotationPeriodMs +) { + var sessionLifetime = new Date().getTime() - this.creationTime; + + if (this.useCount >= rotationPeriodMsgs || + sessionLifetime >= rotationPeriodMs + ) { + console.log( + "Rotating megolm session after " + this.useCount + + " messages, " + sessionLifetime + "ms" + ); + return true; + } + + return false; +}; + + /** * Megolm encryption implementation * @@ -38,15 +84,28 @@ var base = require("./base"); */ function MegolmEncryption(params) { base.EncryptionAlgorithm.call(this, params); - this._prepPromise = null; - this._outboundSessionId = null; - this._discardNewSession = false; + + // OutboundSessionInfo. Null if we haven't yet started setting one up. Note + // that even if this is non-null, it may not be ready for use (in which + // case _outboundSession.sharePromise will be non-null.) + this._outboundSession = null; // devices which have joined since we last sent a message. // userId -> {deviceId -> true}, or // userId -> true this._devicesPendingKeyShare = {}; - this._sharePromise = null; + + // default rotation periods + this._sessionRotationPeriodMsgs = 100; + this._sessionRotationPeriodMs = 7 * 24 * 3600 * 1000; + + if (params.config.rotation_period_ms !== undefined) { + this._sessionRotationPeriodMs = params.config.rotation_period_ms; + } + + if (params.config.rotation_period_msgs !== undefined) { + this._sessionRotationPeriodMsgs = params.config.rotation_period_msgs; + } } utils.inherits(MegolmEncryption, base.EncryptionAlgorithm); @@ -55,34 +114,27 @@ utils.inherits(MegolmEncryption, base.EncryptionAlgorithm); * * @param {module:models/room} room * - * @return {module:client.Promise} Promise which resolves to the megolm - * sessionId when setup is complete. + * @return {module:client.Promise} Promise which resolves to the + * OutboundSessionInfo when setup is complete. */ MegolmEncryption.prototype._ensureOutboundSession = function(room) { var self = this; - if (this._prepPromise) { - // prep already in progress - return this._prepPromise; - } - - var sessionId = this._outboundSessionId; + var session = this._outboundSession; // need to make a brand new session? - if (!sessionId) { - this._prepPromise = this._prepareNewSession(room). - finally(function() { - self._prepPromise = null; - }); - return this._prepPromise; + if (!session || session.needsRotation(self._sessionRotationPeriodMsgs, + self._sessionRotationPeriodMs) + ) { + this._outboundSession = session = this._prepareNewSession(room); } - if (this._sharePromise) { + if (session.sharePromise) { // key share already in progress - return this._sharePromise; + return session.sharePromise; } - // prep already done, but check for new devices + // no share in progress: check for new devices var shareMap = this._devicesPendingKeyShare; this._devicesPendingKeyShare = {}; @@ -99,15 +151,15 @@ MegolmEncryption.prototype._ensureOutboundSession = function(room) { } } - this._sharePromise = this._shareKeyWithDevices( - sessionId, shareMap + session.sharePromise = this._shareKeyWithDevices( + session.sessionId, shareMap ).finally(function() { - self._sharePromise = null; + session.sharePromise = null; }).then(function() { - return sessionId; + return session; }); - return this._sharePromise; + return session.sharePromise; }; /** @@ -115,8 +167,7 @@ MegolmEncryption.prototype._ensureOutboundSession = function(room) { * * @param {module:models/room} room * - * @return {module:client.Promise} Promise which resolves to the megolm - * sessionId when setup is complete. + * @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session */ MegolmEncryption.prototype._prepareNewSession = function(room) { var session_id = this._olmDevice.createOutboundGroupSession(); @@ -131,6 +182,8 @@ MegolmEncryption.prototype._prepareNewSession = function(room) { // so we can reset this. this._devicesPendingKeyShare = {}; + var session = new OutboundSessionInfo(session_id); + var roomMembers = utils.map(room.getJoinedMembers(), function(u) { return u.userId; }); @@ -145,24 +198,17 @@ MegolmEncryption.prototype._prepareNewSession = function(room) { // TODO: we need to give the user a chance to block any devices or users // before we send them the keys; it's too late to download them here. - return this._crypto.downloadKeys( + session.sharePromise = this._crypto.downloadKeys( roomMembers, false ).then(function(res) { return self._shareKeyWithDevices(session_id, shareMap); }).then(function() { - if (self._discardNewSession) { - // we've had cause to reset the session_id since starting this process. - // we'll use the current session for any currently pending events, but - // don't save it as the current _outboundSessionId, so that new events - // will use a new session. - console.log("Session generation complete, but discarding"); - } else { - self._outboundSessionId = session_id; - } - return session_id; + return session; }).finally(function() { - self._discardNewSession = false; + session.sharePromise = null; }); + + return session; }; /** @@ -289,7 +335,7 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap) */ MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) { var self = this; - return this._ensureOutboundSession(room).then(function(session_id) { + return this._ensureOutboundSession(room).then(function(session) { var payloadJson = { room_id: self._roomId, type: eventType, @@ -297,19 +343,20 @@ MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) { }; var ciphertext = self._olmDevice.encryptGroupMessage( - session_id, JSON.stringify(payloadJson) + session.sessionId, JSON.stringify(payloadJson) ); var encryptedContent = { algorithm: olmlib.MEGOLM_ALGORITHM, sender_key: self._olmDevice.deviceCurve25519Key, ciphertext: ciphertext, - session_id: session_id, + session_id: session.sessionId, // Include our device ID so that recipients can send us a // m.new_device message if they don't have our session key. device_id: self._deviceId, }; + session.useCount++; return encryptedContent; }); }; @@ -322,6 +369,11 @@ MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) { * @param {string=} oldMembership previous membership */ MegolmEncryption.prototype.onRoomMembership = function(event, member, oldMembership) { + // if we haven't yet made a session, there's nothing to do here. + if (!this._outboundSession) { + return; + } + var newMembership = member.membership; if (newMembership === 'join') { @@ -335,19 +387,12 @@ MegolmEncryption.prototype.onRoomMembership = function(event, member, oldMembers } // otherwise we assume the user is leaving, and start a new outbound session. - if (this._outboundSessionId) { - console.log("Discarding outbound megolm session due to change in " + - "membership of " + member.userId + " (" + oldMembership + - "->" + newMembership + ")"); - this._outboundSessionId = null; - } + console.log("Discarding outbound megolm session due to change in " + + "membership of " + member.userId + " (" + oldMembership + + "->" + newMembership + ")"); - if (this._prepPromise) { - console.log("Discarding as-yet-incomplete megolm session due to " + - "change in membership of " + member.userId + " (" + - oldMembership + "->" + newMembership + ")"); - this._discardNewSession = true; - } + // this ensures that we will start a new session on the next message. + this._outboundSession = null; }; /** diff --git a/lib/crypto/index.js b/lib/crypto/index.js index cb454705d..b4e599035 100644 --- a/lib/crypto/index.js +++ b/lib/crypto/index.js @@ -716,6 +716,7 @@ Crypto.prototype.setRoomEncryption = function(roomId, config) { olmDevice: this._olmDevice, baseApis: this._baseApis, roomId: roomId, + config: config, }); this._roomAlgorithms[roomId] = alg; };