From f94dbdec0fd8fa2043f04a06bdeb9a02665841c9 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 5 Feb 2024 15:59:02 +0100 Subject: [PATCH] Add utility to check for non migrated legacy db (#4055) * Add utility to check for non migrated legacy db * code review changes * add unit tests for existsAndIsNotMigrated * ensure indexeddb is clean for each state --- .../crypto/store/IndexedDBCryptoStore.spec.ts | 73 +++++++++++++++++++ src/crypto/store/base.ts | 7 ++ .../store/indexeddb-crypto-store-backend.ts | 4 +- src/crypto/store/indexeddb-crypto-store.ts | 47 ++++++++++++ 4 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 spec/unit/crypto/store/IndexedDBCryptoStore.spec.ts diff --git a/spec/unit/crypto/store/IndexedDBCryptoStore.spec.ts b/spec/unit/crypto/store/IndexedDBCryptoStore.spec.ts new file mode 100644 index 000000000..52fd574ba --- /dev/null +++ b/spec/unit/crypto/store/IndexedDBCryptoStore.spec.ts @@ -0,0 +1,73 @@ +/* +Copyright 2024 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 "fake-indexeddb/auto"; +import { IndexedDBCryptoStore } from "../../../../src"; +import { MigrationState } from "../../../../src/crypto/store/base"; + +describe("IndexedDBCryptoStore", () => { + describe("Test `existsAndIsNotMigrated`", () => { + beforeEach(async () => { + // eslint-disable-next-line no-global-assign + indexedDB = new IDBFactory(); + }); + + it("Should be true if there is a legacy database", async () => { + // should detect a store that is not migrated + const store = new IndexedDBCryptoStore(global.indexedDB, "tests"); + await store.startup(); + + const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests"); + + expect(result).toBe(true); + }); + + it("Should be true if there is a legacy database in non migrated state", async () => { + // should detect a store that is not migrated + const store = new IndexedDBCryptoStore(global.indexedDB, "tests"); + await store.startup(); + await store.setMigrationState(MigrationState.NOT_STARTED); + + const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests"); + + expect(result).toBe(true); + }); + + describe.each([ + MigrationState.INITIAL_DATA_MIGRATED, + MigrationState.OLM_SESSIONS_MIGRATED, + MigrationState.MEGOLM_SESSIONS_MIGRATED, + MigrationState.ROOM_SETTINGS_MIGRATED, + ])("Exists and Migration state is %s", (migrationState) => { + it("Should be false if migration has started", async () => { + // should detect a store that is not migrated + const store = new IndexedDBCryptoStore(global.indexedDB, "tests"); + await store.startup(); + await store.setMigrationState(migrationState); + + const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests"); + + expect(result).toBe(false); + }); + }); + + it("Should be false if there is no legacy database", async () => { + const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests"); + + expect(result).toBe(false); + }); + }); +}); diff --git a/src/crypto/store/base.ts b/src/crypto/store/base.ts index 2e01d6815..d523981b2 100644 --- a/src/crypto/store/base.ts +++ b/src/crypto/store/base.ts @@ -306,6 +306,13 @@ export interface ParkedSharedHistory { forwardingCurve25519KeyChain: string[]; } +/** + * Keys for the `account` object store to store the migration state. + * Values are defined in `MigrationState`. + * @internal + */ +export const ACCOUNT_OBJECT_KEY_MIGRATION_STATE = "migrationState"; + /** * A record of which steps have been completed in the libolm to Rust Crypto migration. * diff --git a/src/crypto/store/indexeddb-crypto-store-backend.ts b/src/crypto/store/indexeddb-crypto-store-backend.ts index 454f69e0e..c4908aee1 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.ts +++ b/src/crypto/store/indexeddb-crypto-store-backend.ts @@ -30,6 +30,7 @@ import { ParkedSharedHistory, SecretStorePrivateKeys, SESSION_BATCH_SIZE, + ACCOUNT_OBJECT_KEY_MIGRATION_STATE, } from "./base"; import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index"; import { ICrossSigningKey } from "../../client"; @@ -40,9 +41,6 @@ import { IndexedDBCryptoStore } from "./indexeddb-crypto-store"; const PROFILE_TRANSACTIONS = false; -/* Keys for the `account` object store */ -const ACCOUNT_OBJECT_KEY_MIGRATION_STATE = "migrationState"; - /** * Implementation of a CryptoStore which is backed by an existing * IndexedDB connection. Generally you want IndexedDBCryptoStore diff --git a/src/crypto/store/indexeddb-crypto-store.ts b/src/crypto/store/indexeddb-crypto-store.ts index dc33d5f81..dc104e19e 100644 --- a/src/crypto/store/indexeddb-crypto-store.ts +++ b/src/crypto/store/indexeddb-crypto-store.ts @@ -33,6 +33,7 @@ import { OutgoingRoomKeyRequest, ParkedSharedHistory, SecretStorePrivateKeys, + ACCOUNT_OBJECT_KEY_MIGRATION_STATE, } from "./base"; import { IRoomKeyRequestBody } from "../index"; import { ICrossSigningKey } from "../../client"; @@ -63,6 +64,52 @@ export class IndexedDBCryptoStore implements CryptoStore { return IndexedDBHelpers.exists(indexedDB, dbName); } + /** + * Utility to check if a legacy crypto store exists and has not been migrated. + * Returns true if the store exists and has not been migrated, false otherwise. + */ + public static existsAndIsNotMigrated(indexedDb: IDBFactory, dbName: string): Promise { + return new Promise((resolve, reject) => { + let exists = true; + const openDBRequest = indexedDb.open(dbName); + openDBRequest.onupgradeneeded = (): void => { + // Since we did not provide an explicit version when opening, this event + // should only fire if the DB did not exist before at any version. + exists = false; + }; + openDBRequest.onblocked = (): void => reject(openDBRequest.error); + openDBRequest.onsuccess = (): void => { + const db = openDBRequest.result; + if (!exists) { + db.close(); + // The DB did not exist before, but has been created as part of this + // existence check. Delete it now to restore previous state. Delete can + // actually take a while to complete in some browsers, so don't wait for + // it. This won't block future open calls that a store might issue next to + // properly set up the DB. + indexedDb.deleteDatabase(dbName); + resolve(false); + } else { + const tx = db.transaction([IndexedDBCryptoStore.STORE_ACCOUNT], "readonly"); + const objectStore = tx.objectStore(IndexedDBCryptoStore.STORE_ACCOUNT); + const getReq = objectStore.get(ACCOUNT_OBJECT_KEY_MIGRATION_STATE); + + getReq.onsuccess = (): void => { + const migrationState = getReq.result ?? MigrationState.NOT_STARTED; + resolve(migrationState === MigrationState.NOT_STARTED); + }; + + getReq.onerror = (): void => { + reject(getReq.error); + }; + + db.close(); + } + }; + openDBRequest.onerror = (): void => reject(openDBRequest.error); + }); + } + private backendPromise?: Promise; private backend?: CryptoStore;