Skip to content

Commit

Permalink
Implement CryptoStore.deleteEndToEnd{,InboundGroup}SessionsBatch
Browse files Browse the repository at this point in the history
  • Loading branch information
richvdh committed Dec 18, 2023
1 parent 34b4ab7 commit 269faaf
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 37 deletions.
129 changes: 92 additions & 37 deletions spec/unit/crypto/store/CryptoStore.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")],
Expand Down Expand Up @@ -59,7 +59,7 @@ describe.each([
});
});

describe("getEndToEndSessionsBatch", () => {
describe("get/delete EndToEndSessionsBatch", () => {
beforeEach(async () => {
await store.startup();
});
Expand All @@ -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}`,
Expand All @@ -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}`,
Expand All @@ -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);
}
18 changes: 18 additions & 0 deletions src/crypto/store/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,15 @@ export interface CryptoStore {
*/
getEndToEndSessionsBatch(): Promise<ISessionInfo[] | null>;

/**
* 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<void>;

// Inbound Group Sessions
getEndToEndInboundGroupSession(
senderCurve25519Key: string,
Expand Down Expand Up @@ -175,6 +184,15 @@ export interface CryptoStore {
*/
getEndToEndInboundGroupSessionsBatch(): Promise<ISession[] | null>;

/**
* 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<void>;

// Device Data
getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void;
storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void;
Expand Down
48 changes: 48 additions & 0 deletions src/crypto/store/indexeddb-crypto-store-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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, <Error>e);
}
});
}

// Inbound group sessions

public getEndToEndInboundGroupSession(
Expand Down Expand Up @@ -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<void> {
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, <Error>e);
}
});
}

public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void {
const objectStore = txn.objectStore("device_data");
const getReq = objectStore.get("-");
Expand Down
24 changes: 24 additions & 0 deletions src/crypto/store/indexeddb-crypto-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
return this.backend!.deleteEndToEndSessionsBatch(sessions);
}

// Inbound group sessions

/**
Expand Down Expand Up @@ -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<void> {
return this.backend!.deleteEndToEndInboundGroupSessionsBatch(sessions);
}

// End-to-end device tracking

/**
Expand Down
36 changes: 36 additions & 0 deletions src/crypto/store/localStorage-crypto-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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(
Expand Down Expand Up @@ -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<void> {
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));
}
Expand Down
Loading

0 comments on commit 269faaf

Please sign in to comment.