From e3ba6d8cb00da8244a0d37f3a14497a2c9e4c11c Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Tue, 26 Nov 2024 04:54:28 -0800 Subject: [PATCH] Get it working in browser, then work backward toward Node testing if possible. --- src/lib/pool.ts | 8 +- src/lib/wallet.ts | 22 +---- src/lib/workers.ts | 5 + src/lib/workers/ckdBip44.ts | 170 +++++++++++++++++----------------- src/lib/workers/ckdBlake2b.ts | 63 ++++++------- 5 files changed, 122 insertions(+), 146 deletions(-) create mode 100644 src/lib/workers.ts diff --git a/src/lib/pool.ts b/src/lib/pool.ts index f37a5ba..0c3f9fd 100644 --- a/src/lib/pool.ts +++ b/src/lib/pool.ts @@ -34,16 +34,10 @@ export class Pool { constructor (url: string) { for (let i = navigator.hardwareConcurrency - 1; i > 0; i--) { - let workerUrl - if (globalThis.window == null) { - workerUrl = url - } else { - workerUrl = new URL(url, import.meta.url) - } const thread = { isBusy: false, //@ts-expect-error - worker: new Worker(workerUrl, { type: 'module', eval: true }) + worker: new Worker(url, { type: 'module', eval: true }) } thread.worker.addEventListener('message', (message) => { this.#report(thread, message.data ?? message) diff --git a/src/lib/wallet.ts b/src/lib/wallet.ts index 3a46851..e0e553b 100644 --- a/src/lib/wallet.ts +++ b/src/lib/wallet.ts @@ -1,14 +1,10 @@ // SPDX-FileCopyrightText: 2024 Chris Duncan // SPDX-License-Identifier: GPL-3.0-or-later -import blake2b from 'blake2b' +import { ckdBip44, ckdBlake2b } from './workers.js' import { Account } from './account.js' -import { nanoCKD } from './bip32-key-derivation.js' import { Bip39Mnemonic } from './bip39-mnemonic.js' -import { ckdBip44Worker } from './workers/ckdBip44.js' -import { ckdBlake2bWorker } from './workers/ckdBlake2b.js' import { ADDRESS_GAP, SEED_LENGTH_BIP44, SEED_LENGTH_BLAKE2B } from './constants.js' -import { dec, hex } from './convert.js' import { Entropy } from './entropy.js' import { Pool } from './pool.js' import { Rpc } from './rpc.js' @@ -267,13 +263,7 @@ export class Bip44Wallet extends Wallet { throw new Error(`Bip44Wallet cannot be instantiated directly. Use 'await Bip44Wallet.create()' instead.`) } super(seed, mnemonic, id) - if (globalThis.Window) { - const ckdBlob = new Blob([ckdBip44Worker], { type: 'application/javascript' }) - const ckdUrl = URL.createObjectURL(ckdBlob) - this.#pool = new Pool(ckdUrl) - } else { - this.#pool = new Pool(ckdBip44Worker) - } + this.#pool = new Pool(ckdBip44) Bip44Wallet.#isInternal = false } @@ -466,13 +456,7 @@ export class Blake2bWallet extends Wallet { throw new Error(`Blake2bWallet cannot be instantiated directly. Use 'await Blake2bWallet.create()' instead.`) } super(seed, mnemonic, id) - if (globalThis.Window) { - const ckdBlob = new Blob([ckdBlake2bWorker], { type: 'application/javascript' }) - const ckdUrl = URL.createObjectURL(ckdBlob) - this.#pool = new Pool(ckdUrl) - } else { - this.#pool = new Pool(ckdBlake2bWorker) - } + this.#pool = new Pool(ckdBlake2b) Blake2bWallet.#isInternal = false } diff --git a/src/lib/workers.ts b/src/lib/workers.ts new file mode 100644 index 0000000..8828c7d --- /dev/null +++ b/src/lib/workers.ts @@ -0,0 +1,5 @@ +import ckdBip44 from './workers/ckdBip44.js' +import ckdBlake2b from './workers/ckdBlake2b.js' +export { ckdBip44, ckdBlake2b } +// import './workers/nano-nacl.js' +// import './workers/passkey.js' diff --git a/src/lib/workers/ckdBip44.ts b/src/lib/workers/ckdBip44.ts index 42216eb..56a48c7 100644 --- a/src/lib/workers/ckdBip44.ts +++ b/src/lib/workers/ckdBip44.ts @@ -6,102 +6,100 @@ type ExtendedKey = { chainCode: DataView } -async function ckdBip44 () { - const BIP44_COIN_NANO = 165 - const BIP44_PURPOSE = 44 - const HARDENED_OFFSET = 0x80000000 - const SLIP10_ED25519 = 'ed25519 seed' - let addEventListener = globalThis.addEventListener - let postMessage = globalThis.postMessage - if (addEventListener == null || postMessage == null) { - const { isMainThread, parentPort } = await import('node:worker_threads') - if (!isMainThread && parentPort) { - addEventListener = Object.getPrototypeOf(parentPort).addListener.bind(parentPort) - postMessage = Object.getPrototypeOf(parentPort).postMessage.bind(parentPort) - } +const BIP44_COIN_NANO = 165 +const BIP44_PURPOSE = 44 +const HARDENED_OFFSET = 0x80000000 +const SLIP10_ED25519 = 'ed25519 seed' +let addEventListener = globalThis.addEventListener +let postMessage = globalThis.postMessage +if (addEventListener == null || postMessage == null) { + const { isMainThread, parentPort } = await import('node:worker_threads') + if (!isMainThread && parentPort) { + addEventListener = Object.getPrototypeOf(parentPort).addListener.bind(parentPort) + postMessage = Object.getPrototypeOf(parentPort).postMessage.bind(parentPort) } +} - /** - * Listens for messages from a calling function. - */ - addEventListener('message', (message) => { - const { seed, index } = message.data ?? message - nanoCKD(seed, index).then(key => postMessage({ index, key })) - }) +/** +* Listens for messages from a calling function. +*/ +addEventListener('message', (message) => { + const { seed, index } = message.data ?? message + nanoCKD(seed, index).then(key => postMessage({ index, key })) +}) - /** - * Derives a private child key following the BIP-32 and BIP-44 derivation path - * registered to the Nano block lattice. Only hardened child keys are defined. - * - * @param {string} seed - Hexadecimal seed derived from mnemonic phrase - * @param {number} index - Account number between 0 and 2^31-1 - * @returns Private child key for the account - */ - async function nanoCKD (seed: string, index: number): Promise { - if (seed.length < 32 || seed.length > 128) { - throw new RangeError(`Invalid seed length`) - } - if (!Number.isSafeInteger(index) || index < 0 || index > 0x7fffffff) { - throw new RangeError(`Invalid child key index 0x${index.toString(16)}`) - } - const masterKey = await slip10(SLIP10_ED25519, seed) - const purposeKey = await CKDpriv(masterKey, BIP44_PURPOSE + HARDENED_OFFSET) - const coinKey = await CKDpriv(purposeKey, BIP44_COIN_NANO + HARDENED_OFFSET) - const accountKey = await CKDpriv(coinKey, index + HARDENED_OFFSET) - const privateKey = new Uint8Array(accountKey.privateKey.buffer) - return privateKey.reduce((key, byte) => key.concat(byte.toString(16).padStart(2, '0')), '') +/** +* Derives a private child key following the BIP-32 and BIP-44 derivation path +* registered to the Nano block lattice. Only hardened child keys are defined. +* +* @param {string} seed - Hexadecimal seed derived from mnemonic phrase +* @param {number} index - Account number between 0 and 2^31-1 +* @returns Private child key for the account +*/ +async function nanoCKD (seed: string, index: number): Promise { + if (seed.length < 32 || seed.length > 128) { + throw new RangeError(`Invalid seed length`) } - - async function slip10 (curve: string, S: string): Promise { - const key = new TextEncoder().encode(curve) - const data = new Uint8Array(64) - data.set(S.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16))) - const I = await hmac(key, data) - const IL = new DataView(I.buffer.slice(0, I.length / 2)) - const IR = new DataView(I.buffer.slice(I.length / 2)) - return ({ privateKey: IL, chainCode: IR }) + if (!Number.isSafeInteger(index) || index < 0 || index > 0x7fffffff) { + throw new RangeError(`Invalid child key index 0x${index.toString(16)}`) } + const masterKey = await slip10(SLIP10_ED25519, seed) + const purposeKey = await CKDpriv(masterKey, BIP44_PURPOSE + HARDENED_OFFSET) + const coinKey = await CKDpriv(purposeKey, BIP44_COIN_NANO + HARDENED_OFFSET) + const accountKey = await CKDpriv(coinKey, index + HARDENED_OFFSET) + const privateKey = new Uint8Array(accountKey.privateKey.buffer) + return privateKey.reduce((key, byte) => key.concat(byte.toString(16).padStart(2, '0')), '') +} - async function CKDpriv ({ privateKey, chainCode }: ExtendedKey, index: number): Promise { - const key = new Uint8Array(chainCode.buffer) - const data = new Uint8Array(37) - data.set([0]) - data.set(ser256(privateKey), 1) - data.set(ser32(index), 33) - const I = await hmac(key, data) - const IL = new DataView(I.buffer.slice(0, I.length / 2)) - const IR = new DataView(I.buffer.slice(I.length / 2)) - return ({ privateKey: IL, chainCode: IR }) - } +async function slip10 (curve: string, S: string): Promise { + const key = new TextEncoder().encode(curve) + const data = new Uint8Array(64) + data.set(S.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16))) + const I = await hmac(key, data) + const IL = new DataView(I.buffer.slice(0, I.length / 2)) + const IR = new DataView(I.buffer.slice(I.length / 2)) + return ({ privateKey: IL, chainCode: IR }) +} - function ser32 (integer: number): Uint8Array { - if (typeof integer !== 'number') { - throw new TypeError(`Expected a number, received ${typeof integer}`) - } - if (integer > 0xffffffff) { - throw new RangeError(`Expected 32-bit integer, received ${integer.toString(2).length}-bit value: ${integer}`) - } - const view = new DataView(new ArrayBuffer(4)) - view.setUint32(0, integer, false) - return new Uint8Array(view.buffer) - } +async function CKDpriv ({ privateKey, chainCode }: ExtendedKey, index: number): Promise { + const key = new Uint8Array(chainCode.buffer) + const data = new Uint8Array(37) + data.set([0]) + data.set(ser256(privateKey), 1) + data.set(ser32(index), 33) + const I = await hmac(key, data) + const IL = new DataView(I.buffer.slice(0, I.length / 2)) + const IR = new DataView(I.buffer.slice(I.length / 2)) + return ({ privateKey: IL, chainCode: IR }) +} - function ser256 (integer: DataView): Uint8Array { - if (integer.constructor !== DataView) { - throw new TypeError(`Expected DataView, received ${typeof integer}`) - } - if (integer.byteLength > 32) { - throw new RangeError(`Expected 32-byte integer, received ${integer.byteLength}-byte value: ${integer}`) - } - return new Uint8Array(integer.buffer) +function ser32 (integer: number): Uint8Array { + if (typeof integer !== 'number') { + throw new TypeError(`Expected a number, received ${typeof integer}`) + } + if (integer > 0xffffffff) { + throw new RangeError(`Expected 32-bit integer, received ${integer.toString(2).length}-bit value: ${integer}`) } + const view = new DataView(new ArrayBuffer(4)) + view.setUint32(0, integer, false) + return new Uint8Array(view.buffer) +} - async function hmac (key: Uint8Array, data: Uint8Array): Promise { - const { subtle } = globalThis.crypto - const pk = await subtle.importKey('raw', key, { name: 'HMAC', hash: 'SHA-512' }, false, ['sign']) - const signature = await subtle.sign('HMAC', pk, data) - return new Uint8Array(signature) +function ser256 (integer: DataView): Uint8Array { + if (integer.constructor !== DataView) { + throw new TypeError(`Expected DataView, received ${typeof integer}`) } + if (integer.byteLength > 32) { + throw new RangeError(`Expected 32-byte integer, received ${integer.byteLength}-byte value: ${integer}`) + } + return new Uint8Array(integer.buffer) +} + +async function hmac (key: Uint8Array, data: Uint8Array): Promise { + const { subtle } = globalThis.crypto + const pk = await subtle.importKey('raw', key, { name: 'HMAC', hash: 'SHA-512' }, false, ['sign']) + const signature = await subtle.sign('HMAC', pk, data) + return new Uint8Array(signature) } -export const ckdBip44Worker = `(${ckdBip44.toString()})()` +export default import.meta.url diff --git a/src/lib/workers/ckdBlake2b.ts b/src/lib/workers/ckdBlake2b.ts index 55f9743..10a477d 100644 --- a/src/lib/workers/ckdBlake2b.ts +++ b/src/lib/workers/ckdBlake2b.ts @@ -3,42 +3,37 @@ import blake2b from 'blake2b' -async function ckdBlake2b () { - let blake2b: any - - let addEventListener = globalThis.addEventListener - let postMessage = globalThis.postMessage - if (addEventListener == null || postMessage == null) { - blake2b = (await import('blake2b')).default - const { isMainThread, parentPort } = await import('node:worker_threads') - if (!isMainThread && parentPort) { - addEventListener = Object.getPrototypeOf(parentPort).addListener.bind(parentPort) - postMessage = Object.getPrototypeOf(parentPort).postMessage.bind(parentPort) - } +let addEventListener = globalThis.addEventListener +let postMessage = globalThis.postMessage +if (addEventListener == null || postMessage == null) { + const { isMainThread, parentPort } = await import('node:worker_threads') + if (!isMainThread && parentPort) { + addEventListener = Object.getPrototypeOf(parentPort).addListener.bind(parentPort) + postMessage = Object.getPrototypeOf(parentPort).postMessage.bind(parentPort) } +} - /** - * Listens for messages from a calling function. - */ - addEventListener('message', (message) => { - const { seed, index } = message.data ?? message - ckdBlake2b(seed, index).then(key => postMessage({ index, key })) - }) +/** +* Listens for messages from a calling function. +*/ +addEventListener('message', (message) => { + const { seed, index } = message.data ?? message + ckdBlake2b(seed, index).then(key => postMessage({ index, key })) +}) - /** - * Derives BLAKE2b account private keys. - * - * @param {number} index - Index of the account - * @returns {Promise} - */ - async function ckdBlake2b (seed: string, index: number): Promise { - const indexHex = index.toString(16).padStart(8, '0').toUpperCase() - const inputHex = `${seed}${indexHex}`.padStart(72, '0') - const inputArray = (inputHex.match(/.{1,2}/g) ?? []).map(h => parseInt(h, 16)) - const inputBytes = Uint8Array.from(inputArray) - const hash = blake2b(32).update(inputBytes).digest('hex') - return hash - } +/** +* Derives BLAKE2b account private keys. +* +* @param {number} index - Index of the account +* @returns {Promise} +*/ +async function ckdBlake2b (seed: string, index: number): Promise { + const indexHex = index.toString(16).padStart(8, '0').toUpperCase() + const inputHex = `${seed}${indexHex}`.padStart(72, '0') + const inputArray = (inputHex.match(/.{1,2}/g) ?? []).map(h => parseInt(h, 16)) + const inputBytes = Uint8Array.from(inputArray) + const hash = blake2b(32).update(inputBytes).digest('hex') + return hash } -export const ckdBlake2bWorker = `(${ckdBlake2b.toString()})()` +export default import.meta.url -- 2.34.1