]> git.codecow.com Git - libnemo.git/commitdiff
Refactor vault for minification.
authorChris Duncan <chris@zoso.dev>
Thu, 4 Sep 2025 18:45:15 +0000 (11:45 -0700)
committerChris Duncan <chris@zoso.dev>
Thu, 4 Sep 2025 18:45:15 +0000 (11:45 -0700)
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.

esbuild-prod.mjs
esbuild.mjs
src/lib/constants.ts
src/lib/crypto/bip39.ts
src/lib/crypto/bip44.ts
src/lib/crypto/wallet-aes-gcm.ts
src/lib/vault/index.ts
src/lib/vault/vault-worker.ts

index 50ff3590072ef941e15f68c762d949cda94fb65a..f29b71d8236d987acad75862dba5947c7df1df42 100644 (file)
@@ -5,8 +5,6 @@ import { build } from 'esbuild'
 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)
index b886fb5be64ae962f24e2d6fb475a7c5fed51238..2732f6437525e9acf1cdff9de290bf4287f311b4 100644 (file)
@@ -8,6 +8,7 @@ import { build } from 'esbuild'
 */
 const sharedOptions = {
        bundle: true,
+       minify: true,
        platform: 'browser',
        loader: {
                '.d.ts': 'copy'
index 31bbe041275e2cba8b175e70af841be98db0c878..d215a7def7fd23cf933f35061d233e28e72a0717 100644 (file)
@@ -19,7 +19,6 @@ export const PREFIX = 'nano_'
 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 = 'ΣΎ'
@@ -55,7 +54,6 @@ export default `
        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}'
index 545cb71aa136205ae537124b0aa8d0ef32fa2b5b..2843821dd70151f7d6fde2a87dd2a52f8a5bc41f 100644 (file)
@@ -1,12 +1,12 @@
 //! 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
@@ -157,8 +157,8 @@ export class Bip39 {
        * 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
@@ -189,16 +189,19 @@ export class Bip39 {
                        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
@@ -244,7 +247,7 @@ export class Bip39 {
                        }\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
index 05958cd078529276458cc97c7e67b6d12bdb9a26..b0989bd613ef6f54c4b4db33ce59a71e461ecfa3 100644 (file)
@@ -1,14 +1,16 @@
 //! 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
@@ -37,8 +39,8 @@ export class Bip44 {
                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))
@@ -61,7 +63,7 @@ export class Bip44 {
                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])
index be08c9d24d35e44d0a437b38d42a7a745cedc260..b2e1013c7facef9be99d4716a5fe4a4f00b96f5c 100644 (file)
@@ -4,12 +4,13 @@
 //! 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 => {
@@ -32,7 +33,7 @@ export class WalletAesGcm {
                }
                // 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)
index 2944d27c93bee8acc911c73b179bb143b17abba3..103096660d4faa906f26e9aa66f1e7c08f8ee965 100644 (file)
@@ -3,8 +3,6 @@
 
 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'
@@ -19,12 +17,7 @@ type Task = {
 
 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}
@@ -33,7 +26,15 @@ export class Vault {
                        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[] = []
index e4b7c0ae532c48c471d50a20607b3e82ba2dd9f0..363ede0278fb727e749f8265dd2ac399d9340307 100644 (file)
@@ -1,10 +1,8 @@
 //! 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'
@@ -13,6 +11,7 @@ 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'
@@ -26,9 +25,9 @@ export class VaultWorker {
                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)
@@ -79,9 +78,9 @@ export class VaultWorker {
                                                }
                                        }
                                        //@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)
@@ -91,12 +90,22 @@ export class VaultWorker {
                                                }
                                                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)
+                                       })
+                       }
+               }
        }
 
        /**
@@ -317,7 +326,7 @@ export class VaultWorker {
                        }
                        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]
@@ -457,7 +466,7 @@ export class VaultWorker {
                                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 {
@@ -466,7 +475,7 @@ export class VaultWorker {
                        } 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())