From 63c57e8e02a453a322c398ff46ec6fa74d21cf9d Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 9 Jan 2020 22:19:35 -0500 Subject: [PATCH 1/8] record, report, and notify about olm errors --- spec/unit/crypto/algorithms/megolm.spec.js | 193 ++++++++++++++++++ src/crypto/OlmDevice.js | 12 ++ src/crypto/algorithms/megolm.js | 112 +++++++++- src/crypto/index.js | 9 +- .../store/indexeddb-crypto-store-backend.js | 76 ++++++- src/crypto/store/indexeddb-crypto-store.js | 18 ++ src/crypto/store/localStorage-crypto-store.js | 57 ++++++ src/crypto/store/memory-crypto-store.js | 51 +++++ 8 files changed, 517 insertions(+), 11 deletions(-) diff --git a/spec/unit/crypto/algorithms/megolm.spec.js b/spec/unit/crypto/algorithms/megolm.spec.js index 57f6a3fe4..a3134d477 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.js +++ b/spec/unit/crypto/algorithms/megolm.spec.js @@ -11,6 +11,7 @@ import logger from '../../../../lib/logger'; import TestClient from '../../../TestClient'; import olmlib from '../../../../lib/crypto/olmlib'; import Room from '../../../../lib/models/room'; +import DeviceInfo from '../../../../lib/crypto/deviceinfo'; const MatrixEvent = sdk.MatrixEvent; const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; @@ -453,6 +454,99 @@ describe("MegolmDecryption", function() { bobClient2.stopClient(); }); + it("notifies devices when unable to create olm session", async function() { + const aliceClient = (new TestClient( + "@alice:example.com", "alicedevice", + )).client; + const bobClient = (new TestClient( + "@bob:example.com", "bobdevice", + )).client; + await Promise.all([ + aliceClient.initCrypto(), + bobClient.initCrypto(), + ]); + const aliceDevice = aliceClient._crypto._olmDevice; + const bobDevice = bobClient._crypto._olmDevice; + + const encryptionCfg = { + "algorithm": "m.megolm.v1.aes-sha2", + }; + const roomId = "!someroom"; + const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {}); + const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {}); + aliceClient.store.storeRoom(aliceRoom); + bobClient.store.storeRoom(bobRoom); + await aliceClient.setRoomEncryption(roomId, encryptionCfg); + await bobClient.setRoomEncryption(roomId, encryptionCfg); + + aliceRoom.getEncryptionTargetMembers = async () => { + return [ + { + userId: "@alice:example.com", + membership: "join", + }, + { + userId: "@bob:example.com", + membership: "join", + }, + ]; + }; + const BOB_DEVICES = { + bobdevice: { + user_id: "@bob:example.com", + device_id: "bobdevice", + algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], + keys: { + "ed25519:bobdevice": bobDevice.deviceEd25519Key, + "curve25519:bobdevice": bobDevice.deviceCurve25519Key, + }, + known: true, + verified: 1, + }, + }; + + aliceClient._crypto._deviceList.storeDevicesForUser( + "@bob:example.com", BOB_DEVICES, + ); + aliceClient._crypto._deviceList.downloadKeys = async function(userIds) { + return this._getDevicesFromStore(userIds); + }; + + aliceClient.claimOneTimeKeys = async () => { + // Bob has no one-time keys + return { + one_time_keys: {}, + }; + }; + + let run = false; + aliceClient.sendToDevice = async (msgtype, contentMap) => { + run = true; + expect(msgtype).toBe("org.matrix.room_key.withheld"); + expect(contentMap).toStrictEqual({ + '@bob:example.com': { + bobdevice: { + algorithm: "m.megolm.v1.aes-sha2", + code: 'm.no_olm', + reason: 'Unable to establish a secure channel.', + sender_key: aliceDevice.deviceCurve25519Key, + }, + }, + }); + }; + + const event = new MatrixEvent({ + type: "m.room.message", + sender: "@alice:example.com", + room_id: roomId, + event_id: "$event", + content: {}, + }); + await aliceClient._crypto.encryptEvent(event, aliceRoom); + + expect(run).toBe(true); + }); + it("throws an error describing why it doesn't have a key", async function() { const aliceClient = (new TestClient( "@alice:example.com", "alicedevice", @@ -495,4 +589,103 @@ describe("MegolmDecryption", function() { }, }))).rejects.toThrow("The sender has blocked you."); }); + + it("throws an error describing the lack of an olm session", async function() { + const aliceClient = (new TestClient( + "@alice:example.com", "alicedevice", + )).client; + const bobClient = (new TestClient( + "@bob:example.com", "bobdevice", + )).client; + await Promise.all([ + aliceClient.initCrypto(), + bobClient.initCrypto(), + ]); + const bobDevice = bobClient._crypto._olmDevice; + + const roomId = "!someroom"; + + const now = Date.now(); + + aliceClient._crypto._onToDeviceEvent(new MatrixEvent({ + type: "org.matrix.room_key.withheld", + sender: "@bob:example.com", + content: { + algorithm: "m.megolm.v1.aes-sha2", + room_id: roomId, + session_id: "session_id", + sender_key: bobDevice.deviceCurve25519Key, + code: "m.no_olm", + reason: "Unable to establish a secure channel.", + }, + })); + + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + + await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({ + type: "m.room.encrypted", + sender: "@bob:example.com", + event_id: "$event", + room_id: roomId, + content: { + algorithm: "m.megolm.v1.aes-sha2", + ciphertext: "blablabla", + device_id: "bobdevice", + sender_key: bobDevice.deviceCurve25519Key, + session_id: "session_id", + }, + origin_server_ts: now, + }))).rejects.toThrow("The sender was unable to establish a secure channel."); + }); + + it("throws an error to indicate a wedged olm session", async function() { + const aliceClient = (new TestClient( + "@alice:example.com", "alicedevice", + )).client; + const bobClient = (new TestClient( + "@bob:example.com", "bobdevice", + )).client; + await Promise.all([ + aliceClient.initCrypto(), + bobClient.initCrypto(), + ]); + const bobDevice = bobClient._crypto._olmDevice; + + const roomId = "!someroom"; + + const now = Date.now(); + + // pretend we got an event that we can't decrypt + aliceClient._crypto._onToDeviceEvent(new MatrixEvent({ + type: "m.room.encrypted", + sender: "@bob:example.com", + content: { + msgtype: "m.bad.encrypted", + algorithm: "m.megolm.v1.aes-sha2", + session_id: "session_id", + sender_key: bobDevice.deviceCurve25519Key, + }, + })); + + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + + await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({ + type: "m.room.encrypted", + sender: "@bob:example.com", + event_id: "$event", + room_id: roomId, + content: { + algorithm: "m.megolm.v1.aes-sha2", + ciphertext: "blablabla", + device_id: "bobdevice", + sender_key: bobDevice.deviceCurve25519Key, + session_id: "session_id", + }, + origin_server_ts: now, + }))).rejects.toThrow("The secure channel with the sender was corrupted."); + }); }); diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 7a0434682..d613511a4 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -674,6 +674,18 @@ OlmDevice.prototype.matchesSession = async function( return matches; }; +OlmDevice.prototype.recordSessionProblem = async function(deviceKey, type, fixed) { + await this._cryptoStore.storeEndToEndSessionProblem(deviceKey, type, fixed); +}; + +OlmDevice.prototype.sessionMayHaveProblems = async function(deviceKey, timestamp) { + return await this._cryptoStore.getEndToEndSessionProblem(deviceKey, timestamp); +}; + +OlmDevice.prototype.filterOutNotifiedErrorDevices = async function(devices) { + return await this._cryptoStore.filterOutNotifiedErrorDevices(devices); +}; + // Outbound group session // ====================== diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index 909211d6a..ecdeed6c0 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -253,8 +253,10 @@ MegolmEncryption.prototype._ensureOutboundSession = async function( } } + const errorDevices = []; + await self._shareKeyWithDevices( - session, shareMap, + session, shareMap, errorDevices, ); // are there any new blocked devices that we need to notify? @@ -281,6 +283,18 @@ MegolmEncryption.prototype._ensureOutboundSession = async function( } } + + 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); } @@ -345,7 +359,7 @@ MegolmEncryption.prototype._prepareNewSession = async function() { * @return {array>} */ MegolmEncryption.prototype._splitUserDeviceMap = function( - session, chainIndex, devicemap, devicesByUser, + session, chainIndex, devicemap, devicesByUser, errorDevices, ) { const maxToDeviceMessagesPerRequest = 20; @@ -377,6 +391,8 @@ MegolmEncryption.prototype._splitUserDeviceMap = function( // to claim a one-time-key for dead devices on every message. session.markSharedWithDevice(userId, deviceId, chainIndex); + errorDevices.push({userId, deviceInfo}); + // ensureOlmSessionsForUsers has already done the logging, // so just skip it. continue; @@ -532,6 +548,10 @@ MegolmEncryption.prototype._sendBlockedNotificationsToDevices = async function( const message = Object.assign({}, payload); message.code = blockedInfo.code; message.reason = blockedInfo.reason; + if (message.code === "m.no_olm") { + delete message.room_id; + delete message.session_id; + } if (!contentMap[userId]) { contentMap[userId] = {}; @@ -646,7 +666,9 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function( * @param {object} devicesByUser * map from userid to list of devices */ -MegolmEncryption.prototype._shareKeyWithDevices = async function(session, devicesByUser) { +MegolmEncryption.prototype._shareKeyWithDevices = async function( + session, devicesByUser, errorDevices, +) { const key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId); const payload = { type: "m.room_key", @@ -664,7 +686,7 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device ); const userDeviceMaps = this._splitUserDeviceMap( - session, key.chain_index, devicemap, devicesByUser, + session, key.chain_index, devicemap, devicesByUser, errorDevices, ); for (let i = 0; i < userDeviceMaps.length; i++) { @@ -895,6 +917,11 @@ function MegolmDecryption(params) { } utils.inherits(MegolmDecryption, base.DecryptionAlgorithm); +const PROBLEM_DESCRIPTIONS = { + no_olm: "The sender was unable to establish a secure channel.", + unknown: "The secure channel with the sender was corrupted.", +}; + /** * @inheritdoc * @@ -961,6 +988,26 @@ MegolmDecryption.prototype.decryptEvent = async function(event) { // event is still in the pending list; if not, a retry will have been // scheduled, so we needn't send out the request here.) this._requestKeysForEvent(event); + + const problem = await this._olmDevice.sessionMayHaveProblems( + content.sender_key, event.getTs(), + ); + if (problem) { + let problemDescription = PROBLEM_DESCRIPTIONS[problem.type] + || PROBLEM_DESCRIPTIONS.unknown; + if (problem.fixed) { + problemDescription += + " Trying to create a new secure channel and re-requesting the keys"; + } + throw new base.DecryptionError( + "MEGOLM_UNKNOWN_INBOUND_SESSION_ID", + problemDescription, + { + session: content.sender_key + '|' + content.session_id, + }, + ); + } + throw new base.DecryptionError( "MEGOLM_UNKNOWN_INBOUND_SESSION_ID", "The sender's device has not sent us the keys for this message.", @@ -1150,11 +1197,60 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { */ MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) { const content = event.getContent(); + const senderKey = content.sender_key; - await this._olmDevice.addInboundGroupSessionWithheld( - content.room_id, content.sender_key, content.session_id, content.code, - content.reason, - ); + if (content.code === "m.no_olm") { + const sender = event.getSender(); + // if the sender says that they haven't been able to establish an olm + // session, let's proactively establish one + if (await this._olmDevice.getSessionIdForDevice(senderKey)) { + // a session has already been established, so we don't need to + // create a new one. + await this._olmDevice.recordSessionProblem(senderKey, "no_olm", true); + return; + } + const device = this._crypto._deviceList.getDeviceByIdentityKey( + content.algorithm, senderKey, + ); + if (!device) { + logger.info( + "Couldn't find device for identity key " + senderKey + + ": not establishing session", + ); + await this._olmDevice.recordSessionProblem(senderKey, "no_olm", false); + return; + } + await olmlib.ensureOlmSessionsForDevices( + this._olmDevice, this._baseApis, {[sender]: [device]}, false, + ); + const encryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: this._olmDevice.deviceCurve25519Key, + ciphertext: {}, + }; + await olmlib.encryptMessageForDevice( + encryptedContent.ciphertext, + this._userId, + this._deviceId, + this._olmDevice, + sender, + device, + {type: "m.dummy"}, + ); + + await this._olmDevice.recordSessionProblem(senderKey, "no_olm", true); + + await this._baseApis.sendToDevice("m.room.encrypted", { + [sender]: { + [device.deviceId]: encryptedContent, + }, + }); + } else { + await this._olmDevice.addInboundGroupSessionWithheld( + content.room_id, senderKey, content.session_id, content.code, + content.reason, + ); + } }; /** diff --git a/src/crypto/index.js b/src/crypto/index.js index 6e3f8e534..0f09028c3 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -2458,8 +2458,8 @@ Crypto.prototype._onRoomKeyEvent = function(event) { Crypto.prototype._onRoomKeyWithheldEvent = function(event) { const content = event.getContent(); - if (!content.room_id || !content.session_id || !content.algorithm - || !content.sender_key) { + if ((content.code !== "m.no_olm" && (!content.room_id || !content.session_id)) + || !content.algorithm || !content.sender_key) { logger.error("key withheld event is missing fields"); return; } @@ -2609,6 +2609,9 @@ Crypto.prototype._onToDeviceBadEncrypted = async function(event) { "Couldn't find device for identity key " + deviceKey + ": not re-establishing session", ); + await this._olmDevice.recordSessionProblem( + deviceKey, "wedged", false, + ); return; } const devicesByUser = {}; @@ -2640,6 +2643,8 @@ Crypto.prototype._onToDeviceBadEncrypted = async function(event) { {type: "m.dummy"}, ); + await this._olmDevice.recordSessionProblem(deviceKey, "wedged", true); + await this._baseApis.sendToDevice("m.room.encrypted", { [sender]: { [device.deviceId]: encryptedContent, diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index acac5c9f6..7b78f2e09 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -19,7 +19,7 @@ limitations under the License. import logger from '../../logger'; import utils from '../../utils'; -export const VERSION = 8; +export const VERSION = 9; /** * Implementation of a CryptoStore which is backed by an existing @@ -426,6 +426,70 @@ export class Backend { }); } + async storeEndToEndSessionProblem(deviceKey, type, fixed) { + const txn = this._db.transaction("session_problems", "readwrite"); + const objectStore = txn.objectStore("session_problems"); + objectStore.put({ + deviceKey, + type, + fixed, + time: Date.now(), + }); + return promiseifyTxn(txn); + } + + async getEndToEndSessionProblem(deviceKey, timestamp) { + let result; + const txn = this._db.transaction("session_problems", "readwrite"); + const objectStore = txn.objectStore("session_problems"); + const index = objectStore.index("deviceKey"); + const req = index.getAll(deviceKey); + req.onsuccess = (event) => { + const problems = req.result; + if (!problems.length) { + result = null; + return; + } + const lastProblem = problems[problems.length - 1]; + for (const problem of problems) { + if (problem.time > timestamp) { + result = Object.assign({}, problem, {fixed: lastProblem.fixed}); + return; + } + } + if (lastProblem.fixed) { + result = null; + } else { + result = lastProblem; + } + }; + await promiseifyTxn(txn); + return result; + } + + async filterOutNotifiedErrorDevices(devices) { + const txn = this._db.transaction("notified_error_devices", "readwrite"); + const objectStore = txn.objectStore("notified_error_devices"); + + const ret = []; + + await Promise.all(devices.map((device) => { + return new Promise((resolve) => { + const {userId, deviceInfo} = device; + const getReq = objectStore.get([userId, deviceInfo.deviceId]); + getReq.onsuccess = function() { + if (!getReq.result) { + objectStore.put({userId, deviceId: deviceInfo.deviceId}); + ret.push(device); + } + resolve(); + }; + }); + })); + + return ret; + } + // Inbound group sessions getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { @@ -699,6 +763,16 @@ export function upgradeDatabase(db, oldVersion) { keyPath: ["senderCurve25519Key", "sessionId"], }); } + if (oldVersion < 9) { + const problemsStore = db.createObjectStore("session_problems", { + keyPath: ["deviceKey", "time"], + }); + problemsStore.createIndex("deviceKey"); + + db.createObjectStore("notified_error_devices", { + keyPath: ["userId", "deviceId"], + }); + } // Expand as needed. } diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 66c353681..14de039fb 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -409,6 +409,24 @@ export default class IndexedDBCryptoStore { }); } + storeEndToEndSessionProblem(deviceKey, type, fixed) { + return this._backendPromise.then(async (backend) => { + await backend.storeEndToEndSessionProblem(deviceKey, type, fixed); + }); + } + + getEndToEndSessionProblem(deviceKey, timestamp) { + return this._backendPromise.then(async (backend) => { + await backend.getEndToEndSessionProblem(deviceKey, timestamp); + }); + } + + filterOutNotifiedErrorDevices(devices) { + return this._backendPromise.then(async (backend) => { + return await backend.filterOutNotifiedErrorDevices(devices); + }); + } + // Inbound group sessions /** diff --git a/src/crypto/store/localStorage-crypto-store.js b/src/crypto/store/localStorage-crypto-store.js index 1f8220d15..c75d5341b 100644 --- a/src/crypto/store/localStorage-crypto-store.js +++ b/src/crypto/store/localStorage-crypto-store.js @@ -31,6 +31,7 @@ import MemoryCryptoStore from './memory-crypto-store'; const E2E_PREFIX = "crypto."; const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account"; const KEY_CROSS_SIGNING_KEYS = E2E_PREFIX + "cross_signing_keys"; +const KEY_NOTIFIED_ERROR_DEVICES = E2E_PREFIX + "notified_error_devices"; const KEY_DEVICE_DATA = E2E_PREFIX + "device_data"; const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/"; const KEY_INBOUND_SESSION_WITHHELD_PREFIX = E2E_PREFIX + "inboundgroupsessions.withheld/"; @@ -41,6 +42,10 @@ function keyEndToEndSessions(deviceKey) { return E2E_PREFIX + "sessions/" + deviceKey; } +function keyEndToEndSessionProblems(deviceKey) { + return E2E_PREFIX + "session.problems/" + deviceKey; +} + function keyEndToEndInboundGroupSession(senderKey, sessionId) { return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId; } @@ -128,6 +133,58 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { ); } + async storeEndToEndSessionProblem(deviceKey, type, fixed) { + const key = keyEndToEndSessionProblems(deviceKey); + const problems = getJsonItem(this.store, key) || []; + problems.push({type, fixed, time: Date.now()}); + problems.sort((a, b) => { + return a.time - b.time; + }); + setJsonItem(this.store, key, problems); + } + + async getEndToEndSessionProblem(deviceKey, timestamp) { + const key = keyEndToEndSessionProblems(deviceKey); + const problems = getJsonItem(this.store, key) || []; + if (!problems.length) { + return null; + } + const lastProblem = problems[problems.length - 1]; + for (const problem of problems) { + if (problem.time > timestamp) { + return Object.assign({}, problem, {fixed: lastProblem.fixed}); + } + } + if (lastProblem.fixed) { + return null; + } else { + return lastProblem; + } + } + + async filterOutNotifiedErrorDevices(devices) { + const notifiedErrorDevices = + getJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {}; + const ret = []; + + for (const device of devices) { + const {userId, deviceInfo} = device; + if (userId in notifiedErrorDevices) { + if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) { + ret.push(device); + notifiedErrorDevices[userId][deviceInfo.deviceId] = true; + } + } else { + ret.push(device); + notifiedErrorDevices[userId] = {[deviceInfo.deviceId]: true }; + } + } + + setJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES, notifiedErrorDevices); + + return ret; + } + // Inbound Group Sessions getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { diff --git a/src/crypto/store/memory-crypto-store.js b/src/crypto/store/memory-crypto-store.js index 952af6696..f88b5e673 100644 --- a/src/crypto/store/memory-crypto-store.js +++ b/src/crypto/store/memory-crypto-store.js @@ -36,6 +36,10 @@ export default class MemoryCryptoStore { // Map of {devicekey -> {sessionId -> session pickle}} this._sessions = {}; + // Map of {devicekey -> array of problems} + this._sessionProblems = {}; + // Map of {userId -> deviceId -> true} + this._notifiedErrorDevices = {}; // Map of {senderCurve25519Key+'/'+sessionId -> session data object} this._inboundGroupSessions = {}; this._inboundGroupSessionsWithheld = {}; @@ -275,6 +279,53 @@ export default class MemoryCryptoStore { deviceSessions[sessionId] = sessionInfo; } + async storeEndToEndSessionProblem(deviceKey, type, fixed) { + const problems = this._sessionProblems[deviceKey] + = this._sessionProblems[deviceKey] || []; + problems.push({type, fixed, time: Date.now()}); + problems.sort((a, b) => { + return a.time - b.time; + }); + } + + async getEndToEndSessionProblem(deviceKey, timestamp) { + const problems = this._sessionProblems[deviceKey] || []; + if (!problems.length) { + return null; + } + const lastProblem = problems[problems.length - 1]; + for (const problem of problems) { + if (problem.time > timestamp) { + return Object.assign({}, problem, {fixed: lastProblem.fixed}); + } + } + if (lastProblem.fixed) { + return null; + } else { + return lastProblem; + } + } + + async filterOutNotifiedErrorDevices(devices) { + const notifiedErrorDevices = this._notifiedErrorDevices; + const ret = []; + + for (const device of devices) { + const {userId, deviceInfo} = device; + if (userId in notifiedErrorDevices) { + if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) { + ret.push(device); + notifiedErrorDevices[userId][deviceInfo.deviceId] = true; + } + } else { + ret.push(device); + notifiedErrorDevices[userId] = {[deviceInfo.deviceId]: true }; + } + } + + return ret; + } + // Inbound Group Sessions getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { From b3a16cb85216ee55929a5c15e86dc177458e092a Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 10 Jan 2020 13:36:11 -0500 Subject: [PATCH 2/8] lint (and add a comment) --- spec/unit/crypto/algorithms/megolm.spec.js | 1 - src/crypto/algorithms/megolm.js | 8 ++++++++ src/crypto/store/indexeddb-crypto-store-backend.js | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/spec/unit/crypto/algorithms/megolm.spec.js b/spec/unit/crypto/algorithms/megolm.spec.js index a3134d477..e1da35db5 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.js +++ b/spec/unit/crypto/algorithms/megolm.spec.js @@ -11,7 +11,6 @@ import logger from '../../../../lib/logger'; import TestClient from '../../../TestClient'; import olmlib from '../../../../lib/crypto/olmlib'; import Room from '../../../../lib/models/room'; -import DeviceInfo from '../../../../lib/crypto/deviceinfo'; const MatrixEvent = sdk.MatrixEvent; const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index ecdeed6c0..82d54dfc6 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -356,6 +356,10 @@ MegolmEncryption.prototype._prepareNewSession = async function() { * @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>} */ MegolmEncryption.prototype._splitUserDeviceMap = function( @@ -665,6 +669,10 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function( * * @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 */ MegolmEncryption.prototype._shareKeyWithDevices = async function( session, devicesByUser, errorDevices, diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index 7b78f2e09..5b81f581e 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -467,6 +467,7 @@ export class Backend { return result; } + // FIXME: we should probably prune this when devices get deleted async filterOutNotifiedErrorDevices(devices) { const txn = this._db.transaction("notified_error_devices", "readwrite"); const objectStore = txn.objectStore("notified_error_devices"); From ed3fded8e819873eda869765a5c59b56f339ffec Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 13 Jan 2020 13:43:00 +0000 Subject: [PATCH 3/8] Send device messages for the same user in same API call. Currently we split the device messages up to limit the number per call, but that can end up splitting messages to a given users device over separate API calls. This is fine, but means that the server can't e.g. bundle them into a single EDU for remote users or sanity check that the client is sending to the right set of devices (i.e. its device list cache isn't wrong). --- src/crypto/algorithms/megolm.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index 909211d6a..e2c461be0 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -330,6 +330,9 @@ 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. + * * @private * * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session @@ -386,11 +389,6 @@ MegolmEncryption.prototype._splitUserDeviceMap = function( "share keys with device " + userId + ":" + deviceId, ); - if (entriesInCurrentSlice > maxToDeviceMessagesPerRequest) { - // the current slice is filled up. Start inserting into the next slice - entriesInCurrentSlice = 0; - currentSliceId++; - } if (!mapSlices[currentSliceId]) { mapSlices[currentSliceId] = []; } @@ -402,6 +400,17 @@ MegolmEncryption.prototype._splitUserDeviceMap = function( 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 > maxToDeviceMessagesPerRequest) { + // the current slice is filled up. Start inserting into the next slice + entriesInCurrentSlice = 0; + currentSliceId++; + } } return mapSlices; }; From ee51357dbc64fadca8fc0b15d809af90b58c2ff0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 13 Jan 2020 17:37:38 +0000 Subject: [PATCH 4/8] Add an option to ignore unverified devices Hopefully all necessary information is on the docstring. Default behaviour remains unchanged. --- src/client.js | 29 +++++++++++++++++++++++++++++ src/crypto/algorithms/megolm.js | 4 +++- src/crypto/index.js | 24 ++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 1ba3c4604..c3a7acdb7 100644 --- a/src/client.js +++ b/src/client.js @@ -946,6 +946,35 @@ MatrixClient.prototype.getGlobalBlacklistUnverifiedDevices = function() { return this._crypto.getGlobalBlacklistUnverifiedDevices(); }; +/** + * Set whether sendMessage in a room with unknown and unverified devices + * should throw an error and not send them message. This has 'Global' for + * symmertry with setGlobalBlacklistUnverifiedDevices but there is currently + * no room-level equivalent for this setting. + * + * This API is currently UNSTABLE and may change or be removed without notice. + * + * @param {boolean} value whether error on unknown devices + */ +MatrixClient.prototype.setGlobalErrorOnUnknownDevices = function(value) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + this._crypto.setGlobalErrorOnUnknownDevices(value); +}; + +/** + * @return {boolean} whether to error on unknown devices + * + * This API is currently UNSTABLE and may change or be removed without notice. + */ +MatrixClient.prototype.getGlobalErrorOnUnknownDevices = function() { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + return this._crypto.getGlobalErrorOnUnknownDevices(); +}; + /** * Add methods that call the corresponding method in this._crypto * diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index 909211d6a..05fe708ca 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -736,7 +736,9 @@ MegolmEncryption.prototype.encryptMessage = async function(room, eventType, cont // check if any of these devices are not yet known to the user. // if so, warn the user so they can verify or ignore. - self._checkForUnknownDevices(devicesInRoom); + if (this._crypto.getGlobalErrorOnUnknownDevices()) { + self._checkForUnknownDevices(devicesInRoom); + } const session = await self._ensureOutboundSession(devicesInRoom, blocked); const payloadJson = { diff --git a/src/crypto/index.js b/src/crypto/index.js index 6e3f8e534..b7de7f17a 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -176,6 +176,7 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId, this._deviceKeys = {}; this._globalBlacklistUnverifiedDevices = false; + this._globalErrorOnUnknownDevices = true; this._outgoingRoomKeyRequestManager = new OutgoingRoomKeyRequestManager( baseApis, this._deviceId, this._cryptoStore, @@ -1190,6 +1191,29 @@ Crypto.prototype.getGlobalBlacklistUnverifiedDevices = function() { return this._globalBlacklistUnverifiedDevices; }; +/** + * Set whether sendMessage in a room with unknown and unverified devices + * should throw an error and not send them message. This has 'Global' for + * symmertry with setGlobalBlacklistUnverifiedDevices but there is currently + * no room-level equivalent for this setting. + * + * This API is currently UNSTABLE and may change or be removed without notice. + * + * @param {boolean} value whether error on unknown devices + */ +Crypto.prototype.setGlobalErrorOnUnknownDevices = function(value) { + this._globalErrorOnUnknownDevices = value; +}; + +/** + * @return {boolean} whether to error on unknown devices + * + * This API is currently UNSTABLE and may change or be removed without notice. + */ +Crypto.prototype.getGlobalErrorOnUnknownDevices = function() { + return this._globalErrorOnUnknownDevices; +}; + /** * Upload the device keys to the homeserver. * @return {object} A promise that will resolve when the keys are uploaded. From 260040b919cc6b23714c23f2246b731020e0fdea Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 14 Jan 2020 10:07:54 +0000 Subject: [PATCH 5/8] Rename var to match new function --- src/crypto/algorithms/megolm.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index e2c461be0..e2b356d36 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -331,7 +331,8 @@ 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. + * devices we encrypt to per API call. Also filters out devices we don't have + * a session with. * * @private * @@ -350,7 +351,7 @@ MegolmEncryption.prototype._prepareNewSession = async function() { MegolmEncryption.prototype._splitUserDeviceMap = function( session, chainIndex, devicemap, devicesByUser, ) { - const maxToDeviceMessagesPerRequest = 20; + const maxUsersPerRequest = 20; // use an array where the slices of a content map gets stored const mapSlices = []; @@ -406,7 +407,7 @@ MegolmEncryption.prototype._splitUserDeviceMap = function( // 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 > maxToDeviceMessagesPerRequest) { + if (entriesInCurrentSlice > maxUsersPerRequest) { // the current slice is filled up. Start inserting into the next slice entriesInCurrentSlice = 0; currentSliceId++; From b4a93d2dc386f063932b24f85b5dde996cc1cdd3 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 14 Jan 2020 10:08:46 +0000 Subject: [PATCH 6/8] Also apply cahnge to '_splitBlockedDevices' --- src/crypto/algorithms/megolm.js | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index e2b356d36..1a778c136 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -417,6 +417,9 @@ MegolmEncryption.prototype._splitUserDeviceMap = function( }; /** + * Splits the user device map into multiple chunks to reduce the number of + * devices we encrypt to per API call. + * * @private * * @param {object} devicesByUser map from userid to list of devices @@ -424,7 +427,7 @@ MegolmEncryption.prototype._splitUserDeviceMap = function( * @return {array>} the blocked devices, split into chunks */ MegolmEncryption.prototype._splitBlockedDevices = function(devicesByUser) { - const maxToDeviceMessagesPerRequest = 20; + const maxUsersPerRequest = 20; // use an array where the slices of a content map gets stored let currentSlice = []; @@ -434,17 +437,22 @@ MegolmEncryption.prototype._splitBlockedDevices = function(devicesByUser) { const userBlockedDevicesToShareWith = devicesByUser[userId]; for (const blockedInfo of userBlockedDevicesToShareWith) { - if (currentSlice.length > maxToDeviceMessagesPerRequest) { - // the current slice is filled up. Start inserting into the next slice - currentSlice = []; - mapSlices.push(currentSlice); - } - currentSlice.push({ userId: userId, blockedInfo: blockedInfo, }); } + + // 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 (currentSlice.length > maxUsersPerRequest) { + // the current slice is filled up. Start inserting into the next slice + currentSlice = []; + mapSlices.push(currentSlice); + } } if (currentSlice.length === 0) { mapSlices.pop(); From 81ab2aca37e3c3967b4f744c13f28aecb00ff11d Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Jan 2020 14:10:17 +0000 Subject: [PATCH 7/8] spelling Co-Authored-By: J. Ryan Stinnett --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index c3a7acdb7..72c57d60c 100644 --- a/src/client.js +++ b/src/client.js @@ -949,7 +949,7 @@ MatrixClient.prototype.getGlobalBlacklistUnverifiedDevices = function() { /** * Set whether sendMessage in a room with unknown and unverified devices * should throw an error and not send them message. This has 'Global' for - * symmertry with setGlobalBlacklistUnverifiedDevices but there is currently + * symmetry with setGlobalBlacklistUnverifiedDevices but there is currently * no room-level equivalent for this setting. * * This API is currently UNSTABLE and may change or be removed without notice. From f19013143a2a061fa6b1cb47f962147e73c787a6 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 14 Jan 2020 23:47:05 -0500 Subject: [PATCH 8/8] fix indexedDB storage and retry decryption when we get an olm error --- src/crypto/algorithms/base.js | 10 +++ src/crypto/algorithms/megolm.js | 84 +++++++++++++++---- src/crypto/index.js | 51 ++++++++++- .../store/indexeddb-crypto-store-backend.js | 5 +- src/crypto/store/indexeddb-crypto-store.js | 2 +- 5 files changed, 132 insertions(+), 20 deletions(-) diff --git a/src/crypto/algorithms/base.js b/src/crypto/algorithms/base.js index d5e38f477..d8b3644fb 100644 --- a/src/crypto/algorithms/base.js +++ b/src/crypto/algorithms/base.js @@ -159,6 +159,16 @@ class DecryptionAlgorithm { shareKeysWithDevice(keyRequest) { throw new Error("shareKeysWithDevice not supported for this DecryptionAlgorithm"); } + + /** + * Retry decrypting all the events from a sender that haven't been + * decrypted yet. + * + * @param {string} senderKey the sender's key + */ + async retryDecryptionFromSender(senderKey) { + // ignore by default + } } export {DecryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272 diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index 82d54dfc6..4a9ea71a7 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -997,15 +997,17 @@ MegolmDecryption.prototype.decryptEvent = async function(event) { // scheduled, so we needn't send out the request here.) this._requestKeysForEvent(event); + // See if there was a problem with the olm session at the time the + // event was sent. Use a fuzz factor of 2 minutes. const problem = await this._olmDevice.sessionMayHaveProblems( - content.sender_key, event.getTs(), + content.sender_key, event.getTs() - 120000, ); if (problem) { let problemDescription = PROBLEM_DESCRIPTIONS[problem.type] || PROBLEM_DESCRIPTIONS.unknown; if (problem.fixed) { problemDescription += - " Trying to create a new secure channel and re-requesting the keys"; + " Trying to create a new secure channel and re-requesting the keys."; } throw new base.DecryptionError( "MEGOLM_UNKNOWN_INBOUND_SESSION_ID", @@ -1071,11 +1073,16 @@ MegolmDecryption.prototype._requestKeysForEvent = function(event) { */ MegolmDecryption.prototype._addEventToPendingList = function(event) { const content = event.getWireContent(); - const k = content.sender_key + "|" + content.session_id; - if (!this._pendingEvents[k]) { - this._pendingEvents[k] = new Set(); + const senderKey = content.sender_key; + const sessionId = content.session_id; + if (!this._pendingEvents[senderKey]) { + this._pendingEvents[senderKey] = new Map(); } - this._pendingEvents[k].add(event); + const senderPendingEvents = this._pendingEvents[senderKey]; + if (!senderPendingEvents.has(sessionId)) { + senderPendingEvents.set(sessionId, new Set()); + } + senderPendingEvents.get(sessionId).add(event); }; /** @@ -1087,14 +1094,20 @@ MegolmDecryption.prototype._addEventToPendingList = function(event) { */ MegolmDecryption.prototype._removeEventFromPendingList = function(event) { const content = event.getWireContent(); - const k = content.sender_key + "|" + content.session_id; - if (!this._pendingEvents[k]) { + const senderKey = content.sender_key; + const sessionId = content.session_id; + const senderPendingEvents = this._pendingEvents[senderKey]; + const pendingEvents = senderPendingEvents && senderPendingEvents.get(sessionId); + if (!pendingEvents) { return; } - this._pendingEvents[k].delete(event); - if (this._pendingEvents[k].size === 0) { - delete this._pendingEvents[k]; + pendingEvents.delete(event); + if (pendingEvents.size === 0) { + senderPendingEvents.delete(senderKey); + } + if (senderPendingEvents.size === 0) { + delete this._pendingEvents[senderKey]; } }; @@ -1211,10 +1224,17 @@ MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) { const sender = event.getSender(); // if the sender says that they haven't been able to establish an olm // session, let's proactively establish one + + // Note: after we record that the olm session has had a problem, we + // trigger retrying decryption for all the messages from the sender's + // key, so that we can update the error message to indicate the olm + // session problem. + if (await this._olmDevice.getSessionIdForDevice(senderKey)) { // a session has already been established, so we don't need to // create a new one. await this._olmDevice.recordSessionProblem(senderKey, "no_olm", true); + this.retryDecryptionFromSender(senderKey); return; } const device = this._crypto._deviceList.getDeviceByIdentityKey( @@ -1226,6 +1246,7 @@ MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) { ": not establishing session", ); await this._olmDevice.recordSessionProblem(senderKey, "no_olm", false); + this.retryDecryptionFromSender(senderKey); return; } await olmlib.ensureOlmSessionsForDevices( @@ -1247,6 +1268,7 @@ MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) { ); await this._olmDevice.recordSessionProblem(senderKey, "no_olm", true); + this.retryDecryptionFromSender(senderKey); await this._baseApis.sendToDevice("m.room.encrypted", { [sender]: { @@ -1404,13 +1426,20 @@ MegolmDecryption.prototype.importRoomKey = function(session) { * @return {Boolean} whether all messages were successfully decrypted */ MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionId) { - const k = senderKey + "|" + sessionId; - const pending = this._pendingEvents[k]; + const senderPendingEvents = this._pendingEvents[senderKey]; + if (!senderPendingEvents) { + return true; + } + + const pending = senderPendingEvents.get(sessionId); if (!pending) { return true; } - delete this._pendingEvents[k]; + pending.delete(sessionId); + if (pending.size === 0) { + this._pendingEvents[senderKey]; + } await Promise.all([...pending].map(async (ev) => { try { @@ -1420,7 +1449,32 @@ MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionI } })); - return !this._pendingEvents[k]; + // ev.attemptDecryption will re-add to this._pendingEvents if an event + // couldn't be decrypted + return !((this._pendingEvents[senderKey] || {})[sessionId]); +}; + +MegolmDecryption.prototype.retryDecryptionFromSender = async function(senderKey) { + const senderPendingEvents = this._pendingEvents[senderKey]; + logger.warn(senderPendingEvents); + if (!senderPendingEvents) { + return true; + } + + delete this._pendingEvents[senderKey]; + + await Promise.all([...senderPendingEvents].map(async ([_sessionId, pending]) => { + await Promise.all([...pending].map(async (ev) => { + try { + logger.warn(ev.getId()); + await ev.attemptDecryption(this._crypto); + } catch (e) { + // don't die if something goes wrong + } + })); + })); + + return !this._pendingEvents[senderKey]; }; base.registerAlgorithm( diff --git a/src/crypto/index.js b/src/crypto/index.js index 0f09028c3..dcbc4ddc2 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -2464,10 +2464,25 @@ Crypto.prototype._onRoomKeyWithheldEvent = function(event) { return; } + logger.info( + `Got room key withheld event from ${event.getSender()} (${content.sender_key}) ` + + `for ${content.algorithm}/${content.room_id}/${content.session_id} ` + + `with reason ${content.code} (${content.reason})`, + ); + const alg = this._getRoomDecryptor(content.room_id, content.algorithm); if (alg.onRoomKeyWithheldEvent) { alg.onRoomKeyWithheldEvent(event); } + if (!content.room_id) { + // retry decryption for all events sent by the sender_key. This will + // update the events to show a message indicating that the olm session was + // wedged. + const roomDecryptors = this._getRoomDecryptors(content.algorithm); + for (const decryptor of roomDecryptors) { + decryptor.retryDecryptionFromSender(content.sender_key); + } + } }; /** @@ -2583,6 +2598,16 @@ Crypto.prototype._onToDeviceBadEncrypted = async function(event) { const algorithm = content.algorithm; const deviceKey = content.sender_key; + // retry decryption for all events sent by the sender_key. This will + // update the events to show a message indicating that the olm session was + // wedged. + const retryDecryption = () => { + const roomDecryptors = this._getRoomDecryptors(olmlib.MEGOLM_ALGORITHM); + for (const decryptor of roomDecryptors) { + decryptor.retryDecryptionFromSender(deviceKey); + } + }; + if (sender === undefined || deviceKey === undefined || deviceKey === undefined) { return; } @@ -2596,6 +2621,8 @@ Crypto.prototype._onToDeviceBadEncrypted = async function(event) { "New session already forced with device " + sender + ":" + deviceKey + " at " + lastNewSessionForced + ": not forcing another", ); + await this._olmDevice.recordSessionProblem(deviceKey, "wedged", true); + retryDecryption(); return; } @@ -2609,9 +2636,8 @@ Crypto.prototype._onToDeviceBadEncrypted = async function(event) { "Couldn't find device for identity key " + deviceKey + ": not re-establishing session", ); - await this._olmDevice.recordSessionProblem( - deviceKey, "wedged", false, - ); + await this._olmDevice.recordSessionProblem(deviceKey, "wedged", false); + retryDecryption(); return; } const devicesByUser = {}; @@ -2644,6 +2670,7 @@ Crypto.prototype._onToDeviceBadEncrypted = async function(event) { ); await this._olmDevice.recordSessionProblem(deviceKey, "wedged", true); + retryDecryption(); await this._baseApis.sendToDevice("m.room.encrypted", { [sender]: { @@ -2926,6 +2953,24 @@ Crypto.prototype._getRoomDecryptor = function(roomId, algorithm) { }; +/** + * Get all the room decryptors for a given encryption algorithm. + * + * @param {string} algorithm The encryption algorithm + * + * @return {array} An array of room decryptors + */ +Crypto.prototype._getRoomDecryptors = function(algorithm) { + const decryptors = []; + for (const d of Object.values(this._roomDecryptors)) { + if (algorithm in d) { + decryptors.push(d[algorithm]); + } + } + return decryptors; +}; + + /** * sign the given object with our ed25519 key * diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index 5b81f581e..bafa9db60 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -450,6 +450,9 @@ export class Backend { result = null; return; } + problems.sort((a, b) => { + return a.time - b.time; + }); const lastProblem = problems[problems.length - 1]; for (const problem of problems) { if (problem.time > timestamp) { @@ -768,7 +771,7 @@ export function upgradeDatabase(db, oldVersion) { const problemsStore = db.createObjectStore("session_problems", { keyPath: ["deviceKey", "time"], }); - problemsStore.createIndex("deviceKey"); + problemsStore.createIndex("deviceKey", "deviceKey"); db.createObjectStore("notified_error_devices", { keyPath: ["userId", "deviceId"], diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 14de039fb..52881ff66 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -417,7 +417,7 @@ export default class IndexedDBCryptoStore { getEndToEndSessionProblem(deviceKey, timestamp) { return this._backendPromise.then(async (backend) => { - await backend.getEndToEndSessionProblem(deviceKey, timestamp); + return await backend.getEndToEndSessionProblem(deviceKey, timestamp); }); }