]> git.codecow.com Git - libnemo.git/commitdiff
Extract vault parsers into separate file.
authorChris Duncan <chris@codecow.com>
Mon, 11 May 2026 20:54:59 +0000 (13:54 -0700)
committerChris Duncan <chris@codecow.com>
Mon, 11 May 2026 20:54:59 +0000 (13:54 -0700)
src/lib/vault/parsers.ts [new file with mode: 0644]
src/lib/vault/vault-worker.ts

diff --git a/src/lib/vault/parsers.ts b/src/lib/vault/parsers.ts
new file mode 100644 (file)
index 0000000..5c0639c
--- /dev/null
@@ -0,0 +1,169 @@
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@codecow.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+/**
+ * Action for selecting method execution
+ */
+export function parseAction (data: Record<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
+}
+
+/**
+ * Parse inbound message from main thread to worker into typechecked variables.
+ */
+export function parseData (action: string, data: Record<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
+               }
+
+               // 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<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
+ */
+export function parseIv (action: string, data: Record<string, unknown>): 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<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 })
+       }
+       return data.type
+}
+
index 39fc3ae429db3af01a97ad4c8b2b400478363ca5..4ff5e96766cc316e969eab389787a3cfcabda4dd 100644 (file)
@@ -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<any>): 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<string, unknown>
+       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<Record<string, boolean | number | ArrayBuffer> | 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<any>): void => {
                                }
                        }
                })
-               .then((result: Record<string, boolean | number | ArrayBuffer> | void) => {
+               .then((result: Record<string, boolean | number | string | ArrayBuffer> | void) => {
                        result ??= {}
                        const transfer: ArrayBuffer[] = []
                        if (result) {
@@ -373,105 +381,6 @@ function verify (seed?: ArrayBuffer, mnemonicPhrase?: string): Record<string, bo
        }
 }
 
-/**
-* Parse inbound message from main thread into typechecked variables.
-*/
-function _extractData (action: string, data: Record<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
-               }
-
-               // 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<ArrayBuffer> {
        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<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
-function _parseData (data: unknown): Record<string, unknown> {
-       if (data == null) {
-               throw new TypeError('Worker received no data')
-       }
-       if (typeof data !== 'object') {
-               throw new Error('Invalid data')
-       }
-       return data as Record<string, unknown>
-}
-
-// Salt created to derive CryptoKey from password; subsequently required to
-// derive the same key for unlock requests
-function _parseKeySalt (action: string, data: Record<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
-function _parseIv (action: string, data: Record<string, unknown>): 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<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 })
-       }
-       return data.type
-}
-