You've already forked matrix-js-sdk
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:
@@ -38,12 +38,17 @@ var base = require("./base");
|
|||||||
* @property {Number} creationTime when the session was created (ms since the epoch)
|
* @property {Number} creationTime when the session was created (ms since the epoch)
|
||||||
* @property {module:client.Promise?} sharePromise If a share operation is in progress,
|
* @property {module:client.Promise?} sharePromise If a share operation is in progress,
|
||||||
* a promise which resolves when it is complete.
|
* 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) {
|
function OutboundSessionInfo(sessionId) {
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
this.useCount = 0;
|
this.useCount = 0;
|
||||||
this.creationTime = new Date().getTime();
|
this.creationTime = new Date().getTime();
|
||||||
this.sharePromise = null;
|
this.sharePromise = null;
|
||||||
|
this.sharedWithDevices = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -90,11 +95,6 @@ function MegolmEncryption(params) {
|
|||||||
// case _outboundSession.sharePromise will be non-null.)
|
// case _outboundSession.sharePromise will be non-null.)
|
||||||
this._outboundSession = 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
|
// default rotation periods
|
||||||
this._sessionRotationPeriodMsgs = 100;
|
this._sessionRotationPeriodMsgs = 100;
|
||||||
this._sessionRotationPeriodMs = 7 * 24 * 3600 * 1000;
|
this._sessionRotationPeriodMs = 7 * 24 * 3600 * 1000;
|
||||||
@@ -134,32 +134,55 @@ MegolmEncryption.prototype._ensureOutboundSession = function(room) {
|
|||||||
return session.sharePromise;
|
return session.sharePromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
// no share in progress: check for new devices
|
// no share in progress: check if we need to share with any devices
|
||||||
var shareMap = this._devicesPendingKeyShare;
|
var prom = this._getDevicesInRoom(room).then(function(devicesInRoom) {
|
||||||
this._devicesPendingKeyShare = {};
|
var shareMap = {};
|
||||||
|
|
||||||
// check each user is (still) a member of the room
|
for (var userId in devicesInRoom) {
|
||||||
for (var userId in shareMap) {
|
if (!devicesInRoom.hasOwnProperty(userId)) {
|
||||||
if (!shareMap.hasOwnProperty(userId)) {
|
continue;
|
||||||
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?
|
return self._shareKeyWithDevices(
|
||||||
var member = room.getMember(userId);
|
session, shareMap
|
||||||
if (member.membership !== "join") {
|
);
|
||||||
delete shareMap[userId];
|
}).finally(function() {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
session.sharePromise = this._shareKeyWithDevices(
|
|
||||||
session.sessionId, shareMap
|
|
||||||
).finally(function() {
|
|
||||||
session.sharePromise = null;
|
session.sharePromise = null;
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
return session;
|
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}
|
key.key, {ed25519: this._olmDevice.deviceEd25519Key}
|
||||||
);
|
);
|
||||||
|
|
||||||
// we're going to share the key with all current members of the room,
|
return new OutboundSessionInfo(session_id);
|
||||||
// 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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*
|
*
|
||||||
* @param {string} session_id
|
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
|
||||||
*
|
*
|
||||||
* @param {Object<string, Object<string, boolean>|boolean>} shareMap
|
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
|
||||||
* Map from userid to either: true (meaning this is a new user in the room,
|
* map from userid to list of devices
|
||||||
* 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).
|
|
||||||
*
|
*
|
||||||
* @return {module:client.Promise} Promise which resolves once the key sharing
|
* @return {module:client.Promise} Promise which resolves once the key sharing
|
||||||
* message has been sent.
|
* message has been sent.
|
||||||
*/
|
*/
|
||||||
MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap) {
|
MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUser) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var key = this._olmDevice.getOutboundGroupSessionKey(session_id);
|
var key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId);
|
||||||
var payload = {
|
var payload = {
|
||||||
type: "m.room_key",
|
type: "m.room_key",
|
||||||
content: {
|
content: {
|
||||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||||
room_id: this._roomId,
|
room_id: this._roomId,
|
||||||
session_id: session_id,
|
session_id: session.sessionId,
|
||||||
session_key: key.key,
|
session_key: key.key,
|
||||||
chain_index: key.chain_index,
|
chain_index: key.chain_index,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// we downloaded the user's device list when they joined the room, or when
|
var contentMap = {};
|
||||||
// the new device announced itself, so there is no need to do so now.
|
|
||||||
|
|
||||||
return self._crypto.ensureOlmSessionsForUsers(
|
return olmlib.ensureOlmSessionsForDevices(
|
||||||
utils.keys(shareMap)
|
this._olmDevice, this._baseApis, devicesByUser
|
||||||
).then(function(devicemap) {
|
).then(function(devicemap) {
|
||||||
var contentMap = {};
|
|
||||||
var haveTargets = false;
|
var haveTargets = false;
|
||||||
|
|
||||||
for (var userId in devicemap) {
|
for (var userId in devicesByUser) {
|
||||||
if (!devicemap.hasOwnProperty(userId)) {
|
if (!devicesByUser.hasOwnProperty(userId)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var devicesToShareWith = shareMap[userId];
|
var devicesToShareWith = devicesByUser[userId];
|
||||||
var sessionResults = devicemap[userId];
|
var sessionResults = devicemap[userId];
|
||||||
|
|
||||||
for (var deviceId in sessionResults) {
|
for (var i = 0; i < devicesToShareWith.length; i++) {
|
||||||
if (!sessionResults.hasOwnProperty(deviceId)) {
|
var deviceInfo = devicesToShareWith[i];
|
||||||
continue;
|
var deviceId = deviceInfo.deviceId;
|
||||||
}
|
|
||||||
|
|
||||||
if (devicesToShareWith === true) {
|
|
||||||
// all devices
|
|
||||||
} else if (!devicesToShareWith[deviceId]) {
|
|
||||||
// not a new device
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sessionResult = sessionResults[deviceId];
|
var sessionResult = sessionResults[deviceId];
|
||||||
if (!sessionResult.sessionId) {
|
if (!sessionResult.sessionId) {
|
||||||
@@ -288,8 +269,6 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap)
|
|||||||
"sharing keys with device " + userId + ":" + deviceId
|
"sharing keys with device " + userId + ":" + deviceId
|
||||||
);
|
);
|
||||||
|
|
||||||
var deviceInfo = sessionResult.device;
|
|
||||||
|
|
||||||
var encryptedContent = {
|
var encryptedContent = {
|
||||||
algorithm: olmlib.OLM_ALGORITHM,
|
algorithm: olmlib.OLM_ALGORITHM,
|
||||||
sender_key: self._olmDevice.deviceCurve25519Key,
|
sender_key: self._olmDevice.deviceCurve25519Key,
|
||||||
@@ -321,6 +300,27 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap)
|
|||||||
|
|
||||||
// TODO: retries
|
// TODO: retries
|
||||||
return self._baseApis.sendToDevice("m.room.encrypted", contentMap);
|
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
|
* @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' || newMembership === 'invite') {
|
||||||
this._onNewRoomMember(member.userId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newMembership === 'invite' && oldMembership !== 'join') {
|
|
||||||
// we don't (yet) share keys with invited members, so nothing to do yet
|
|
||||||
return;
|
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
|
* @param {module:models/room} room
|
||||||
*/
|
|
||||||
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 {string} userId owner of the device
|
* @return {module:client.Promise} Promise which resolves to a map
|
||||||
* @param {string} deviceId deviceId of the device
|
* from userId to deviceId to deviceInfo
|
||||||
*/
|
*/
|
||||||
MegolmEncryption.prototype.onNewDevice = function(userId, deviceId) {
|
MegolmEncryption.prototype._getDevicesInRoom = function(room) {
|
||||||
var d = this._devicesPendingKeyShare[userId];
|
// 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 are happy to use a cached version here: we assume that if we already
|
||||||
// we already want to share keys with all devices for this user
|
// have a list of the user's devices, then we already share an e2e room
|
||||||
return;
|
// with them, which means that they will have announced any new devices via
|
||||||
}
|
// an m.new_device.
|
||||||
|
return this._crypto.downloadKeys(roomMembers, false);
|
||||||
if (!d) {
|
|
||||||
this._devicesPendingKeyShare[userId] = d = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
d[deviceId] = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Megolm decryption implementation
|
* Megolm decryption implementation
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -541,4 +541,77 @@ describe("megolm", function() {
|
|||||||
}).nodeify(done);
|
}).nodeify(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it("We shouldn't attempt to send to blocked devices", function(done) {
|
||||||
|
// establish an olm session with alice
|
||||||
|
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||||
|
|
||||||
|
var olmEvent = encryptOlmEvent({
|
||||||
|
senderKey: testSenderKey,
|
||||||
|
recipient: aliceTestClient,
|
||||||
|
p2pSession: p2pSession,
|
||||||
|
});
|
||||||
|
|
||||||
|
var syncResponse = {
|
||||||
|
next_batch: 1,
|
||||||
|
to_device: {
|
||||||
|
events: [olmEvent],
|
||||||
|
},
|
||||||
|
rooms: {
|
||||||
|
join: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
syncResponse.rooms.join[ROOM_ID] = {
|
||||||
|
state: {
|
||||||
|
events: [
|
||||||
|
test_utils.mkEvent({
|
||||||
|
type: 'm.room.encryption',
|
||||||
|
skey: '',
|
||||||
|
content: {
|
||||||
|
algorithm: 'm.megolm.v1.aes-sha2',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
test_utils.mkMembership({
|
||||||
|
mship: 'join',
|
||||||
|
sender: '@bob:xyz',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||||
|
|
||||||
|
return aliceTestClient.httpBackend.flush('/sync', 1).then(function() {
|
||||||
|
console.log('Forcing alice to download our device keys');
|
||||||
|
|
||||||
|
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||||
|
device_keys: {
|
||||||
|
'@bob:xyz': {
|
||||||
|
'DEVICE_ID': testDeviceKeys,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return q.all([
|
||||||
|
aliceTestClient.client.downloadKeys(['@bob:xyz']),
|
||||||
|
aliceTestClient.httpBackend.flush('/keys/query', 1),
|
||||||
|
]);
|
||||||
|
}).then(function() {
|
||||||
|
console.log('Telling alice to block our device');
|
||||||
|
aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
|
||||||
|
|
||||||
|
console.log('Telling alice to send a megolm message');
|
||||||
|
aliceTestClient.httpBackend.when(
|
||||||
|
'PUT', '/send/'
|
||||||
|
).respond(200, {
|
||||||
|
event_id: '$event_id',
|
||||||
|
});
|
||||||
|
|
||||||
|
return q.all([
|
||||||
|
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||||
|
aliceTestClient.httpBackend.flush(),
|
||||||
|
]);
|
||||||
|
}).nodeify(done);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user