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

crypto: use memberlist to derive recipient list

When we send an encrypted message with Olm, we need to know who to send it
to. Currently that is based on a user list passed to setRoomEncryption. We want
to be able to change the list as people join and leave the room; I also want to
not have to keep the member list in local storage.

So, use the member list at the point of enabling encryption to set up sessions,
and at the point of sending a message to encrypt the message.

Further work here includes:

 * updating the react-sdk not to bother setting the member list
 * monitoring for new users/devices in the room, and setting up new sessions
   with them
This commit is contained in:
Richard van der Hoff
2016-06-21 17:40:06 +01:00
parent cb6566a198
commit d0e90cd8c9
2 changed files with 310 additions and 142 deletions

View File

@@ -643,16 +643,47 @@ MatrixClient.prototype.setRoomEncryption = function(roomId, config) {
return q.reject(new Error("End-to-End encryption disabled")); return q.reject(new Error("End-to-End encryption disabled"));
} }
var self = this;
if (config.algorithm === OLM_ALGORITHM) { if (config.algorithm === OLM_ALGORITHM) {
if (!config.members) { this.sessionStore.storeEndToEndRoom(roomId, config);
throw new Error(
"Config must include a 'members' list with a list of userIds" var room = this.getRoom(roomId);
);
if (!room) {
console.warn("Enabling encryption in unknown room " + roomId);
return q({});
} }
var users = utils.map(room.getJoinedMembers(), function(u) {
return u.userId;
});
return self.downloadKeys(users, true).then(function(res) {
return self._ensureOlmSessionsForUsers(users);
});
} else {
throw new Error("Unknown algorithm: " + config.algorithm);
}
};
/**
* Try to make sure we have established olm sessions for the given users.
*
* @param {string[]} users list of user ids
*
* @return {module:client.Promise} resolves once the sessions are complete, to
* an object with keys <tt>missingUsers</tt> (a list of users with no known
* olm devices), and <tt>missingDevices</tt> a list of olm devices with no
* known one-time keys.
*
* @private
*/
MatrixClient.prototype._ensureOlmSessionsForUsers = function(users) {
var devicesWithoutSession = []; var devicesWithoutSession = [];
var userWithoutDevices = []; var userWithoutDevices = [];
for (var i = 0; i < config.members.length; ++i) { for (var i = 0; i < users.length; ++i) {
var userId = config.members[i]; var userId = users[i];
var devices = this.sessionStore.getEndToEndDevicesForUser(userId); var devices = this.sessionStore.getEndToEndDevicesForUser(userId);
if (!devices) { if (!devices) {
userWithoutDevices.push(userId); userWithoutDevices.push(userId);
@@ -671,8 +702,14 @@ MatrixClient.prototype.setRoomEncryption = function(roomId, config) {
} }
} }
} }
var deferred = q.defer();
if (devicesWithoutSession.length > 0) { if (devicesWithoutSession.length === 0) {
return q({
missingUsers: userWithoutDevices,
missingDevices: []
});
}
var queries = {}; var queries = {};
for (i = 0; i < devicesWithoutSession.length; ++i) { for (i = 0; i < devicesWithoutSession.length; ++i) {
var device = devicesWithoutSession[i]; var device = devicesWithoutSession[i];
@@ -683,10 +720,10 @@ MatrixClient.prototype.setRoomEncryption = function(roomId, config) {
var path = "/keys/claim"; var path = "/keys/claim";
var content = {one_time_keys: queries}; var content = {one_time_keys: queries};
var self = this; var self = this;
this._http.authedRequestWithPrefix( return this._http.authedRequestWithPrefix(
undefined, "POST", path, undefined, content, undefined, "POST", path, undefined, content,
httpApi.PREFIX_UNSTABLE httpApi.PREFIX_UNSTABLE
).done(function(res) { ).then(function(res) {
var missing = {}; var missing = {};
for (i = 0; i < devicesWithoutSession.length; ++i) { for (i = 0; i < devicesWithoutSession.length; ++i) {
var device = devicesWithoutSession[i]; var device = devicesWithoutSession[i];
@@ -709,24 +746,13 @@ MatrixClient.prototype.setRoomEncryption = function(roomId, config) {
missing[device[0]].push([device[1]]); missing[device[0]].push([device[1]]);
} }
} }
deferred.resolve({
return {
missingUsers: userWithoutDevices, missingUsers: userWithoutDevices,
missingDevices: missing missingDevices: missing
});
});
} else {
deferred.resolve({
missingUsers: userWithoutDevices,
missingDevices: []
});
}
this.sessionStore.storeEndToEndRoom(roomId, config);
return deferred.promise;
} else {
throw new Error("Unknown algorithm: " + config.algorithm);
}
}; };
});
};
/** /**
* Disable encryption for a room. * Disable encryption for a room.
@@ -1177,9 +1203,23 @@ function _encryptMessage(client, roomId, e2eRoomInfo, eventType, content) {
} }
if (e2eRoomInfo.algorithm === OLM_ALGORITHM) { if (e2eRoomInfo.algorithm === OLM_ALGORITHM) {
var room = client.getRoom(roomId);
if (!room) {
throw new Error("Cannot send encrypted messages in unknown rooms");
}
// pick the list of recipients based on the membership list.
//
// TODO: there is a race condition here! What if a new user turns up
// just as you are sending a secret message?
var users = utils.map(room.getJoinedMembers(), function(u) {
return u.userId;
});
var participantKeys = []; var participantKeys = [];
for (var i = 0; i < e2eRoomInfo.members.length; ++i) { for (var i = 0; i < users.length; ++i) {
var userId = e2eRoomInfo.members[i]; var userId = users[i];
var devices = client.sessionStore.getEndToEndDevicesForUser(userId); var devices = client.sessionStore.getEndToEndDevicesForUser(userId);
for (var deviceId in devices) { for (var deviceId in devices) {
if (devices.hasOwnProperty(deviceId)) { if (devices.hasOwnProperty(deviceId)) {

View File

@@ -41,29 +41,27 @@ var aliMessages;
var bobMessages; var bobMessages;
function aliUploadsKeys() { /**
var uploadPath = "/keys/upload/" + aliDeviceId; * Set an expectation that the client will upload device keys and a number of
aliHttpBackend.when("POST", uploadPath).respond(200, function(path, content) { * one-time keys; then flush the http requests.
*
* @param {string} deviceId expected device id in upload request
* @param {object} httpBackend
*
* @return {promise} completes once the http requests have completed, returning the
* content of the upload request.
*/
function expectKeyUpload(deviceId, httpBackend) {
var uploadPath = "/keys/upload/" + deviceId;
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
expect(content.one_time_keys).toEqual({}); expect(content.one_time_keys).toEqual({});
aliDeviceKeys = content.device_keys;
return {one_time_key_counts: {curve25519: 0}}; return {one_time_key_counts: {curve25519: 0}};
}); });
return q.all([
aliClient.uploadKeys(0),
aliHttpBackend.flush(uploadPath, 1),
]).then(function() {
console.log("ali uploaded keys");
});
}
function bobUploadsKeys() { var uploadContent;
var uploadPath = "/keys/upload/bvcxz"; httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
bobHttpBackend.when("POST", uploadPath).respond(200, function(path, content) { uploadContent = content;
expect(content.one_time_keys).toEqual({});
bobHttpBackend.when("POST", uploadPath).respond(200, function(path, content) {
expect(content.one_time_keys).not.toEqual({}); expect(content.one_time_keys).not.toEqual({});
bobDeviceKeys = content.device_keys;
bobOneTimeKeys = content.one_time_keys;
var count = 0; var count = 0;
for (var key in content.one_time_keys) { for (var key in content.one_time_keys) {
if (content.one_time_keys.hasOwnProperty(key)) { if (content.one_time_keys.hasOwnProperty(key)) {
@@ -73,10 +71,41 @@ function bobUploadsKeys() {
expect(count).toEqual(5); expect(count).toEqual(5);
return {one_time_key_counts: {curve25519: count}}; return {one_time_key_counts: {curve25519: count}};
}); });
return {one_time_key_counts: {}};
return httpBackend.flush(uploadPath, 2).then(function() {
return uploadContent;
}); });
bobClient.uploadKeys(5).catch(utils.failTest); }
return bobHttpBackend.flush().then(function() {
/**
* Set an expectation that ali will upload device keys and a number of one-time keys;
* then flush the http requests.
*
* <p>Updates <tt>aliDeviceKeys</tt>
*
* @return {promise} completes once the http requests have completed.
*/
function expectAliKeyUpload() {
return expectKeyUpload(aliDeviceId, aliHttpBackend).then(function(content) {
aliDeviceKeys = content.device_keys;
});
}
/**
* Set an expectation that bob will upload device keys and a number of one-time keys;
* then flush the http requests.
*
* <p>Updates <tt>bobDeviceKeys</tt>, <tt>bobOneTimeKeys</tt>,
* <tt>bobDeviceCurve25519Key</tt>, <tt>bobDeviceEd25519Key</tt>
*
* @return {promise} completes once the http requests have completed.
*/
function expectBobKeyUpload() {
return expectKeyUpload(bobDeviceId, bobHttpBackend).then(function(content) {
bobDeviceKeys = content.device_keys;
bobOneTimeKeys = content.one_time_keys;
expect(bobDeviceKeys).toBeDefined(); expect(bobDeviceKeys).toBeDefined();
expect(bobOneTimeKeys).toBeDefined(); expect(bobOneTimeKeys).toBeDefined();
bobDeviceCurve25519Key = bobDeviceKeys.keys["curve25519:bvcxz"]; bobDeviceCurve25519Key = bobDeviceKeys.keys["curve25519:bvcxz"];
@@ -84,7 +113,21 @@ function bobUploadsKeys() {
}); });
} }
function aliDownloadsKeys() { function bobUploadsKeys() {
bobClient.uploadKeys(5).catch(utils.failTest);
return expectBobKeyUpload();
}
/**
* Set an expectation that ali will query bobs keys; then flush the http request.
*
* @return {promise} resolves once the http request has completed.
*/
function aliQueryKeys() {
// can't query keys before bob has uploaded them
expect(bobDeviceKeys).toBeDefined();
var bobKeys = {}; var bobKeys = {};
bobKeys[bobDeviceId] = bobDeviceKeys; bobKeys[bobDeviceId] = bobDeviceKeys;
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) { aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
@@ -93,23 +136,18 @@ function aliDownloadsKeys() {
result[bobUserId] = bobKeys; result[bobUserId] = bobKeys;
return {device_keys: result}; return {device_keys: result};
}); });
var p1 = aliClient.downloadKeys([bobUserId]).then(function() { return aliHttpBackend.flush("/keys/query", 1);
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([{
id: "bvcxz",
key: bobDeviceEd25519Key,
verified: false,
}]);
});
var p2 = aliHttpBackend.flush();
return q.all([p1, p2]).then(function() {
var devices = aliStorage.getEndToEndDevicesForUser(bobUserId);
expect(devices[bobDeviceId].keys).toEqual(bobDeviceKeys.keys);
expect(devices[bobDeviceId].verified).toBe(false);
});
} }
function bobDownloadsKeys() { /**
* Set an expectation that bob will query alis keys; then flush the http request.
*
* @return {promise} which resolves once the http request has completed.
*/
function bobQueryKeys() {
// can't query keys before ali has uploaded them
expect(aliDeviceKeys).toBeDefined();
var aliKeys = {}; var aliKeys = {};
aliKeys[aliDeviceId] = aliDeviceKeys; aliKeys[aliDeviceId] = aliDeviceKeys;
bobHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) { bobHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
@@ -118,13 +156,35 @@ function bobDownloadsKeys() {
result[aliUserId] = aliKeys; result[aliUserId] = aliKeys;
return {device_keys: result}; return {device_keys: result};
}); });
return q.all([ return bobHttpBackend.flush("/keys/query", 1);
bobClient.downloadKeys([aliUserId]), }
bobHttpBackend.flush(),
]);
function aliDownloadsKeys() {
// can't query keys before bob has uploaded them
expect(bobDeviceEd25519Key).toBeDefined();
var p1 = aliClient.downloadKeys([bobUserId]).then(function() {
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([{
id: "bvcxz",
key: bobDeviceEd25519Key,
verified: false,
}]);
});
var p2 = aliQueryKeys();
return q.all([p1, p2]).then(function() {
var devices = aliStorage.getEndToEndDevicesForUser(bobUserId);
expect(devices[bobDeviceId].keys).toEqual(bobDeviceKeys.keys);
expect(devices[bobDeviceId].verified).toBe(false);
});
} }
function aliEnablesEncryption() { function aliEnablesEncryption() {
// can't query keys before bob has uploaded them
expect(bobOneTimeKeys).toBeDefined();
aliQueryKeys().catch(utils.failTest);
aliHttpBackend.when("POST", "/keys/claim").respond(200, function(path, content) { aliHttpBackend.when("POST", "/keys/claim").respond(200, function(path, content) {
expect(content.one_time_keys[bobUserId][bobDeviceId]).toEqual("curve25519"); expect(content.one_time_keys[bobUserId][bobDeviceId]).toEqual("curve25519");
for (var keyId in bobOneTimeKeys) { for (var keyId in bobOneTimeKeys) {
@@ -142,7 +202,6 @@ function aliEnablesEncryption() {
}); });
var p = aliClient.setRoomEncryption(roomId, { var p = aliClient.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2", algorithm: "m.olm.v1.curve25519-aes-sha2",
members: [aliUserId, bobUserId]
}).then(function(res) { }).then(function(res) {
expect(res.missingUsers).toEqual([]); expect(res.missingUsers).toEqual([]);
expect(res.missingDevices).toEqual({}); expect(res.missingDevices).toEqual({});
@@ -153,11 +212,10 @@ function aliEnablesEncryption() {
} }
function bobEnablesEncryption() { function bobEnablesEncryption() {
bobQueryKeys().catch(utils.failTest);
return bobClient.setRoomEncryption(roomId, { return bobClient.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2", algorithm: "m.olm.v1.curve25519-aes-sha2",
members: [aliUserId, bobUserId]
}).then(function(res) { }).then(function(res) {
console.log("bob enabled encryption");
expect(res.missingUsers).toEqual([]); expect(res.missingUsers).toEqual([]);
expect(res.missingDevices).toEqual({}); expect(res.missingDevices).toEqual({});
expect(bobClient.isRoomEncrypted(roomId)).toBeTruthy(); expect(bobClient.isRoomEncrypted(roomId)).toBeTruthy();
@@ -233,24 +291,88 @@ function recvMessage(httpBackend, client, message) {
}; };
httpBackend.when("GET", "/sync").respond(200, syncData); httpBackend.when("GET", "/sync").respond(200, syncData);
var deferred = q.defer(); var deferred = q.defer();
client.on("event", function(event) { var onEvent = function(event) {
console.log(client.credentials.userId + " received event",
event);
// ignore the m.room.member events
if (event.getType() == "m.room.member") {
return;
}
expect(event.getType()).toEqual("m.room.message"); expect(event.getType()).toEqual("m.room.message");
expect(event.getContent()).toEqual({ expect(event.getContent()).toEqual({
msgtype: "m.text", msgtype: "m.text",
body: "Hello, World" body: "Hello, World"
}); });
expect(event.isEncrypted()).toBeTruthy(); expect(event.isEncrypted()).toBeTruthy();
client.removeListener("event", onEvent);
deferred.resolve(); deferred.resolve();
}); };
startClient(httpBackend, client);
client.on("event", onEvent);
httpBackend.flush(); httpBackend.flush();
return deferred.promise; return deferred.promise;
} }
function aliStartClient() {
expectAliKeyUpload().catch(utils.failTest);
startClient(aliHttpBackend, aliClient);
return aliHttpBackend.flush().then(function() {
console.log("Ali client started");
});
}
function bobStartClient() {
expectBobKeyUpload().catch(utils.failTest);
startClient(bobHttpBackend, bobClient);
return bobHttpBackend.flush().then(function() {
console.log("Bob client started");
});
}
/**
* Set http responses for the requests which are made when a client starts, and
* start the client.
*
* @param {object} httpBackend
* @param {MatrixClient} client
*/
function startClient(httpBackend, client) { function startClient(httpBackend, client) {
client.startClient();
httpBackend.when("GET", "/pushrules").respond(200, {}); httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" }); httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
// send a sync response including our test room.
var syncData = {
next_batch: "x",
rooms: {
join: { }
}
};
syncData.rooms.join[roomId] = {
state: {
events: [
utils.mkMembership({
mship: "join",
user: aliUserId,
}),
utils.mkMembership({
mship: "join",
user: bobUserId,
}),
]
},
timeline: {
events: []
}
};
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
} }
@@ -285,6 +407,11 @@ describe("MatrixClient crypto", function() {
request: bobHttpBackend.requestFn, request: bobHttpBackend.requestFn,
}); });
bobOneTimeKeys = undefined;
aliDeviceKeys = undefined;
bobDeviceKeys = undefined;
bobDeviceCurve25519Key = undefined;
bobDeviceEd25519Key = undefined;
aliMessages = []; aliMessages = [];
bobMessages = []; bobMessages = [];
}); });
@@ -319,7 +446,7 @@ describe("MatrixClient crypto", function() {
it("Ali enables encryption", function(done) { it("Ali enables encryption", function(done) {
q() q()
.then(bobUploadsKeys) .then(bobUploadsKeys)
.then(aliDownloadsKeys) .then(aliStartClient)
.then(aliEnablesEncryption) .then(aliEnablesEncryption)
.catch(utils.failTest).done(done); .catch(utils.failTest).done(done);
}); });
@@ -327,7 +454,7 @@ describe("MatrixClient crypto", function() {
it("Ali sends a message", function(done) { it("Ali sends a message", function(done) {
q() q()
.then(bobUploadsKeys) .then(bobUploadsKeys)
.then(aliDownloadsKeys) .then(aliStartClient)
.then(aliEnablesEncryption) .then(aliEnablesEncryption)
.then(aliSendsMessage) .then(aliSendsMessage)
.catch(utils.failTest).done(done); .catch(utils.failTest).done(done);
@@ -336,9 +463,10 @@ describe("MatrixClient crypto", function() {
it("Bob receives a message", function(done) { it("Bob receives a message", function(done) {
q() q()
.then(bobUploadsKeys) .then(bobUploadsKeys)
.then(aliDownloadsKeys) .then(aliStartClient)
.then(aliEnablesEncryption) .then(aliEnablesEncryption)
.then(aliSendsMessage) .then(aliSendsMessage)
.then(bobStartClient)
.then(bobRecvMessage) .then(bobRecvMessage)
.catch(utils.failTest).done(done); .catch(utils.failTest).done(done);
}); });
@@ -346,9 +474,10 @@ describe("MatrixClient crypto", function() {
it("Bob receives two pre-key messages", function(done) { it("Bob receives two pre-key messages", function(done) {
q() q()
.then(bobUploadsKeys) .then(bobUploadsKeys)
.then(aliDownloadsKeys) .then(aliStartClient)
.then(aliEnablesEncryption) .then(aliEnablesEncryption)
.then(aliSendsMessage) .then(aliSendsMessage)
.then(bobStartClient)
.then(bobRecvMessage) .then(bobRecvMessage)
.then(aliSendsMessage) .then(aliSendsMessage)
.then(bobRecvMessage) .then(bobRecvMessage)
@@ -358,12 +487,11 @@ describe("MatrixClient crypto", function() {
it("Bob replies to the message", function(done) { it("Bob replies to the message", function(done) {
q() q()
.then(bobUploadsKeys) .then(bobUploadsKeys)
.then(aliDownloadsKeys) .then(aliStartClient)
.then(aliEnablesEncryption) .then(aliEnablesEncryption)
.then(aliSendsMessage) .then(aliSendsMessage)
.then(bobStartClient)
.then(bobRecvMessage) .then(bobRecvMessage)
.then(aliUploadsKeys)
.then(bobDownloadsKeys)
.then(bobEnablesEncryption) .then(bobEnablesEncryption)
.then(bobSendsMessage).then(function(ciphertext) { .then(bobSendsMessage).then(function(ciphertext) {
expect(ciphertext.type).toEqual(1); expect(ciphertext.type).toEqual(1);