1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-09 10:22:46 +03:00

Merge pull request #376 from matrix-org/rav/delay_otk_generation

Upload one-time keys on /sync rather than a timer
This commit is contained in:
Richard van der Hoff
2017-02-21 08:46:59 +00:00
committed by GitHub
5 changed files with 300 additions and 195 deletions

View File

@@ -21,6 +21,7 @@ import sdk from '..';
import testUtils from './test-utils'; import testUtils from './test-utils';
import MockHttpBackend from './mock-request'; import MockHttpBackend from './mock-request';
import expect from 'expect'; import expect from 'expect';
import q from 'q';
/** /**
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient * Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
@@ -49,6 +50,10 @@ export default function TestClient(userId, deviceId, accessToken) {
this.oneTimeKeys = {}; this.oneTimeKeys = {};
} }
TestClient.prototype.toString = function() {
return 'TestClient[' + this.userId + ']';
};
/** /**
* start the client, and wait for it to initialise. * start the client, and wait for it to initialise.
* *
@@ -57,7 +62,11 @@ export default function TestClient(userId, deviceId, accessToken) {
TestClient.prototype.start = function() { TestClient.prototype.start = function() {
this.httpBackend.when("GET", "/pushrules").respond(200, {}); this.httpBackend.when("GET", "/pushrules").respond(200, {});
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" }); this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
this.expectKeyUpload(); this.expectDeviceKeyUpload();
// we let the client do a very basic initial sync, which it needs before
// it will upload one-time keys.
this.httpBackend.when("GET", "/sync").respond(200, { next_batch: 1 });
this.client.startClient({ this.client.startClient({
// set this so that we can get hold of failed events // set this so that we can get hold of failed events
@@ -65,7 +74,7 @@ TestClient.prototype.start = function() {
}); });
return this.httpBackend.flush().then(() => { return this.httpBackend.flush().then(() => {
console.log('TestClient[' + this.userId + ']: started'); console.log(this + ': started');
}); });
}; };
@@ -77,24 +86,62 @@ TestClient.prototype.stop = function() {
}; };
/** /**
* Set up expectations that the client will upload device and one-time keys. * Set up expectations that the client will upload device keys.
*/ */
TestClient.prototype.expectKeyUpload = function() { TestClient.prototype.expectDeviceKeyUpload = function() {
const self = this; const self = this;
this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) { this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) {
expect(content.one_time_keys).toBe(undefined); expect(content.one_time_keys).toBe(undefined);
expect(content.device_keys).toBeTruthy(); expect(content.device_keys).toBeTruthy();
console.log(self + ': received device keys');
// we expect this to happen before any one-time keys are uploaded.
expect(Object.keys(self.oneTimeKeys).length).toEqual(0);
self.deviceKeys = content.device_keys; self.deviceKeys = content.device_keys;
return {one_time_key_counts: {signed_curve25519: 0}}; return {one_time_key_counts: {signed_curve25519: 0}};
}); });
this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) { };
expect(content.device_keys).toBe(undefined);
expect(content.one_time_keys).toBeTruthy();
expect(content.one_time_keys).toNotEqual({}); /**
self.oneTimeKeys = content.one_time_keys; * If one-time keys have already been uploaded, return them. Otherwise,
return {one_time_key_counts: { * set up an expectation that the keys will be uploaded, and wait for
signed_curve25519: Object.keys(self.oneTimeKeys).length, * that to happen.
}}; *
* @returns {Promise} for the one-time keys
*/
TestClient.prototype.awaitOneTimeKeyUpload = function() {
if (Object.keys(this.oneTimeKeys).length != 0) {
// already got one-time keys
return q(this.oneTimeKeys);
}
this.httpBackend.when("POST", "/keys/upload")
.respond(200, (path, content) => {
expect(content.device_keys).toBe(undefined);
expect(content.one_time_keys).toBe(undefined);
return {one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys).length,
}};
});
this.httpBackend.when("POST", "/keys/upload")
.respond(200, (path, content) => {
expect(content.device_keys).toBe(undefined);
expect(content.one_time_keys).toBeTruthy();
expect(content.one_time_keys).toNotEqual({});
console.log('%s: received %i one-time keys', this,
Object.keys(content.one_time_keys).length);
this.oneTimeKeys = content.one_time_keys;
return {one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys).length,
}};
});
return this.httpBackend.flush('/keys/upload', 2).then((flushed) => {
expect(flushed).toEqual(2);
return this.oneTimeKeys;
}); });
}; };

