Skip to content

Commit

Permalink
Rotate megolm sessions
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
richvdh committed Oct 20, 2016
1 parent 7a7f345 commit 19c2577
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 55 deletions.
1 change: 1 addition & 0 deletions lib/crypto/algorithms/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
155 changes: 100 additions & 55 deletions lib/crypto/algorithms/megolm.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand All @@ -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);

Expand All @@ -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 = {};

Expand All @@ -99,24 +151,23 @@ 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;
};

/**
* @private
*
* @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();
Expand All @@ -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;
});
Expand All @@ -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;
};

/**
Expand Down Expand Up @@ -289,27 +335,28 @@ 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,
content: 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;
});
};
Expand All @@ -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') {
Expand All @@ -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;
};

/**
Expand Down
1 change: 1 addition & 0 deletions lib/crypto/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,7 @@ Crypto.prototype.setRoomEncryption = function(roomId, config) {
olmDevice: this._olmDevice,
baseApis: this._baseApis,
roomId: roomId,
config: config,
});
this._roomAlgorithms[roomId] = alg;
};
Expand Down

0 comments on commit 19c2577

Please sign in to comment.