From ae1b10fa32728aa9d2a99df7f58d53594883cb1b Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Sun, 27 Jul 2025 22:42:17 -0700 Subject: [PATCH] Password conversion worker. --- src/lib/workers/index.ts | 2 +- src/lib/workers/passkey.ts | 90 +++++++++++++++++++++++++++++++++ src/lib/workers/worker-queue.ts | 2 + 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/lib/workers/passkey.ts diff --git a/src/lib/workers/index.ts b/src/lib/workers/index.ts index 92b040a..7cfcfc4 100644 --- a/src/lib/workers/index.ts +++ b/src/lib/workers/index.ts @@ -1,4 +1,4 @@ //! SPDX-FileCopyrightText: 2025 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later -export { Bip44CkdWorker, NanoNaClWorker, SafeWorker } from './worker-queue' +export { Bip44CkdWorker, NanoNaClWorker, PasskeyWorker, SafeWorker } from './worker-queue' diff --git a/src/lib/workers/passkey.ts b/src/lib/workers/passkey.ts new file mode 100644 index 0000000..35ebdc6 --- /dev/null +++ b/src/lib/workers/passkey.ts @@ -0,0 +1,90 @@ +//! SPDX-FileCopyrightText: 2025 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +'use strict' + +import { parentPort } from 'node:worker_threads' + +/** +* Converts a user password to a `CryptoKey`. +*/ +export class Passkey { + static #parentPort: any + + static { + NODE: this.#parentPort = parentPort + const listener = async (message: MessageEvent): Promise => { + const { data } = message + if (data === 'STOP') { + BROWSER: close() + NODE: process.exit() + } else { + try { + const { purpose, password, salt } = this.#extractData(data) + const key = await this.#createAesKey(purpose, password, salt) + //@ts-expect-error + BROWSER: postMessage({ key, salt }, [key, salt]) + //@ts-expect-error + NODE: parentPort?.postMessage({ key, salt }, [key, salt]) + } catch (err) { + console.error(err) + BROWSER: postMessage({ error: 'Failed to derive key from password', cause: err }) + NODE: parentPort?.postMessage({ error: 'Failed to derive key from password', cause: err }) + } + } + } + BROWSER: addEventListener('message', listener) + NODE: this.#parentPort?.on('message', listener) + } + + static #extractData (data: unknown) { + 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 (!('password' in dataObject) || !(dataObject.password instanceof ArrayBuffer)) { + throw new TypeError('Password must be ArrayBuffer') + } + const password: ArrayBuffer = dataObject.password + if (!('purpose' in dataObject)) { + throw new TypeError('Key purpose is required') + } + if (dataObject.purpose !== 'encrypt' && dataObject.purpose !== 'decrypt') { + throw new TypeError('Invalid key purpose') + } + const purpose: 'encrypt' | 'decrypt' = dataObject.purpose + if (purpose === 'decrypt' && !('salt' in dataObject)) { + throw new TypeError('Salt required for decryption key') + } + if (dataObject.salt != null && !(dataObject.salt instanceof ArrayBuffer)) { + throw new TypeError('Salt must be ArrayBuffer') + } + const salt: ArrayBuffer = dataObject.salt ?? globalThis.crypto.getRandomValues(new Uint8Array(32)).buffer + return { purpose, password, salt } + } + + 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 + } + const derivedKeyType: AesKeyGenParams = { + name: 'AES-GCM', + length: 256 + } + return await globalThis.crypto.subtle.deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, [purpose]) + } +} + +let importWorkerThreads = '' +NODE: importWorkerThreads = `import { parentPort } from 'node:worker_threads'` +export default ` + ${importWorkerThreads} + const Passkey = ${Passkey} +` diff --git a/src/lib/workers/worker-queue.ts b/src/lib/workers/worker-queue.ts index 717058d..088857e 100644 --- a/src/lib/workers/worker-queue.ts +++ b/src/lib/workers/worker-queue.ts @@ -4,6 +4,7 @@ import { Worker as NodeWorker } from 'node:worker_threads' import { default as bip44 } from './bip44-ckd' import { default as nacl } from './nano-nacl' +import { default as passkey } from './passkey' import { default as safe } from './safe' import { Data, NamedData } from '#types' @@ -115,4 +116,5 @@ export class WorkerQueue { export const Bip44CkdWorker = new WorkerQueue(bip44) export const NanoNaClWorker = new WorkerQueue(nacl) +export const PasskeyWorker = new WorkerQueue(passkey) export const SafeWorker = new WorkerQueue(safe) -- 2.47.3