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

@@ -303,17 +303,15 @@ MatrixClient.prototype.getDeviceEd25519Key = function() {
};
/**
* Upload the device keys to the homeserver and ensure that the
* homeserver has enough one-time keys.
* @param {number} maxKeys The maximum number of keys to generate
* Upload the device keys to the homeserver.
* @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) {
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) {
this._crypto.uploadKeys(5).done();
const tenMinutes = 1000 * 60 * 10;
const self = this;
this._uploadIntervalID = global.setInterval(function() {
self._crypto.uploadKeys(5).done();
}, tenMinutes);
this._crypto.uploadDeviceKeys().done();
}
// periodically poll for turn servers if we support voip
@@ -2729,9 +2722,6 @@ MatrixClient.prototype.stopClient = function() {
this._syncApi.stop();
this._syncApi = null;
}
if (this._crypto) {
global.clearInterval(this._uploadIntervalID);
}
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._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
this._roomEncryptors = {};
@@ -111,6 +118,11 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId,
function _registerEventHandlers(crypto, eventEmitter) {
eventEmitter.on("sync", function(syncState, oldState, data) {
try {
if (syncState === "STOPPED") {
crypto._clientRunning = false;
} else if (syncState === "PREPARED") {
crypto._clientRunning = true;
}
if (syncState === "SYNCING") {
crypto._onSyncCompleted(data);
}
@@ -187,61 +199,11 @@ Crypto.prototype.getGlobalBlacklistUnverifiedDevices = function() {
};
/**
* Upload the device keys to the homeserver and ensure that the
* homeserver has enough one-time keys.
* @param {number} maxKeys The maximum number of keys to generate
* Upload the device keys to the homeserver.
* @return {object} A promise that will resolve when the keys are uploaded.
*/
Crypto.prototype.uploadKeys = function(maxKeys) {
const self = 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) {
Crypto.prototype.uploadDeviceKeys = function() {
const crypto = this;
const userId = crypto._userId;
const deviceId = crypto._deviceId;
@@ -260,6 +222,90 @@ function _uploadDeviceKeys(crypto) {
// same one as used in login.
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
@@ -805,6 +851,14 @@ Crypto.prototype._onSyncCompleted = function(syncData) {
// catch up on any new devices we got told about during the sync.
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);
}
};
/**