web_lib

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

web.ts (5487B)


      1 import { err_msg, handle_err, type IdbClientConfig, type ResolveError } from "@radroots/utils";
      2 import { createStore, del as idb_del, type UseStore } from "idb-keyval";
      3 import { crypto_registry_clear_key_entry, crypto_registry_clear_store_index, crypto_registry_get_store_index } from "../crypto/registry.js";
      4 import { WebCryptoService } from "../crypto/service.js";
      5 import type { LegacyKeyConfig } from "../crypto/types.js";
      6 import { IDB_CONFIG_CIPHER_AES_GCM } from "../idb/config.js";
      7 import { idb_store_ensure, idb_store_exists } from "../idb/store.js";
      8 import { is_error } from "../utils/resolve.js";
      9 import { cl_cipher_error } from "./error.js";
     10 import type { ClientCipherDecryptResolve, ClientCipherEncryptResolve, ClientCipherResetResolve, IClientCipher, WebAesGcmCipherConfig } from "./types.js";
     11 
     12 const DEFAULT_IDB_CONFIG: IdbClientConfig = IDB_CONFIG_CIPHER_AES_GCM;
     13 
     14 const DEFAULT_WEB_AES_GCM_CONFIG = {
     15     key_name: "radroots.aes-gcm.key",
     16     algorithm: "AES-GCM",
     17     key_length: 256,
     18     iv_length: 12
     19 } as const;
     20 
     21 export interface IWebAesGcmCipher extends IClientCipher { }
     22 
     23 export class WebAesGcmCipher implements IWebAesGcmCipher {
     24     private readonly db_name: string;
     25     private readonly store_name: string;
     26     private readonly key_name: string;
     27     private readonly algorithm_name: string;
     28     private readonly iv_length: number;
     29     private legacy_store: UseStore | null;
     30     private readonly store_id: string;
     31     private readonly crypto: WebCryptoService;
     32     private readonly legacy_key_config: LegacyKeyConfig;
     33     private store_ready: Promise<void> | null = null;
     34 
     35     constructor(config?: WebAesGcmCipherConfig) {
     36         const idb_config = config?.idb_config ?? {};
     37         this.db_name = idb_config.database ?? DEFAULT_IDB_CONFIG.database;
     38         this.store_name = idb_config.store ?? DEFAULT_IDB_CONFIG.store;
     39         this.key_name = config?.key_name ?? DEFAULT_WEB_AES_GCM_CONFIG.key_name;
     40         this.algorithm_name = config?.algorithm ?? DEFAULT_WEB_AES_GCM_CONFIG.algorithm;
     41         this.iv_length = Number.isInteger(config?.iv_length) && (config?.iv_length ?? 0) > 0
     42             ? config?.iv_length ?? DEFAULT_WEB_AES_GCM_CONFIG.iv_length
     43             : DEFAULT_WEB_AES_GCM_CONFIG.iv_length;
     44 
     45         this.legacy_store = null;
     46         this.store_id = this.key_name;
     47         this.crypto = new WebCryptoService();
     48         this.legacy_key_config = {
     49             idb_config: {
     50                 database: this.db_name,
     51                 store: this.store_name
     52             },
     53             key_name: this.key_name,
     54             iv_length: this.iv_length,
     55             algorithm: this.algorithm_name
     56         };
     57         this.crypto.register_store_config({
     58             store_id: this.store_id,
     59             legacy_key: this.legacy_key_config,
     60             iv_length: this.iv_length
     61         });
     62     }
     63 
     64     public get_config(): IdbClientConfig {
     65         return {
     66             database: this.db_name,
     67             store: this.store_name
     68         };
     69     }
     70 
     71     private ensure_env(): ResolveError<void> {
     72         if (typeof indexedDB === "undefined") return err_msg(cl_cipher_error.idb_undefined);
     73         if (!globalThis.crypto || !globalThis.crypto.subtle) return err_msg(cl_cipher_error.crypto_undefined);
     74         return;
     75     }
     76 
     77     private async get_store(): Promise<ResolveError<UseStore>> {
     78         const env_err = this.ensure_env();
     79         if (env_err) return env_err;
     80         try {
     81             if (!this.store_ready) this.store_ready = idb_store_ensure(this.db_name, this.store_name);
     82             await this.store_ready;
     83             if (!this.legacy_store) this.legacy_store = createStore(this.db_name, this.store_name);
     84             return this.legacy_store;
     85         } catch (e) {
     86             return handle_err(e);
     87         }
     88     }
     89 
     90     public async reset(): Promise<ClientCipherResetResolve> {
     91         const env_err = this.ensure_env();
     92         if (env_err) return env_err;
     93         try {
     94             const index = await crypto_registry_get_store_index(this.store_id);
     95             if (is_error(index)) return index;
     96             if (index) {
     97                 const cleared = await crypto_registry_clear_store_index(this.store_id);
     98                 if (is_error(cleared)) return cleared;
     99                 for (const key_id of index.key_ids) {
    100                     const res = await crypto_registry_clear_key_entry(key_id);
    101                     if (is_error(res)) return res;
    102                 }
    103             }
    104             const has_store = await idb_store_exists(this.db_name, this.store_name);
    105             if (has_store) {
    106                 const store = await this.get_store();
    107                 if (is_error(store)) return store;
    108                 await idb_del(this.key_name, store);
    109             }
    110             return { pass: true } as const;
    111         } catch (e) {
    112             return handle_err(e);
    113         }
    114     }
    115 
    116     public async encrypt(data: Uint8Array): Promise<ClientCipherEncryptResolve> {
    117         const env_err = this.ensure_env();
    118         if (env_err) return env_err;
    119         return await this.crypto.encrypt(this.store_id, data);
    120     }
    121 
    122     public async decrypt(blob: Uint8Array): Promise<ClientCipherDecryptResolve> {
    123         const env_err = this.ensure_env();
    124         if (env_err) return env_err;
    125         if (blob.byteLength <= this.iv_length) return err_msg(cl_cipher_error.invalid_ciphertext);
    126         const outcome = await this.crypto.decrypt_record(this.store_id, blob);
    127         if (is_error(outcome)) return outcome;
    128         return outcome.plaintext;
    129     }
    130 }