From: Chris Duncan Date: Mon, 11 May 2026 20:54:59 +0000 (-0700) Subject: Extract vault parsers into separate file. X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=345853aa1ff674f6df34ca3fd7d1775fd44cdc2b;p=libnemo.git Extract vault parsers into separate file. --- diff --git a/src/lib/vault/parsers.ts b/src/lib/vault/parsers.ts new file mode 100644 index 0000000..5c0639c --- /dev/null +++ b/src/lib/vault/parsers.ts @@ -0,0 +1,169 @@ +//! SPDX-FileCopyrightText: 2025 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +/** + * Action for selecting method execution + */ +export function parseAction (data: Record) { + 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 +} + +/** + * Parse inbound message from main thread to worker into typechecked variables. + */ +export function parseData (action: string, data: Record) { + 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 + } + + // 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 + + // 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 + } + + // 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') + } + } + 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') + } + } + 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 }) + } +} + +/** + * Salt created to derive CryptoKey from password; subsequently required to + * derive the same key for unlock requests + */ +export function parseKeySalt (action: string, data: Record): 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 + */ +export function parseIv (action: string, data: Record): ArrayBuffer | undefined { + 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 + */ +export function parseType (action: string, data: Record) { + 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 }) + } + return data.type +} + diff --git a/src/lib/vault/vault-worker.ts b/src/lib/vault/vault-worker.ts index 39fc3ae..4ff5e96 100644 --- a/src/lib/vault/vault-worker.ts +++ b/src/lib/vault/vault-worker.ts @@ -7,6 +7,7 @@ import { BIP44_COIN_NANO } from '../constants' import { Bip39, Bip44, Blake2b, WalletAesGcm } from '../crypto' import { WalletType } from '../wallet' import { Passkey } from './passkey' +import { parseAction, parseData, parseIv, parseKeySalt, parseType } from './parsers' import { VaultTimer } from './vault-timer' const encoder = new TextEncoder() @@ -19,18 +20,25 @@ let _seed: ArrayBuffer | undefined = undefined let _mnemonic: ArrayBuffer | undefined = undefined const listener = (event: MessageEvent): void => { - const { url, id } = event.data + if (event.data == null) { + throw new TypeError('Worker received no data') + } + if (typeof event.data !== 'object') { + throw new Error('Invalid data') + } + const data = event.data as Record + const { url, id } = data + if (typeof id !== 'number' && typeof id !== 'string') return BROWSER: if (url !== location.href) return NODE: if (url !== threadId.toString()) return NODE: if (parentPort == null) setTimeout(() => listener(event), 0) - const data = _parseData(event.data) - const action = _parseAction(data) - const keySalt = _parseKeySalt(action, 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) + const type = parseType(action, data) + const iv = parseIv(action, data) + const { seed, mnemonicPhrase, mnemonicSalt, index, encrypted, message, timeout } = parseData(action, data) switch (action) { case 'STOP': { BROWSER: close() @@ -68,7 +76,7 @@ const listener = (event: MessageEvent): void => { } } }) - .then((result: Record | void) => { + .then((result: Record | void) => { result ??= {} const transfer: ArrayBuffer[] = [] if (result) { @@ -373,105 +381,6 @@ function verify (seed?: ArrayBuffer, mnemonicPhrase?: string): Record) { - 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 - } - - // 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 - - // 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 - } - - // 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') - } - } - 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') - } - } - 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 }) - } -} - function _ckd (index: number): Promise { if (_seed == null) { throw new Error('Wallet seed not found') @@ -563,74 +472,3 @@ function _load (type?: 'BIP-44' | 'BLAKE2b' | 'Exodus', key?: CryptoKey, keySalt throw new Error('Failed to load wallet', { cause: err }) } } - -// Action for selecting method execution -function _parseAction (data: Record) { - 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 -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 -function _parseKeySalt (action: string, data: Record): 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 -function _parseIv (action: string, data: Record): ArrayBuffer | undefined { - 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 -function _parseType (action: string, data: Record) { - 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 }) - } - return data.type -} -