From 03f69b78585a8a957c8a94f84a45b50963bcc1a2 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Tue, 2 Sep 2025 22:44:38 -0700 Subject: [PATCH] Refactor data extraction in vault worker. --- src/lib/vault/vault-worker.ts | 414 +++++++++++++++++----------------- src/lib/wallet/sign.ts | 2 +- src/lib/wallet/update.ts | 5 +- test/test.tools.mjs | 2 +- 4 files changed, 211 insertions(+), 212 deletions(-) diff --git a/src/lib/vault/vault-worker.ts b/src/lib/vault/vault-worker.ts index d0554af..fb49762 100644 --- a/src/lib/vault/vault-worker.ts +++ b/src/lib/vault/vault-worker.ts @@ -26,84 +26,69 @@ export class VaultWorker { this.#seed = undefined this.#mnemonic = undefined NODE: this.#parentPort = parentPort - const listener = (message: MessageEvent): Promise => { - return this.#extractData(message.data) - .then(extracted => { - const { - action, - type, - key, - keySalt, - iv, - seed, - mnemonicPhrase, - mnemonicSalt, - index, - encrypted, - data - } = extracted - let result: Promise + const listener = (event: MessageEvent): void => { + const data = this.#parseData(event.data) + const action = this.#parseAction(data) + const type = this.#parseType(action, data) + const keySalt = this.#parseKeySalt(action, data) + const iv = this.#parseIv(action, data) + const { seed, mnemonicPhrase, mnemonicSalt, index, encrypted, message } = this.#extractData(action, data) + this.#createPasskey(action, keySalt, data) + .then((key: CryptoKey | undefined): Promise => { switch (action) { case 'STOP': { BROWSER: close() NODE: process.exit() } case 'create': { - result = this.create(type, key, keySalt, mnemonicSalt) - break + return this.create(type, key, keySalt, mnemonicSalt) } case 'derive': { - result = this.derive(index) - break + return this.derive(index) } case 'load': { - result = this.#load(type, key, keySalt, mnemonicPhrase ?? seed, mnemonicSalt) + return this.#load(type, key, keySalt, mnemonicPhrase ?? seed, mnemonicSalt) break } case 'lock': { - result = Promise.resolve(this.lock()) - break + return Promise.resolve(this.lock()) } case 'sign': { - result = this.sign(index, data) - break + return this.sign(index, message) } case 'unlock': { - result = this.unlock(type, key, iv, encrypted) - break + return this.unlock(type, key, iv, encrypted) } case 'update': { - result = this.update(key, keySalt) - break + return this.update(key, keySalt) } case 'verify': { - result = Promise.resolve(this.verify(seed, mnemonicPhrase)) - break + return Promise.resolve(this.verify(seed, mnemonicPhrase)) } default: { throw new Error(`Unknown wallet action '${action}'`) } } - return result.then(result => { - const transfer = [] - for (const k of Object.keys(result)) { - if (result[k] instanceof ArrayBuffer || result[k] instanceof CryptoKey) { - transfer.push(result[k]) - } + }) + .then(result => { + const transfer = [] + for (const k of Object.keys(result)) { + if (result[k] instanceof ArrayBuffer || result[k] instanceof CryptoKey) { + transfer.push(result[k]) } - //@ts-expect-error - BROWSER: postMessage(result, transfer) - //@ts-expect-error - NODE: parentPort?.postMessage(result, transfer) - }) + } + //@ts-expect-error + BROWSER: postMessage(result, transfer) + //@ts-expect-error + NODE: parentPort?.postMessage(result, transfer) }) .catch((err: any) => { console.error(err) - for (const key of Object.keys(message.data)) { - if (message.data[key] instanceof ArrayBuffer && !message.data[key].detached) { - new Uint8Array(message.data[key]).fill(0).buffer.transfer?.() + for (const key of Object.keys(event.data)) { + if (event.data[key] instanceof ArrayBuffer && !event.data[key].detached) { + new Uint8Array(event.data[key]).fill(0).buffer.transfer?.() } - message.data[key] = undefined + event.data[key] = undefined } BROWSER: postMessage({ error: 'Failed to process Vault request', cause: err }) NODE: parentPort?.postMessage({ error: 'Failed to process Vault request', cause: err }) @@ -340,30 +325,49 @@ export class VaultWorker { } } - #createAesKey (purpose: 'encrypt' | 'decrypt', keySalt: ArrayBuffer, password?: ArrayBuffer): Promise { - if (password == null) { + #createPasskey (action: string, salt: ArrayBuffer, data: { [key: string]: unknown }) { + // Allowlisted wallet actions + if (['create', 'load', 'unlock', 'update'].includes(action)) { + + // Create local copy of password ASAP, then clear bytes from original buffer + if (!(data.password instanceof ArrayBuffer)) { + throw new TypeError('Password must be ArrayBuffer') + } + + const password = data.password.slice() + new Uint8Array(data.password).fill(0) + delete data.password + + // Only unlocking should decrypt the vault; other sensitive actions should + // throw if the vault is still locked and encrypted + const purpose = action === 'unlock' ? 'decrypt' : 'encrypt' + + return crypto.subtle + .importKey('raw', password, 'PBKDF2', false, ['deriveKey']) + .then(derivationKey => { + new Uint8Array(password).fill(0).buffer.transfer?.() + const derivationAlgorithm: Pbkdf2Params = { + name: 'PBKDF2', + hash: 'SHA-512', + iterations: 210000, + salt + } + const derivedKeyType: AesKeyGenParams = { + name: 'AES-GCM', + length: 256 + } + return crypto.subtle + .deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, [purpose]) + }) + .catch(err => { + console.error(err) + throw new Error('Failed to derive CryptoKey from password', { cause: err }) + }) + } else if (data.password !== undefined) { + throw new Error('Password is not allowed for this action', { cause: action }) + } else { return Promise.resolve(undefined) } - return crypto.subtle - .importKey('raw', password, 'PBKDF2', false, ['deriveKey']) - .then(derivationKey => { - new Uint8Array(password).fill(0).buffer.transfer?.() - const derivationAlgorithm: Pbkdf2Params = { - name: 'PBKDF2', - hash: 'SHA-512', - iterations: 210000, - salt: keySalt - } - const derivedKeyType: AesKeyGenParams = { - name: 'AES-GCM', - length: 256 - } - return crypto.subtle - .deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, [purpose]) - }) - .catch(err => { - throw new Error(err) - }) } #decryptWallet (type: string, key: CryptoKey, iv: ArrayBuffer, encrypted: ArrayBuffer): Promise { @@ -423,154 +427,83 @@ export class VaultWorker { /** * Parse inbound message from main thread into typechecked variables. */ - #extractData (message: unknown) { + #extractData (action: string, data: { [key: string]: unknown }) { try { - // Message itself - if (message == null) { - throw new TypeError('Worker received no data') - } - if (typeof message !== 'object') { - throw new Error('Invalid data') - } - const messageData = message as { [key: string]: unknown } - - // Action for selecting method execution - if (!('action' in messageData)) { - throw new TypeError('Wallet action is required') - } - if (messageData.action !== 'STOP' - && messageData.action !== 'create' - && messageData.action !== 'derive' - && messageData.action !== 'load' - && messageData.action !== 'lock' - && messageData.action !== 'sign' - && messageData.action !== 'unlock' - && messageData.action !== 'update' - && messageData.action !== 'verify') { - throw new TypeError('Invalid wallet action') - } - const action = messageData.action - - // Password for lock/unlock key - if (messageData.password != null && !(messageData.password instanceof ArrayBuffer)) { - throw new TypeError('Password must be ArrayBuffer') - } - let password = messageData.password?.slice() - if (messageData.password instanceof ArrayBuffer) { - new Uint8Array(messageData.password).fill(0) - delete messageData.password + // Import requires seed or mnemonic phrase + if (action === 'load' && data.seed == null && data.mnemonicPhrase == null) { + throw new TypeError('Seed or mnemonic phrase required to load wallet') } - // IV for crypto key, included if unlocking or generated if creating - if (action === 'unlock' && !(messageData.iv instanceof ArrayBuffer)) { - throw new TypeError('Initialization vector required to unlock wallet') + // Seed to load + if (action === 'load' && 'seed' in data && !(data.seed instanceof ArrayBuffer)) { + throw new TypeError('Seed required to load wallet') } - const iv: ArrayBuffer = action === 'unlock' && messageData.iv instanceof ArrayBuffer - ? messageData.iv - : crypto.getRandomValues(new Uint8Array(32)).buffer - - // Salt for decryption key to unlock - if (action === 'unlock' && !(messageData.keySalt instanceof ArrayBuffer)) { - throw new TypeError('Salt required to unlock wallet') + const seed = data.seed instanceof ArrayBuffer + ? data.seed.slice() + : undefined + if (data.seed instanceof ArrayBuffer) { + new Uint8Array(data.seed).fill(0) + delete data.seed } - const keySalt: ArrayBuffer = action === 'unlock' && messageData.keySalt instanceof ArrayBuffer - ? messageData.keySalt - : crypto.getRandomValues(new Uint8Array(32)).buffer - // CryptoKey from password, decryption key if unlocking else encryption key - return this.#createAesKey(action === 'unlock' ? 'decrypt' : 'encrypt', keySalt, password) - .then(key => { - if (password?.detached === false) { - new Uint8Array(password).fill(0) - password = undefined - } + // Mnemonic phrase to load + if (action === 'load' && 'mnemonicPhrase' in data && typeof data.mnemonicPhrase !== 'string') { + throw new TypeError('Invalid mnemonic phrase') + } + const mnemonicPhrase = typeof data.mnemonicPhrase === 'string' + ? data.mnemonicPhrase + : undefined + delete data.mnemonicPhrase - // Type of wallet - if (messageData.type !== undefined && messageData.type !== 'BIP-44' && messageData.type !== 'BLAKE2b') { - throw new TypeError('Invalid wallet type', { cause: messageData.type }) - } - const type: 'BIP-44' | 'BLAKE2b' | undefined = messageData.type - - // Import requires seed or mnemonic phrase - if (action === 'load' && messageData.seed == null && messageData.mnemonicPhrase == null) { - throw new TypeError('Seed or mnemonic phrase required to load wallet') - } + // Mnemonic salt for mnemonic phrase to load + if (action === 'load' && data.mnemonicSalt != undefined && typeof data.mnemonicSalt !== 'string') { + throw new TypeError('Invalid mnemonic salt for mnemonic phrase') + } + const mnemonicSalt = typeof data.mnemonicSalt === 'string' + ? data.mnemonicSalt + : undefined + delete data.mnemonicSalt - // Seed to load - if (action === 'load' && 'seed' in messageData && !(messageData.seed instanceof ArrayBuffer)) { - throw new TypeError('Seed required to load wallet') - } - const seed = messageData.seed instanceof ArrayBuffer - ? messageData.seed.slice() - : undefined - if (messageData.seed instanceof ArrayBuffer) { - new Uint8Array(messageData.seed).fill(0) - delete messageData.seed - } + // Encrypted seed and possibly mnemonic + if (action === 'unlock') { + if (data.encrypted == null) { + throw new TypeError('Wallet encrypted secrets not found') + } + if (!(data.encrypted instanceof ArrayBuffer)) { + throw new TypeError('Invalid wallet encrypted secrets') + } + } + const encrypted = data.encrypted instanceof ArrayBuffer + ? data.encrypted.slice() + : undefined + if (data.encrypted instanceof ArrayBuffer) { + new Uint8Array(data.encrypted).fill(0) + delete data.encrypted + } - // Mnemonic phrase to load - if (action === 'load' && 'mnemonicPhrase' in message && typeof messageData.mnemonicPhrase !== 'string') { - throw new TypeError('Invalid mnemonic phrase') - } - const mnemonicPhrase = typeof messageData.mnemonicPhrase === 'string' - ? messageData.mnemonicPhrase - : undefined - delete messageData.mnemonicPhrase - - // Mnemonic salt for mnemonic phrase to load - if (action === 'load' && messageData.mnemonicSalt != undefined && typeof messageData.mnemonicSalt !== 'string') { - throw new TypeError('Invalid mnemonic salt for mnemonic phrase') - } - const mnemonicSalt = typeof messageData.mnemonicSalt === 'string' - ? messageData.mnemonicSalt - : undefined - delete messageData.mnemonicSalt - - // Encrypted seed and possibly mnemonic - if (action === 'unlock') { - if (messageData.encrypted == null) { - throw new TypeError('Wallet encrypted secrets not found') - } - if (!(messageData.encrypted instanceof ArrayBuffer)) { - throw new TypeError('Invalid wallet encrypted secrets') - } - } - const encrypted = messageData.encrypted instanceof ArrayBuffer - ? messageData.encrypted.slice() - : undefined - if (messageData.encrypted instanceof ArrayBuffer) { - new Uint8Array(messageData.encrypted).fill(0) - delete messageData.encrypted - } + // Index for child account to derive or sign + if ((action === 'derive' || action === 'sign') && typeof data.index !== 'number') { + throw new TypeError('Index is required to derive an account private key') + } + const index = typeof data.index === 'number' + ? data.index + : undefined - // Index for child account to derive or sign - if ((action === 'derive' || action === 'sign') && typeof messageData.index !== 'number') { - throw new TypeError('Index is required to derive an account private key') - } - const index = typeof messageData.index === 'number' - ? messageData.index - : undefined - - // Data to sign - if (action === 'sign') { - if (messageData.data == null) { - throw new TypeError('Data to sign not found') - } - if (!(messageData.data instanceof ArrayBuffer)) { - throw new TypeError('Invalid data to sign') - } - } - const data = messageData.data instanceof ArrayBuffer - ? messageData.data - : undefined - delete messageData.data + // Data to sign + if (action === 'sign') { + if (data.message == null) { + throw new TypeError('Data to sign not found') + } + if (!(data.message instanceof ArrayBuffer)) { + throw new TypeError('Invalid data to sign') + } + } + const message = data.message instanceof ArrayBuffer + ? data.message + : undefined + delete data.message - return { action, type, key, iv, keySalt, seed, mnemonicPhrase, mnemonicSalt, encrypted, index, data } - }) - .catch(err => { - console.error(err) - throw new Error('Failed to create AES CryptoKey', { cause: err }) - }) + return { seed, mnemonicPhrase, mnemonicSalt, encrypted, index, message } } catch (err) { console.error(err) throw new Error('Failed to extract data', { cause: err }) @@ -642,4 +575,73 @@ export class VaultWorker { throw new Error('Failed to load wallet', { cause: err }) } } + + // Action for selecting method execution + #parseAction (data: { [key: string]: unknown }) { + if (data.action == null) { + throw new TypeError('Wallet action is required') + } + if (data.action !== 'STOP' + && data.action !== 'create' + && data.action !== 'derive' + && data.action !== 'load' + && data.action !== 'lock' + && data.action !== 'sign' + && data.action !== 'unlock' + && data.action !== 'update' + && data.action !== 'verify') { + throw new TypeError('Invalid wallet action') + } + return data.action + } + + // Worker message data itself + #parseData (data: unknown) { + if (data == null) { + throw new TypeError('Worker received no data') + } + if (typeof data !== 'object') { + throw new Error('Invalid data') + } + return data as { [key: string]: unknown } + } + + // Salt created to derive CryptoKey from password; subsequently required to + // derive the same key for unlock requests + #parseKeySalt (action: string, data: { [key: string]: unknown }): ArrayBuffer { + if (action === 'unlock') { + if (data.keySalt instanceof ArrayBuffer) { + return data.keySalt + } else { + throw new TypeError('Key salt required to unlock wallet') + } + } else { + return crypto.getRandomValues(new Uint8Array(32)).buffer + } + } + + // Initialization vector created to encrypt and lock the vault; subsequently + // required to decrypt and unlock the vault + #parseIv (action: string, data: { [key: string]: unknown }) { + if (action === 'unlock') { + if (!(data.iv instanceof ArrayBuffer)) { + throw new TypeError('Initialization vector required to unlock wallet') + } + } else if (data.iv !== undefined) { + throw new Error('IV is not allowed for this action', { cause: action }) + } + return data.iv + } + + // Algorithm used for wallet functions + #parseType (action: string, data: { [key: string]: unknown }) { + if (['create', 'load', 'unlock'].includes(action)) { + if (data.type !== 'BIP-44' && data.type !== 'BLAKE2b') { + throw new TypeError(`Type is required to ${action} wallet`) + } + } else if (data.type !== undefined) { + throw new Error('Type is not allowed for this action', { cause: action }) + } + return data.type + } } diff --git a/src/lib/wallet/sign.ts b/src/lib/wallet/sign.ts index 918cdd8..234d850 100644 --- a/src/lib/wallet/sign.ts +++ b/src/lib/wallet/sign.ts @@ -30,7 +30,7 @@ export async function _sign (wallet: Wallet, vault: Vault, index: unknown, block const { signature } = await vault.request({ action: 'sign', index, - data: hex.toBuffer(block.hash) + message: hex.toBuffer(block.hash) }) block.signature = bytes.toHex(new Uint8Array(signature)) } diff --git a/src/lib/wallet/update.ts b/src/lib/wallet/update.ts index 4e30de5..50d3f92 100644 --- a/src/lib/wallet/update.ts +++ b/src/lib/wallet/update.ts @@ -21,12 +21,9 @@ export async function _update (wallet: Wallet, vault: Vault, password: unknown): if (typeof password !== 'string') { throw new TypeError('Password must be a string') } - const { encrypted } = await _get(wallet.id) const response = await vault.request({ action: 'update', - type: wallet.type, - password: utf8.toBuffer(password), - encrypted + password: utf8.toBuffer(password) }) password = undefined record.iv = response.iv diff --git a/test/test.tools.mjs b/test/test.tools.mjs index cda3585..20ae465 100644 --- a/test/test.tools.mjs +++ b/test/test.tools.mjs @@ -19,7 +19,7 @@ let Block */ let Rpc /** -* @type {import('../dist/types.d.ts').Tools} +* @type {typeof import('../dist/types.d.ts').Tools} */ let Tools /** -- 2.47.3