mirror of
https://github.com/Blah-IM/Weblah.git
synced 2025-05-01 00:31:08 +00:00
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:
parent
8a91ea13fd
commit
2771381a13
4 changed files with 125 additions and 119 deletions
7
package-lock.json
generated
7
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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
117
src/lib/accountKeyStore.ts
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue