registry.ts (8560B)
1 import { createStore, del as idb_del, get as idb_get, keys as idb_keys, set as idb_set, type UseStore } from "idb-keyval"; 2 import { err_msg, handle_err, type IdbClientConfig, type ResolveError } from "@radroots/utils"; 3 import { IDB_CONFIG_CRYPTO_REGISTRY } from "../idb/config.js"; 4 import { idb_store_ensure } from "../idb/store.js"; 5 import { is_error } from "../utils/resolve.js"; 6 import { cl_crypto_error } from "./error.js"; 7 import type { CryptoKeyEntry, CryptoRegistryExport, CryptoStoreIndex } from "./types.js"; 8 9 const CRYPTO_IDB_CONFIG: IdbClientConfig = IDB_CONFIG_CRYPTO_REGISTRY; 10 11 let crypto_store: UseStore | null = null; 12 const STORE_INDEX_PREFIX = "store:"; 13 const KEY_ENTRY_PREFIX = "key:"; 14 const DEVICE_MATERIAL_KEY = "device:material"; 15 16 const ensure_idb = async (): Promise<ResolveError<void>> => { 17 if (typeof indexedDB === "undefined") return err_msg(cl_crypto_error.idb_undefined); 18 try { 19 await idb_store_ensure(CRYPTO_IDB_CONFIG.database, CRYPTO_IDB_CONFIG.store); 20 return; 21 } catch (e) { 22 return handle_err(e); 23 } 24 }; 25 26 const get_crypto_store = async (): Promise<ResolveError<UseStore>> => { 27 const ensured = await ensure_idb(); 28 if (is_error(ensured)) return ensured; 29 if (!crypto_store) crypto_store = createStore(CRYPTO_IDB_CONFIG.database, CRYPTO_IDB_CONFIG.store); 30 return crypto_store; 31 }; 32 33 const store_index_key = (store_id: string): string => `${STORE_INDEX_PREFIX}${store_id}`; 34 const key_entry_key = (key_id: string): string => `${KEY_ENTRY_PREFIX}${key_id}`; 35 36 const is_string_array = (value: unknown): value is string[] => 37 Array.isArray(value) && value.every((item) => typeof item === "string"); 38 39 const is_record = (value: unknown): value is Record<string, unknown> => 40 typeof value === "object" && value !== null && !Array.isArray(value); 41 42 const is_crypto_store_index = (value: unknown): value is CryptoStoreIndex => { 43 if (!is_record(value)) return false; 44 return typeof value.store_id === "string" 45 && typeof value.active_key_id === "string" 46 && typeof value.created_at === "number" 47 && is_string_array(value.key_ids); 48 }; 49 50 const is_crypto_key_entry = (value: unknown): value is CryptoKeyEntry => { 51 if (!is_record(value)) return false; 52 return typeof value.key_id === "string" 53 && typeof value.store_id === "string" 54 && typeof value.created_at === "number" 55 && typeof value.status === "string" 56 && value.wrapped_key instanceof Uint8Array 57 && value.wrap_iv instanceof Uint8Array 58 && value.kdf_salt instanceof Uint8Array 59 && typeof value.kdf_iterations === "number" 60 && typeof value.iv_length === "number" 61 && typeof value.algorithm === "string" 62 && typeof value.provider_id === "string"; 63 }; 64 65 export const crypto_registry_get_store_index = async (store_id: string): Promise<ResolveError<CryptoStoreIndex | null>> => { 66 try { 67 const store = await get_crypto_store(); 68 if (is_error(store)) return store; 69 const record = await idb_get(store_index_key(store_id), store); 70 if (!record) return null; 71 if (!is_crypto_store_index(record)) return err_msg(cl_crypto_error.registry_failure); 72 return record; 73 } catch (e) { 74 return handle_err(e); 75 } 76 }; 77 78 export const crypto_registry_set_store_index = async (index: CryptoStoreIndex): Promise<ResolveError<void>> => { 79 try { 80 const store = await get_crypto_store(); 81 if (is_error(store)) return store; 82 await idb_set(store_index_key(index.store_id), index, store); 83 return; 84 } catch (e) { 85 return handle_err(e); 86 } 87 }; 88 89 export const crypto_registry_get_key_entry = async (key_id: string): Promise<ResolveError<CryptoKeyEntry | null>> => { 90 try { 91 const store = await get_crypto_store(); 92 if (is_error(store)) return store; 93 const record = await idb_get(key_entry_key(key_id), store); 94 if (!record) return null; 95 if (!is_crypto_key_entry(record)) return err_msg(cl_crypto_error.registry_failure); 96 return record; 97 } catch (e) { 98 return handle_err(e); 99 } 100 }; 101 102 export const crypto_registry_set_key_entry = async (entry: CryptoKeyEntry): Promise<ResolveError<void>> => { 103 try { 104 const store = await get_crypto_store(); 105 if (is_error(store)) return store; 106 await idb_set(key_entry_key(entry.key_id), entry, store); 107 return; 108 } catch (e) { 109 return handle_err(e); 110 } 111 }; 112 113 export const crypto_registry_list_store_indices = async (): Promise<ResolveError<CryptoStoreIndex[]>> => { 114 try { 115 const store = await get_crypto_store(); 116 if (is_error(store)) return store; 117 const keys = await idb_keys(store); 118 const store_keys = keys.filter((key): key is string => typeof key === "string" && key.startsWith(STORE_INDEX_PREFIX)); 119 const out: CryptoStoreIndex[] = []; 120 for (const key of store_keys) { 121 const record = await idb_get(key, store); 122 if (!record) continue; 123 if (!is_crypto_store_index(record)) return err_msg(cl_crypto_error.registry_failure); 124 out.push(record); 125 } 126 return out; 127 } catch (e) { 128 return handle_err(e); 129 } 130 }; 131 132 export const crypto_registry_list_key_entries = async (): Promise<ResolveError<CryptoKeyEntry[]>> => { 133 try { 134 const store = await get_crypto_store(); 135 if (is_error(store)) return store; 136 const keys = await idb_keys(store); 137 const entry_keys = keys.filter((key): key is string => typeof key === "string" && key.startsWith(KEY_ENTRY_PREFIX)); 138 const out: CryptoKeyEntry[] = []; 139 for (const key of entry_keys) { 140 const record = await idb_get(key, store); 141 if (!record) continue; 142 if (!is_crypto_key_entry(record)) return err_msg(cl_crypto_error.registry_failure); 143 out.push(record); 144 } 145 return out; 146 } catch (e) { 147 return handle_err(e); 148 } 149 }; 150 151 export const crypto_registry_export = async (): Promise<ResolveError<CryptoRegistryExport>> => { 152 const stores = await crypto_registry_list_store_indices(); 153 if (is_error(stores)) return stores; 154 const keys = await crypto_registry_list_key_entries(); 155 if (is_error(keys)) return keys; 156 return { stores, keys }; 157 }; 158 159 export const crypto_registry_import = async (registry: CryptoRegistryExport): Promise<ResolveError<void>> => { 160 const store = await get_crypto_store(); 161 if (is_error(store)) return store; 162 for (const store_index of registry.stores) { 163 const res = await crypto_registry_set_store_index(store_index); 164 if (is_error(res)) return res; 165 } 166 for (const entry of registry.keys) { 167 const res = await crypto_registry_set_key_entry(entry); 168 if (is_error(res)) return res; 169 } 170 return; 171 }; 172 173 export const crypto_registry_get_device_material = async (): Promise<ResolveError<Uint8Array | null>> => { 174 try { 175 const store = await get_crypto_store(); 176 if (is_error(store)) return store; 177 const record = await idb_get(DEVICE_MATERIAL_KEY, store); 178 if (!record) return null; 179 if (record instanceof Uint8Array) return record; 180 if (record instanceof ArrayBuffer) return new Uint8Array(record); 181 if (ArrayBuffer.isView(record)) return new Uint8Array(record.buffer, record.byteOffset, record.byteLength); 182 return err_msg(cl_crypto_error.registry_failure); 183 } catch (e) { 184 return handle_err(e); 185 } 186 }; 187 188 export const crypto_registry_set_device_material = async (material: Uint8Array): Promise<ResolveError<void>> => { 189 try { 190 const store = await get_crypto_store(); 191 if (is_error(store)) return store; 192 await idb_set(DEVICE_MATERIAL_KEY, material, store); 193 return; 194 } catch (e) { 195 return handle_err(e); 196 } 197 }; 198 199 export const crypto_registry_clear_store_index = async (store_id: string): Promise<ResolveError<void>> => { 200 try { 201 const store = await get_crypto_store(); 202 if (is_error(store)) return store; 203 await idb_del(store_index_key(store_id), store); 204 return; 205 } catch (e) { 206 return handle_err(e); 207 } 208 }; 209 210 export const crypto_registry_clear_key_entry = async (key_id: string): Promise<ResolveError<void>> => { 211 try { 212 const store = await get_crypto_store(); 213 if (is_error(store)) return store; 214 await idb_del(key_entry_key(key_id), store); 215 return; 216 } catch (e) { 217 return handle_err(e); 218 } 219 };