From b48ac2186a1b34d470ccc0d60d2cabafb40814f7 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Tue, 31 Mar 2026 01:32:07 -0700 Subject: [PATCH] Rewrite vault worker to eliminate extraneous class wrapper. Refactor vault host and worker to check domain and job ID prior to accepting worker results in order to prevent unrelated workers from contaminating results. --- src/lib/vault/index.ts | 36 +- src/lib/vault/vault-worker.ts | 1094 ++++++++++++++++----------------- 2 files changed, 563 insertions(+), 567 deletions(-) diff --git a/src/lib/vault/index.ts b/src/lib/vault/index.ts index 0273de5..80d3097 100644 --- a/src/lib/vault/index.ts +++ b/src/lib/vault/index.ts @@ -5,6 +5,7 @@ import { Worker as NodeWorker } from 'node:worker_threads' import { Data } from '../database' type Task = { + url: string id: number data: Record> reject: (value: any) => void @@ -56,6 +57,7 @@ export class Vault { return reject('Worker terminated') } const task: Task = { + url: this.#url, id: performance.now(), data, resolve, @@ -76,7 +78,7 @@ export class Vault { this.#job = this.#queue.shift() this.#isIdle = this.#job == null if (this.#job != null) { - const { data, reject } = this.#job + const { url, id, data, reject } = this.#job try { const buffers: ArrayBuffer[] = [] for (const d of Object.values(data)) { @@ -84,6 +86,8 @@ export class Vault { buffers.push(d) } } + data.url = url + data.id = id BROWSER: this.#worker.postMessage(data, buffers) NODE: this.#worker.postMessage({ data }, buffers) } catch (err) { @@ -102,19 +106,27 @@ export class Vault { return } if (this.#job == null) { - throw new Error('Worker returned results but had nowhere to report it.') + throw new Error('Vault worker returned results but had no job to report it.') } - const { resolve, reject } = this.#job - try { - if (results?.error != null) { - reject(results) - } else { - resolve(results) + const { url, id, resolve, reject } = this.#job + if (url == null) { + throw new Error('Vault worker job missing URL') + } + if (id == null) { + throw new Error('Vault worker job missing ID') + } + if (results?.url === url && results?.id === id) { + try { + if (results?.error != null) { + reject(results) + } else { + resolve(results) + } + } catch (err) { + reject(err) + } finally { + this.#process() } - } catch (err) { - reject(err) - } finally { - this.#process() } } } diff --git a/src/lib/vault/vault-worker.ts b/src/lib/vault/vault-worker.ts index e813cd6..57aa1f9 100644 --- a/src/lib/vault/vault-worker.ts +++ b/src/lib/vault/vault-worker.ts @@ -2,6 +2,7 @@ //! SPDX-License-Identifier: GPL-3.0-or-later import * as nano25519 from 'nano25519' +import { parentPort } from 'node:worker_threads' import { BIP44_COIN_NANO } from '../constants' import { Bip39, Bip44, Blake2b, WalletAesGcm } from '../crypto' import { WalletType } from '../wallet' @@ -9,634 +10,617 @@ import { Passkey } from './passkey' import { VaultTimer } from './vault-timer' /** -* Cross-platform worker for managing wallet secrets. -*/ -export class VaultWorker { - #encoder: TextEncoder = new TextEncoder() - #locked: boolean - #timeout: number - #timer: VaultTimer - #type?: 'BIP-44' | 'BLAKE2b' | 'Exodus' - #seed?: ArrayBuffer - #mnemonic?: ArrayBuffer - #parentPort?: any - - constructor () { - this.#locked = true - this.#timeout = 120_000 - this.#timer = new VaultTimer(() => { }, 0) - this.#type = undefined - this.#seed = undefined - this.#mnemonic = undefined - - const listener = (event: MessageEvent): void => { - NODE: if (this.#parentPort == null) setTimeout(() => listener(event), 0) - const data = this.#parseData(event.data) - const action = this.#parseAction(data) - const keySalt = this.#parseKeySalt(action, data) - Passkey.create(action, keySalt, data) - .then((key: CryptoKey | undefined): Promise | void> => { - const type = this.#parseType(action, data) - const iv = this.#parseIv(action, data) - const { seed, mnemonicPhrase, mnemonicSalt, index, encrypted, message, timeout } = this.#extractData(action, data) - switch (action) { - case 'STOP': { - BROWSER: close() - NODE: process.exit() - } - case 'config': { - return this.config(timeout) - } - case 'create': { - return this.create(type, key, keySalt, mnemonicSalt) - } - case 'derive': { - return this.derive(index) - } - case 'load': { - return this.load(type, key, keySalt, mnemonicPhrase ?? seed, mnemonicSalt) - } - case 'lock': { - return this.lock() - } - case 'sign': { - return this.sign(index, message) - } - case 'unlock': { - return this.unlock(type, key, iv, encrypted) - } - case 'update': { - return this.update(key, keySalt) - } - case 'verify': { - return Promise.resolve(this.verify(seed, mnemonicPhrase)) - } - default: { - throw new Error(`Unknown wallet action '${action}'`) - } - } - }) - .then((result: Record | void) => { - const transfer = [] - if (result) { - for (const r of Object.values(result)) { - if (r instanceof ArrayBuffer) { - transfer.push(r) - } - } - } - //@ts-expect-error - BROWSER: postMessage(result, transfer) - NODE: this.#parentPort?.postMessage(result, transfer) - }) - .catch((err: any) => { - console.error(err) - for (let data of Object.values(event.data)) { - if (data instanceof ArrayBuffer && !data.detached) { - new Uint8Array(data).fill(0).buffer.transfer?.() - } - data = undefined - } - BROWSER: postMessage({ error: 'Failed to process Vault request', cause: err }) - NODE: this.#parentPort?.postMessage({ error: 'Failed to process Vault request', cause: err }) - }) - } - BROWSER: addEventListener('message', listener) - NODE: { - if (this.#parentPort == null) { - import('node:worker_threads') - .then(({ parentPort }) => { - this.#parentPort = parentPort - this.#parentPort.on('message', listener) - }) - } - } - } + * Cross-platform worker for managing wallet secrets. + */ +const _encoder: TextEncoder = new TextEncoder() +let _locked: boolean = true +let _timeout: number = 120_000 +let _timer: VaultTimer = new VaultTimer(() => { }, 0) +let _type: 'BIP-44' | 'BLAKE2b' | 'Exodus' | undefined = undefined +let _seed: ArrayBuffer | undefined = undefined +let _mnemonic: ArrayBuffer | undefined = undefined - /** - * Configures vault settings. The wallet must be unlocked prior to - * configuration. - */ - config (timeout?: number): Promise { - try { - this.#timer?.pause() - if (this.#locked) { - throw new Error('Wallet is locked') - } - if (typeof timeout === 'number') { - if (timeout < 10) { - throw new RangeError('Timeout must be at least 10 seconds') +const listener = (event: MessageEvent): void => { + const { url, id } = event.data + if (url !== location.href) return + NODE: if (parentPort == null) setTimeout(() => listener(event), 0) + const data = _parseData(event.data) + const action = _parseAction(data) + const keySalt = _parseKeySalt(action, data) + Passkey.create(action, keySalt, data) + .then((key: CryptoKey | undefined): Promise | void> => { + const type = _parseType(action, data) + const iv = _parseIv(action, data) + const { seed, mnemonicPhrase, mnemonicSalt, index, encrypted, message, timeout } = _extractData(action, data) + switch (action) { + case 'STOP': { + BROWSER: close() + NODE: process.exit() + } + case 'config': { + return config(timeout) + } + case 'create': { + return create(type, key, keySalt, mnemonicSalt) + } + case 'derive': { + return derive(index) + } + case 'load': { + return load(type, key, keySalt, mnemonicPhrase ?? seed, mnemonicSalt) + } + case 'lock': { + return lock() } - if (timeout > 600) { - throw new RangeError('Timeout must be at most 10 minutes') + case 'sign': { + return sign(index, message) + } + case 'unlock': { + return unlock(type, key, iv, encrypted) + } + case 'update': { + return update(key, keySalt) + } + case 'verify': { + return Promise.resolve(verify(seed, mnemonicPhrase)) + } + default: { + throw new Error(`Unknown wallet action '${action}'`) } - this.#timeout = timeout * 1000 - this.#timer = new VaultTimer(() => this.lock(), this.#timeout) } - return Promise.resolve() - } catch (err) { - console.error(err) - this.#timer?.resume() - throw new Error('Failed to configure Vault', { cause: err }) - } - } - - /** - * Generates a new mnemonic and seed and then returns the initialization vector - * vector, salt, and encrypted data representing the wallet in a locked state. - */ - create (type?: WalletType, key?: CryptoKey, keySalt?: ArrayBuffer, mnemonicSalt?: string): Promise> { - if (type !== 'BIP-44' && type !== 'BLAKE2b') { - throw new TypeError('Unsupported software wallet algorithm', { cause: type }) - } - try { - const entropy = crypto.getRandomValues(new Uint8Array(32)) - return Bip39.fromEntropy(entropy) - .then(bip39 => this.#load(type, key, keySalt, bip39.phrase, mnemonicSalt)) - .then(({ iv, salt, encrypted }) => { - entropy.fill(0) - if (this.#seed == null || this.#mnemonic == null) { - throw new Error('Failed to generate seed and mnemonic') + }) + .then((result: Record | void) => { + result ??= {} + const transfer: ArrayBuffer[] = [] + if (result) { + for (const r of Object.values(result)) { + if (r instanceof ArrayBuffer) { + // transfer.push(r) } - return { iv, salt, encrypted, seed: this.#seed.slice(), mnemonic: this.#mnemonic.slice() } - }) - } catch (err) { - console.error(err) - throw new Error('Failed to create wallet', { cause: err }) - } finally { - this.lock() - } - } - - /** - * Derives the private and public keys of a child account from the current - * wallet seed at a specified index and then returns the public key. The wallet - * must be unlocked prior to derivation. - */ - derive (index?: number): Promise> { - try { - this.#timer.pause() - if (this.#locked) { - throw new Error('Wallet is locked') + } } - if (this.#seed == null) { - throw new Error('Wallet seed not found') + result.url = url + result.id = id + //@ts-expect-error + BROWSER: postMessage(result, transfer) + NODE: parentPort?.postMessage(result, transfer) + }) + .catch((err: any) => { + for (let data of Object.values(event.data)) { + if (data instanceof ArrayBuffer && !data.detached) { + new Uint8Array(data).fill(0).buffer.transfer?.() + } + data = undefined } - if (this.#type !== 'BIP-44' && this.#type !== 'BLAKE2b' && this.#type !== 'Exodus') { - throw new Error('Invalid wallet type') + BROWSER: postMessage({ error: 'Failed to process Vault request', cause: err }) + NODE: parentPort?.postMessage({ error: 'Failed to process Vault request', cause: err }) + }) +} +BROWSER: addEventListener('message', listener) +NODE: parentPort?.on('message', listener) + +/** + * Configures vault settings. The wallet must be unlocked prior to + * configuration. + */ +async function config (timeout?: number): Promise { + try { + _timer?.pause() + if (_locked) { + throw new Error('Wallet is locked') + } + if (typeof timeout === 'number') { + if (timeout < 10) { + throw new RangeError('Timeout must be at least 10 seconds') } - if (typeof index !== 'number') { - throw new Error('Invalid wallet account index') + if (timeout > 600) { + throw new RangeError('Timeout must be at most 10 minutes') } - const derive = this.#type === 'BIP-44' - ? Bip44.ckd('ed25519 seed', this.#seed, BIP44_COIN_NANO, index) - : this.#type === 'Exodus' - ? Bip44.ckd('Bitcoin seed', this.#seed, 0x100, index, 0, 0) - : Blake2b.ckd(this.#seed, index) - return derive.then(result => { - const prv = new Uint8Array(result) - const pub = nano25519.derive(prv) - this.#timer = new VaultTimer(() => this.lock(), this.#timeout) - return { index, publicKey: pub.buffer } - }) - } catch (err) { - console.error(err) - this.#timer.resume() - throw new Error('Failed to derive account', { cause: err }) + timeout = timeout * 1000 + _timer = new VaultTimer(() => lock(), timeout) } + return Promise.resolve() + } catch (err) { + console.error(err) + _timer?.resume() + throw new Error('Failed to configure Vault', { cause: err }) } +} - /** - * Encrypts an existing seed or mnemonic+salt and returns the initialization - * vector, salt, and encrypted data representing the wallet in a locked state. - */ - load (type?: WalletType, key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise> { - if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Exodus') { - throw new TypeError('Unsupported software wallet algorithm', { cause: type }) - } - return this.#load(type, key, keySalt, secret, mnemonicSalt) - .then(record => { - if (this.#seed == null) { - throw new Error('Wallet seed not found') +/** +* Generates a new mnemonic and seed and then returns the initialization vector +* vector, salt, and encrypted data representing the wallet in a locked state. +*/ +async function create (type?: WalletType, key?: CryptoKey, keySalt?: ArrayBuffer, mnemonicSalt?: string): Promise> { + if (type !== 'BIP-44' && type !== 'BLAKE2b') { + throw new TypeError('Unsupported software wallet algorithm', { cause: type }) + } + try { + const entropy = crypto.getRandomValues(new Uint8Array(32)) + return Bip39.fromEntropy(entropy) + .then(bip39 => _load(type, key, keySalt, bip39.phrase, mnemonicSalt)) + .then(({ iv, salt, encrypted }) => { + entropy.fill(0) + if (_seed == null || _mnemonic == null) { + throw new Error('Failed to generate seed and mnemonic') } - return record - }) - .catch(err => { - console.error(err) - throw new Error('Failed to load wallet', { cause: err }) + return { iv, salt, encrypted, seed: _seed.slice(), mnemonic: _mnemonic.slice() } }) - .finally(() => this.lock()) + } catch (err) { + console.error(err) + throw new Error('Failed to create wallet', { cause: err }) + } finally { + lock() } +} - lock (): Promise { - this.#mnemonic = undefined - this.#seed = undefined - this.#locked = true - this.#timer?.pause() - BROWSER: postMessage('locked') - NODE: this.#parentPort?.postMessage('locked') - return Promise.resolve() +/** +* Derives the private and public keys of a child account from the current +* wallet seed at a specified index and then returns the public key. The wallet +* must be unlocked prior to derivation. +*/ +async function derive (index?: number): Promise> { + try { + _timer.pause() + if (_locked) { + throw new Error('Wallet is locked') + } + if (_seed == null) { + throw new Error('Wallet seed not found') + } + if (_type !== 'BIP-44' && _type !== 'BLAKE2b' && _type !== 'Exodus') { + throw new Error('Invalid wallet type') + } + if (typeof index !== 'number') { + throw new Error('Invalid wallet account index') + } + const derive = _type === 'BIP-44' + ? Bip44.ckd('ed25519 seed', _seed, BIP44_COIN_NANO, index) + : _type === 'Exodus' + ? Bip44.ckd('Bitcoin seed', _seed, 0x100, index, 0, 0) + : Blake2b.ckd(_seed, index) + return derive.then(result => { + const prv = new Uint8Array(result) + const pub = nano25519.derive(prv) + _timer = new VaultTimer(() => lock(), _timeout) + return { index, publicKey: pub.buffer } + }) + } catch (err) { + console.error(err) + _timer.resume() + throw new Error('Failed to derive account', { cause: err }) } +} - /** - * Derives the account private key at a specified index, signs the input data, - * and returns a signature. The wallet must be unlocked prior to verification. - */ - sign (index?: number, data?: ArrayBuffer): Promise> { - try { - this.#timer.pause() - if (this.#locked) { - throw new Error('Wallet is locked') - } - if (this.#seed == null) { +/** +* Encrypts an existing seed or mnemonic+salt and returns the initialization +* vector, salt, and encrypted data representing the wallet in a locked state. +*/ +async function load (type?: WalletType, key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise> { + if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Exodus') { + throw new TypeError('Unsupported software wallet algorithm', { cause: type }) + } + return _load(type, key, keySalt, secret, mnemonicSalt) + .then(record => { + if (_seed == null) { throw new Error('Wallet seed not found') } - if (index == null) { - throw new Error('Wallet account index is required to sign') - } - if (data == null) { - throw new Error('Data to sign not found') - } - const derive = this.#type === 'BLAKE2b' - ? Blake2b.ckd(this.#seed, index) - : Bip44.ckd(this.#type === 'Exodus' ? 'Bitcoin seed' : 'ed25519 seed', this.#seed, BIP44_COIN_NANO, index) - return derive.then(result => { - const prv = new Uint8Array(result) - const pub = nano25519.derive(prv) - const sig = nano25519.sign(new Uint8Array(data), new Uint8Array([...prv, ...pub])) - this.#timer = new VaultTimer(() => this.lock(), this.#timeout) - return { signature: sig.buffer } - }) - } catch (err) { + return record + }) + .catch(err => { console.error(err) - this.#timer.resume() - throw new Error('Failed to sign message', { cause: err }) + throw new Error('Failed to load wallet', { cause: err }) + }) + .finally(() => lock()) +} + +async function lock (): Promise { + _mnemonic = undefined + _seed = undefined + _locked = true + _timer?.pause() + BROWSER: postMessage('locked') + NODE: parentPort?.postMessage('locked') + return Promise.resolve() +} + +/** +* Derives the account private key at a specified index, signs the input data, +* and returns a signature. The wallet must be unlocked prior to verification. +*/ +async function sign (index?: number, data?: ArrayBuffer): Promise> { + try { + _timer.pause() + if (_locked) { + throw new Error('Wallet is locked') + } + if (_seed == null) { + throw new Error('Wallet seed not found') + } + if (index == null) { + throw new Error('Wallet account index is required to sign') + } + if (data == null) { + throw new Error('Data to sign not found') } + const derive = _type === 'BLAKE2b' + ? Blake2b.ckd(_seed, index) + : Bip44.ckd(_type === 'Exodus' ? 'Bitcoin seed' : 'ed25519 seed', _seed, BIP44_COIN_NANO, index) + return derive.then(result => { + const prv = new Uint8Array(result) + const pub = nano25519.derive(prv) + const sig = nano25519.sign(new Uint8Array(data), new Uint8Array([...prv, ...pub])) + _timer = new VaultTimer(() => lock(), _timeout) + return { signature: sig.buffer } + }) + } catch (err) { + console.error(err) + _timer.resume() + throw new Error('Failed to sign message', { cause: err }) } +} - /** - * Decrypts the input and sets the seed and, if it is included, the mnemonic. - */ - unlock (type?: WalletType, key?: CryptoKey, iv?: ArrayBuffer, encrypted?: ArrayBuffer): Promise { - if (type == null) { - throw new TypeError('Wallet type is required') - } - if (type === 'Ledger') { - this.#locked = false +/** +* Decrypts the input and sets the seed and, if it is included, the mnemonic. +*/ +async function unlock (type?: WalletType, key?: CryptoKey, iv?: ArrayBuffer, encrypted?: ArrayBuffer): Promise { + if (type == null) { + throw new TypeError('Wallet type is required') + } + if (type === 'Ledger') { + _locked = false + BROWSER: postMessage('unlocked') + NODE: parentPort?.postMessage('unlocked') + return Promise.resolve() + } + if (key == null) { + throw new TypeError('Wallet password is required') + } + if (iv == null) { + throw new TypeError('Wallet IV is required') + } + if (encrypted == null) { + throw new TypeError('Wallet encrypted data is required') + } + _timer?.pause() + return WalletAesGcm.decrypt(type, key, iv, encrypted) + .then(({ mnemonic, seed }) => { + if (!(seed instanceof ArrayBuffer)) { + throw new TypeError('Invalid seed') + } + if (mnemonic != null && !(mnemonic instanceof ArrayBuffer)) { + throw new TypeError('Invalid mnemonic') + } + type = type + seed = seed + mnemonic = mnemonic + _locked = false + _timer = new VaultTimer(lock, _timeout) BROWSER: postMessage('unlocked') - NODE: this.#parentPort?.postMessage('unlocked') - return Promise.resolve() + NODE: parentPort?.postMessage('unlocked') + }) + .catch(err => { + console.error(err) + _timer?.resume() + throw new Error('Failed to unlock wallet', { cause: err }) + }) +} + +/** +* Re-encrypts the wallet with a new password. +*/ +async function update (key?: CryptoKey, salt?: ArrayBuffer): Promise> { + try { + _timer.pause() + if (_locked) { + throw new Error('Wallet is locked') } - if (key == null) { - throw new TypeError('Wallet password is required') + if (_seed == null) { + throw new Error('Wallet seed not found') } - if (iv == null) { - throw new TypeError('Wallet IV is required') + if (_type == null) { + throw new Error('Wallet type not found') } - if (encrypted == null) { - throw new TypeError('Wallet encrypted data is required') + if (key == null || salt == null) { + throw new TypeError('Wallet password is required') } - this.#timer?.pause() - return WalletAesGcm.decrypt(type, key, iv, encrypted) - .then(({ mnemonic, seed }) => { - if (!(seed instanceof ArrayBuffer)) { - throw new TypeError('Invalid seed') - } - if (mnemonic != null && !(mnemonic instanceof ArrayBuffer)) { - throw new TypeError('Invalid mnemonic') - } - this.#type = type - this.#seed = seed - this.#mnemonic = mnemonic - this.#locked = false - this.#timer = new VaultTimer(this.lock.bind(this), this.#timeout) - BROWSER: postMessage('unlocked') - NODE: this.#parentPort?.postMessage('unlocked') - }) - .catch(err => { - console.error(err) - this.#timer?.resume() - throw new Error('Failed to unlock wallet', { cause: err }) + return WalletAesGcm.encrypt(_type, key, _seed, _mnemonic) + .then(({ iv, encrypted }) => { + _timer = new VaultTimer(() => lock(), _timeout) + return { iv, salt, encrypted } }) + } catch (err) { + console.error(err) + _timer.resume() + throw new Error('Failed to update wallet password', { cause: err }) } +} - /** - * Re-encrypts the wallet with a new password. - */ - update (key?: CryptoKey, salt?: ArrayBuffer): Promise> { - try { - this.#timer.pause() - if (this.#locked) { - throw new Error('Wallet is locked') - } - if (this.#seed == null) { - throw new Error('Wallet seed not found') - } - if (this.#type == null) { - throw new Error('Wallet type not found') - } - if (key == null || salt == null) { - throw new TypeError('Wallet password is required') - } - return WalletAesGcm.encrypt(this.#type, key, this.#seed, this.#mnemonic) - .then(({ iv, encrypted }) => { - this.#timer = new VaultTimer(() => this.lock(), this.#timeout) - return { iv, salt, encrypted } - }) - } catch (err) { - console.error(err) - this.#timer.resume() - throw new Error('Failed to update wallet password', { cause: err }) +/** +* Checks the seed and, if it exists, the mnemonic against input. The wallet +* must be unlocked prior to verification. +*/ +function verify (seed?: ArrayBuffer, mnemonicPhrase?: string): Record { + try { + if (_locked) { + throw new Error('Wallet is locked') } - } - - /** - * Checks the seed and, if it exists, the mnemonic against input. The wallet - * must be unlocked prior to verification. - */ - verify (seed?: ArrayBuffer, mnemonicPhrase?: string): Record { - try { - if (this.#locked) { - throw new Error('Wallet is locked') - } - if (this.#seed == null) { - throw new Error('Wallet seed not found') - } - if (seed == null && mnemonicPhrase == null) { - throw new Error('Seed or mnemonic phrase is required') - } - if (seed != null && mnemonicPhrase != null) { - throw new Error('Seed or mnemonic phrase must be verified separately') - } - let isVerified = false - if (seed != null) { - let diff = 0 - const userSeed = new Uint8Array(seed) - const thisSeed = new Uint8Array(this.#seed) - for (let i = 0; i < userSeed.byteLength; i++) { - diff |= userSeed[i] ^ thisSeed[i] - } - isVerified = diff === 0 - } - if (mnemonicPhrase != null) { - console.log(new TextDecoder().decode((new Uint8Array(this.#mnemonic ?? [])))) - let diff = 0 - const userMnemonic = this.#encoder.encode(mnemonicPhrase) - const thisMnemonic = new Uint8Array(this.#mnemonic ?? []) - for (let i = 0; i < userMnemonic.byteLength; i++) { - diff |= userMnemonic[i] ^ thisMnemonic[i] - } - isVerified = diff === 0 - } - return { isVerified } - } catch (err) { - console.error(err) - throw new Error('Failed to verify wallet', { cause: err }) + if (seed == null) { + throw new Error('Wallet seed not found') } + if (seed == null && mnemonicPhrase == null) { + throw new Error('Seed or mnemonic phrase is required') + } + if (seed != null && mnemonicPhrase != null) { + throw new Error('Seed or mnemonic phrase must be verified separately') + } + let isVerified = false + if (seed != null) { + let diff = 0 + const userSeed = new Uint8Array(seed) + const thisSeed = new Uint8Array(seed) + for (let i = 0; i < userSeed.byteLength; i++) { + diff |= userSeed[i] ^ thisSeed[i] + } + isVerified = diff === 0 + } + if (mnemonicPhrase != null) { + console.log(new TextDecoder().decode((new Uint8Array(_mnemonic ?? [])))) + let diff = 0 + const userMnemonic = _encoder.encode(mnemonicPhrase) + const thisMnemonic = new Uint8Array(_mnemonic ?? []) + for (let i = 0; i < userMnemonic.byteLength; i++) { + diff |= userMnemonic[i] ^ thisMnemonic[i] + } + isVerified = diff === 0 + } + return { isVerified } + } catch (err) { + console.error(err) + throw new Error('Failed to verify wallet', { cause: err }) } +} - /** - * Parse inbound message from main thread into typechecked variables. - */ - #extractData (action: string, data: { [key: string]: unknown }) { - try { - // 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') - } +/** +* Parse inbound message from main thread into typechecked variables. +*/ +function _extractData (action: string, data: { [key: string]: unknown }) { + try { + // 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') + } - // Seed to load - if (action === 'load' && 'seed' in data && !(data.seed instanceof ArrayBuffer)) { - throw new TypeError('Seed required to load 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 - } + // Seed to load + if (action === 'load' && 'seed' in data && !(data.seed instanceof ArrayBuffer)) { + throw new TypeError('Seed required to load 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 + } - // 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 + // 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 - // 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 + // 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 - // 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') - } + // Encrypted seed and possibly mnemonic + if (action === 'unlock') { + if (data.encrypted == null) { + throw new TypeError('Wallet encrypted secrets not found') } - const encrypted = data.encrypted instanceof ArrayBuffer - ? data.encrypted.slice() - : undefined - if (data.encrypted instanceof ArrayBuffer) { - new Uint8Array(data.encrypted).fill(0) - delete data.encrypted + 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 + } - // 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 data.index !== 'number') { + throw new TypeError('Index is required to derive an account private key') + } + const index = typeof data.index === 'number' + ? data.index + : undefined - // 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') - } + // Data to sign + if (action === 'sign') { + if (data.message == null) { + throw new TypeError('Data to sign not found') } - const message = data.message instanceof ArrayBuffer - ? data.message - : undefined - delete data.message - - // Vault configuration - if (action === 'config') { - if (data.timeout == null) { - throw new TypeError('Configuration not found') - } - if (typeof data.timeout !== 'number') { - throw new TypeError('Invalid timeout configuration') - } + if (!(data.message instanceof ArrayBuffer)) { + throw new TypeError('Invalid data to sign') } - const timeout = typeof data.timeout === 'number' - ? data.timeout - : undefined - - return { seed, mnemonicPhrase, mnemonicSalt, encrypted, index, message, timeout } - } catch (err) { - console.error(err) - throw new Error('Failed to extract data', { cause: err }) } - } + const message = data.message instanceof ArrayBuffer + ? data.message + : undefined + delete data.message - /** - * Encrypts an existing seed or mnemonic+salt and returns the initialization - * vector, salt, and encrypted data representing the wallet in a locked state. - */ - #load (type?: 'BIP-44' | 'BLAKE2b' | 'Exodus', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise> { - try { - if (!this.#locked) { - throw new Error('Wallet is in use') - } - if (key == null || keySalt == null) { - throw new Error('Wallet password is required') + // Vault configuration + if (action === 'config') { + if (data.timeout == null) { + throw new TypeError('Configuration not found') } - if (type == null) { - throw new TypeError('Wallet type is required') + if (typeof data.timeout !== 'number') { + throw new TypeError('Invalid timeout configuration') } - if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Exodus') { - throw new TypeError('Invalid wallet type') + } + const timeout = typeof data.timeout === 'number' + ? data.timeout + : undefined + + return { seed, mnemonicPhrase, mnemonicSalt, encrypted, index, message, timeout } + } catch (err) { + console.error(err) + throw new Error('Failed to extract data', { cause: err }) + } +} + +/** +* Encrypts an existing seed or mnemonic+salt and returns the initialization +* vector, salt, and encrypted data representing the wallet in a locked state. +*/ +function _load (type?: 'BIP-44' | 'BLAKE2b' | 'Exodus', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise> { + try { + if (!_locked) { + throw new Error('Wallet is in use') + } + if (key == null || keySalt == null) { + throw new Error('Wallet password is required') + } + if (type == null) { + throw new TypeError('Wallet type is required') + } + if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Exodus') { + throw new TypeError('Invalid wallet type') + } + if (secret == null) { + throw new TypeError('Seed or mnemonic is required') + } + if (typeof secret !== 'string' && mnemonicSalt !== undefined) { + throw new TypeError('Mnemonic must be a string') + } + if (type === 'BIP-44') { + if (secret instanceof ArrayBuffer && (secret.byteLength < 16 || secret.byteLength > 64)) { + throw new RangeError('Seed for BIP-44 wallet must be 16-64 bytes') } - if (secret == null) { - throw new TypeError('Seed or mnemonic is required') + } + if (type === 'BLAKE2b') { + if (secret instanceof ArrayBuffer && secret.byteLength !== 32) { + throw new RangeError('Seed for BLAKE2b wallet must be 32 bytes') } - if (typeof secret !== 'string' && mnemonicSalt !== undefined) { - throw new TypeError('Mnemonic must be a string') + } + if (type === 'Exodus') { + if (typeof secret === 'string' && secret.split(' ').length !== 12) { + throw new RangeError('Mnemonic for Exodus wallet must be 12 words') } - if (type === 'BIP-44') { - if (secret instanceof ArrayBuffer && (secret.byteLength < 16 || secret.byteLength > 64)) { - throw new RangeError('Seed for BIP-44 wallet must be 16-64 bytes') - } + if (secret instanceof ArrayBuffer && secret.byteLength !== 64) { + throw new RangeError('Seed for Exodus wallet must be 64 bytes') } + } + _type = type + let seed: Promise + if (secret instanceof ArrayBuffer) { if (type === 'BLAKE2b') { - if (secret instanceof ArrayBuffer && secret.byteLength !== 32) { - throw new RangeError('Seed for BLAKE2b wallet must be 32 bytes') - } - } - if (type === 'Exodus') { - if (typeof secret === 'string' && secret.split(' ').length !== 12) { - throw new RangeError('Mnemonic for Exodus wallet must be 12 words') - } - if (secret instanceof ArrayBuffer && secret.byteLength !== 64) { - throw new RangeError('Seed for Exodus wallet must be 64 bytes') - } - } - this.#type = type - let seed: Promise - if (secret instanceof ArrayBuffer) { - if (type === 'BLAKE2b') { - seed = Bip39.fromEntropy(new Uint8Array(secret)) - .then(bip39 => { - this.#mnemonic = new Uint8Array(this.#encoder.encode(bip39.phrase ?? '')).buffer - return secret - }) - } else { - seed = Promise.resolve(secret) - } - } else { - seed = Bip39.fromPhrase(secret) + seed = Bip39.fromEntropy(new Uint8Array(secret)) .then(bip39 => { - this.#mnemonic = new Uint8Array(this.#encoder.encode(bip39.phrase ?? '')).buffer - const derive = type === 'BLAKE2b' - ? Promise.resolve(bip39.toBlake2bSeed()) - : bip39.toBip39Seed(mnemonicSalt ?? '') - return derive.then(s => s.buffer) + _mnemonic = new Uint8Array(_encoder.encode(bip39.phrase ?? '')).buffer + return secret }) + } else { + seed = Promise.resolve(secret) } - return seed.then(seed => { - this.#seed = seed - return WalletAesGcm.encrypt(type, key, this.#seed, this.#mnemonic) - .then(({ iv, encrypted }) => ({ iv, salt: keySalt, encrypted })) - }) - } catch (err) { - throw new Error('Failed to load wallet', { cause: err }) + } else { + seed = Bip39.fromPhrase(secret) + .then(bip39 => { + _mnemonic = new Uint8Array(_encoder.encode(bip39.phrase ?? '')).buffer + const derive = type === 'BLAKE2b' + ? Promise.resolve(bip39.toBlake2bSeed()) + : bip39.toBip39Seed(mnemonicSalt ?? '') + return derive.then(s => s.buffer) + }) } + return seed.then(seed => { + _seed = seed + return WalletAesGcm.encrypt(type, key, _seed, _mnemonic) + .then(({ iv, encrypted }) => ({ iv, salt: keySalt, encrypted })) + }) + } catch (err) { + 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 !== 'config' - && 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 +// Action for selecting method execution +function _parseAction (data: { [key: string]: unknown }) { + if (data.action == null) { + throw new TypeError('Wallet action is required') } + if (data.action !== 'STOP' + && data.action !== 'config' + && 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 } +// Worker message data itself +function _parseData (data: unknown): Record { + if (data == null) { + throw new TypeError('Worker received no data') + } + if (typeof data !== 'object') { + throw new Error('Invalid data') } + return data as Record +} - // 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') - } +// Salt created to derive CryptoKey from password; subsequently required to +// derive the same key for unlock requests +function _parseKeySalt (action: string, data: { [key: string]: unknown }): ArrayBuffer { + if (action === 'unlock') { + if (data.keySalt instanceof ArrayBuffer) { + return data.keySalt } else { - return crypto.getRandomValues(new Uint8Array(32)).buffer + 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 }) +// Initialization vector created to encrypt and lock the vault; subsequently +// required to decrypt and unlock the vault +function _parseIv (action: string, data: { [key: string]: unknown }) { + if (action === 'unlock') { + if (!(data.iv instanceof ArrayBuffer)) { + throw new TypeError('Initialization vector required to unlock wallet') } - return data.iv + } 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' && data.type !== 'Exodus' && data.type !== 'Ledger') { - 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 }) +// Algorithm used for wallet functions +function _parseType (action: string, data: { [key: string]: unknown }) { + if (['create', 'load', 'unlock'].includes(action)) { + if (data.type !== 'BIP-44' && data.type !== 'BLAKE2b' && data.type !== 'Exodus' && data.type !== 'Ledger') { + throw new TypeError(`Type is required to ${action} wallet`) } - return data.type + } else if (data.type !== undefined) { + throw new Error('Type is not allowed for this action', { cause: action }) } + return data.type } -const v = new VaultWorker() -- 2.47.3