--- /dev/null
+// SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+/**
+* Web Worker which generates an AES-GCM `CryptoKey`. The message sent to this
+* worker should be an object containing a password string and initialization
+* vector bytes which are used to generate an intermediate PBKDF2 `CryptoKey`.
+* This intermediate key is used to derive the final key that is returned to the
+* calling function as an ArrayBuffer. This buffer can ultimately be processed
+* using the `importKey()` method of the SubtleCrypto interface.
+*/
+function passkey () {
+ /**
+ * Message listener for this Web Worker thread.
+ */
+ addEventListener('message', async (message: any) => {
+ await polyfill()
+ const { password, iv } = message.data ?? message
+ const keyBuffer = await keygen(password, iv)
+ postMessage(keyBuffer, { transfer: [keyBuffer] })
+ })
+
+ /**
+ * Derives an AES-GCM key from a password and initialization vector.
+ *
+ * @param {string} password - User-provided text to use as initial key data
+ * @param {Uint8Array} iv - Initialization vector in byte representation
+ * @returns {Promise<ArrayBuffer>}
+ */
+ async function keygen (password: string, iv: Uint8Array): Promise<ArrayBuffer> {
+ const extractable = true
+ const notExtractable = false
+ const passkey = await crypto.subtle.importKey(
+ 'raw',
+ new TextEncoder().encode(password),
+ 'PBKDF2',
+ notExtractable,
+ ['deriveBits', 'deriveKey']
+ )
+ const key = await crypto.subtle.deriveKey(
+ { name: 'PBKDF2', hash: 'SHA-512', salt: iv, iterations: 210000 },
+ passkey,
+ { name: 'AES-GCM', length: 256 },
+ extractable,
+ ['encrypt']
+ )
+ return crypto.subtle.exportKey('raw', key)
+ }
+
+ /**
+ * Polyfill for window methods which do not exist when executing Node.js tests.
+ */
+ async function polyfill () {
+ if (addEventListener == null || postMessage == null) {
+ const { isMainThread, parentPort } = await import('node:worker_threads')
+ if (!isMainThread && parentPort) {
+ var addEventListener = Object.getPrototypeOf(parentPort).addListener.bind(parentPort)
+ var postMessage = Object.getPrototypeOf(parentPort).postMessage.bind(parentPort)
+ }
+ }
+ }
+}
+
+const worker = new Blob(['(', passkey.toString(), ')()'], { type: 'application/javascript' })
+const workerUrl = URL.createObjectURL(worker)
+export { workerUrl }
import { buffer, hex, utf8 } from './convert.js'
import { Entropy } from './entropy.js'
+import { workerUrl } from './passkey.js'
import { Thread } from './thread.js'
const { subtle } = globalThis.crypto
const ERR_MSG = 'Failed to store item in Safe'
export class Safe {
- #storage = globalThis.sessionStorage
- #thread = new Thread(new URL('./placeholder.js', import.meta.url))
+ #storage: Storage
+ #thread: Thread
+
+ constructor () {
+ this.#storage = globalThis.sessionStorage
+ this.#thread = new Thread(workerUrl)
+ URL.revokeObjectURL(workerUrl)
+ }
/**
* Encrypts data with a password and stores it in the Safe.
const iv = new Entropy()
if (typeof passkey === 'string') {
try {
+ // this.#thread.work({ password: passkey, iv: iv.bytes })
passkey = await subtle.importKey('raw', utf8.toBytes(passkey), 'PBKDF2', false, ['deriveBits', 'deriveKey'])
passkey = await subtle.deriveKey({ name: 'PBKDF2', hash: 'SHA-512', salt: iv.bytes, iterations: 210000 }, passkey, { name: 'AES-GCM', length: 256 }, false, ['encrypt'])
} catch (err) {