You've already forked matrix-js-sdk
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:
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user