]> git.codecow.com Git - libnemo.git/commitdiff
Password conversion worker.
authorChris Duncan <chris@zoso.dev>
Mon, 28 Jul 2025 05:42:17 +0000 (22:42 -0700)
committerChris Duncan <chris@zoso.dev>
Mon, 28 Jul 2025 05:42:17 +0000 (22:42 -0700)
src/lib/workers/index.ts
src/lib/workers/passkey.ts [new file with mode: 0644]
src/lib/workers/worker-queue.ts

index 92b040ae6ad6bbd86facc46eab05b57b6e1c8595..7cfcfc4ee8a44d04c76f03ba801aa41f3b67183a 100644 (file)
@@ -1,4 +1,4 @@
 //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
 //! 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 (file)
index 0000000..35ebdc6
--- /dev/null
@@ -0,0 +1,90 @@
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! 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<any>): Promise<void> => {
+                       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<CryptoKey> {
+               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}
+`
index 717058dbfe9c35f8e8e36a14355965b68f265725..088857e7fef2c611b381230e936de2d5b6560b25 100644 (file)
@@ -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)