]> git.codecow.com Git - libnemo.git/commitdiff
Add password-to-CryptoKey convert worker.
authorChris Duncan <chris@zoso.dev>
Sat, 26 Jul 2025 19:35:01 +0000 (12:35 -0700)
committerChris Duncan <chris@zoso.dev>
Sat, 26 Jul 2025 19:35:01 +0000 (12:35 -0700)
src/lib/workers/passkey.ts [new file with mode: 0644]

diff --git a/src/lib/workers/passkey.ts b/src/lib/workers/passkey.ts
new file mode 100644 (file)
index 0000000..80c9f54
--- /dev/null
@@ -0,0 +1,78 @@
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! 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<ArrayBuffer>): Promise<NamedData<CryptoKey>> {
+               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<CryptoKey> {
+               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<ArrayBuffer> } {
+               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}
+`