From 2771381a1337ef51cc21a91f88227d117dadb29a Mon Sep 17 00:00:00 2001 From: Shibo Lyu Date: Sat, 12 Oct 2024 01:02:59 +0800 Subject: [PATCH] refactor: actKeyStore -> accountKeyStore Now it stores encoded id key too, if needed. Also use idb instead of wrangling IndexedDB API ourselves. --- package-lock.json | 7 +++ package.json | 1 + src/lib/accountKeyStore.ts | 117 ++++++++++++++++++++++++++++++++++++ src/lib/actKeyStore.ts | 119 ------------------------------------- 4 files changed, 125 insertions(+), 119 deletions(-) create mode 100644 src/lib/accountKeyStore.ts delete mode 100644 src/lib/actKeyStore.ts diff --git a/package-lock.json b/package-lock.json index ff73267..1727ed8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@zeabur/svelte-adapter": "^1.0.0", "bits-ui": "^0.21.16", "canonicalize": "^2.0.0", + "idb": "^8.0.0", "svelte-boring-avatars": "^1.2.6", "svelte-hero-icons": "^5.2.0", "svelte-persisted-store": "^0.11.0", @@ -3492,6 +3493,12 @@ "node": ">= 6" } }, + "node_modules/idb": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.0.tgz", + "integrity": "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw==", + "license": "ISC" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", diff --git a/package.json b/package.json index 59da851..777c405 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@zeabur/svelte-adapter": "^1.0.0", "bits-ui": "^0.21.16", "canonicalize": "^2.0.0", + "idb": "^8.0.0", "svelte-boring-avatars": "^1.2.6", "svelte-hero-icons": "^5.2.0", "svelte-persisted-store": "^0.11.0", diff --git a/src/lib/accountKeyStore.ts b/src/lib/accountKeyStore.ts new file mode 100644 index 0000000..c4baa91 --- /dev/null +++ b/src/lib/accountKeyStore.ts @@ -0,0 +1,117 @@ +import { BlahKeyPair, BlahPublicKey, type EncodedBlahKeyPair } from '@blah-im/core/crypto'; +import { openDB, type DBSchema, type IDBPDatabase } from 'idb'; + +const IDB_NAME = 'blah-accounts'; +const IDB_OBJECT_STORE_NAME = 'accounts'; + +type SavedObject = { + idKeyId: string; + encodedIdKeyPair?: EncodedBlahKeyPair; + actKeyId: string; + actKeyPrivate: CryptoKey; +}; + +type AccountCredentials = { + idKeyId: string; + encodedIdKeyPair?: EncodedBlahKeyPair; + actKeyPair: BlahKeyPair; +}; + +interface AccountKeyStoreDB extends DBSchema { + [IDB_OBJECT_STORE_NAME]: { + key: string; + value: SavedObject; + indexes: { actKeyId: string }; + }; +} + +async function savedObjectToAccountCredentials( + savedObject: SavedObject +): Promise { + const publicKey = await BlahPublicKey.fromID(savedObject.actKeyId); + return { + idKeyId: savedObject.idKeyId, + encodedIdKeyPair: savedObject.encodedIdKeyPair, + actKeyPair: new BlahKeyPair(publicKey, savedObject.actKeyPrivate) + }; +} + +export class AccountKeyStore { + private db: IDBPDatabase; + + private constructor(db: IDBPDatabase) { + this.db = db; + } + + static async open(): Promise { + const db = await openDB(IDB_NAME, 1, { + upgrade(db) { + if (!db.objectStoreNames.contains(IDB_OBJECT_STORE_NAME)) { + const objStore = db.createObjectStore(IDB_OBJECT_STORE_NAME, { keyPath: 'idKeyId' }); + objStore.createIndex('actKeyId', 'actKeyId'); + } + } + }); + + return new AccountKeyStore(db); + } + + async addAccount( + idKeyId: string, + actKeyPair: BlahKeyPair, + encodedIdKeyPair?: EncodedBlahKeyPair + ): Promise { + const newObject: SavedObject = { + idKeyId, + encodedIdKeyPair, + actKeyId: actKeyPair.id, + actKeyPrivate: actKeyPair.privateKey + }; + + const tx = this.db.transaction(IDB_OBJECT_STORE_NAME, 'readwrite'); + const currentObject = await tx.store.get(idKeyId); + await tx.store.put({ ...currentObject, ...newObject }); + await tx.done; + + return { idKeyId, encodedIdKeyPair, actKeyPair }; + } + + async remove(idKeyId: string): Promise { + await this.db.delete(IDB_OBJECT_STORE_NAME, idKeyId); + } + + async removeIDKeyPrivateOnly(idKeyId: string): Promise { + const tx = this.db.transaction(IDB_OBJECT_STORE_NAME, 'readwrite'); + const currentObject = await tx.store.get(idKeyId); + if (!currentObject) { + await tx.done; + return; + } + delete currentObject.encodedIdKeyPair; + await tx.store.put(currentObject); + await tx.done; + } + + async fetchAccount(idKeyId: string): Promise { + const result = await this.db.get(IDB_OBJECT_STORE_NAME, idKeyId); + if (!result) return null; + return await savedObjectToAccountCredentials(result); + } + + async fetchAllAccounts(): Promise { + const list: AccountCredentials[] = []; + + const transaction = this.db.transaction(IDB_OBJECT_STORE_NAME, 'readonly'); + let cursor = await transaction.store.openCursor(); + while (cursor) { + list.push(await savedObjectToAccountCredentials(cursor.value)); + cursor = await cursor.continue(); + } + + return list; + } + + close() { + this.db.close(); + } +} diff --git a/src/lib/actKeyStore.ts b/src/lib/actKeyStore.ts deleted file mode 100644 index 31315e1..0000000 --- a/src/lib/actKeyStore.ts +++ /dev/null @@ -1,119 +0,0 @@ -// Loosely based on https://github.com/infotechinc/key-storage-in-browser/blob/master/keystore.js - -import { BlahKeyPair, BlahPublicKey } from '@blah-im/core/crypto'; - -type SavedObject = { - idKeyId: string; - actKeyId: string; - privateKey: CryptoKey; -}; - -type QueryResult = { - idKeyId: string; - keypair: BlahKeyPair; -}; - -async function savedObjectToQueryResult(savedObject: SavedObject): Promise { - const publicKey = await BlahPublicKey.fromID(savedObject.actKeyId); - return { - idKeyId: savedObject.idKeyId, - keypair: new BlahKeyPair(publicKey, savedObject.privateKey) - }; -} - -export class ActKeyStore { - private db: IDBDatabase | null = null; - private dbName: string = 'WeblahActKeyStore'; - private objectStoreName: string = 'keys'; - - async open(): Promise { - if (!window.indexedDB) throw new Error('IndexedDB is not supported.'); - - const req = indexedDB.open(this.dbName, 1); - - return new Promise((fulfill, reject) => { - req.onsuccess = () => { - this.db = req.result; - fulfill(this); - }; - req.onerror = () => reject(req.error); - req.onblocked = () => reject(new Error('Database already open')); - req.onupgradeneeded = () => { - this.db = req.result; - if (!this.db.objectStoreNames.contains(this.objectStoreName)) { - const objStore = this.db.createObjectStore(this.objectStoreName); - objStore.createIndex('idKeyId', 'idKeyId', { unique: false }); - objStore.createIndex('actKeyId', 'actKeyId', { unique: true }); - } - }; - }); - } - - async saveActKeyPair(keypair: BlahKeyPair, idKeyId: string): Promise { - if (!this.db) throw new Error('ActKeyStore is not open.'); - - const savedObject: SavedObject = { - idKeyId, - actKeyId: keypair.id, - privateKey: keypair.privateKey - }; - - const transaction = this.db.transaction(this.objectStoreName, 'readwrite'); - - return await new Promise((fulfill, reject) => { - transaction.onerror = () => reject(transaction.error); - transaction.onabort = () => reject(transaction.error); - transaction.oncomplete = () => fulfill({ idKeyId, keypair }); - const objectStore = transaction.objectStore(this.objectStoreName); - objectStore.add(savedObject); - }); - } - - async fetchActKeyPair(actKeyId: string): Promise { - if (!this.db) throw new Error('ActKeyStore is not open.'); - - const transaction = this.db.transaction(this.objectStoreName, 'readonly'); - const objectStore = transaction.objectStore(this.objectStoreName); - - const request: IDBRequest = objectStore.index('actKeyId').get(actKeyId); - - const result = await new Promise((fulfill, reject) => { - request.onsuccess = () => fulfill(request.result); - request.onerror = () => reject(request.error); - }); - - if (!result) return null; - return await savedObjectToQueryResult(result); - } - - async fetchAllKeyPairs(): Promise { - if (!this.db) throw new Error('ActKeyStore is not open.'); - - const list: QueryResult[] = []; - - const transaction = this.db.transaction([this.objectStoreName], 'readonly'); - - return new Promise((fulfill, reject) => { - transaction.onerror = () => reject(transaction.error); - transaction.onabort = () => reject(transaction.error); - - const objectStore = transaction.objectStore(this.objectStoreName); - const cursor = objectStore.openCursor(); - - cursor.onsuccess = async () => { - const result = cursor.result; - if (result) { - list.push(await savedObjectToQueryResult(result.value)); - result.continue(); - } else { - fulfill(list); - } - }; - }); - } - - close() { - this.db?.close(); - this.db = null; - } -}