From 1be0e376e3b9949bb157fb544a6a78269758feb3 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Thu, 21 Nov 2024 15:10:35 -0800 Subject: [PATCH] Save work on testing browser compatibility. --- src/lib/pool.ts | 11 ++- src/lib/wallet.ts | 18 +++- src/lib/workers/ckdBip44.ts | 156 ++++++++++++++++++---------------- src/lib/workers/ckdBlake2b.ts | 64 +++++++------- test.html | 10 +++ 5 files changed, 154 insertions(+), 105 deletions(-) create mode 100644 test.html diff --git a/src/lib/pool.ts b/src/lib/pool.ts index b153485..f37a5ba 100644 --- a/src/lib/pool.ts +++ b/src/lib/pool.ts @@ -32,11 +32,18 @@ export class Pool { return true } - constructor (url: string | URL) { + 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, - worker: new Worker(new URL(url, import.meta.url), { type: 'module' }) + //@ts-expect-error + worker: new Worker(workerUrl, { 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 a1b74cb..3a46851 100644 --- a/src/lib/wallet.ts +++ b/src/lib/wallet.ts @@ -5,6 +5,8 @@ import blake2b from 'blake2b' 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' @@ -265,7 +267,13 @@ export class Bip44Wallet extends Wallet { throw new Error(`Bip44Wallet cannot be instantiated directly. Use 'await Bip44Wallet.create()' instead.`) } super(seed, mnemonic, id) - this.#pool = new Pool('./workers/ckdBip44.js') + 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) + } Bip44Wallet.#isInternal = false } @@ -458,7 +466,13 @@ export class Blake2bWallet extends Wallet { throw new Error(`Blake2bWallet cannot be instantiated directly. Use 'await Blake2bWallet.create()' instead.`) } super(seed, mnemonic, id) - this.#pool = new Pool('./workers/ckdBlake2b.js') + 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) + } Blake2bWallet.#isInternal = false } diff --git a/src/lib/workers/ckdBip44.ts b/src/lib/workers/ckdBip44.ts index 369742f..2f889ef 100644 --- a/src/lib/workers/ckdBip44.ts +++ b/src/lib/workers/ckdBip44.ts @@ -1,93 +1,105 @@ // SPDX-FileCopyrightText: 2024 Chris Duncan // SPDX-License-Identifier: GPL-3.0-or-later -import { BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET, SLIP10_ED25519 } from '../constants.js' -import { bytes, dec, hex, utf8 } from '../convert.js' type ExtendedKey = { privateKey: string chainCode: string } -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) +async function ckdBip44 () { + let BIP44_COIN_NANO: any, BIP44_PURPOSE: any, HARDENED_OFFSET: any, SLIP10_ED25519: any + let bytes: any, dec: any, hex: any, utf8: any + let addEventListener = globalThis.addEventListener + let postMessage = globalThis.postMessage + if (addEventListener == null || postMessage == null) { + ({ BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET, SLIP10_ED25519 } = await import(new URL('dist/lib/constants.js', import.meta.url).toString())); + ({ bytes, dec, hex, utf8 } = await import(new URL('./dist/lib/convert.js', import.meta.url).toString())) + 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) + } + } else { + ({ BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET, SLIP10_ED25519 } = await import('../constants.js')); + ({ bytes, dec, hex, utf8 } = await import('../convert.js')) } -} -/** -* 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 -*/ -export async function nanoCKD (seed: string, index: number): Promise { - if (!Number.isSafeInteger(index) || index < 0 || index > 0x7fffffff) { - throw new RangeError(`Invalid child key index 0x${index.toString(16)}`) + /** + * 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 (!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) + return accountKey.privateKey } - 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) - return accountKey.privateKey -} - -async function slip10 (curve: string, S: string): Promise { - const key = utf8.toBytes(curve) - const data = hex.toBytes(S) - const I = await hmac(key, data) - const IL = I.slice(0, I.length / 2) - const IR = I.slice(I.length / 2) - return ({ privateKey: IL, chainCode: IR }) -} -async function CKDpriv ({ privateKey, chainCode }: ExtendedKey, index: number): Promise { - const key = hex.toBytes(chainCode) - const data = hex.toBytes(`00${bytes.toHex(ser256(privateKey))}${bytes.toHex(ser32(index))}`) - const I = await hmac(key, data) - const IL = I.slice(0, I.length / 2) - const IR = I.slice(I.length / 2) - return ({ privateKey: IL, chainCode: IR }) -} + async function slip10 (curve: string, S: string): Promise { + const key = utf8.toBytes(curve) + const data = hex.toBytes(S) + const I = await hmac(key, data) + const IL = I.slice(0, I.length / 2) + const IR = I.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}`) + async function CKDpriv ({ privateKey, chainCode }: ExtendedKey, index: number): Promise { + const key = hex.toBytes(chainCode) + const data = hex.toBytes(`00${bytes.toHex(ser256(privateKey))}${bytes.toHex(ser32(index))}`) + const I = await hmac(key, data) + const IL = I.slice(0, I.length / 2) + const IR = I.slice(I.length / 2) + return ({ privateKey: IL, chainCode: IR }) } - const bits = dec.toBin(integer) - if (bits.length > 32) { - throw new RangeError(`Expected 32-bit integer, received ${bits.length}-bit value: ${integer}`) + + function ser32 (integer: number): Uint8Array { + if (typeof integer !== 'number') { + throw new TypeError(`Expected a number, received ${typeof integer}`) + } + const bits = dec.toBin(integer) + if (bits.length > 32) { + throw new RangeError(`Expected 32-bit integer, received ${bits.length}-bit value: ${integer}`) + } + return dec.toBytes(integer, 4) } - return dec.toBytes(integer, 4) -} -function ser256 (integer: string): Uint8Array { - if (typeof integer !== 'string') { - throw new TypeError(`Expected string, received ${typeof integer}`) + function ser256 (integer: string): Uint8Array { + if (typeof integer !== 'string') { + throw new TypeError(`Expected string, received ${typeof integer}`) + } + const bits = hex.toBin(integer) + if (bits.length > 256) { + throw new RangeError(`Expected 256-bit integer, received ${bits.length}-bit value: ${integer}`) + } + return hex.toBytes(integer, 32) } - const bits = hex.toBin(integer) - if (bits.length > 256) { - throw new RangeError(`Expected 256-bit integer, received ${bits.length}-bit value: ${integer}`) + + 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 bytes.toHex(new Uint8Array(signature)) } - return hex.toBytes(integer, 32) } -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 bytes.toHex(new Uint8Array(signature)) -} +const ckdBip44Worker = `(${ckdBip44.toString()})()` +// const ckdBip44Worker = `data:text/javascript,(${ckdBip44.toString()})()` +export { ckdBip44Worker } + diff --git a/src/lib/workers/ckdBlake2b.ts b/src/lib/workers/ckdBlake2b.ts index e807fd3..2995aed 100644 --- a/src/lib/workers/ckdBlake2b.ts +++ b/src/lib/workers/ckdBlake2b.ts @@ -1,37 +1,43 @@ // SPDX-FileCopyrightText: 2024 Chris Duncan // SPDX-License-Identifier: GPL-3.0-or-later -import blake2b from 'blake2b' +async function ckdBlake2b () { + const { default: blake2b } = await import('blake2b') -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) + 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 + } } + +const ckdBlake2bWorker = `(${ckdBlake2b.toString()})()` +// const ckdBlake2bWorker = `data:text/javascript,(${ckdBlake2b.toString()})()` +export { ckdBlake2bWorker } diff --git a/test.html b/test.html new file mode 100644 index 0000000..f7cc454 --- /dev/null +++ b/test.html @@ -0,0 +1,10 @@ + + + + + + + + + + -- 2.34.1