web_lib

Common web application libraries
git clone https://radroots.dev/git/web_lib.git
Log | Files | Refs | LICENSE

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 }