1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-29 16:43:09 +03:00

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).
This commit is contained in:
Richard van der Hoff
2016-10-20 15:42:06 +01:00
parent 7a7f345f28
commit 19c257703c
3 changed files with 102 additions and 55 deletions

View File

@@ -51,6 +51,7 @@ module.exports.DECRYPTION_CLASSES = {};
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper * @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface * @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 {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) { var EncryptionAlgorithm = function(params) {
this._userId = params.userId; this._userId = params.userId;

View File

@@ -27,6 +27,52 @@ var utils = require("../../utils");
var olmlib = require("../olmlib"); var olmlib = require("../olmlib");
var base = require("./base"); 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 * Megolm encryption implementation
* *
@@ -38,15 +84,28 @@ var base = require("./base");
*/ */
function MegolmEncryption(params) { function MegolmEncryption(params) {
base.EncryptionAlgorithm.call(this, params); base.EncryptionAlgorithm.call(this, params);
this._prepPromise = null;
this._outboundSessionId = null; // OutboundSessionInfo. Null if we haven't yet started setting one up. Note
this._discardNewSession = false; // 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. // devices which have joined since we last sent a message.
// userId -> {deviceId -> true}, or // userId -> {deviceId -> true}, or
// userId -> true // userId -> true
this._devicesPendingKeyShare = {}; 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); utils.inherits(MegolmEncryption, base.EncryptionAlgorithm);
@@ -55,34 +114,27 @@ utils.inherits(MegolmEncryption, base.EncryptionAlgorithm);
* *
* @param {module:models/room} room * @param {module:models/room} room
* *
* @return {module:client.Promise} Promise which resolves to the megolm * @return {module:client.Promise} Promise which resolves to the
* sessionId when setup is complete. * OutboundSessionInfo when setup is complete.
*/ */
MegolmEncryption.prototype._ensureOutboundSession = function(room) { MegolmEncryption.prototype._ensureOutboundSession = function(room) {
var self = this; var self = this;
if (this._prepPromise) { var session = this._outboundSession;
// prep already in progress
return this._prepPromise;
}
var sessionId = this._outboundSessionId;
// need to make a brand new session? // need to make a brand new session?
if (!sessionId) { if (!session || session.needsRotation(self._sessionRotationPeriodMsgs,
this._prepPromise = this._prepareNewSession(room). self._sessionRotationPeriodMs)
finally(function() { ) {
self._prepPromise = null; this._outboundSession = session = this._prepareNewSession(room);
});
return this._prepPromise;
} }
if (this._sharePromise) { if (session.sharePromise) {
// key share already in progress // 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; var shareMap = this._devicesPendingKeyShare;
this._devicesPendingKeyShare = {}; this._devicesPendingKeyShare = {};
@@ -99,15 +151,15 @@ MegolmEncryption.prototype._ensureOutboundSession = function(room) {
} }
} }
this._sharePromise = this._shareKeyWithDevices( session.sharePromise = this._shareKeyWithDevices(
sessionId, shareMap session.sessionId, shareMap
).finally(function() { ).finally(function() {
self._sharePromise = null; session.sharePromise = null;
}).then(function() { }).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 * @param {module:models/room} room
* *
* @return {module:client.Promise} Promise which resolves to the megolm * @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session
* sessionId when setup is complete.
*/ */
MegolmEncryption.prototype._prepareNewSession = function(room) { MegolmEncryption.prototype._prepareNewSession = function(room) {
var session_id = this._olmDevice.createOutboundGroupSession(); var session_id = this._olmDevice.createOutboundGroupSession();
@@ -131,6 +182,8 @@ MegolmEncryption.prototype._prepareNewSession = function(room) {
// so we can reset this. // so we can reset this.
this._devicesPendingKeyShare = {}; this._devicesPendingKeyShare = {};
var session = new OutboundSessionInfo(session_id);
var roomMembers = utils.map(room.getJoinedMembers(), function(u) { var roomMembers = utils.map(room.getJoinedMembers(), function(u) {
return u.userId; 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 // 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. // 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 roomMembers, false
).then(function(res) { ).then(function(res) {
return self._shareKeyWithDevices(session_id, shareMap); return self._shareKeyWithDevices(session_id, shareMap);
}).then(function() { }).then(function() {
if (self._discardNewSession) { return session;
// 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;
}).finally(function() { }).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) { MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
var self = this; var self = this;
return this._ensureOutboundSession(room).then(function(session_id) { return this._ensureOutboundSession(room).then(function(session) {
var payloadJson = { var payloadJson = {
room_id: self._roomId, room_id: self._roomId,
type: eventType, type: eventType,
@@ -297,19 +343,20 @@ MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
}; };
var ciphertext = self._olmDevice.encryptGroupMessage( var ciphertext = self._olmDevice.encryptGroupMessage(
session_id, JSON.stringify(payloadJson) session.sessionId, JSON.stringify(payloadJson)
); );
var encryptedContent = { var encryptedContent = {
algorithm: olmlib.MEGOLM_ALGORITHM, algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key, sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: ciphertext, ciphertext: ciphertext,
session_id: session_id, session_id: session.sessionId,
// Include our device ID so that recipients can send us a // Include our device ID so that recipients can send us a
// m.new_device message if they don't have our session key. // m.new_device message if they don't have our session key.
device_id: self._deviceId, device_id: self._deviceId,
}; };
session.useCount++;
return encryptedContent; return encryptedContent;
}); });
}; };
@@ -322,6 +369,11 @@ MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
* @param {string=} oldMembership previous membership * @param {string=} oldMembership previous membership
*/ */
MegolmEncryption.prototype.onRoomMembership = function(event, member, oldMembership) { 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; var newMembership = member.membership;
if (newMembership === 'join') { 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. // 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 " +
console.log("Discarding outbound megolm session due to change in " + "membership of " + member.userId + " (" + oldMembership +
"membership of " + member.userId + " (" + oldMembership + "->" + newMembership + ")");
"->" + newMembership + ")");
this._outboundSessionId = null;
}
if (this._prepPromise) { // this ensures that we will start a new session on the next message.
console.log("Discarding as-yet-incomplete megolm session due to " + this._outboundSession = null;
"change in membership of " + member.userId + " (" +
oldMembership + "->" + newMembership + ")");
this._discardNewSession = true;
}
}; };
/** /**

View File

@@ -716,6 +716,7 @@ Crypto.prototype.setRoomEncryption = function(roomId, config) {
olmDevice: this._olmDevice, olmDevice: this._olmDevice,
baseApis: this._baseApis, baseApis: this._baseApis,
roomId: roomId, roomId: roomId,
config: config,
}); });
this._roomAlgorithms[roomId] = alg; this._roomAlgorithms[roomId] = alg;
}; };