From: Chris Duncan Date: Mon, 18 Nov 2024 18:02:46 +0000 (-0800) Subject: Create web worker to generate AES-GCM keys for locking wallets and accounts since... X-Git-Url: https://zoso.dev/?a=commitdiff_plain;h=100487fc1ce9824f12c83918c875e409ff2ca0c9;p=libnemo.git Create web worker to generate AES-GCM keys for locking wallets and accounts since generating them from a password involves the long-running PBKDF2 process. --- diff --git a/src/lib/passkey.ts b/src/lib/passkey.ts new file mode 100644 index 0000000..e413fff --- /dev/null +++ b/src/lib/passkey.ts @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// 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} + */ + async function keygen (password: string, iv: Uint8Array): Promise { + 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 } diff --git a/src/lib/safe.ts b/src/lib/safe.ts index 9fc4119..91df9fa 100644 --- a/src/lib/safe.ts +++ b/src/lib/safe.ts @@ -3,13 +3,20 @@ 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. @@ -42,6 +49,7 @@ export class 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) {