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