View File

@@ -44,15 +44,13 @@ const bobAccessToken = "fewgfkuesa";
let aliMessages; let aliMessages;
let bobMessages; let bobMessages;
function bobUploadsDeviceKeys() {
function bobUploadsKeys() { bobTestClient.expectDeviceKeyUpload();
bobTestClient.expectKeyUpload();
return q.all([ return q.all([
bobTestClient.client.uploadKeys(5), bobTestClient.client.uploadKeys(),
bobTestClient.httpBackend.flush(), bobTestClient.httpBackend.flush(),
]).then(() => { ]).then(() => {
expect(Object.keys(bobTestClient.oneTimeKeys).length).toEqual(5); expect(Object.keys(bobTestClient.deviceKeys).length).toNotEqual(0);
expect(bobTestClient.deviceKeys).toNotEqual({});
}); });
} }
@@ -107,34 +105,33 @@ function expectBobQueryKeys() {
* @return {promise} resolves once the http request has completed. * @return {promise} resolves once the http request has completed.
*/ */
function expectAliClaimKeys() { function expectAliClaimKeys() {
// can't query keys before bob has uploaded them return bobTestClient.awaitOneTimeKeyUpload().then((keys) => {
expect(bobTestClient.oneTimeKeys).toNotEqual({}); aliTestClient.httpBackend.when(
"POST", "/keys/claim",
aliTestClient.httpBackend.when( ).respond(200, function(path, content) {
"POST", "/keys/claim", const claimType = content.one_time_keys[bobUserId][bobDeviceId];
).respond(200, function(path, content) { expect(claimType).toEqual("signed_curve25519");
const claimType = content.one_time_keys[bobUserId][bobDeviceId]; let keyId = null;
expect(claimType).toEqual("signed_curve25519"); for (keyId in keys) {
let keyId = null; if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) {
for (keyId in bobTestClient.oneTimeKeys) { if (keyId.indexOf(claimType + ":") === 0) {
if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) { break;
if (keyId.indexOf(claimType + ":") === 0) { }
break;
} }
} }
} const result = {};
const result = {}; result[bobUserId] = {};
result[bobUserId] = {}; result[bobUserId][bobDeviceId] = {};
result[bobUserId][bobDeviceId] = {}; result[bobUserId][bobDeviceId][keyId] = keys[keyId];
result[bobUserId][bobDeviceId][keyId] = bobTestClient.oneTimeKeys[keyId]; return {one_time_keys: result};
return {one_time_keys: result}; });
}); }).then(() => {
// it can take a while to process the key query, so give it some extra
// it can take a while to process the key query, so give it some extra // time, and make sure the claim actually happens rather than ploughing on
// time, and make sure the claim actually happens rather than ploughing on // confusingly.
// confusingly. return aliTestClient.httpBackend.flush("/keys/claim", 1, 20).then((r) => {
return aliTestClient.httpBackend.flush("/keys/claim", 1, 20).then((r) => { expect(r).toEqual(1);
expect(r).toEqual(1); });
}); });
} }
@@ -345,13 +342,13 @@ function recvMessage(httpBackend, client, sender, message) {
/** /**
* Set http responses for the requests which are made when a client starts, and * Send an initial sync response to the client (which just includes the member
* start the client. * list for our test room).
* *
* @param {TestClient} testClient * @param {TestClient} testClient
* @returns {Promise} which resolves when the client has done its initial requests * @returns {Promise} which resolves when the sync has been flushed.
*/ */
function startClient(testClient) { function firstSync(testClient) {
// send a sync response including our test room. // send a sync response including our test room.
const syncData = { const syncData = {
next_batch: "x", next_batch: "x",
@@ -377,7 +374,7 @@ function startClient(testClient) {
}, },
}; };
testClient.httpBackend.when("GET", "/sync").respond(200, syncData); testClient.httpBackend.when("GET", "/sync").respond(200, syncData);
return testClient.start(); return testClient.httpBackend.flush("/sync", 1);
} }
@@ -426,22 +423,21 @@ describe("MatrixClient crypto", function() {
}, },
); );
it("Bob uploads without one-time keys and with one-time keys", function(done) { it("Bob uploads device keys", function() {
q() return q()
.then(bobUploadsKeys) .then(bobUploadsDeviceKeys);
.catch(testUtils.failTest).done(done);
}); });
it("Ali downloads Bobs keys", function(done) { it("Ali downloads Bobs device keys", function(done) {
q() q()
.then(bobUploadsKeys) .then(bobUploadsDeviceKeys)
.then(aliDownloadsKeys) .then(aliDownloadsKeys)
.catch(testUtils.failTest).done(done); .catch(testUtils.failTest).done(done);
}); });
it("Ali gets keys with an invalid signature", function(done) { it("Ali gets keys with an invalid signature", function(done) {
q() q()
.then(bobUploadsKeys) .then(bobUploadsDeviceKeys)
.then(function() { .then(function() {
// tamper bob's keys // tamper bob's keys
const bobDeviceKeys = bobTestClient.deviceKeys; const bobDeviceKeys = bobTestClient.deviceKeys;
@@ -533,18 +529,22 @@ describe("MatrixClient crypto", function() {
}).catch(testUtils.failTest).done(done); }).catch(testUtils.failTest).done(done);
}); });
it("Ali enables encryption", function(done) {
q() it("Bob starts his client and uploads device keys and one-time keys", function() {
.then(bobUploadsKeys) return q()
.then(() => startClient(aliTestClient)) .then(() => bobTestClient.start())
.then(aliEnablesEncryption) .then(() => bobTestClient.awaitOneTimeKeyUpload())
.catch(testUtils.failTest).done(done); .then((keys) => {
expect(Object.keys(keys).length).toEqual(5);
expect(Object.keys(bobTestClient.deviceKeys).length).toNotEqual(0);
});
}); });
it("Ali sends a message", function(done) { it("Ali sends a message", function(done) {
q() q()
.then(bobUploadsKeys) .then(() => aliTestClient.start())
.then(() => startClient(aliTestClient)) .then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
.then(aliEnablesEncryption) .then(aliEnablesEncryption)
.then(aliSendsFirstMessage) .then(aliSendsFirstMessage)
.catch(testUtils.failTest).nodeify(done); .catch(testUtils.failTest).nodeify(done);
@@ -552,22 +552,22 @@ describe("MatrixClient crypto", function() {
it("Bob receives a message", function(done) { it("Bob receives a message", function(done) {
q() q()
.then(bobUploadsKeys) .then(() => aliTestClient.start())
.then(() => startClient(aliTestClient)) .then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
.then(aliEnablesEncryption) .then(aliEnablesEncryption)
.then(aliSendsFirstMessage) .then(aliSendsFirstMessage)
.then(() => startClient(bobTestClient))
.then(bobRecvMessage) .then(bobRecvMessage)
.catch(testUtils.failTest).done(done); .catch(testUtils.failTest).done(done);
}); });
it("Bob receives a message with a bogus sender", function(done) { it("Bob receives a message with a bogus sender", function(done) {
q() q()
.then(bobUploadsKeys) .then(() => aliTestClient.start())
.then(() => startClient(aliTestClient)) .then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
.then(aliEnablesEncryption) .then(aliEnablesEncryption)
.then(aliSendsFirstMessage) .then(aliSendsFirstMessage)
.then(() => startClient(bobTestClient))
.then(function() { .then(function() {
const message = aliMessages.shift(); const message = aliMessages.shift();
const syncData = { const syncData = {
@@ -620,8 +620,9 @@ describe("MatrixClient crypto", function() {
it("Ali blocks Bob's device", function(done) { it("Ali blocks Bob's device", function(done) {
q() q()
.then(bobUploadsKeys) .then(() => aliTestClient.start())
.then(() => startClient(aliTestClient)) .then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
.then(aliEnablesEncryption) .then(aliEnablesEncryption)
.then(aliDownloadsKeys) .then(aliDownloadsKeys)
.then(function() { .then(function() {
@@ -638,36 +639,37 @@ 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(() => aliTestClient.start())
.then(() => startClient(aliTestClient)) .then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
.then(aliEnablesEncryption) .then(aliEnablesEncryption)
.then(aliSendsFirstMessage) .then(aliSendsFirstMessage)
.then(() => startClient(bobTestClient))
.then(bobRecvMessage) .then(bobRecvMessage)
.then(aliSendsMessage) .then(aliSendsMessage)
.then(bobRecvMessage) .then(bobRecvMessage)
.catch(testUtils.failTest).done(done); .catch(testUtils.failTest).done(done);
}); });
it("Bob replies to the message", function(done) { it("Bob replies to the message", function() {
q() return q()
.then(() => startClient(aliTestClient)) .then(() => aliTestClient.start())
.then(() => startClient(bobTestClient)) .then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
.then(() => firstSync(bobTestClient))
.then(aliEnablesEncryption) .then(aliEnablesEncryption)
.then(aliSendsFirstMessage) .then(aliSendsFirstMessage)
.then(bobRecvMessage) .then(bobRecvMessage)
.then(bobEnablesEncryption) .then(bobEnablesEncryption)
.then(bobSendsReplyMessage).then(function(ciphertext) { .then(bobSendsReplyMessage).then(function(ciphertext) {
expect(ciphertext.type).toEqual(1); expect(ciphertext.type).toEqual(1);
}).then(aliRecvMessage) }).then(aliRecvMessage);
.catch(testUtils.failTest).done(done);
}); });
it("Ali does a key query when she gets a new_device event", function(done) { it("Ali does a key query when she gets a new_device event", function() {
q() return q()
.then(bobUploadsKeys) .then(() => aliTestClient.start())
.then(() => startClient(aliTestClient)) .then(() => firstSync(aliTestClient))
.then(function() { .then(function() {
const syncData = { const syncData = {
next_batch: '2', next_batch: '2',
@@ -686,15 +688,22 @@ describe("MatrixClient crypto", function() {
}; };
aliTestClient.httpBackend.when('GET', '/sync').respond(200, syncData); aliTestClient.httpBackend.when('GET', '/sync').respond(200, syncData);
return aliTestClient.httpBackend.flush('/sync', 1); return aliTestClient.httpBackend.flush('/sync', 1);
}).then(expectAliQueryKeys) }).then(() => {
.nodeify(done); aliTestClient.expectKeyQuery({
device_keys: {
[bobUserId]: {},
},
});
return aliTestClient.httpBackend.flush('/keys/query', 1);
});
}); });
it("Ali does a key query when encryption is enabled", function(done) { it("Ali does a key query when encryption is enabled", function() {
// enabling encryption in the room should make alice download devices // enabling encryption in the room should make alice download devices
// for both members. // for both members.
q() return q()
.then(() => startClient(aliTestClient)) .then(() => aliTestClient.start())
.then(() => firstSync(aliTestClient))
.then(() => { .then(() => {
const syncData = { const syncData = {
next_batch: '2', next_batch: '2',
@@ -727,6 +736,6 @@ describe("MatrixClient crypto", function() {
}, },
}); });
return aliTestClient.httpBackend.flush('/keys/query', 1); return aliTestClient.httpBackend.flush('/keys/query', 1);
}).nodeify(done); });
}); });
}); });

View File

@@ -39,17 +39,19 @@ const ROOM_ID = "!room:id";
* *
* @param {Olm.Account} olmAccount * @param {Olm.Account} olmAccount
* @param {TestClient} recipientTestClient * @param {TestClient} recipientTestClient
* @return {Olm.Session} * @return {Promise} promise for Olm.Session
*/ */
function createOlmSession(olmAccount, recipientTestClient) { function createOlmSession(olmAccount, recipientTestClient) {
const otkId = utils.keys(recipientTestClient.oneTimeKeys)[0]; return recipientTestClient.awaitOneTimeKeyUpload().then((keys) => {
const otk = recipientTestClient.oneTimeKeys[otkId]; const otkId = utils.keys(keys)[0];
const otk = keys[otkId];
const session = new Olm.Session(); const session = new Olm.Session();
session.create_outbound( session.create_outbound(
olmAccount, recipientTestClient.getDeviceKey(), otk.key, olmAccount, recipientTestClient.getDeviceKey(), otk.key,
); );
return session; return session;
});
} }
/** /**
@@ -302,9 +304,9 @@ describe("megolm", function() {
}); });
it("Alice receives a megolm message", function(done) { it("Alice receives a megolm message", function(done) {
return aliceTestClient.start().then(function() { return aliceTestClient.start().then(() => {
const p2pSession = createOlmSession(testOlmAccount, aliceTestClient); return createOlmSession(testOlmAccount, aliceTestClient);
}).then((p2pSession) => {
const groupSession = new Olm.OutboundGroupSession(); const groupSession = new Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();
@@ -353,9 +355,9 @@ describe("megolm", function() {
// https://github.com/vector-im/riot-web/issues/2273 // https://github.com/vector-im/riot-web/issues/2273
let roomKeyEncrypted; let roomKeyEncrypted;
return aliceTestClient.start().then(function() { return aliceTestClient.start().then(() => {
const p2pSession = createOlmSession(testOlmAccount, aliceTestClient); return createOlmSession(testOlmAccount, aliceTestClient);
}).then((p2pSession) => {
const groupSession = new Olm.OutboundGroupSession(); const groupSession = new Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();
@@ -413,9 +415,9 @@ describe("megolm", function() {
}); });
it("Alice gets a second room_key message", function(done) { it("Alice gets a second room_key message", function(done) {
return aliceTestClient.start().then(function() { return aliceTestClient.start().then(() => {
const p2pSession = createOlmSession(testOlmAccount, aliceTestClient); return createOlmSession(testOlmAccount, aliceTestClient);
}).then((p2pSession) => {
const groupSession = new Olm.OutboundGroupSession(); const groupSession = new Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();
@@ -483,11 +485,13 @@ describe("megolm", function() {
it('Alice sends a megolm message', function(done) { it('Alice sends a megolm message', function(done) {
let p2pSession; let p2pSession;
return aliceTestClient.start().then(function() { return aliceTestClient.start().then(() => {
const syncResponse = getSyncResponse(['@bob:xyz']);
// establish an olm session with alice // establish an olm session with alice
p2pSession = createOlmSession(testOlmAccount, aliceTestClient); return createOlmSession(testOlmAccount, aliceTestClient);
}).then((_p2pSession) => {
p2pSession = _p2pSession;
const syncResponse = getSyncResponse(['@bob:xyz']);
const olmEvent = encryptOlmEvent({ const olmEvent = encryptOlmEvent({
senderKey: testSenderKey, senderKey: testSenderKey,
@@ -595,11 +599,11 @@ describe("megolm", function() {
it("We shouldn't attempt to send to blocked devices", function(done) { it("We shouldn't attempt to send to blocked devices", function(done) {
return aliceTestClient.start().then(function() { return aliceTestClient.start().then(() => {
const syncResponse = getSyncResponse(['@bob:xyz']);
// establish an olm session with alice // establish an olm session with alice
const p2pSession = createOlmSession(testOlmAccount, aliceTestClient); return createOlmSession(testOlmAccount, aliceTestClient);
}).then((p2pSession) => {
const syncResponse = getSyncResponse(['@bob:xyz']);
const olmEvent = encryptOlmEvent({ const olmEvent = encryptOlmEvent({
senderKey: testSenderKey, senderKey: testSenderKey,
@@ -644,11 +648,13 @@ describe("megolm", function() {
let p2pSession; let p2pSession;
let megolmSessionId; let megolmSessionId;
return aliceTestClient.start().then(function() { return aliceTestClient.start().then(() => {
const syncResponse = getSyncResponse(['@bob:xyz']);
// establish an olm session with alice // establish an olm session with alice
p2pSession = createOlmSession(testOlmAccount, aliceTestClient); return createOlmSession(testOlmAccount, aliceTestClient);
}).then((_p2pSession) => {
p2pSession = _p2pSession;
const syncResponse = getSyncResponse(['@bob:xyz']);
const olmEvent = encryptOlmEvent({ const olmEvent = encryptOlmEvent({
senderKey: testSenderKey, senderKey: testSenderKey,
@@ -873,11 +879,11 @@ describe("megolm", function() {
}; };
}); });
return aliceTestClient.start().then(function() { return aliceTestClient.start().then(() => {
const syncResponse = getSyncResponse(['@bob:xyz']);
// establish an olm session with alice // establish an olm session with alice
p2pSession = createOlmSession(testOlmAccount, aliceTestClient); return createOlmSession(testOlmAccount, aliceTestClient);
}).then((p2pSession) => {
const syncResponse = getSyncResponse(['@bob:xyz']);
const olmEvent = encryptOlmEvent({ const olmEvent = encryptOlmEvent({
senderKey: testSenderKey, senderKey: testSenderKey,
@@ -1056,10 +1062,9 @@ describe("megolm", function() {
let messageEncrypted; let messageEncrypted;
return aliceTestClient.start().then(() => { return aliceTestClient.start().then(() => {
const p2pSession = createOlmSession( // establish an olm session with alice
testOlmAccount, aliceTestClient, return createOlmSession(testOlmAccount, aliceTestClient);
); }).then((p2pSession) => {
const groupSession = new Olm.OutboundGroupSession(); const groupSession = new Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();

View File

@@ -303,17 +303,15 @@ MatrixClient.prototype.getDeviceEd25519Key = function() {
}; };
/** /**
* Upload the device keys to the homeserver and ensure that the * Upload the device keys to the homeserver.
* homeserver has enough one-time keys.
* @param {number} maxKeys The maximum number of keys to generate
* @return {object} A promise that will resolve when the keys are uploaded. * @return {object} A promise that will resolve when the keys are uploaded.
*/ */
MatrixClient.prototype.uploadKeys = function(maxKeys) { MatrixClient.prototype.uploadKeys = function() {
if (this._crypto === null) { if (this._crypto === null) {
throw new Error("End-to-end encryption disabled"); throw new Error("End-to-end encryption disabled");
} }
return this._crypto.uploadKeys(maxKeys); return this._crypto.uploadDeviceKeys();
}; };
/** /**
@@ -2690,12 +2688,7 @@ MatrixClient.prototype.startClient = function(opts) {
} }
if (this._crypto) { if (this._crypto) {
this._crypto.uploadKeys(5).done(); this._crypto.uploadDeviceKeys().done();
const tenMinutes = 1000 * 60 * 10;
const self = this;
this._uploadIntervalID = global.setInterval(function() {
self._crypto.uploadKeys(5).done();
}, tenMinutes);
} }
// periodically poll for turn servers if we support voip // periodically poll for turn servers if we support voip
@@ -2729,9 +2722,6 @@ MatrixClient.prototype.stopClient = function() {
this._syncApi.stop(); this._syncApi.stop();
this._syncApi = null; this._syncApi = null;
} }
if (this._crypto) {
global.clearInterval(this._uploadIntervalID);
}
global.clearTimeout(this._checkTurnServersTimeoutID); global.clearTimeout(this._checkTurnServersTimeoutID);
}; };

View File

@@ -63,6 +63,13 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId,
this._deviceList = new DeviceList(baseApis, sessionStore, this._olmDevice); this._deviceList = new DeviceList(baseApis, sessionStore, this._olmDevice);
this._initialDeviceListInvalidationDone = false; this._initialDeviceListInvalidationDone = false;
this._clientRunning = false;
// the last time we did a check for the number of one-time-keys on the
// server.
this._lastOneTimeKeyCheck = null;
this._oneTimeKeyCheckInProgress = false;
// EncryptionAlgorithm instance for each room // EncryptionAlgorithm instance for each room
this._roomEncryptors = {}; this._roomEncryptors = {};
@@ -111,6 +118,11 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId,
function _registerEventHandlers(crypto, eventEmitter) { function _registerEventHandlers(crypto, eventEmitter) {
eventEmitter.on("sync", function(syncState, oldState, data) { eventEmitter.on("sync", function(syncState, oldState, data) {
try { try {
if (syncState === "STOPPED") {
crypto._clientRunning = false;
} else if (syncState === "PREPARED") {
crypto._clientRunning = true;
}
if (syncState === "SYNCING") { if (syncState === "SYNCING") {
crypto._onSyncCompleted(data); crypto._onSyncCompleted(data);
} }
@@ -187,61 +199,11 @@ Crypto.prototype.getGlobalBlacklistUnverifiedDevices = function() {
}; };
/** /**
* Upload the device keys to the homeserver and ensure that the * Upload the device keys to the homeserver.
* homeserver has enough one-time keys.
* @param {number} maxKeys The maximum number of keys to generate
* @return {object} A promise that will resolve when the keys are uploaded. * @return {object} A promise that will resolve when the keys are uploaded.
*/ */
Crypto.prototype.uploadKeys = function(maxKeys) { Crypto.prototype.uploadDeviceKeys = function() {
const self = this; const crypto = this;
return _uploadDeviceKeys(this).then(function(res) {
// We need to keep a pool of one time public keys on the server so that
// other devices can start conversations with us. But we can only store
// a finite number of private keys in the olm Account object.
// To complicate things further then can be a delay between a device
// claiming a public one time key from the server and it sending us a
// message. We need to keep the corresponding private key locally until
// we receive the message.
// But that message might never arrive leaving us stuck with duff
// private keys clogging up our local storage.
// So we need some kind of enginering compromise to balance all of
// these factors.
// We first find how many keys the server has for us.
const keyCount = res.one_time_key_counts.signed_curve25519 || 0;
// We then check how many keys we can store in the Account object.
const maxOneTimeKeys = self._olmDevice.maxNumberOfOneTimeKeys();
// Try to keep at most half that number on the server. This leaves the
// rest of the slots free to hold keys that have been claimed from the
// server but we haven't recevied a message for.
// If we run out of slots when generating new keys then olm will
// discard the oldest private keys first. This will eventually clean
// out stale private keys that won't receive a message.
const keyLimit = Math.floor(maxOneTimeKeys / 2);
// We work out how many new keys we need to create to top up the server
// If there are too many keys on the server then we don't need to
// create any more keys.
let numberToGenerate = Math.max(keyLimit - keyCount, 0);
if (maxKeys !== undefined) {
// Creating keys can be an expensive operation so we limit the
// number we generate in one go to avoid blocking the application
// for too long.
numberToGenerate = Math.min(numberToGenerate, maxKeys);
}
if (numberToGenerate <= 0) {
// If we don't need to generate any keys then we are done.
return;
}
// Ask olm to generate new one time keys, then upload them to synapse.
self._olmDevice.generateOneTimeKeys(numberToGenerate);
return _uploadOneTimeKeys(self);
});
};
// returns a promise which resolves to the response
function _uploadDeviceKeys(crypto) {
const userId = crypto._userId; const userId = crypto._userId;
const deviceId = crypto._deviceId; const deviceId = crypto._deviceId;
@@ -260,6 +222,90 @@ function _uploadDeviceKeys(crypto) {
// same one as used in login. // same one as used in login.
device_id: deviceId, device_id: deviceId,
}); });
};
// check if it's time to upload one-time keys, and do so if so.
function _maybeUploadOneTimeKeys(crypto) {
// frequency with which to check & upload one-time keys
const uploadPeriod = 1000 * 60; // one minute
// max number of keys to upload at once
// Creating keys can be an expensive operation so we limit the
// number we generate in one go to avoid blocking the application
// for too long.
const maxKeysPerCycle = 5;
if (crypto._oneTimeKeyCheckInProgress) {
return;
}
const now = Date.now();
if (crypto._lastOneTimeKeyCheck !== null &&
now - crypto._lastOneTimeKeyCheck < uploadPeriod
) {
// we've done a key upload recently.
return;
}
crypto._lastOneTimeKeyCheck = now;
function uploadLoop(numberToGenerate) {
if (numberToGenerate <= 0) {
// If we don't need to generate any more keys then we are done.
return;
}
const keysThisLoop = Math.min(numberToGenerate, maxKeysPerCycle);
// Ask olm to generate new one time keys, then upload them to synapse.
crypto._olmDevice.generateOneTimeKeys(keysThisLoop);
return _uploadOneTimeKeys(crypto).then(() => {
return uploadLoop(numberToGenerate - keysThisLoop);
});
}
crypto._oneTimeKeyCheckInProgress = true;
q().then(() => {
// ask the server how many keys we have
return crypto._baseApis.uploadKeysRequest({}, {
device_id: crypto._deviceId,
});
}).then((res) => {
// We need to keep a pool of one time public keys on the server so that
// other devices can start conversations with us. But we can only store
// a finite number of private keys in the olm Account object.
// To complicate things further then can be a delay between a device
// claiming a public one time key from the server and it sending us a
// message. We need to keep the corresponding private key locally until
// we receive the message.
// But that message might never arrive leaving us stuck with duff
// private keys clogging up our local storage.
// So we need some kind of enginering compromise to balance all of
// these factors.
// We first find how many keys the server has for us.
const keyCount = res.one_time_key_counts.signed_curve25519 || 0;
// We then check how many keys we can store in the Account object.
const maxOneTimeKeys = crypto._olmDevice.maxNumberOfOneTimeKeys();
// Try to keep at most half that number on the server. This leaves the
// rest of the slots free to hold keys that have been claimed from the
// server but we haven't recevied a message for.
// If we run out of slots when generating new keys then olm will
// discard the oldest private keys first. This will eventually clean
// out stale private keys that won't receive a message.
const keyLimit = Math.floor(maxOneTimeKeys / 2);
// We work out how many new keys we need to create to top up the server
// If there are too many keys on the server then we don't need to
// create any more keys.
const numberToGenerate = Math.max(keyLimit - keyCount, 0);
return uploadLoop(numberToGenerate);
}).catch((e) => {
console.error("Error uploading one-time keys", e.stack || e);
}).finally(() => {
crypto._oneTimeKeyCheckInProgress = false;
}).done();
} }
// returns a promise which resolves to the response // returns a promise which resolves to the response
@@ -805,6 +851,14 @@ Crypto.prototype._onSyncCompleted = function(syncData) {
// catch up on any new devices we got told about during the sync. // catch up on any new devices we got told about during the sync.
this._deviceList.refreshOutdatedDeviceLists(); this._deviceList.refreshOutdatedDeviceLists();
} }
// we don't start uploading one-time keys until we've caught up with
// to-device messages, to help us avoid throwing away one-time-keys that we
// are about to receive messages for
// (https://github.com/vector-im/riot-web/issues/2782).
if (!syncData.catchingUp) {
_maybeUploadOneTimeKeys(this);
}
}; };
/** /**