From: Chris Duncan Date: Wed, 16 Jul 2025 14:20:10 +0000 (-0700) Subject: Use different salt and IV when storing in safe. Encode entire object of buffers as... X-Git-Tag: v0.10.5~56^2~29 X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=fa41ecc4f34d7bc8b3fb959efd402e20dce771fe;p=libnemo.git Use different salt and IV when storing in safe. Encode entire object of buffers as base32 before stringifying and encrypting as single object. --- diff --git a/src/lib/workers/safe.ts b/src/lib/workers/safe.ts index 404a446..ac194f0 100644 --- a/src/lib/workers/safe.ts +++ b/src/lib/workers/safe.ts @@ -4,7 +4,7 @@ 'use strict' import { WorkerInterface } from './worker-interface' -import { default as Convert, bytes } from '#src/lib/convert.js' +import { default as Convert, base32, bytes } from '#src/lib/convert.js' import { Entropy } from '#src/lib/entropy.js' import { Data, Headers, SafeRecord } from '#types' @@ -15,6 +15,8 @@ export class Safe extends WorkerInterface { static DB_NAME = 'libnemo' static STORE_NAME = 'Safe' static ERR_MSG = 'Failed to store item in Safe' + static #decoder: TextDecoder = new TextDecoder() + static #encoder: TextEncoder = new TextEncoder() static #storage: IDBDatabase static { @@ -43,8 +45,7 @@ export class Safe extends WorkerInterface { result = `unknown Safe method ${method}` } } - } catch (err) { - console.log(err) + } catch { result = false } return result @@ -56,7 +57,7 @@ export class Safe extends WorkerInterface { static async destroy (name: string): Promise { try { return await this.#delete(name) - } catch (err) { + } catch { throw new Error(this.ERR_MSG) } } @@ -67,42 +68,48 @@ export class Safe extends WorkerInterface { static async set (name: string, data: Data): Promise { const { password } = data delete data.password - let passkey: CryptoKey - try { - if (await this.#exists(name)) throw new Error('Record is already locked') - passkey = await globalThis.crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey']) - } catch { - throw new Error(this.ERR_MSG) - } finally { - bytes.erase(password) - } - if (this.#isInvalid(name, passkey, data)) { - throw new Error(this.ERR_MSG) - } try { - const iv = await Entropy.create() + if (await this.#exists(name)) { + throw null + } + const derivationKey = await globalThis.crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey']) + if (this.#isInvalid(name, derivationKey, data)) { + throw null + } + + const base32: { [key: string]: string } = {} + for (const d of Object.keys(data)) { + base32[d] = bytes.toBase32(new Uint8Array(data[d])) + } + const serialized = JSON.stringify(base32) + const encoded = this.#encoder.encode(serialized) + + const salt = await Entropy.create() const derivationAlgorithm: Pbkdf2Params = { name: 'PBKDF2', hash: 'SHA-512', - salt: iv.bytes, + salt: salt.bytes, iterations: 210000 } const derivedKeyType: AesKeyGenParams = { name: 'AES-GCM', length: 256 } - passkey = await globalThis.crypto.subtle.deriveKey(derivationAlgorithm, passkey, derivedKeyType, false, ['encrypt']) - for (const d of Object.keys(data)) { - data[d] = await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv.buffer }, passkey, data[d]) - } + const encryptionKey = await globalThis.crypto.subtle.deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, ['encrypt']) + + const iv = await Entropy.create() + const encrypted = await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv.buffer }, encryptionKey, encoded) const record: SafeRecord = { iv: iv.hex, - data + salt: salt.hex, + encrypted } return await this.#add(record, name) } catch (err) { throw new Error(this.ERR_MSG) + } finally { + bytes.erase(password) } } @@ -112,50 +119,47 @@ export class Safe extends WorkerInterface { static async get (name: string, data: Data): Promise { const { password } = data delete data.password - let passkey: CryptoKey - try { - passkey = await globalThis.crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey']) - } catch { - return null - } finally { - bytes.erase(password) - } - if (this.#isInvalid(name, passkey)) { - return null - } - let record: SafeRecord try { - record = await this.#get(name) - } catch { - return null - } - if (record == null) { - return null - } + const derivationKey = await globalThis.crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey']) + if (this.#isInvalid(name, derivationKey)) { + throw null + } - try { - const iv = await Entropy.import(record.iv) - const { data } = record + const record: SafeRecord = await this.#get(name) + if (record == null) { + throw null + } + const { encrypted } = record + + const salt = await Entropy.import(record.salt) const derivationAlgorithm: Pbkdf2Params = { name: 'PBKDF2', hash: 'SHA-512', - salt: iv.bytes, + salt: salt.bytes, iterations: 210000 } const derivedKeyType: AesKeyGenParams = { name: 'AES-GCM', length: 256 } - passkey = await globalThis.crypto.subtle.deriveKey(derivationAlgorithm, passkey, derivedKeyType, false, ['decrypt']) - for (const d of Object.keys(data)) { - const decrypted = await globalThis.crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv.buffer }, passkey, data[d]) - data[d] = decrypted + const decryptionKey = await globalThis.crypto.subtle.deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, ['decrypt']) + + const iv = await Entropy.import(record.iv) + const decrypted = await globalThis.crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv.buffer }, decryptionKey, encrypted) + const decoded = this.#decoder.decode(decrypted) + const deserialized: { [key: string]: string } = JSON.parse(decoded) + + const bytes: Data = {} + for (const d of Object.keys(deserialized)) { + bytes[d] = new Uint8Array(base32.toBytes(deserialized[d])).buffer } await this.destroy(name) - return data + return bytes } catch (err) { return null + } finally { + bytes.erase(password) } } @@ -169,7 +173,7 @@ export class Safe extends WorkerInterface { if (typeof data === 'object') { try { JSON.stringify(data, (k, v) => typeof v === 'bigint' ? v.toString() : v) - } catch (err) { + } catch { return true } } diff --git a/src/types.d.ts b/src/types.d.ts index 7a360ae..4754eb3 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -17,7 +17,8 @@ export type KeyPair = { export type SafeRecord = { iv: string - data: Data + salt: string + encrypted: ArrayBuffer } export type UnknownNumber = number | unknown