From 269faaf0ebbc6541542573917f67d050a6b9c542 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 18 Dec 2023 18:15:09 +0000 Subject: [PATCH] Implement `CryptoStore.deleteEndToEnd{,InboundGroup}SessionsBatch` --- spec/unit/crypto/store/CryptoStore.spec.ts | 129 +++++++++++++----- src/crypto/store/base.ts | 18 +++ .../store/indexeddb-crypto-store-backend.ts | 48 +++++++ src/crypto/store/indexeddb-crypto-store.ts | 24 ++++ src/crypto/store/localStorage-crypto-store.ts | 36 +++++ src/crypto/store/memory-crypto-store.ts | 34 +++++ 6 files changed, 252 insertions(+), 37 deletions(-) diff --git a/spec/unit/crypto/store/CryptoStore.spec.ts b/spec/unit/crypto/store/CryptoStore.spec.ts index 43141ec30fc..1cc7177407b 100644 --- a/spec/unit/crypto/store/CryptoStore.spec.ts +++ b/spec/unit/crypto/store/CryptoStore.spec.ts @@ -17,7 +17,7 @@ limitations under the License. import "fake-indexeddb/auto"; import "jest-localstorage-mock"; import { IndexedDBCryptoStore, LocalStorageCryptoStore, MemoryCryptoStore } from "../../../../src"; -import { CryptoStore, MigrationState } from "../../../../src/crypto/store/base"; +import { CryptoStore, MigrationState, SESSION_BATCH_SIZE } from "../../../../src/crypto/store/base"; describe.each([ ["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(global.indexedDB, "tests")], @@ -59,7 +59,7 @@ describe.each([ }); }); - describe("getEndToEndSessionsBatch", () => { + describe("get/delete EndToEndSessionsBatch", () => { beforeEach(async () => { await store.startup(); }); @@ -72,9 +72,50 @@ describe.each([ // First store some sessions in the db const N_DEVICES = 6; const N_SESSIONS_PER_DEVICE = 6; + await createSessions(N_DEVICES, N_SESSIONS_PER_DEVICE); + + // Then, get a batch and check it looks right. + const batch = await store.getEndToEndSessionsBatch(); + expect(batch!.length).toEqual(N_DEVICES * N_SESSIONS_PER_DEVICE); + for (let i = 0; i < N_DEVICES; i++) { + for (let j = 0; j < N_SESSIONS_PER_DEVICE; j++) { + const r = batch![i * N_DEVICES + j]; + + expect(r.deviceKey).toEqual(`device${i}`); + expect(r.sessionId).toEqual(`session${j}`); + } + } + }); + + it("returns another batch of sessions after the first batch is deleted", async () => { + // First store some sessions in the db + const N_DEVICES = 8; + const N_SESSIONS_PER_DEVICE = 8; + await createSessions(N_DEVICES, N_SESSIONS_PER_DEVICE); + + // Get the first batch + const batch = (await store.getEndToEndSessionsBatch())!; + expect(batch.length).toEqual(SESSION_BATCH_SIZE); + + // ... and delete. + await store.deleteEndToEndSessionsBatch(batch); + + // Fetch a second batch + const batch2 = (await store.getEndToEndSessionsBatch())!; + expect(batch2.length).toEqual(N_DEVICES * N_SESSIONS_PER_DEVICE - SESSION_BATCH_SIZE); + + // ... and delete. + await store.deleteEndToEndSessionsBatch(batch2); + + // the batch should now be null. + expect(await store.getEndToEndSessionsBatch()).toBe(null); + }); + + /** Create a bunch of fake Olm sessions and stash them in the DB. */ + async function createSessions(nDevices: number, nSessionsPerDevice: number) { await store.doTxn("readwrite", IndexedDBCryptoStore.STORE_SESSIONS, (txn) => { - for (let i = 0; i < N_DEVICES; i++) { - for (let j = 0; j < N_SESSIONS_PER_DEVICE; j++) { + for (let i = 0; i < nDevices; i++) { + for (let j = 0; j < nSessionsPerDevice; j++) { store.storeEndToEndSession( `device${i}`, `session${j}`, @@ -87,42 +128,64 @@ describe.each([ } } }); + } + }); - // Then, get a batch and check it looks right. - const batch = await store.getEndToEndSessionsBatch(); + describe("get/delete EndToEndInboundGroupSessionsBatch", () => { + beforeEach(async () => { + await store.startup(); + }); + + it("returns null at first", async () => { + expect(await store.getEndToEndInboundGroupSessionsBatch()).toBe(null); + }); + + it("returns a batch of sessions", async () => { + const N_DEVICES = 6; + const N_SESSIONS_PER_DEVICE = 6; + await createSessions(N_DEVICES, N_SESSIONS_PER_DEVICE); + + const batch = await store.getEndToEndInboundGroupSessionsBatch(); expect(batch!.length).toEqual(N_DEVICES * N_SESSIONS_PER_DEVICE); for (let i = 0; i < N_DEVICES; i++) { for (let j = 0; j < N_SESSIONS_PER_DEVICE; j++) { const r = batch![i * N_DEVICES + j]; - expect(r.deviceKey).toEqual(`device${i}`); + expect(r.senderKey).toEqual(pad43(`device${i}`)); expect(r.sessionId).toEqual(`session${j}`); } } }); - // TODO: add a test which deletes them and gets the next batch - }); + it("returns another batch of sessions after the first batch is deleted", async () => { + // First store some sessions in the db + const N_DEVICES = 8; + const N_SESSIONS_PER_DEVICE = 8; + await createSessions(N_DEVICES, N_SESSIONS_PER_DEVICE); - describe("getEndToEndInboundGroupSessionsBatch", () => { - beforeEach(async () => { - await store.startup(); - }); + // Get the first batch + const batch = (await store.getEndToEndInboundGroupSessionsBatch())!; + expect(batch.length).toEqual(SESSION_BATCH_SIZE); - it("returns null at first", async () => { + // ... and delete. + await store.deleteEndToEndInboundGroupSessionsBatch(batch); + + // Fetch a second batch + const batch2 = (await store.getEndToEndInboundGroupSessionsBatch())!; + expect(batch2.length).toEqual(N_DEVICES * N_SESSIONS_PER_DEVICE - SESSION_BATCH_SIZE); + + // ... and delete. + await store.deleteEndToEndInboundGroupSessionsBatch(batch2); + + // the batch should now be null. expect(await store.getEndToEndInboundGroupSessionsBatch()).toBe(null); }); - it("returns a batch of sessions", async () => { - /** Pad a string to 43 characters long */ - function pad43(x: string): string { - return x + ".".repeat(43 - x.length); - } - const N_DEVICES = 6; - const N_SESSIONS_PER_DEVICE = 6; + /** Create a bunch of fake megolm sessions and stash them in the DB. */ + async function createSessions(nDevices: number, nSessionsPerDevice: number) { await store.doTxn("readwrite", IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, (txn) => { - for (let i = 0; i < N_DEVICES; i++) { - for (let j = 0; j < N_SESSIONS_PER_DEVICE; j++) { + for (let i = 0; i < nDevices; i++) { + for (let j = 0; j < nSessionsPerDevice; j++) { store.storeEndToEndInboundGroupSession( pad43(`device${i}`), `session${j}`, @@ -137,19 +200,11 @@ describe.each([ } } }); - - const batch = await store.getEndToEndInboundGroupSessionsBatch(); - expect(batch!.length).toEqual(N_DEVICES * N_SESSIONS_PER_DEVICE); - for (let i = 0; i < N_DEVICES; i++) { - for (let j = 0; j < N_SESSIONS_PER_DEVICE; j++) { - const r = batch![i * N_DEVICES + j]; - - expect(r.senderKey).toEqual(pad43(`device${i}`)); - expect(r.sessionId).toEqual(`session${j}`); - } - } - }); - - // TODO: add a test which deletes them and gets the next batch + } }); }); + +/** Pad a string to 43 characters long */ +function pad43(x: string): string { + return x + ".".repeat(43 - x.length); +} diff --git a/src/crypto/store/base.ts b/src/crypto/store/base.ts index 9ee07aaad8b..3a363ca7f09 100644 --- a/src/crypto/store/base.ts +++ b/src/crypto/store/base.ts @@ -140,6 +140,15 @@ export interface CryptoStore { */ getEndToEndSessionsBatch(): Promise; + /** + * Delete a batch of end-to-end sessions from the database. + * + * Any sessions in the list which are not found are silently ignored. + * + * @internal + */ + deleteEndToEndSessionsBatch(sessions: { deviceKey?: string; sessionId?: string }[]): Promise; + // Inbound Group Sessions getEndToEndInboundGroupSession( senderCurve25519Key: string, @@ -175,6 +184,15 @@ export interface CryptoStore { */ getEndToEndInboundGroupSessionsBatch(): Promise; + /** + * Delete a batch of Megolm sessions from the database. + * + * Any sessions in the list which are not found are silently ignored. + * + * @internal + */ + deleteEndToEndInboundGroupSessionsBatch(sessions: { senderKey: string; sessionId: string }[]): Promise; + // Device Data getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void; storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void; diff --git a/src/crypto/store/indexeddb-crypto-store-backend.ts b/src/crypto/store/indexeddb-crypto-store-backend.ts index 2ac6493d635..2117b7af284 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.ts +++ b/src/crypto/store/indexeddb-crypto-store-backend.ts @@ -663,6 +663,29 @@ export class Backend implements CryptoStore { return result; } + /** + * Delete a batch of Olm sessions from the database. + * + * Implementation of {@link CryptoStore.deleteEndToEndSessionsBatch}. + * + * @internal + */ + public async deleteEndToEndSessionsBatch(sessions: { deviceKey: string; sessionId: string }[]): Promise { + await this.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], async (txn) => { + try { + const objectStore = txn.objectStore(IndexedDBCryptoStore.STORE_SESSIONS); + for (const { deviceKey, sessionId } of sessions) { + const req = objectStore.delete([deviceKey, sessionId]); + await new Promise((resolve) => { + req.onsuccess = resolve; + }); + } + } catch (e) { + abortWithException(txn, e); + } + }); + } + // Inbound group sessions public getEndToEndInboundGroupSession( @@ -824,6 +847,31 @@ export class Backend implements CryptoStore { return result; } + /** + * Delete a batch of Megolm sessions from the database. + * + * Implementation of {@link CryptoStore#deleteEndToEndInboundGroupSessionsBatch}. + * + * @internal + */ + public async deleteEndToEndInboundGroupSessionsBatch( + sessions: { senderKey: string; sessionId: string }[], + ): Promise { + await this.doTxn("readwrite", [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], async (txn) => { + try { + const objectStore = txn.objectStore(IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS); + for (const { senderKey, sessionId } of sessions) { + const req = objectStore.delete([senderKey, sessionId]); + await new Promise((resolve) => { + req.onsuccess = resolve; + }); + } + } catch (e) { + abortWithException(txn, e); + } + }); + } + public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void { const objectStore = txn.objectStore("device_data"); const getReq = objectStore.get("-"); diff --git a/src/crypto/store/indexeddb-crypto-store.ts b/src/crypto/store/indexeddb-crypto-store.ts index 6ebcc7eb193..caaa8091a87 100644 --- a/src/crypto/store/indexeddb-crypto-store.ts +++ b/src/crypto/store/indexeddb-crypto-store.ts @@ -513,6 +513,17 @@ export class IndexedDBCryptoStore implements CryptoStore { return this.backend!.getEndToEndSessionsBatch(); } + /** + * Delete a batch of Olm sessions from the database. + * + * Implementation of {@link CryptoStore.deleteEndToEndSessionsBatch}. + * + * @internal + */ + public deleteEndToEndSessionsBatch(sessions: { deviceKey: string; sessionId: string }[]): Promise { + return this.backend!.deleteEndToEndSessionsBatch(sessions); + } + // Inbound group sessions /** @@ -600,6 +611,19 @@ export class IndexedDBCryptoStore implements CryptoStore { return this.backend!.getEndToEndInboundGroupSessionsBatch(); } + /** + * Delete a batch of Megolm sessions from the database. + * + * Implementation of {@link CryptoStore#deleteEndToEndInboundGroupSessionsBatch}. + * + * @internal + */ + public deleteEndToEndInboundGroupSessionsBatch( + sessions: { senderKey: string; sessionId: string }[], + ): Promise { + return this.backend!.deleteEndToEndInboundGroupSessionsBatch(sessions); + } + // End-to-end device tracking /** diff --git a/src/crypto/store/localStorage-crypto-store.ts b/src/crypto/store/localStorage-crypto-store.ts index 091e2a628b3..432dfcb765f 100644 --- a/src/crypto/store/localStorage-crypto-store.ts +++ b/src/crypto/store/localStorage-crypto-store.ts @@ -266,6 +266,26 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { return result; } + /** + * Delete a batch of Olm sessions from the database. + * + * Implementation of {@link CryptoStore.deleteEndToEndSessionsBatch}. + * + * @internal + */ + public async deleteEndToEndSessionsBatch(sessions: { deviceKey: string; sessionId: string }[]): Promise { + for (const { deviceKey, sessionId } of sessions) { + const deviceSessions = this._getEndToEndSessions(deviceKey) || {}; + delete deviceSessions[sessionId]; + if (Object.keys(deviceSessions).length === 0) { + // No more sessions for this device. + this.store.removeItem(keyEndToEndSessions(deviceKey)); + } else { + setJsonItem(this.store, keyEndToEndSessions(deviceKey), deviceSessions); + } + } + } + // Inbound Group Sessions public getEndToEndInboundGroupSession( @@ -367,6 +387,22 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { return result; } + /** + * Delete a batch of Megolm sessions from the database. + * + * Implementation of {@link CryptoStore#deleteEndToEndInboundGroupSessionsBatch}. + * + * @internal + */ + public async deleteEndToEndInboundGroupSessionsBatch( + sessions: { senderKey: string; sessionId: string }[], + ): Promise { + for (const { senderKey, sessionId } of sessions) { + const k = keyEndToEndInboundGroupSession(senderKey, sessionId); + this.store.removeItem(k); + } + } + public getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void { func(getJsonItem(this.store, KEY_DEVICE_DATA)); } diff --git a/src/crypto/store/memory-crypto-store.ts b/src/crypto/store/memory-crypto-store.ts index 1ceead23777..ef5d1a2ebfd 100644 --- a/src/crypto/store/memory-crypto-store.ts +++ b/src/crypto/store/memory-crypto-store.ts @@ -450,6 +450,24 @@ export class MemoryCryptoStore implements CryptoStore { return result; } + /** + * Delete a batch of Olm sessions from the database. + * + * Implementation of {@link CryptoStore.deleteEndToEndSessionsBatch}. + * + * @internal + */ + public async deleteEndToEndSessionsBatch(sessions: { deviceKey: string; sessionId: string }[]): Promise { + for (const { deviceKey, sessionId } of sessions) { + const deviceSessions = this.sessions[deviceKey] || {}; + delete deviceSessions[sessionId]; + if (Object.keys(deviceSessions).length === 0) { + // No more sessions for this device. + delete this.sessions[deviceKey]; + } + } + } + // Inbound Group Sessions public getEndToEndInboundGroupSession( @@ -538,6 +556,22 @@ export class MemoryCryptoStore implements CryptoStore { return result; } + /** + * Delete a batch of Megolm sessions from the database. + * + * Implementation of {@link CryptoStore#deleteEndToEndInboundGroupSessionsBatch}. + * + * @internal + */ + public async deleteEndToEndInboundGroupSessionsBatch( + sessions: { senderKey: string; sessionId: string }[], + ): Promise { + for (const { senderKey, sessionId } of sessions) { + const k = senderKey + "/" + sessionId; + delete this.inboundGroupSessions[k]; + } + } + // Device Data public getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void {