From bffbcaffa296f5c1104ba94d661aa44f4c1db569 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Fri, 29 Aug 2025 22:23:06 -0700 Subject: [PATCH] Replace async-await with Promise chains. --- src/lib/vault/index.ts | 10 +- src/lib/vault/vault-worker.ts | 600 +++++++++++++++++++--------------- 2 files changed, 333 insertions(+), 277 deletions(-) diff --git a/src/lib/vault/index.ts b/src/lib/vault/index.ts index 9868052..39bdce9 100644 --- a/src/lib/vault/index.ts +++ b/src/lib/vault/index.ts @@ -62,11 +62,11 @@ export class Vault { Vault.#instances.push(this) } - async prioritize (data: NamedData): Promise> { + prioritize (data: NamedData): Promise> { return this.#assign(data, task => this.#queue.unshift(task)) } - async request (data: NamedData): Promise> { + request (data: NamedData): Promise> { return this.#assign(data, task => this.#queue.push(task)) } @@ -76,8 +76,8 @@ export class Vault { this.#isTerminated = true } - async #assign (data: NamedData, enqueue: (task: Task) => number): Promise> { - return new Promise(async (resolve, reject): Promise => { + #assign (data: NamedData, enqueue: (task: Task) => number): Promise> { + return new Promise((resolve, reject): void => { if (this.#isTerminated) { reject('Worker terminated') } @@ -87,7 +87,7 @@ export class Vault { resolve, reject } - await enqueue(task) + enqueue(task) if (this.#isIdle) this.#process() }) } diff --git a/src/lib/vault/vault-worker.ts b/src/lib/vault/vault-worker.ts index 2f4efb8..57840a6 100644 --- a/src/lib/vault/vault-worker.ts +++ b/src/lib/vault/vault-worker.ts @@ -24,8 +24,8 @@ export class VaultWorker { static { NODE: this.#parentPort = parentPort - const listener = async (message: MessageEvent): Promise => { - try { + const listener = (message: MessageEvent): Promise => { + return this.#extractData(message.data).then(extracted => { const { action, type, @@ -38,70 +38,73 @@ export class VaultWorker { index, encrypted, data - } = await this.#extractData(message.data) - let result: NamedData + } = extracted + let result: Promise switch (action) { case 'STOP': { BROWSER: close() NODE: process.exit() } case 'create': { - result = await this.create(type, key, keySalt, mnemonicSalt) + result = this.create(type, key, keySalt, mnemonicSalt) break } case 'derive': { - result = await this.derive(index) + result = this.derive(index) break } case 'load': { - result = await this.load(type, key, keySalt, mnemonicPhrase ?? seed, mnemonicSalt) + result = this.load(type, key, keySalt, mnemonicPhrase ?? seed, mnemonicSalt) break } case 'lock': { - result = this.lock() + result = Promise.resolve(this.lock()) break } case 'sign': { - result = await this.sign(index, data) + result = this.sign(index, data) break } case 'unlock': { - result = await this.unlock(type, key, iv, encrypted) + result = this.unlock(type, key, iv, encrypted) break } case 'update': { - result = await this.update(key, keySalt) + result = this.update(key, keySalt) break } case 'verify': { - result = this.verify(seed, mnemonicPhrase) + result = Promise.resolve(this.verify(seed, mnemonicPhrase)) break } default: { throw new Error(`Unknown wallet action '${action}'`) } } - const transfer = [] - for (const k of Object.keys(result)) { - if (result[k] instanceof ArrayBuffer || result[k] instanceof CryptoKey) { - transfer.push(result[k]) + 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]) + } } - } - //@ts-expect-error - BROWSER: postMessage(result, transfer) - //@ts-expect-error - NODE: parentPort?.postMessage(result, transfer) - } catch (err) { - 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?.() + //@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?.() + } + message.data[key] = undefined } - message.data[key] = undefined - } - BROWSER: postMessage({ error: 'Failed to process Vault request', cause: err }) - NODE: parentPort?.postMessage({ error: 'Failed to process Vault request', cause: err }) - } + 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: this.#parentPort?.on('message', listener) @@ -111,15 +114,19 @@ export class VaultWorker { * Generates a new mnemonic and seed and then returns the initialization vector * vector, salt, and encrypted data representing the wallet in a locked state. */ - static async create (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, mnemonicSalt?: string): Promise> { + static create (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, mnemonicSalt?: string): Promise> { try { const entropy = crypto.getRandomValues(new Uint8Array(32)) - const { phrase: mnemonicPhrase } = await Bip39.fromEntropy(entropy) - const record = await this.#load(type, key, keySalt, mnemonicPhrase, mnemonicSalt) - if (this.#seed == null || this.#mnemonic == null) { - throw new Error('Failed to generate seed and mnemonic') - } - return { ...record, seed: this.#seed.slice(), mnemonic: this.#mnemonic.slice() } + return Bip39.fromEntropy(entropy) + .then(bip39 => { + return this.#load(type, key, keySalt, bip39.phrase, mnemonicSalt) + .then(record => { + if (this.#seed == null || this.#mnemonic == null) { + throw new Error('Failed to generate seed and mnemonic') + } + return { ...record, seed: this.#seed.slice(), mnemonic: this.#mnemonic.slice() } + }) + }) } catch (err) { console.error(err) throw new Error('Failed to create wallet', { cause: err }) @@ -133,8 +140,9 @@ export class VaultWorker { * wallet seed at a specified index and then returns the public key. The wallet * must be unlocked prior to derivation. */ - static async derive (index?: number): Promise> { + static derive (index?: number): Promise> { try { + this.#timeout.pause() if (this.#locked) { throw new Error('Wallet is locked') } @@ -147,13 +155,14 @@ export class VaultWorker { if (typeof index !== 'number') { throw new Error('Invalid wallet account index') } - this.#timeout.pause() - const prv = this.#type === 'BIP-44' - ? await Bip44.ckd(this.#seed, BIP44_COIN_NANO, index) - : await this.#deriveBlake2bPrivateKey(this.#seed, index) - const pub = await NanoNaCl.convert(new Uint8Array(prv)) - this.#timeout = new VaultTimer(() => this.lock(), 120000) - return { index, publicKey: pub.buffer } + const derive = this.#type === 'BIP-44' + ? Bip44.ckd(this.#seed, BIP44_COIN_NANO, index) + : Promise.resolve(this.#deriveBlake2bPrivateKey(this.#seed, index)) + return derive.then(prv => { + const pub = NanoNaCl.convert(new Uint8Array(prv)) + this.#timeout = new VaultTimer(() => this.lock(), 120000) + return { index, publicKey: pub.buffer } + }) } catch (err) { console.error(err) this.#timeout.resume() @@ -165,19 +174,19 @@ export class VaultWorker { * Encrypts an existing seed or mnemonic+salt and returns the initialization * vector, salt, and encrypted data representing the wallet in a locked state. */ - static async load (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise> { - try { - const record = await this.#load(type, key, keySalt, secret, mnemonicSalt) - if (this.#seed == null) { - throw new Error('Wallet seed not found') - } - return record - } catch (err) { - console.error(err) - throw new Error('Failed to load wallet', { cause: err }) - } finally { - this.lock() - } + static load (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise> { + return this.#load(type, key, keySalt, secret, mnemonicSalt) + .then(record => { + if (this.#seed == null) { + throw new Error('Wallet seed not found') + } + return record + }) + .catch(err => { + console.error(err) + throw new Error('Failed to load wallet', { cause: err }) + }) + .finally(() => this.lock()) } static lock (): NamedData { @@ -192,8 +201,9 @@ export class VaultWorker { * 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. */ - static async sign (index?: number, data?: ArrayBuffer): Promise> { + static sign (index?: number, data?: ArrayBuffer): Promise> { try { + this.#timeout.pause() if (this.#locked) { throw new Error('Wallet is locked') } @@ -206,13 +216,14 @@ export class VaultWorker { if (data == null) { throw new Error('Data to sign not found') } - this.#timeout.pause() - const prv = this.#type === 'BIP-44' - ? await Bip44.ckd(this.#seed, BIP44_COIN_NANO, index) - : await this.#deriveBlake2bPrivateKey(this.#seed, index) - const sig = await NanoNaCl.detached(new Uint8Array(data), new Uint8Array(prv)) - this.#timeout = new VaultTimer(() => this.lock(), 120000) - return { signature: sig.buffer } + const derive = this.#type === 'BIP-44' + ? Bip44.ckd(this.#seed, BIP44_COIN_NANO, index) + : Promise.resolve(this.#deriveBlake2bPrivateKey(this.#seed, index)) + return derive.then(prv => { + const sig = NanoNaCl.detached(new Uint8Array(data), new Uint8Array(prv)) + this.#timeout = new VaultTimer(() => this.lock(), 120000) + return { signature: sig.buffer } + }) } catch (err) { console.error(err) this.#timeout.resume() @@ -223,43 +234,45 @@ export class VaultWorker { /** * Decrypts the input and sets the seed and, if it is included, the mnemonic. */ - static async unlock (type?: string, key?: CryptoKey, iv?: ArrayBuffer, encrypted?: ArrayBuffer): Promise> { - try { - if (type == null) { - throw new TypeError('Wallet type is required') - } - 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') - } - this.#timeout?.pause() - await this.#decryptWallet(type, key, iv, encrypted) - if (!(this.#seed instanceof ArrayBuffer)) { - throw new TypeError('Invalid seed') - } - if (this.#mnemonic != null && !(this.#mnemonic instanceof ArrayBuffer)) { - throw new TypeError('Invalid mnemonic') - } - this.#locked = false - this.#timeout = new VaultTimer(() => this.lock(), 120000) - return { isUnlocked: !this.#locked } - } catch (err) { - console.error(err) - this.#timeout?.resume() - throw new Error('Failed to unlock wallet', { cause: err }) + static unlock (type?: string, key?: CryptoKey, iv?: ArrayBuffer, encrypted?: ArrayBuffer): Promise> { + if (type == null) { + throw new TypeError('Wallet type is required') + } + 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') + } + this.#timeout?.pause() + return this.#decryptWallet(type, key, iv, encrypted) + .then(() => { + if (!(this.#seed instanceof ArrayBuffer)) { + throw new TypeError('Invalid seed') + } + if (this.#mnemonic != null && !(this.#mnemonic instanceof ArrayBuffer)) { + throw new TypeError('Invalid mnemonic') + } + this.#locked = false + this.#timeout = new VaultTimer(() => this.lock(), 120000) + return { isUnlocked: !this.#locked } + }) + .catch(err => { + console.error(err) + this.#timeout?.resume() + throw new Error('Failed to unlock wallet', { cause: err }) + }) } /** * Re-encrypts the wallet with a new password. */ - static async update (key?: CryptoKey, keySalt?: ArrayBuffer): Promise> { + static update (key?: CryptoKey, keySalt?: ArrayBuffer): Promise> { try { + this.#timeout.pause() if (this.#locked) { throw new Error('Wallet is locked') } @@ -269,10 +282,11 @@ export class VaultWorker { if (key == null || keySalt == null) { throw new TypeError('Wallet password is required') } - this.#timeout.pause() - const { iv, encrypted } = await this.#encryptWallet(key) - this.#timeout = new VaultTimer(() => this.lock(), 120000) - return { iv, salt: keySalt, encrypted } + return this.#encryptWallet(key) + .then(({ iv, encrypted }) => { + this.#timeout = new VaultTimer(() => this.lock(), 120000) + return { iv, salt: keySalt, encrypted } + }) } catch (err) { console.error(err) this.#timeout.resume() @@ -324,29 +338,46 @@ export class VaultWorker { } } - static async #createAesKey (purpose: 'encrypt' | 'decrypt', password: ArrayBuffer, keySalt: ArrayBuffer): Promise { - const derivationKey = await crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveKey']) - 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 await crypto.subtle.deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, [purpose]) + static #createAesKey (purpose: 'encrypt' | 'decrypt', keySalt: ArrayBuffer, password?: ArrayBuffer): Promise { + return new Promise((resolve, reject): void => { + if (password == null) { + resolve(undefined) + return + } + try { + 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 + } + crypto.subtle + .deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, [purpose]) + .then(resolve) + }) + } catch (err) { + reject(err) + } + }) } - static async #decryptWallet (type: string, key: CryptoKey, iv: ArrayBuffer, encrypted: ArrayBuffer): Promise { + static #decryptWallet (type: string, key: CryptoKey, iv: ArrayBuffer, encrypted: ArrayBuffer): Promise { const seedLength = type === 'BIP-44' ? 64 : 32 const additionalData = utf8.toBytes(type) - const decrypted = new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv, additionalData }, key, encrypted)) - this.#seed = decrypted.buffer.slice(0, seedLength) - this.#mnemonic = decrypted.buffer.slice(seedLength) - decrypted.fill(0) + return crypto.subtle.decrypt({ name: 'AES-GCM', iv, additionalData }, key, encrypted) + .then(decrypted => { + this.#seed = decrypted.slice(0, seedLength) + this.#mnemonic = decrypted.slice(seedLength) + new Uint8Array(decrypted).fill(0) + }) } /** @@ -370,7 +401,7 @@ export class VaultWorker { return new Blake2b(32).update(s).update(i).digest().buffer } - static async #encryptWallet (key: CryptoKey): Promise> { + static #encryptWallet (key: CryptoKey): Promise> { if (this.#type == null) { throw new Error('Invalid wallet type') } @@ -383,164 +414,175 @@ export class VaultWorker { const iv = crypto.getRandomValues(new Uint8Array(12)).buffer const additionalData = utf8.toBytes(this.#type) const encoded = new Uint8Array([...seed, ...mnemonic]) - const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv, additionalData }, key, encoded) - encoded.fill(0) - return { iv, encrypted } + return crypto.subtle.encrypt({ name: 'AES-GCM', iv, additionalData }, key, encoded) + .then(encrypted => { + encoded.fill(0) + return { iv, encrypted } + }) } /** * Parse inbound message from main thread into typechecked variables. */ - static async #extractData (message: unknown) { - // 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 - } - - // 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') - } - 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 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 - const key = password instanceof ArrayBuffer - ? await this.#createAesKey(action === 'unlock' ? 'decrypt' : 'encrypt', password, keySalt) - : undefined - if (password instanceof ArrayBuffer && !password.detached) { - new Uint8Array(password).fill(0) - password = undefined - } - - // 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') - } + static #extractData (message: 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 + } + + // 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') + } + 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 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 + } - // 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 - } + // 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 - // 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 + // 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') + } - // 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 - } + // 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 + } - // 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 + // 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 + } - // 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') - } + // 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 + + 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 }) + }) + } catch (err) { + console.error(err) + throw new Error('Failed to extract data', { cause: err }) } - const data = messageData.data instanceof ArrayBuffer - ? messageData.data - : undefined - delete messageData.data - - return { action, type, key, iv, keySalt, seed, mnemonicPhrase, mnemonicSalt, encrypted, index, data } } /** * Encrypts an existing seed or mnemonic+salt and returns the initialization * vector, salt, and encrypted data representing the wallet in a locked state. */ - static async #load (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise> { + static #load (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise> { try { if (!this.#locked) { throw new Error('Wallet is in use') @@ -571,20 +613,34 @@ export class VaultWorker { } } this.#type = type + let seed: Promise if (secret instanceof ArrayBuffer) { - this.#seed = secret if (type === 'BLAKE2b') { - this.#mnemonic = utf8.toBuffer((await Bip39.fromEntropy(new Uint8Array(secret))).phrase ?? '') + seed = Bip39.fromEntropy(new Uint8Array(secret)) + .then(bip39 => { + this.#mnemonic = utf8.toBuffer(bip39.phrase ?? '') + return secret + }) + } else { + seed = Promise.resolve(secret) } } else { - const mnemonic = await Bip39.fromPhrase(secret) - this.#mnemonic = utf8.toBuffer(mnemonic.phrase ?? '') - this.#seed = type === 'BIP-44' - ? (await mnemonic.toBip39Seed(mnemonicSalt ?? '')).buffer - : (await mnemonic.toBlake2bSeed()).buffer - } - const { iv, encrypted } = await this.#encryptWallet(key) - return { iv, salt: keySalt, encrypted } + seed = Bip39.fromPhrase(secret) + .then(bip39 => { + this.#mnemonic = utf8.toBuffer(bip39.phrase ?? '') + const derive = type === 'BIP-44' + ? bip39.toBip39Seed(mnemonicSalt ?? '') + : Promise.resolve(bip39.toBlake2bSeed()) + return derive.then(s => s.buffer) + }) + } + return seed.then(seed => { + this.#seed = seed + return this.#encryptWallet(key) + .then(({ iv, encrypted }) => { + return { iv, salt: keySalt, encrypted } + }) + }) } catch (err) { throw new Error('Failed to load wallet', { cause: err }) } -- 2.47.3