refactor: actKeyStore -> accountKeyStore

Now it stores encoded id key too, if needed. Also use idb instead of wrangling IndexedDB API ourselves.
This commit is contained in:
Shibo Lyu 2024-10-12 01:02:59 +08:00
parent 8a91ea13fd
commit 2771381a13
4 changed files with 125 additions and 119 deletions

7
package-lock.json generated
View file

@ -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",

View file

@ -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",

117
src/lib/accountKeyStore.ts Normal file
View file

@ -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<AccountCredentials> {
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<AccountKeyStoreDB>;
private constructor(db: IDBPDatabase<AccountKeyStoreDB>) {
this.db = db;
}
static async open(): Promise<AccountKeyStore> {
const db = await openDB<AccountKeyStoreDB>(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<AccountCredentials> {
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<void> {
await this.db.delete(IDB_OBJECT_STORE_NAME, idKeyId);
}
async removeIDKeyPrivateOnly(idKeyId: string): Promise<void> {
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<AccountCredentials | null> {
const result = await this.db.get(IDB_OBJECT_STORE_NAME, idKeyId);
if (!result) return null;
return await savedObjectToAccountCredentials(result);
}
async fetchAllAccounts(): Promise<AccountCredentials[]> {
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();
}
}

View file

@ -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<QueryResult> {
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<ActKeyStore> {
if (!window.indexedDB) throw new Error('IndexedDB is not supported.');
const req = indexedDB.open(this.dbName, 1);
return new Promise<ActKeyStore>((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<QueryResult> {
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<QueryResult | null> {
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<SavedObject> = objectStore.index('actKeyId').get(actKeyId);
const result = await new Promise<SavedObject | null>((fulfill, reject) => {
request.onsuccess = () => fulfill(request.result);
request.onerror = () => reject(request.error);
});
if (!result) return null;
return await savedObjectToQueryResult(result);
}
async fetchAllKeyPairs(): Promise<QueryResult[]> {
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;
}
}