1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-12-19 10:22:30 +03:00

Check devices to share keys with on each send

Instead of trying to maintain a list of devices we need to share with, just
check all the devices for all the users on each send.

This should fix https://github.com/vector-im/vector-web/issues/2568, and
generally mean we're less likely to get out of sync.
This commit is contained in:
Richard van der Hoff
2016-11-15 21:50:25 +00:00
parent 766e837775
commit 769a0cb76f
2 changed files with 171 additions and 127 deletions

View File

@@ -38,12 +38,17 @@ var base = require("./base");
* @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.
*
* @property {object} sharedWithDevices
* devices with which we have shared the session key
* userId -> {deviceId -> msgindex}
*/
function OutboundSessionInfo(sessionId) {
this.sessionId = sessionId;
this.useCount = 0;
this.creationTime = new Date().getTime();
this.sharePromise = null;
this.sharedWithDevices = {};
}
@@ -90,11 +95,6 @@ function MegolmEncryption(params) {
// 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 = {};
// default rotation periods
this._sessionRotationPeriodMsgs = 100;
this._sessionRotationPeriodMs = 7 * 24 * 3600 * 1000;
@@ -134,32 +134,55 @@ MegolmEncryption.prototype._ensureOutboundSession = function(room) {
return session.sharePromise;
}
// no share in progress: check for new devices
var shareMap = this._devicesPendingKeyShare;
this._devicesPendingKeyShare = {};
// no share in progress: check if we need to share with any devices
var prom = this._getDevicesInRoom(room).then(function(devicesInRoom) {
var shareMap = {};
// check each user is (still) a member of the room
for (var userId in shareMap) {
if (!shareMap.hasOwnProperty(userId)) {
continue;
for (var userId in devicesInRoom) {
if (!devicesInRoom.hasOwnProperty(userId)) {
continue;
}
var userDevices = devicesInRoom[userId];
for (var deviceId in userDevices) {
if (!userDevices.hasOwnProperty(deviceId)) {
continue;
}
var deviceInfo = userDevices[deviceId];
if (deviceInfo.isBlocked()) {
continue;
}
var key = deviceInfo.getIdentityKey();
if (key == self._olmDevice.deviceCurve25519Key) {
// don't bother sending to ourself
continue;
}
if (
!session.sharedWithDevices[userId] ||
session.sharedWithDevices[userId][deviceId] === undefined
) {
shareMap[userId] = shareMap[userId] || [];
shareMap[userId].push(deviceInfo);
}
}
}
// XXX what about rooms where invitees can see the content?
var member = room.getMember(userId);
if (member.membership !== "join") {
delete shareMap[userId];
}
}
session.sharePromise = this._shareKeyWithDevices(
session.sessionId, shareMap
).finally(function() {
return self._shareKeyWithDevices(
session, shareMap
);
}).finally(function() {
session.sharePromise = null;
}).then(function() {
return session;
});
return session.sharePromise;
session.sharePromise = prom;
return prom;
};
/**
@@ -178,95 +201,53 @@ MegolmEncryption.prototype._prepareNewSession = function(room) {
key.key, {ed25519: this._olmDevice.deviceEd25519Key}
);
// we're going to share the key with all current members of the 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;
});
var shareMap = {};
for (var i = 0; i < roomMembers.length; i++) {
var userId = roomMembers[i];
shareMap[userId] = true;
}
var self = this;
// 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.
session.sharePromise = this._crypto.downloadKeys(
roomMembers, false
).then(function(res) {
return self._shareKeyWithDevices(session_id, shareMap);
}).then(function() {
return session;
}).finally(function() {
session.sharePromise = null;
});
return session;
return new OutboundSessionInfo(session_id);
};
/**
* @private
*
* @param {string} session_id
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {Object<string, Object<string, boolean>|boolean>} shareMap
* Map from userid to either: true (meaning this is a new user in the room,
* so all of his devices need the keys); or a map from deviceid to true
* (meaning this user has one or more new devices, which need the keys).
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
*
* @return {module:client.Promise} Promise which resolves once the key sharing
* message has been sent.
*/
MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap) {
MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUser) {
var self = this;
var key = this._olmDevice.getOutboundGroupSessionKey(session_id);
var key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId);
var payload = {
type: "m.room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: this._roomId,
session_id: session_id,
session_id: session.sessionId,
session_key: key.key,
chain_index: key.chain_index,
}
};
// we downloaded the user's device list when they joined the room, or when
// the new device announced itself, so there is no need to do so now.
var contentMap = {};
return self._crypto.ensureOlmSessionsForUsers(
utils.keys(shareMap)
return olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, devicesByUser
).then(function(devicemap) {
var contentMap = {};
var haveTargets = false;
for (var userId in devicemap) {
if (!devicemap.hasOwnProperty(userId)) {
for (var userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
var devicesToShareWith = shareMap[userId];
var devicesToShareWith = devicesByUser[userId];
var sessionResults = devicemap[userId];
for (var deviceId in sessionResults) {
if (!sessionResults.hasOwnProperty(deviceId)) {
continue;
}
if (devicesToShareWith === true) {
// all devices
} else if (!devicesToShareWith[deviceId]) {
// not a new device
continue;
}
for (var i = 0; i < devicesToShareWith.length; i++) {
var deviceInfo = devicesToShareWith[i];
var deviceId = deviceInfo.deviceId;
var sessionResult = sessionResults[deviceId];
if (!sessionResult.sessionId) {
@@ -288,8 +269,6 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap)
"sharing keys with device " + userId + ":" + deviceId
);
var deviceInfo = sessionResult.device;
var encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
@@ -321,6 +300,27 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap)
// TODO: retries
return self._baseApis.sendToDevice("m.room.encrypted", contentMap);
}).then(function() {
// Add the devices we have shared with to session.sharedWithDevices.
//
// we deliberately iterate over devicesByUser (ie, the devices we
// attempted to share with) rather than the contentMap (those we did
// share with), because we don't want to try to claim a one-time-key
// for dead devices on every message.
for (var userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
if (!session.sharedWithDevices[userId]) {
session.sharedWithDevices[userId] = {};
}
var devicesToShareWith = devicesByUser[userId];
for (var i = 0; i < devicesToShareWith.length; i++) {
var deviceInfo = devicesToShareWith[i];
session.sharedWithDevices[userId][deviceInfo.deviceId] =
key.chain_index;
}
}
});
};
@@ -369,20 +369,9 @@ 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') {
this._onNewRoomMember(member.userId);
return;
}
if (newMembership === 'invite' && oldMembership !== 'join') {
// we don't (yet) share keys with invited members, so nothing to do yet
if (newMembership === 'join' || newMembership === 'invite') {
return;
}
@@ -396,44 +385,26 @@ MegolmEncryption.prototype.onRoomMembership = function(event, member, oldMembers
};
/**
* handle a new user joining a room
* Get the list of devices for all users in the room
*
* @param {string} userId new member
*/
MegolmEncryption.prototype._onNewRoomMember = function(userId) {
// make sure we have a list of this user's devices. We are happy to use a
// cached version here: we assume that if we already have a list of the
// user's devices, then we already share an e2e room with them, which means
// that they will have announced any new devices via an m.new_device.
this._crypto.downloadKeys([userId], false).done();
// also flag this user up for needing a keyshare.
this._devicesPendingKeyShare[userId] = true;
};
/**
* @inheritdoc
* @param {module:models/room} room
*
* @param {string} userId owner of the device
* @param {string} deviceId deviceId of the device
* @return {module:client.Promise} Promise which resolves to a map
* from userId to deviceId to deviceInfo
*/
MegolmEncryption.prototype.onNewDevice = function(userId, deviceId) {
var d = this._devicesPendingKeyShare[userId];
MegolmEncryption.prototype._getDevicesInRoom = function(room) {
// XXX what about rooms where invitees can see the content?
var roomMembers = utils.map(room.getJoinedMembers(), function(u) {
return u.userId;
});
if (d === true) {
// we already want to share keys with all devices for this user
return;
}
if (!d) {
this._devicesPendingKeyShare[userId] = d = {};
}
d[deviceId] = true;
// We are happy to use a cached version here: we assume that if we already
// have a list of the user's devices, then we already share an e2e room
// with them, which means that they will have announced any new devices via
// an m.new_device.
return this._crypto.downloadKeys(roomMembers, false);
};
/**
* Megolm decryption implementation
*