From ab0d40794fc6bf1532c558f09089934b6dcd01ba Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Thu, 17 Jul 2025 06:13:41 -0700 Subject: [PATCH] Add logging to Safe now that prod build removes logging statements. Leave retrieved records in the db until explicitly destroyed. Refactor key derivation and record data validation. --- src/lib/workers/safe.ts | 120 +++++++++++++++++++--------------------- 1 file changed, 56 insertions(+), 64 deletions(-) diff --git a/src/lib/workers/safe.ts b/src/lib/workers/safe.ts index 970b0df..6543a19 100644 --- a/src/lib/workers/safe.ts +++ b/src/lib/workers/safe.ts @@ -22,21 +22,21 @@ export class Safe extends WorkerInterface { } static async work (headers: Headers, data: Data): Promise { - const { method, name, password, store } = headers + const { method, name, store, password } = headers this.#storage = await this.#open(this.DB_NAME) let result try { switch (method) { case 'set': { - result = await this.set(store, password, data) + result = await this.set(data, store, password) break } case 'get': { - result = await this.get(store, password, name) + result = await this.get(name, store, password) break } case 'destroy': { - result = await this.destroy(store, name) + result = await this.destroy(name, store) break } default: { @@ -44,6 +44,7 @@ export class Safe extends WorkerInterface { } } } catch (err) { + console.log(err) result = false } return result @@ -52,10 +53,11 @@ export class Safe extends WorkerInterface { /** * Removes data from the Safe without decrypting. */ - static async destroy (store: string, name: string): Promise { + static async destroy (name: string, store: string): Promise { try { - return await this.#delete(store, name) - } catch { + return await this.#delete(name, store) + } catch (err) { + console.log(err) throw new Error(this.ERR_MSG) } } @@ -63,28 +65,20 @@ export class Safe extends WorkerInterface { /** * Encrypts data with a password byte array and stores it in the Safe. */ - static async set (store: string, password: ArrayBuffer, data: Data): Promise { - try { - const derivationKey = await globalThis.crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey']) - if (this.#isInvalid(store, derivationKey, data)) { - throw new Error('Failed to import key') - } + static async set (data: Data | unknown, store: string | unknown, password: ArrayBuffer | unknown): Promise { + this.#isDataValid(data) + if (typeof store !== 'string' || store === '') { + throw new Error('Invalid database store name') + } + if (!(password instanceof ArrayBuffer)) { + throw new Error('Invalid password') + } + try { const records: SafeRecord[] = [] for (const label of Object.keys(data)) { const salt = await Entropy.create() - const derivationAlgorithm: Pbkdf2Params = { - name: 'PBKDF2', - hash: 'SHA-512', - salt: salt.bytes, - iterations: 210000 - } - const derivedKeyType: AesKeyGenParams = { - name: 'AES-GCM', - length: 256 - } - const encryptionKey = await globalThis.crypto.subtle.deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, ['encrypt']) - + const encryptionKey = await Safe.#createAesKey('encrypt', password, salt.buffer) const iv = await Entropy.create() const encrypted = await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv.buffer }, encryptionKey, data[label]) const record: SafeRecord = { @@ -95,7 +89,6 @@ export class Safe extends WorkerInterface { } records.push(record) } - return await this.#put(records, store) } catch (err) { throw new Error(this.ERR_MSG) @@ -107,62 +100,51 @@ export class Safe extends WorkerInterface { /** * Retrieves data from the Safe and decrypts it with a password byte array. */ - static async get (store: string, password: ArrayBuffer, name: string): Promise { - try { - const derivationKey = await globalThis.crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey']) - if (this.#isInvalid(store, derivationKey)) { - throw new Error('Failed to import key') - } + static async get (name: string | unknown, store: string | unknown, password: ArrayBuffer | unknown): Promise { + if (typeof name !== 'string' || name === '') { + throw new Error('Invalid database field name') + } + if (typeof store !== 'string' || store === '') { + throw new Error('Invalid database store name') + } + if (!(password instanceof ArrayBuffer)) { + throw new Error('Invalid password') + } + try { const record: SafeRecord = await this.#get(name, store) if (record == null) { throw new Error('Failed to find record') } - const { label, encrypted } = record - const salt = await Entropy.import(record.salt) - const derivationAlgorithm: Pbkdf2Params = { - name: 'PBKDF2', - hash: 'SHA-512', - salt: salt.bytes, - iterations: 210000 - } - const derivedKeyType: AesKeyGenParams = { - name: 'AES-GCM', - length: 256 - } - const decryptionKey = await globalThis.crypto.subtle.deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, ['decrypt']) - + const decryptionKey = await Safe.#createAesKey('decrypt', password, salt.buffer) const iv = await Entropy.import(record.iv) - const decrypted = await globalThis.crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv.buffer }, decryptionKey, encrypted) - - await this.destroy(store, name) - return { [label]: decrypted } + const decrypted = await globalThis.crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv.buffer }, decryptionKey, record.encrypted) + return { [record.label]: decrypted } } catch (err) { + console.log(err) return null } finally { bytes.erase(password) } } - static #isInvalid (name: string, passkey: CryptoKey, data?: any): boolean { - if (typeof name !== 'string' || name === '') { - return true - } - if (!(passkey instanceof CryptoKey)) { - return true + static async #createAesKey (purpose: 'encrypt' | 'decrypt', password: ArrayBuffer, salt: ArrayBuffer): Promise { + const derivationKey = await globalThis.crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey']) + const derivationAlgorithm: Pbkdf2Params = { + name: 'PBKDF2', + hash: 'SHA-512', + iterations: 210000, + salt } - if (typeof data === 'object') { - try { - JSON.stringify(data, (k, v) => typeof v === 'bigint' ? v.toString() : v) - } catch { - return true - } + const derivedKeyType: AesKeyGenParams = { + name: 'AES-GCM', + length: 256 } - return false + return await globalThis.crypto.subtle.deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, [purpose]) } - static async #delete (store: string, name: string): Promise { + static async #delete (name: string, store: string): Promise { const transaction = this.#storage.transaction(store, 'readwrite') const db = transaction.objectStore(store) return new Promise((resolve, reject) => { @@ -192,6 +174,16 @@ export class Safe extends WorkerInterface { }) } + static #isDataValid (data: unknown): asserts data is Data { + if (typeof data !== 'object') { + throw new Error('Invalid data') + } + const dataObject = data as { [key: string]: unknown } + if (Object.keys(dataObject).some(k => !(dataObject[k] instanceof ArrayBuffer))) { + throw new Error('Invalid data') + } + } + static async #open (database: string): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open(database, 1) -- 2.47.3