--- /dev/null
+//! 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}
+`