From 98d955ef1f9086cc79427f08d853e5b4a9416f69 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 9 Mar 2020 18:27:43 -0400 Subject: [PATCH] refactor megolm encryption to improve perceived speed - allow applications to pre-send decryption keys before the message is sent - establish new olm sessions with a shorter timeout first, and then re-try in the background with a longer timeout without blocking message sending --- spec/unit/crypto/algorithms/megolm.spec.js | 10 +- src/base-apis.js | 8 +- src/client.js | 2 +- src/crypto/algorithms/base.js | 9 + src/crypto/algorithms/megolm.js | 371 ++++++++++++--------- src/crypto/olmlib.js | 82 ++++- 6 files changed, 305 insertions(+), 177 deletions(-) diff --git a/spec/unit/crypto/algorithms/megolm.spec.js b/spec/unit/crypto/algorithms/megolm.spec.js index c2646c726..0dd005feb 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.js +++ b/spec/unit/crypto/algorithms/megolm.spec.js @@ -320,14 +320,14 @@ describe("MegolmDecryption", function() { // this should have claimed a key for alice as it's starting a new session expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith( - [['@alice:home.server', 'aliceDevice']], 'signed_curve25519', + [['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000, ); expect(mockCrypto.downloadKeys).toHaveBeenCalledWith( ['@alice:home.server'], false, ); expect(mockBaseApis.sendToDevice).toHaveBeenCalled(); expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith( - [['@alice:home.server', 'aliceDevice']], 'signed_curve25519', + [['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000, ); mockBaseApis.claimOneTimeKeys.mockReset(); @@ -540,6 +540,12 @@ describe("MegolmDecryption", function() { content: {}, }); await aliceClient._crypto.encryptEvent(event, aliceRoom); + await new Promise((resolve) => { + // encryptMessage retries senders in the background before giving + // up and telling them that there's no olm channel, so we need to + // wait a bit before checking that we got the message + setTimeout(resolve, 100); + }); expect(run).toBe(true); }); diff --git a/src/base-apis.js b/src/base-apis.js index becaa2002..d81f92d5e 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -1776,10 +1776,13 @@ MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, opts) { * * @param {string} [key_algorithm = signed_curve25519] desired key type * + * @param {number} [timeout] the time (in milliseconds) to wait for keys from remote + * servers + * * @return {Promise} Resolves: result object. Rejects: with * an error response ({@link module:http-api.MatrixError}). */ -MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm) { +MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm, timeout) { const queries = {}; if (key_algorithm === undefined) { @@ -1794,6 +1797,9 @@ MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm) { query[deviceId] = key_algorithm; } const content = {one_time_keys: queries}; + if (timeout) { + content.timeout = timeout; + } const path = "/keys/claim"; return this._http.authedRequest(undefined, "POST", path, undefined, content); }; diff --git a/src/client.js b/src/client.js index 4871af6af..fa2a7c8f5 100644 --- a/src/client.js +++ b/src/client.js @@ -2411,7 +2411,7 @@ function _sendEvent(client, room, event, callback) { let promise; // this event may be queued if (client.scheduler) { - // if this returns a promsie then the scheduler has control now and will + // if this returns a promise then the scheduler has control now and will // resolve/reject when it is done. Internally, the scheduler will invoke // processFn which is set to this._sendEventHttpRequest so the same code // path is executed regardless. diff --git a/src/crypto/algorithms/base.js b/src/crypto/algorithms/base.js index d12f9d420..b9125761c 100644 --- a/src/crypto/algorithms/base.js +++ b/src/crypto/algorithms/base.js @@ -60,6 +60,15 @@ export class EncryptionAlgorithm { this._roomId = params.roomId; } + /** + * Perform any background tasks that can be done before a message is ready to + * send, in order to speed up sending of the message. + * + * @param {module:models/room} room the room the event is in + */ + prepareToEncrypt(room) { + } + /** * Encrypt a message event * diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index b74a856a3..51a25e6cc 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -192,8 +192,6 @@ utils.inherits(MegolmEncryption, EncryptionAlgorithm); MegolmEncryption.prototype._ensureOutboundSession = async function( devicesInRoom, blocked, ) { - const self = this; - let session; // takes the previous OutboundSessionInfo, and considers whether to create @@ -201,12 +199,12 @@ MegolmEncryption.prototype._ensureOutboundSession = async function( // Updates `session` to hold the final OutboundSessionInfo. // // returns a promise which resolves once the keyshare is successful. - async function prepareSession(oldSession) { + const prepareSession = async (oldSession) => { session = oldSession; // need to make a brand new session? - if (session && session.needsRotation(self._sessionRotationPeriodMsgs, - self._sessionRotationPeriodMs) + if (session && session.needsRotation(this._sessionRotationPeriodMsgs, + this._sessionRotationPeriodMs) ) { logger.log("Starting new megolm session because we need to rotate."); session = null; @@ -218,32 +216,20 @@ MegolmEncryption.prototype._ensureOutboundSession = async function( } if (!session) { - logger.log(`Starting new megolm session for room ${self._roomId}`); - session = await self._prepareNewSession(); + logger.log(`Starting new megolm session for room ${this._roomId}`); + session = await this._prepareNewSession(); logger.log(`Started new megolm session ${session.sessionId} ` + - `for room ${self._roomId}`); - self._outboundSessions[session.sessionId] = session; + `for room ${this._roomId}`); + this._outboundSessions[session.sessionId] = session; } // now check if we need to share with any devices const shareMap = {}; - for (const userId in devicesInRoom) { - if (!devicesInRoom.hasOwnProperty(userId)) { - continue; - } - - const userDevices = devicesInRoom[userId]; - - for (const deviceId in userDevices) { - if (!userDevices.hasOwnProperty(deviceId)) { - continue; - } - - const deviceInfo = userDevices[deviceId]; - + for (const [userId, userDevices] of Object.entries(devicesInRoom)) { + for (const [deviceId, deviceInfo] of Object.entries(userDevices)) { const key = deviceInfo.getIdentityKey(); - if (key == self._olmDevice.deviceCurve25519Key) { + if (key == this._olmDevice.deviceCurve25519Key) { // don't bother sending to ourself continue; } @@ -260,49 +246,100 @@ MegolmEncryption.prototype._ensureOutboundSession = async function( const errorDevices = []; - await self._shareKeyWithDevices( - session, shareMap, errorDevices, + const key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId); + const payload = { + type: "m.room_key", + content: { + algorithm: olmlib.MEGOLM_ALGORITHM, + room_id: this._roomId, + session_id: session.sessionId, + session_key: key.key, + chain_index: key.chain_index, + }, + }; + const [devicesWithoutSession, olmSessions] = await olmlib.getExistingOlmSessions( + this._olmDevice, this._baseApis, shareMap, ); - // are there any new blocked devices that we need to notify? - const blockedMap = {}; - for (const userId in blocked) { - if (!blocked.hasOwnProperty(userId)) { - continue; - } + await Promise.all([ + (async () => { + // share keys with devices that we already have a session for + await this._shareKeyWithOlmSessions( + session, key, payload, olmSessions, + ); + })(), + (async () => { + // meanwhile, establish olm sessions for devices that we don't + // already have a session for, and share keys with them. Use a + // shorter timeout when fetching one-time keys. + await this._shareKeyWithDevices( + session, key, payload, devicesWithoutSession, errorDevices, 2000, + ); - const userBlockedDevices = blocked[userId]; + (async () => { + // Retry sending keys to devices that we were unable to establish + // an olm session for. This time, we use a longer timeout, but we + // do this in the background and don't block anything else while we + // do this. + const retryDevices = {}; + for (const {userId, deviceInfo} of errorDevices) { + retryDevices[userId] = retryDevices[userId] || []; + retryDevices[userId].push(deviceInfo); + } - for (const deviceId in userBlockedDevices) { - if (!userBlockedDevices.hasOwnProperty(deviceId)) { - continue; + const failedDevices = []; + await this._shareKeyWithDevices( + session, key, payload, retryDevices, failedDevices, + ); + + const blockedMap = {}; + const filteredFailedDevices = + await this._olmDevice.filterOutNotifiedErrorDevices( + failedDevices, + ); + for (const {userId, deviceInfo} of filteredFailedDevices) { + blockedMap[userId] = blockedMap[userId] || {}; + // we use a similar format to what + // olmlib.ensureOlmSessionsForDevices returns, so that + // we can use the same function to split + blockedMap[userId][deviceInfo.deviceId] = { + device: { + code: "m.no_olm", + reason: WITHHELD_MESSAGES["m.no_olm"], + deviceInfo, + }, + }; + + const deviceId = deviceInfo.deviceId; + + // mark this device as "handled" because we don't want to try + // to claim a one-time-key for dead devices on every message. + session.markSharedWithDevice(userId, deviceId, key.chain_index); + } + + // notify devices that we couldn't get an olm session + await this._notifyBlockedDevices(session, blockedMap); + })(); + })(), + (async () => { + // also, notify blocked devices that they're blocked + const blockedMap = {}; + for (const [userId, userBlockedDevices] of Object.entries(blocked)) { + for (const [deviceId, device] of Object.entries(userBlockedDevices)) { + if ( + !session.blockedDevicesNotified[userId] || + session.blockedDevicesNotified[userId][deviceId] === undefined + ) { + blockedMap[userId] = blockedMap[userId] || {}; + blockedMap[userId][deviceId] = {device}; + } + } } - if ( - !session.blockedDevicesNotified[userId] || - session.blockedDevicesNotified[userId][deviceId] === undefined - ) { - blockedMap[userId] = blockedMap[userId] || []; - blockedMap[userId].push(userBlockedDevices[deviceId]); - } - } - } - - - const filteredErrorDevices = - await self._olmDevice.filterOutNotifiedErrorDevices(errorDevices); - for (const {userId, deviceInfo} of filteredErrorDevices) { - blockedMap[userId] = blockedMap[userId] || []; - blockedMap[userId].push({ - code: "m.no_olm", - reason: WITHHELD_MESSAGES["m.no_olm"], - deviceInfo, - }); - } - - // notify blocked devices that they're blocked - await self._notifyBlockedDevices(session, blockedMap); - } + await this._notifyBlockedDevices(session, blockedMap); + })(), + ]); + }; // helper which returns the session prepared by prepareSession function returnSession() { @@ -349,44 +386,29 @@ MegolmEncryption.prototype._prepareNewSession = async function() { }; /** - * Splits the user device map into multiple chunks to reduce the number of - * devices we encrypt to per API call. Also filters out devices we don't have - * a session with. + * Determines what devices in devicesByUser don't have an olm session as given + * in devicemap. * * @private * - * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session + * @param {object} devicemap the devices that have olm sessions, as returned by + * olmlib.ensureOlmSessionsForDevices. + * @param {object} devicesByUser a map of user IDs to array of deviceInfo + * @param {array} [noOlmDevices] an array to fill with devices that don't have + * olm sessions * - * @param {number} chainIndex current chain index - * - * @param {object} devicemap - * mapping from userId to deviceId to {@link module:crypto~OlmSessionResult} - * - * @param {object} devicesByUser - * map from userid to list of devices - * - * @param {array} errorDevices - * array that will be populated with the devices that can't get an - * olm session for - * - * @return {array>} + * @return {array} an array of devices that don't have olm sessions. If + * noOlmDevices is specified, then noOlmDevices will be returned. */ -MegolmEncryption.prototype._splitUserDeviceMap = function( - session, chainIndex, devicemap, devicesByUser, errorDevices, +MegolmEncryption.prototype._getDevicesWithoutSessions = function( + devicemap, devicesByUser, noOlmDevices, ) { - const maxUsersPerRequest = 20; + noOlmDevices = noOlmDevices || []; - // use an array where the slices of a content map gets stored - const mapSlices = []; - let currentSliceId = 0; // start inserting in the first slice - let entriesInCurrentSlice = 0; - - for (const userId of Object.keys(devicesByUser)) { - const devicesToShareWith = devicesByUser[userId]; + for (const [userId, devicesToShareWith] of Object.entries(devicesByUser)) { const sessionResults = devicemap[userId]; - for (let i = 0; i < devicesToShareWith.length; i++) { - const deviceInfo = devicesToShareWith[i]; + for (const deviceInfo of devicesToShareWith) { const deviceId = deviceInfo.deviceId; const sessionResult = sessionResults[deviceId]; @@ -394,45 +416,17 @@ MegolmEncryption.prototype._splitUserDeviceMap = function( // no session with this device, probably because there // were no one-time keys. - // mark this device as "handled" because we don't want to try - // to claim a one-time-key for dead devices on every message. - session.markSharedWithDevice(userId, deviceId, chainIndex); - - errorDevices.push({userId, deviceInfo}); + noOlmDevices.push({userId, deviceInfo}); + delete sessionResults[deviceId]; // ensureOlmSessionsForUsers has already done the logging, // so just skip it. continue; } - - logger.log( - "share keys with device " + userId + ":" + deviceId, - ); - - if (!mapSlices[currentSliceId]) { - mapSlices[currentSliceId] = []; - } - - mapSlices[currentSliceId].push({ - userId: userId, - deviceInfo: deviceInfo, - }); - - entriesInCurrentSlice++; - } - - // We do this in the per-user loop as we prefer that all messages to the - // same user end up in the same API call to make it easier for the - // server (e.g. only have to send one EDU if a remote user, etc). This - // does mean that if a user has many devices we may go over the desired - // limit, but its not a hard limit so that is fine. - if (entriesInCurrentSlice > maxUsersPerRequest) { - // the current slice is filled up. Start inserting into the next slice - entriesInCurrentSlice = 0; - currentSliceId++; } } - return mapSlices; + + return noOlmDevices; }; /** @@ -445,20 +439,18 @@ MegolmEncryption.prototype._splitUserDeviceMap = function( * * @return {array>} the blocked devices, split into chunks */ -MegolmEncryption.prototype._splitBlockedDevices = function(devicesByUser) { - const maxUsersPerRequest = 20; +MegolmEncryption.prototype._splitDevices = function(devicesByUser) { + const maxDevicesPerRequest = 20; // use an array where the slices of a content map gets stored let currentSlice = []; const mapSlices = [currentSlice]; - for (const userId of Object.keys(devicesByUser)) { - const userBlockedDevicesToShareWith = devicesByUser[userId]; - - for (const blockedInfo of userBlockedDevicesToShareWith) { + for (const [userId, userDevices] of Object.entries(devicesByUser)) { + for (const deviceInfo of Object.values(userDevices)) { currentSlice.push({ userId: userId, - blockedInfo: blockedInfo, + deviceInfo: deviceInfo.device, }); } @@ -467,7 +459,7 @@ MegolmEncryption.prototype._splitBlockedDevices = function(devicesByUser) { // server (e.g. only have to send one EDU if a remote user, etc). This // does mean that if a user has many devices we may go over the desired // limit, but its not a hard limit so that is fine. - if (currentSlice.length > maxUsersPerRequest) { + if (currentSlice.length > maxDevicesPerRequest) { // the current slice is filled up. Start inserting into the next slice currentSlice = []; mapSlices.push(currentSlice); @@ -562,7 +554,7 @@ MegolmEncryption.prototype._sendBlockedNotificationsToDevices = async function( for (const val of userDeviceMap) { const userId = val.userId; - const blockedInfo = val.blockedInfo; + const blockedInfo = val.deviceInfo; const deviceInfo = blockedInfo.deviceInfo; const deviceId = deviceInfo.deviceId; @@ -682,37 +674,41 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function( }; /** + * @private + * * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session * + * @param {object} key the session key as returned by + * OlmDevice.getOutboundGroupSessionKey + * + * @param {object} payload the base to-device message payload for sharing keys + * * @param {object} devicesByUser * map from userid to list of devices * * @param {array} errorDevices * array that will be populated with the devices that we can't get an * olm session for + * + * @param {Number} [otkTimeout] The timeout in milliseconds when requesting + * one-time keys for establishing new olm sessions. */ MegolmEncryption.prototype._shareKeyWithDevices = async function( - session, devicesByUser, errorDevices, + session, key, payload, devicesByUser, errorDevices, otkTimeout, ) { - const key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId); - const payload = { - type: "m.room_key", - content: { - algorithm: olmlib.MEGOLM_ALGORITHM, - room_id: this._roomId, - session_id: session.sessionId, - session_key: key.key, - chain_index: key.chain_index, - }, - }; - const devicemap = await olmlib.ensureOlmSessionsForDevices( - this._olmDevice, this._baseApis, devicesByUser, + this._olmDevice, this._baseApis, devicesByUser, otkTimeout, ); - const userDeviceMaps = this._splitUserDeviceMap( - session, key.chain_index, devicemap, devicesByUser, errorDevices, - ); + this._getDevicesWithoutSessions(devicemap, devicesByUser, errorDevices); + + await this._shareKeyWithOlmSessions(session, key, payload, devicemap); +}; + +MegolmEncryption.prototype._shareKeyWithOlmSessions = async function( + session, key, payload, devicemap, +) { + const userDeviceMaps = this._splitDevices(devicemap); for (let i = 0; i < userDeviceMaps.length; i++) { try { @@ -748,7 +744,7 @@ MegolmEncryption.prototype._notifyBlockedDevices = async function( sender_key: this._olmDevice.deviceCurve25519Key, }; - const userDeviceMaps = this._splitBlockedDevices(devicesByUser); + const userDeviceMaps = this._splitDevices(devicesByUser); for (let i = 0; i < userDeviceMaps.length; i++) { try { @@ -766,6 +762,37 @@ MegolmEncryption.prototype._notifyBlockedDevices = async function( } }; +/** + * Perform any background tasks that can be done before a message is ready to + * send, in order to speed up sending of the message. + * + * @param {module:models/room} room the room the event is in + */ +MegolmEncryption.prototype.prepareToEncrypt = function(room) { + logger.log(`Preparing to encrypt events for ${this._roomId}`); + + if (this.encryptionPreparation) { + // We're already preparing something, so don't do anything else. + // FIXME: check if we need to restart + return; + } + + this.encryptionPreparation = (async () => { + const [devicesInRoom, blocked] = await this._getDevicesInRoom(room); + + if (this._crypto.getGlobalErrorOnUnknownDevices()) { + // Drop unknown devices for now. When the message gets sent, we'll + // throw an error, but we'll still be prepared to send to the known + // devices. + this._removeUnknownDevices(devicesInRoom); + } + + await this._ensureOutboundSession(devicesInRoom, blocked); + + delete this.encryptionPreparation; + })(); +}; + /** * @inheritdoc * @@ -776,38 +803,47 @@ MegolmEncryption.prototype._notifyBlockedDevices = async function( * @return {Promise} Promise which resolves to the new event body */ MegolmEncryption.prototype.encryptMessage = async function(room, eventType, content) { - const self = this; logger.log(`Starting to encrypt event for ${this._roomId}`); + if (this.encryptionPreparation) { + // If we started sending keys, wait for it to be done. + // FIXME: check if we need to restart + try { + await this.encryptionPreparation; + } catch (e) { + // ignore any errors -- if the preparation failed, we'll just + // restart everything here + } + } + const [devicesInRoom, blocked] = await this._getDevicesInRoom(room); // check if any of these devices are not yet known to the user. // if so, warn the user so they can verify or ignore. if (this._crypto.getGlobalErrorOnUnknownDevices()) { - self._checkForUnknownDevices(devicesInRoom); + this._checkForUnknownDevices(devicesInRoom); } - const session = await self._ensureOutboundSession(devicesInRoom, blocked); + const session = await this._ensureOutboundSession(devicesInRoom, blocked); const payloadJson = { - room_id: self._roomId, + room_id: this._roomId, type: eventType, content: content, }; - const ciphertext = self._olmDevice.encryptGroupMessage( + const ciphertext = this._olmDevice.encryptGroupMessage( session.sessionId, JSON.stringify(payloadJson), ); - const encryptedContent = { algorithm: olmlib.MEGOLM_ALGORITHM, - sender_key: self._olmDevice.deviceCurve25519Key, + sender_key: this._olmDevice.deviceCurve25519Key, ciphertext: ciphertext, session_id: session.sessionId, // Include our device ID so that recipients can send us a // m.new_device message if they don't have our session key. // XXX: Do we still need this now that m.new_device messages // no longer exist since #483? - device_id: self._deviceId, + device_id: this._deviceId, }; session.useCount++; @@ -855,6 +891,27 @@ MegolmEncryption.prototype._checkForUnknownDevices = function(devicesInRoom) { } }; +/** + * Remove unknown devices from a set of devices. The devicesInRoom parameter + * will be modified. + * + * @param {Object} devicesInRoom userId -> {deviceId -> object} + * devices we should shared the session with. + */ +MegolmEncryption.prototype._removeUnknownDevices = function(devicesInRoom) { + for (const [userId, userDevices] of Object.entries(devicesInRoom)) { + for (const [deviceId, device] of Object.entries(userDevices)) { + if (device.isUnverified() && !device.isKnown()) { + delete userDevices[deviceId]; + } + } + + if (Object.keys(userDevices).length === 0) { + delete devicesInRoom[userId]; + } + } +}; + /** * Get the list of unblocked devices for all users in the room * diff --git a/src/crypto/olmlib.js b/src/crypto/olmlib.js index 7dcdda750..d2218b03e 100644 --- a/src/crypto/olmlib.js +++ b/src/crypto/olmlib.js @@ -113,6 +113,57 @@ export async function encryptMessageForDevice( ); } +/** + * Get the existing olm sessions for the given devices, and the devices that + * don't have olm sessions. + * + * @param {module:crypto/OlmDevice} olmDevice + * + * @param {module:base-apis~MatrixBaseApis} baseApis + * + * @param {object} devicesByUser + * map from userid to list of devices to ensure sessions for + * + * @return {Promise} resolves to an array. The first element of the array is a + * a map of user IDs to arrays of deviceInfo, representing the devices that + * don't have established olm sessions. The second element of the array is + * a map from userId to deviceId to {@link module:crypto~OlmSessionResult} + */ +export async function getExistingOlmSessions( + olmDevice, baseApis, devicesByUser, +) { + const devicesWithoutSession = {}; + const sessions = {}; + + const promises = []; + + for (const [userId, devices] of Object.entries(devicesByUser)) { + for (const deviceInfo of devices) { + const deviceId = deviceInfo.deviceId; + const key = deviceInfo.getIdentityKey(); + promises.push((async () => { + const sessionId = await olmDevice.getSessionIdForDevice( + key, true, + ); + if (sessionId === null) { + devicesWithoutSession[userId] = devicesWithoutSession[userId] || []; + devicesWithoutSession[userId].push(deviceInfo); + } else { + sessions[userId] = sessions[userId] || {}; + sessions[userId][deviceId] = { + device: deviceInfo, + sessionId: sessionId, + }; + } + })()); + } + } + + await Promise.all(promises); + + return [devicesWithoutSession, sessions]; +} + /** * Try to make sure we have established olm sessions for the given devices. * @@ -123,30 +174,33 @@ export async function encryptMessageForDevice( * @param {object} devicesByUser * map from userid to list of devices to ensure sessions for * - * @param {boolean} force If true, establish a new session even if one already exists. - * Optional. + * @param {boolean} [force=false] If true, establish a new session even if one + * already exists. + * + * @param {Number} [otkTimeout] The timeout in milliseconds when requesting + * one-time keys for establishing new olm sessions. * * @return {Promise} resolves once the sessions are complete, to * an Object mapping from userId to deviceId to * {@link module:crypto~OlmSessionResult} */ export async function ensureOlmSessionsForDevices( - olmDevice, baseApis, devicesByUser, force, + olmDevice, baseApis, devicesByUser, force, otkTimeout, ) { + if (typeof force === "number") { + otkTimeout = force; + force = false; + } + const devicesWithoutSession = [ // [userId, deviceId], ... ]; const result = {}; const resolveSession = {}; - for (const userId in devicesByUser) { - if (!devicesByUser.hasOwnProperty(userId)) { - continue; - } + for (const [userId, devices] of Object.entries(devicesByUser)) { result[userId] = {}; - const devices = devicesByUser[userId]; - for (let j = 0; j < devices.length; j++) { - const deviceInfo = devices[j]; + for (const deviceInfo of devices) { const deviceId = deviceInfo.deviceId; const key = deviceInfo.getIdentityKey(); if (!olmDevice._sessionsInProgress[key]) { @@ -197,7 +251,7 @@ export async function ensureOlmSessionsForDevices( let res; try { res = await baseApis.claimOneTimeKeys( - devicesWithoutSession, oneTimeKeyAlgorithm, + devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout, ); } catch (e) { for (const resolver of Object.values(resolveSession)) { @@ -209,12 +263,8 @@ export async function ensureOlmSessionsForDevices( const otk_res = res.one_time_keys || {}; const promises = []; - for (const userId in devicesByUser) { - if (!devicesByUser.hasOwnProperty(userId)) { - continue; - } + for (const [userId, devices] of Object.entries(devicesByUser)) { const userRes = otk_res[userId] || {}; - const devices = devicesByUser[userId]; for (let j = 0; j < devices.length; j++) { const deviceInfo = devices[j]; const deviceId = deviceInfo.deviceId;