web.ts (7044B)
1 import { 2 err_msg, 3 handle_err, 4 IdbClientConfig, 5 text_dec, 6 text_enc, 7 type ResolveError, 8 type ResultObj, 9 type ResultPass, 10 type ResultsList 11 } from "@radroots/utils"; 12 import { clear as idb_clear, del as idb_del, get as idb_get, keys as idb_keys, set as idb_set, type UseStore } from "idb-keyval"; 13 import type { BackupKeystorePayload } from "../backup/types.js"; 14 import { crypto_registry_clear_key_entry, crypto_registry_clear_store_index, crypto_registry_get_store_index } from "../crypto/registry.js"; 15 import type { LegacyKeyConfig } from "../crypto/types.js"; 16 import { WebEncryptedStore } from "../idb/encrypted_store.js"; 17 import { IDB_CONFIG_KEYSTORE, IDB_STORE_CIPHER_SUFFIX } from "../idb/config.js"; 18 import { idb_value_as_bytes } from "../idb/value.js"; 19 import { is_error } from "../utils/resolve.js"; 20 import { cl_keystore_error } from "./error.js"; 21 import type { IClientKeystore, IClientKeystoreValue } from "./types.js"; 22 23 export interface IWebKeystore extends IClientKeystore { 24 get_config(): IdbClientConfig; 25 get_store_id(): string; 26 export_backup(): Promise<ResolveError<BackupKeystorePayload>>; 27 import_backup(payload: BackupKeystorePayload): Promise<ResolveError<void>>; 28 } 29 30 export class WebKeystore implements IWebKeystore { 31 private config: IdbClientConfig; 32 private store_id: string; 33 private encrypted_store: WebEncryptedStore; 34 private legacy_key_config: LegacyKeyConfig; 35 36 constructor(config?: Partial<IdbClientConfig>) { 37 this.config = { 38 database: config?.database ?? IDB_CONFIG_KEYSTORE.database, 39 store: config?.store ?? IDB_CONFIG_KEYSTORE.store 40 }; 41 this.store_id = `keystore:${this.config.database}:${this.config.store}`; 42 const legacy_store = `${this.config.store}${IDB_STORE_CIPHER_SUFFIX}`; 43 44 this.legacy_key_config = { 45 idb_config: { 46 database: this.config.database, 47 store: legacy_store 48 }, 49 key_name: `radroots.keystore.${this.config.store}.aes-gcm.key`, 50 iv_length: 12, 51 algorithm: "AES-GCM" 52 }; 53 54 this.encrypted_store = new WebEncryptedStore({ 55 idb_config: this.config, 56 store_id: this.store_id, 57 idb_error: cl_keystore_error.idb_undefined, 58 legacy_key: this.legacy_key_config, 59 iv_length: 12 60 }); 61 } 62 63 private async get_store(): Promise<ResolveError<UseStore>> { 64 return await this.encrypted_store.get_store(); 65 } 66 67 public get_config(): IdbClientConfig { 68 return { 69 database: this.config.database, 70 store: this.config.store 71 }; 72 } 73 74 public get_store_id(): string { 75 return this.store_id; 76 } 77 78 public async add(key: string, value: string): Promise<ResolveError<ResultObj<string>>> { 79 try { 80 const bytes = text_enc(value); 81 const cipher_bytes = await this.encrypted_store.encrypt_bytes(bytes); 82 if (is_error(cipher_bytes)) return cipher_bytes; 83 const store = await this.get_store(); 84 if (is_error(store)) return store; 85 await idb_set(key, cipher_bytes, store); 86 return { result: key }; 87 } catch (e) { 88 return handle_err(e); 89 } 90 } 91 92 public async remove(key: string): Promise<ResolveError<ResultObj<string>>> { 93 try { 94 const store = await this.get_store(); 95 if (is_error(store)) return store; 96 await idb_del(key, store); 97 return { result: key }; 98 } catch (e) { 99 return handle_err(e); 100 } 101 } 102 103 public async read(key?: string | null): Promise<ResolveError<ResultObj<IClientKeystoreValue>>> { 104 try { 105 if (!key) return err_msg(cl_keystore_error.missing_key); 106 const store = await this.get_store(); 107 if (is_error(store)) return store; 108 const cipher_value = await idb_get(key, store); 109 const cipher_bytes = idb_value_as_bytes(cipher_value); 110 if (!cipher_bytes) return err_msg(cl_keystore_error.corrupt_data); 111 const outcome = await this.encrypted_store.decrypt_record(cipher_bytes); 112 if (is_error(outcome)) return outcome; 113 if (outcome.reencrypted) await idb_set(key, outcome.reencrypted, store); 114 const plain = text_dec(outcome.plaintext); 115 return { result: plain }; 116 } catch (e) { 117 return handle_err(e); 118 } 119 } 120 121 public async keys(): Promise<ResolveError<ResultsList<string>>> { 122 try { 123 const store = await this.get_store(); 124 if (is_error(store)) return store; 125 const all_keys = await idb_keys(store); 126 return { results: all_keys.filter((k): k is string => typeof k === "string") }; 127 } catch (e) { 128 return handle_err(e); 129 } 130 } 131 132 public async export_backup(): Promise<ResolveError<BackupKeystorePayload>> { 133 try { 134 const store = await this.get_store(); 135 if (is_error(store)) return store; 136 const all_keys = await idb_keys(store); 137 const entries: BackupKeystorePayload["entries"] = []; 138 for (const key of all_keys) { 139 if (typeof key !== "string") continue; 140 const value = await this.read(key); 141 if (is_error(value)) return value; 142 if (typeof value.result !== "string") return err_msg(cl_keystore_error.corrupt_data); 143 entries.push({ key, value: value.result }); 144 } 145 return { entries }; 146 } catch (e) { 147 return handle_err(e); 148 } 149 } 150 151 public async import_backup(payload: BackupKeystorePayload): Promise<ResolveError<void>> { 152 try { 153 const store = await this.get_store(); 154 if (is_error(store)) return store; 155 for (const entry of payload.entries) { 156 const res = await this.add(entry.key, entry.value); 157 if (is_error(res)) return res; 158 } 159 return; 160 } catch (e) { 161 return handle_err(e); 162 } 163 } 164 165 public async reset(): Promise<ResolveError<ResultPass>> { 166 try { 167 const store = await this.get_store(); 168 if (is_error(store)) return store; 169 await idb_clear(store); 170 const index = await crypto_registry_get_store_index(this.store_id); 171 if (is_error(index)) return index; 172 if (index) { 173 const cleared = await crypto_registry_clear_store_index(this.store_id); 174 if (is_error(cleared)) return cleared; 175 for (const key_id of index.key_ids) { 176 const res = await crypto_registry_clear_key_entry(key_id); 177 if (is_error(res)) return res; 178 } 179 } 180 return { pass: true } as const; 181 } catch (e) { 182 return handle_err(e); 183 } 184 } 185 }