From 4e8cd77742467d0d6875f6883db265c5d47c651a Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Mon, 18 Nov 2024 12:15:01 -0800 Subject: [PATCH] Refactor passkey function to immediately execute polyfill if needed and export the function as a data URL string instead of a blob URL which doesn't work for creating workers. Test replacing key creation in Safe class with message to worker. Add data shape checks to wallet creation test. Add wallet gen performance tests. --- package.json | 9 ++++++--- src/lib/passkey.ts | 30 +++++++++++++----------------- src/lib/safe.ts | 8 ++++---- src/lib/thread.ts | 4 ++-- test/create-wallet.test.mjs | 24 ++++++++++++++++++++++++ 5 files changed, 49 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 93810e4..276d2cf 100644 --- a/package.json +++ b/package.json @@ -33,14 +33,17 @@ "package.json.license" ], "main": "dist/main.js", - "browser": "dist/main.min.js", + "browser": { + "dist/main.min.js": true, + "node:worker_threads": false + }, "repository": { "type": "git", "url": "git+https://zoso.dev/libnemo.git" }, "scripts": { - "build": "rm -rf dist && tsc && esbuild main.min=dist/main.js global.min=dist/global.js --outdir=dist --target=es2022 --format=esm --platform=browser --bundle --minify --sourcemap", - "test": "npm run build && node --test --env-file .env", + "build": "rm -rf dist && tsc && esbuild main.min=dist/main.js global.min=dist/global.js --outdir=dist --target=es2022 --format=esm --bundle --minify --sourcemap", + "test": "npm run build -- --platform=node && node --test --env-file .env", "test:coverage": "npm run test -- --experimental-test-coverage", "test:coverage:report": "npm run test:coverage -- --test-reporter=lcov --test-reporter-destination=coverage.info && genhtml coverage.info --output-directory test/coverage && rm coverage.info && xdg-open test/coverage/index.html" }, diff --git a/src/lib/passkey.ts b/src/lib/passkey.ts index e413fff..c660e4a 100644 --- a/src/lib/passkey.ts +++ b/src/lib/passkey.ts @@ -9,12 +9,22 @@ * calling function as an ArrayBuffer. This buffer can ultimately be processed * using the `importKey()` method of the SubtleCrypto interface. */ -function passkey () { +async function passkey () { + /** + * Polyfill for window methods which do not exist when executing Node.js tests. + */ + if (globalThis.addEventListener == null || globalThis.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) + } + } + /** * 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] }) @@ -46,21 +56,7 @@ function passkey () { ) 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) +const workerUrl = `data:text/javascript,(${passkey.toString()})()` export { workerUrl } diff --git a/src/lib/safe.ts b/src/lib/safe.ts index 91df9fa..9c541ed 100644 --- a/src/lib/safe.ts +++ b/src/lib/safe.ts @@ -15,7 +15,6 @@ export class Safe { constructor () { this.#storage = globalThis.sessionStorage this.#thread = new Thread(workerUrl) - URL.revokeObjectURL(workerUrl) } /** @@ -49,9 +48,10 @@ 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']) + const keyBuffer = await this.#thread.work({ password: passkey, iv: iv.bytes }) + passkey = await subtle.importKey('raw', keyBuffer, 'AES-GCM', false, ['encrypt']) + // 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) { throw new Error(ERR_MSG) } diff --git a/src/lib/thread.ts b/src/lib/thread.ts index 79addc4..948d603 100644 --- a/src/lib/thread.ts +++ b/src/lib/thread.ts @@ -31,8 +31,8 @@ export class Thread { constructor (url: string | URL) { this.#worker = new Worker(new URL(url, import.meta.url), { type: 'module' }) - this.#worker.addEventListener('message', (event) => { - const result = event.data ?? event + this.#worker.addEventListener('message', (message) => { + const result = message.data ?? message if (this.#task == null) { throw new ReferenceError(`Error resolving Worker result: ${result}`) } diff --git a/test/create-wallet.test.mjs b/test/create-wallet.test.mjs index 4e63dc3..b1ba84b 100644 --- a/test/create-wallet.test.mjs +++ b/test/create-wallet.test.mjs @@ -18,8 +18,11 @@ describe('creating a new wallet', async () => { await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) assert.ok('id' in wallet) + assert.ok(/[A-Fa-f0-9]{32,64}/.test(wallet.id)) assert.ok('mnemonic' in wallet) + assert.ok(/^(?:[a-z]{3,} ){11,23}[a-z]{3,}$/.test(wallet.mnemonic)) assert.ok('seed' in wallet) + assert.ok(/[A-Fa-f0-9]{32,64}/.test(wallet.seed)) }) it('BLAKE2b wallet with random entropy', async () => { @@ -27,8 +30,11 @@ describe('creating a new wallet', async () => { await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) assert.ok('id' in wallet) + assert.ok(/[A-Fa-f0-9]{32,64}/.test(wallet.id)) assert.ok('mnemonic' in wallet) + assert.ok(/^(?:[a-z]{3,} ){11,23}[a-z]{3,}$/.test(wallet.mnemonic)) assert.ok('seed' in wallet) + assert.ok(/[A-Fa-f0-9]{32,64}/.test(wallet.seed)) }) it('BIP-44 replace invalid salt with empty string', async () => { @@ -54,3 +60,21 @@ describe('creating a new wallet', async () => { assert.ok(wallet) }) }) + +describe('wallet generation performance', { skip: true }, async () => { + it('performance test creating BIP-44 wallets', async () => { + const wallets = [] + for (let i = 0x100; i > 0; i--) { + wallets.push(await Bip44Wallet.create(NANO_TEST_VECTORS.PASSWORD)) + } + assert.equal(wallets.length, 0x100) + }) + + it('performance test creating BLAKE2b wallets', async () => { + const wallets = [] + for (let i = 0x100; i > 0; i--) { + wallets.push(await Blake2bWallet.create(NANO_TEST_VECTORS.PASSWORD)) + } + assert.equal(wallets.length, 0x100) + }) +}) -- 2.34.1