mirror of
https://github.com/Blah-IM/Weblah.git
synced 2025-05-02 01:01: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",
|
"@zeabur/svelte-adapter": "^1.0.0",
|
||||||
"bits-ui": "^0.21.16",
|
"bits-ui": "^0.21.16",
|
||||||
"canonicalize": "^2.0.0",
|
"canonicalize": "^2.0.0",
|
||||||
|
"idb": "^8.0.0",
|
||||||
"svelte-boring-avatars": "^1.2.6",
|
"svelte-boring-avatars": "^1.2.6",
|
||||||
"svelte-hero-icons": "^5.2.0",
|
"svelte-hero-icons": "^5.2.0",
|
||||||
"svelte-persisted-store": "^0.11.0",
|
"svelte-persisted-store": "^0.11.0",
|
||||||
|
@ -3492,6 +3493,12 @@
|
||||||
"node": ">= 6"
|
"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": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
|
|
|
@ -42,6 +42,7 @@
|
||||||
"@zeabur/svelte-adapter": "^1.0.0",
|
"@zeabur/svelte-adapter": "^1.0.0",
|
||||||
"bits-ui": "^0.21.16",
|
"bits-ui": "^0.21.16",
|
||||||
"canonicalize": "^2.0.0",
|
"canonicalize": "^2.0.0",
|
||||||
|
"idb": "^8.0.0",
|
||||||
"svelte-boring-avatars": "^1.2.6",
|
"svelte-boring-avatars": "^1.2.6",
|
||||||
"svelte-hero-icons": "^5.2.0",
|
"svelte-hero-icons": "^5.2.0",
|
||||||
"svelte-persisted-store": "^0.11.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