]> zoso.dev Git - libnemo.git/commitdiff
Create web worker to generate AES-GCM keys for locking wallets and accounts since...
authorChris Duncan <chris@zoso.dev>
Mon, 18 Nov 2024 18:02:46 +0000 (10:02 -0800)
committerChris Duncan <chris@zoso.dev>
Mon, 18 Nov 2024 18:02:46 +0000 (10:02 -0800)
src/lib/passkey.ts [new file with mode: 0644]
src/lib/safe.ts

diff --git a/src/lib/passkey.ts b/src/lib/passkey.ts
new file mode 100644 (file)
index 0000000..e413fff
--- /dev/null
@@ -0,0 +1,66 @@
+// 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 }
index 9fc411935b3e915ba4f0c792187af527b85a04b7..91df9fa0984d02be2918404ce31360941f2f667f 100644 (file)
@@ -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) {