From: Chris Duncan Date: Sat, 26 Jul 2025 19:35:01 +0000 (-0700) Subject: Add password-to-CryptoKey convert worker. X-Git-Tag: v0.10.5~50^2~9 X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=1bf600f61fc001cc61c1f2664331cf7773f11fe2;p=libnemo.git Add password-to-CryptoKey convert worker. --- diff --git a/src/lib/workers/passkey.ts b/src/lib/workers/passkey.ts new file mode 100644 index 0000000..80c9f54 --- /dev/null +++ b/src/lib/workers/passkey.ts @@ -0,0 +1,78 @@ +//! SPDX-FileCopyrightText: 2025 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +'use strict' + +import { WorkerInterface } from './worker-interface' +import { PBKDF2_ITERATIONS } from '#src/lib/constants.js' +import { default as Convert, bytes } from '#src/lib/convert.js' +import { Entropy } from '#src/lib/entropy.js' +import { NamedData } from '#types' + +/** +* Converts a user password to a `CryptoKey`. +*/ +export class Passkey extends WorkerInterface { + static { + this.listen() + } + + static async work (data: NamedData): Promise> { + this.#validate(data) + try { + const salt = (method === 'decrypt') + ? data.salt + : (await Entropy.create()).buffer + const key = await this.#createAesKey(method, password, salt) + return { salt, key } + } catch (err) { + throw new Error('Failed to derive key from password', { cause: err }) + } finally { + password.transfer() + } + } + + 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: PBKDF2_ITERATIONS, + salt + } + const derivedKeyType: AesKeyGenParams = { + name: 'AES-GCM', + length: 256 + } + return await globalThis.crypto.subtle.deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, [purpose]) + } + + static #validate (data: unknown): asserts data is NamedData } { + if (data == null) { + throw new TypeError('Worker received no data') + } + if (typeof data !== 'object') { + throw new Error('Invalid data') + } + const dataObject = data as { [key: string]: unknown } + if (!(data.password instanceof ArrayBuffer)) { + throw new TypeError('Password must be ArrayBuffer') + } + if (data.method !== 'encrypt' && data.method !== 'decrypt') { + throw new TypeError('Invalid method') + } + if (data.method === 'decrypt' && !(data.salt instanceof ArrayBuffer)) { + throw new TypeError('Salt required for decryption key') + } +} + +let importWorkerThreads = '' +NODE: importWorkerThreads = `import { parentPort } from 'node:worker_threads'` +export default ` + ${importWorkerThreads} + ${Convert} + const PBKDF2_ITERATIONS = ${PBKDF2_ITERATIONS} + const Entropy = ${Entropy} + const WorkerInterface = ${WorkerInterface} + const Passkey = ${Passkey} +`