--- /dev/null
+//! 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
+}
+
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()
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()
}
}
})
- .then((result: Record<string, boolean | number | ArrayBuffer> | void) => {
+ .then((result: Record<string, boolean | number | string | ArrayBuffer> | void) => {
result ??= {}
const transfer: ArrayBuffer[] = []
if (result) {
}
}
-/**
-* 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')
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
-}
-