From c6d32ea2b09e0d0161b50ac786bfad0ef5157e51 Mon Sep 17 00:00:00 2001 From: Zoe Date: Thu, 2 Apr 2020 14:41:50 +0100 Subject: [PATCH] Expose function to force-reset outgoing room key requests --- .../crypto/outgoing-room-key-requests.spec.js | 87 +++++++++++++++++++ src/crypto/OutgoingRoomKeyRequestManager.js | 17 +++- .../store/indexeddb-crypto-store-backend.js | 19 ++++ src/crypto/store/indexeddb-crypto-store.js | 11 +++ src/crypto/store/memory-crypto-store.js | 13 +++ 5 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 spec/unit/crypto/outgoing-room-key-requests.spec.js diff --git a/spec/unit/crypto/outgoing-room-key-requests.spec.js b/spec/unit/crypto/outgoing-room-key-requests.spec.js new file mode 100644 index 000000000..a27629f60 --- /dev/null +++ b/spec/unit/crypto/outgoing-room-key-requests.spec.js @@ -0,0 +1,87 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { + IndexedDBCryptoStore, +} from '../../../src/crypto/store/indexeddb-crypto-store'; +import {MemoryCryptoStore} from '../../../src/crypto/store/memory-crypto-store'; +import 'fake-indexeddb/auto'; +import 'jest-localstorage-mock'; + +import { + ROOM_KEY_REQUEST_STATES, +} from '../../../src/crypto/OutgoingRoomKeyRequestManager'; + +const requests = [ + { + requestId: "A", + requestBody: { session_id: "A", room_id: "A" }, + state: ROOM_KEY_REQUEST_STATES.SENT, + }, + { + requestId: "B", + requestBody: { session_id: "B", room_id: "B" }, + state: ROOM_KEY_REQUEST_STATES.SENT, + }, + { + requestId: "C", + requestBody: { session_id: "C", room_id: "C" }, + state: ROOM_KEY_REQUEST_STATES.UNSENT, + }, +]; + +describe.each([ + ["IndexedDBCryptoStore", + () => new IndexedDBCryptoStore(global.indexedDB, "tests")], + ["LocalStorageCryptoStore", + () => new IndexedDBCryptoStore(undefined, "tests")], + ["MemoryCryptoStore", () => { + const store = new IndexedDBCryptoStore(undefined, "tests"); + store._backend = new MemoryCryptoStore(); + store._backendPromise = Promise.resolve(store._backend); + return store; + }], +])("Outgoing room key requests [%s]", function(name, dbFactory) { + let store; + + beforeAll(async () => { + store = dbFactory(); + await store.startup(); + await Promise.all(requests.map((request) => + store.getOrAddOutgoingRoomKeyRequest(request), + )); + }); + + it("getAllOutgoingRoomKeyRequestsByState retrieves all entries in a given state", + async () => { + const r = await + store.getAllOutgoingRoomKeyRequestsByState(ROOM_KEY_REQUEST_STATES.SENT); + expect(r).toHaveLength(2); + requests.filter((e) => e.state == ROOM_KEY_REQUEST_STATES.SENT).forEach((e) => { + expect(r).toContainEqual(e); + }); + }); + + test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state", + async () => { + const r = + await store.getOutgoingRoomKeyRequestByState([ROOM_KEY_REQUEST_STATES.SENT]); + expect(r).not.toBeNull(); + expect(r).not.toBeUndefined(); + expect(r.state).toEqual(ROOM_KEY_REQUEST_STATES.SENT); + expect(requests).toContainEqual(r); + }); +}); diff --git a/src/crypto/OutgoingRoomKeyRequestManager.js b/src/crypto/OutgoingRoomKeyRequestManager.js index 803791df4..15dd09c21 100644 --- a/src/crypto/OutgoingRoomKeyRequestManager.js +++ b/src/crypto/OutgoingRoomKeyRequestManager.js @@ -58,7 +58,7 @@ const SEND_KEY_REQUESTS_DELAY_MS = 500; * * @enum {number} */ -const ROOM_KEY_REQUEST_STATES = { +export const ROOM_KEY_REQUEST_STATES = { /** request not yet sent */ UNSENT: 0, @@ -327,6 +327,21 @@ export class OutgoingRoomKeyRequestManager { ); } + /** + * Find anything in `sent` state, and kick it around the loop again. + * This is intended for situations where something substantial has changed, and we + * don't really expect the other end to even care about the cancellation. + * For example, after initialization or self-verification. + * @return {Promise} An array of `sendRoomKeyRequest` outputs. + */ + async cancelAndResendAllOutgoingRequests() { + const outgoings = await this._cryptoStore.getAllOutgoingRoomKeyRequestsByState( + ROOM_KEY_REQUEST_STATES.SENT, + ); + return Promise.all(outgoings.map(({ requestBody, recipients }) => + this.sendRoomKeyRequest(requestBody, recipients, true))); + } + // start the background timer to send queued requests, if the timer isn't // already running _startTimer() { diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index 51c7006de..135d31611 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -60,6 +60,7 @@ export class Backend { return new Promise((resolve, reject) => { const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite"); txn.onerror = reject; + txn.onabort = (ev) => reject(ev.target.error); // first see if we already have an entry for this request. this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => { @@ -203,6 +204,24 @@ export class Backend { return promiseifyTxn(txn).then(() => result); } + /** + * + * @param {Number} wantedState + * @return {Promise>} All elements in a given state + */ + getAllOutgoingRoomKeyRequestsByState(wantedState) { + return new Promise((resolve, reject) => { + const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly"); + const store = txn.objectStore("outgoingRoomKeyRequests"); + const index = store.index("state"); + const request = index.getAll(wantedState); + + request.onsuccess = (ev) => resolve(ev.target.result); + request.onerror = (ev) => reject(ev.target.error); + request.onabort = (ev) => reject(ev.target.error); + }); + } + getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) { let stateIndex = 0; const results = []; diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 44d3afb0a..3dc49858c 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -225,6 +225,17 @@ export class IndexedDBCryptoStore { return this._backend.getOutgoingRoomKeyRequestByState(wantedStates); } + /** + * Look for room key requests by state – + * unlike above, return a list of all entries in one state. + * + * @param {Number} wantedState + * @return {Promise>} Returns an array of requests in the given state + */ + getAllOutgoingRoomKeyRequestsByState(wantedState) { + return this._backend.getAllOutgoingRoomKeyRequestsByState(wantedState); + } + /** * Look for room key requests by target device and state * diff --git a/src/crypto/store/memory-crypto-store.js b/src/crypto/store/memory-crypto-store.js index 5af806a94..5170fb2c3 100644 --- a/src/crypto/store/memory-crypto-store.js +++ b/src/crypto/store/memory-crypto-store.js @@ -166,6 +166,19 @@ export class MemoryCryptoStore { return Promise.resolve(null); } + /** + * + * @param {Number} wantedState + * @return {Promise>} All OutgoingRoomKeyRequests in state + */ + getAllOutgoingRoomKeyRequestsByState(wantedState) { + return Promise.resolve( + this._outgoingRoomKeyRequests.filter( + (r) => r.state == wantedState, + ), + ); + } + getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) { const results = [];