To support name mangling, the stringified Vault worker now redefines the names of its own imports based on each class's mangled name. In the same vein, convert functions and constants have been inlined where used in the Vault. Finally, the worker now dynamically imports NodeJS worker threads when instantiated so that it can be excluded from static imports and avoid mangling.
import { browserOptions, nodeOptions } from './esbuild.mjs'
browserOptions.drop = nodeOptions.drop = ['console', 'debugger']
-browserOptions.minifySyntax = nodeOptions.minifySyntax = true
-browserOptions.minifyWhitespace = nodeOptions.minifyWhitespace = true
// Browser build
await build(browserOptions)
*/
const sharedOptions = {
bundle: true,
+ minify: true,
platform: 'browser',
loader: {
'.d.ts': 'copy'
export const PREFIX_LEGACY = 'xrb_'
export const SEED_LENGTH_BIP44 = 128
export const SEED_LENGTH_BLAKE2B = 64
-export const SLIP10_ED25519 = 'ed25519 seed'
export const DIFFICULTY_RECEIVE = 0xfffffe0000000000n
export const DIFFICULTY_SEND = 0xfffffff800000000n
export const XNO = 'ΣΎ'
const PREFIX_LEGACY = '${PREFIX_LEGACY}'
const SEED_LENGTH_BIP44 = ${SEED_LENGTH_BIP44}
const SEED_LENGTH_BLAKE2B = ${SEED_LENGTH_BLAKE2B}
- const SLIP10_ED25519 = '${SLIP10_ED25519}'
const DIFFICULTY_RECEIVE = ${DIFFICULTY_RECEIVE}
const DIFFICULTY_SEND = ${DIFFICULTY_SEND}
const XNO = '${XNO}'
//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>\r
//! SPDX-License-Identifier: GPL-3.0-or-later\r
\r
-import { bytes, utf8 } from '../convert'\r
-\r
/**\r
* Represents a mnemonic phrase that identifies a wallet as defined by BIP-39.\r
*/\r
export class Bip39 {\r
+ encoder: TextEncoder = new TextEncoder()\r
+\r
/**\r
* SHA-256 hash of entropy that is appended to the entropy and subsequently\r
* used to generate the mnemonic phrase.\r
* Erases seed bytes and releases variable references.\r
*/\r
destroy () {\r
- bytes.erase(this.#bip39Seed)\r
- bytes.erase(this.#blake2bSeed)\r
+ this.#bip39Seed?.fill(0).buffer.transfer?.()\r
+ this.#blake2bSeed?.fill(0).buffer.transfer?.()\r
this.#bip39Seed = undefined\r
this.#blake2bSeed = undefined\r
this.#phrase = undefined\r
throw new Error('BIP-39 mnemonic phrase not found')\r
}\r
if (this.#bip39Seed != null) {\r
- return Promise.resolve(format === 'hex' ? bytes.toHex(this.#bip39Seed) : this.#bip39Seed)\r
+ const seed = format === 'hex'\r
+ ? [...this.#bip39Seed].map(b => b.toString(16).padStart(2, '0')).join('').toUpperCase()\r
+ : this.#bip39Seed\r
+ return Promise.resolve(seed)\r
} else {\r
const salt = (typeof passphrase === 'string') ? passphrase : ''\r
- const keyData = utf8.toBytes(this.phrase)\r
+ const keyData = this.encoder.encode(this.phrase)\r
return crypto.subtle.importKey('raw', keyData, 'PBKDF2', false, ['deriveBits', 'deriveKey'])\r
.then(phraseKey => {\r
const algorithm: Pbkdf2Params = {\r
name: 'PBKDF2',\r
hash: 'SHA-512',\r
- salt: utf8.toBytes(`mnemonic${salt.normalize('NFKD')}`),\r
+ salt: this.encoder.encode(`mnemonic${salt.normalize('NFKD')}`),\r
iterations: 2048\r
}\r
return crypto.subtle.deriveBits(algorithm, phraseKey, 512)\r
}\r
}\r
return format === 'hex'\r
- ? bytes.toHex(this.#blake2bSeed)\r
+ ? [...this.#blake2bSeed].map(b => b.toString(16).padStart(2, '0')).join('').toUpperCase()\r
: this.#blake2bSeed\r
}\r
\r
//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
//! SPDX-License-Identifier: GPL-3.0-or-later
-import { BIP44_PURPOSE, HARDENED_OFFSET, SLIP10_ED25519 } from '../constants'
-
type ExtendedKey = {
privateKey: ArrayBuffer
chainCode: ArrayBuffer
}
export class Bip44 {
+ static get BIP44_PURPOSE (): 44 { return 44 }
+ static get HARDENED_OFFSET (): 0x80000000 { return 0x80000000 }
+ static get SLIP10_ED25519 (): 'ed25519 seed' { return 'ed25519 seed' }
+
/**
* Derives a private child key for a coin by following the specified BIP-32 and
* BIP-44 derivation path. Purpose is always 44'. Only hardened child keys are
if (address !== undefined && (!Number.isSafeInteger(address) || address < 0 || address > 0x7fffffff)) {
throw new RangeError(`Invalid address index 0x${account.toString(16)}`)
}
- return this.slip10(SLIP10_ED25519, seed)
- .then(masterKey => this.CKDpriv(masterKey, BIP44_PURPOSE))
+ return this.slip10(this.SLIP10_ED25519, seed)
+ .then(masterKey => this.CKDpriv(masterKey, this.BIP44_PURPOSE))
.then(purposeKey => this.CKDpriv(purposeKey, coin))
.then(coinKey => this.CKDpriv(coinKey, account))
.then(accountKey => this.CKDpriv(accountKey, change))
if (index === undefined) {
return Promise.resolve({ privateKey, chainCode })
}
- index += HARDENED_OFFSET
+ index += this.HARDENED_OFFSET
const key = new Uint8Array(chainCode)
const data = new Uint8Array(37)
data.set([0])
//! SPDX-License-Identifier: GPL-3.0-or-later
import { NamedData } from "#types"
-import { utf8 } from "../convert"
export class WalletAesGcm {
+ static encoder: TextEncoder = new TextEncoder()
+
static decrypt (type: string, key: CryptoKey, iv: ArrayBuffer, encrypted: ArrayBuffer): Promise<NamedData<ArrayBuffer>> {
const seedLength = type === 'BIP-44' ? 64 : 32
- const additionalData = utf8.toBytes(type)
+ const additionalData = this.encoder.encode(type)
return crypto.subtle
.decrypt({ name: 'AES-GCM', iv, additionalData }, key, encrypted)
.then(decrypted => {
}
// restrict iv to 96 bits per GCM best practice
const iv = crypto.getRandomValues(new Uint8Array(12)).buffer
- const additionalData = utf8.toBytes(type)
+ const additionalData = this.encoder.encode(type)
const encoded = new Uint8Array([...new Uint8Array(seed), ...new Uint8Array(mnemonic ?? [])])
return crypto.subtle
.encrypt({ name: 'AES-GCM', iv, additionalData }, key, encoded)
import { Worker as NodeWorker } from 'node:worker_threads'
import { Data, NamedData } from '#types'
-import { default as Constants } from '../constants'
-import { default as Convert } from '../convert'
import { Bip39, Bip44, Blake2b, NanoNaCl, WalletAesGcm } from '../crypto'
import { Passkey } from './passkey'
import { VaultTimer } from './vault-timer'
export class Vault {
static get #blob () {
- let importWorkerThreads = ''
- NODE: importWorkerThreads = `import { parentPort } from 'node:worker_threads'`
return `
- ${importWorkerThreads}
- ${Convert}
- ${Constants}
const ${Bip39.name} = ${Bip39}
const ${Bip44.name} = ${Bip44}
const ${Blake2b.name} = ${Blake2b}
const ${Passkey.name} = ${Passkey}
const ${VaultTimer.name} = ${VaultTimer}
const ${VaultWorker.name} = ${VaultWorker}
- const v = new ${VaultWorker.name}()
+ ${Bip39.name === 'Bip39' ? '' : `const Bip39 = ${Bip39.name}`}
+ ${Bip44.name === 'Bip44' ? '' : `const Bip44 = ${Bip44.name}`}
+ ${Blake2b.name === 'Blake2b' ? '' : `const Blake2b = ${Blake2b.name}`}
+ ${NanoNaCl.name === 'NanoNaCl' ? '' : `const NanoNaCl = ${NanoNaCl.name}`}
+ ${WalletAesGcm.name === 'WalletAesGcm' ? '' : `const WalletAesGcm = ${WalletAesGcm.name}`}
+ ${Passkey.name === 'Passkey' ? '' : `const Passkey = ${Passkey.name}`}
+ ${VaultTimer.name === 'VaultTimer' ? '' : `const VaultTimer = ${VaultTimer.name}`}
+ ${VaultWorker.name === 'VaultWorker' ? '' : `const VaultWorker = ${VaultWorker.name}`}
+ const v = new VaultWorker()
`
}
static #instances: Vault[] = []
//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
//! SPDX-License-Identifier: GPL-3.0-or-later
-import { parentPort } from 'node:worker_threads'
import { NamedData } from '#types'
import { BIP44_COIN_NANO } from '../constants'
-import { utf8 } from '../convert'
import { Bip39, Bip44, Blake2b, NanoNaCl, WalletAesGcm } from '../crypto'
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: VaultTimer
#type?: 'BIP-44' | 'BLAKE2b'
this.#type = undefined
this.#seed = undefined
this.#mnemonic = undefined
- NODE: this.#parentPort = parentPort
const listener = (event: MessageEvent<any>): 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)
}
}
//@ts-expect-error
+ // postMessage(result, transfer)
BROWSER: postMessage(result, transfer)
- //@ts-expect-error
- NODE: parentPort?.postMessage(result, transfer)
+ NODE: this.#parentPort?.postMessage(result, transfer)
})
.catch((err: any) => {
console.error(err)
}
event.data[key] = undefined
}
+ // 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 })
+ NODE: this.#parentPort?.postMessage({ error: 'Failed to process Vault request', cause: err })
})
}
+ // addEventListener('message', listener)
BROWSER: addEventListener('message', listener)
- NODE: this.#parentPort?.on('message', listener)
+ NODE: {
+ if (this.#parentPort == null) {
+ import('node:worker_threads')
+ .then(({ parentPort }) => {
+ this.#parentPort = parentPort
+ this.#parentPort.on('message', listener)
+ })
+ }
+ }
}
/**
}
if (mnemonicPhrase != null) {
let diff = 0
- const userMnemonic = utf8.toBytes(mnemonicPhrase)
+ 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]
if (type === 'BLAKE2b') {
seed = Bip39.fromEntropy(new Uint8Array(secret))
.then(bip39 => {
- this.#mnemonic = utf8.toBuffer(bip39.phrase ?? '')
+ this.#mnemonic = this.#encoder.encode(bip39.phrase ?? '').buffer
return secret
})
} else {
} else {
seed = Bip39.fromPhrase(secret)
.then(bip39 => {
- this.#mnemonic = utf8.toBuffer(bip39.phrase ?? '')
+ this.#mnemonic = this.#encoder.encode(bip39.phrase ?? '').buffer
const derive = type === 'BIP-44'
? bip39.toBip39Seed(mnemonicSalt ?? '')
: Promise.resolve(bip39.toBlake2bSeed())