From f26264a863b5e85fb5c1b46f6872a140447b3347 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Tue, 26 Nov 2024 11:14:46 -0800 Subject: [PATCH] Exporting import.meta.url does not work since once it is bundled, all files share the same URL and the Pool does not know which worker to spin up. It seems like a completely bundled solution will require creating workers from function strings, and that will require embedding all logic inside the worker with no external imports as far as I have been able to determine. Along with exporting workers as function strings, update BLAKE2b ckd to accept blake2b as a function string for its hashing. --- src/lib/pool.ts | 3 +- src/lib/wallet.ts | 2 +- src/lib/workers/ckdBip44.ts | 155 +++++++++++++++++----------------- src/lib/workers/ckdBlake2b.ts | 48 ++++++----- 4 files changed, 107 insertions(+), 101 deletions(-) diff --git a/src/lib/pool.ts b/src/lib/pool.ts index 0c3f9fd..6b86516 100644 --- a/src/lib/pool.ts +++ b/src/lib/pool.ts @@ -32,7 +32,8 @@ export class Pool { return true } - constructor (url: string) { + constructor (fn: string) { + const url = URL.createObjectURL(new Blob([fn], { type: 'text/javascript' })) for (let i = navigator.hardwareConcurrency - 1; i > 0; i--) { const thread = { isBusy: false, diff --git a/src/lib/wallet.ts b/src/lib/wallet.ts index e0e553b..afa02be 100644 --- a/src/lib/wallet.ts +++ b/src/lib/wallet.ts @@ -572,7 +572,7 @@ export class Blake2bWallet extends Wallet { async ckd (index: number | number[]): Promise { if (!Array.isArray(index)) index = [index] const data: any = [] - index.forEach(i => data.push({ seed: this.seed, index: i })) + index.forEach(i => data.push({ blake2b: blake2b.toString(), index: i, seed: this.seed })) const results: [{ index: number, key: string }] = await this.#pool.work(data) const accounts = [] for (const result of results) { diff --git a/src/lib/workers/ckdBip44.ts b/src/lib/workers/ckdBip44.ts index 95b3b9b..7eaf9c1 100644 --- a/src/lib/workers/ckdBip44.ts +++ b/src/lib/workers/ckdBip44.ts @@ -6,91 +6,94 @@ type ExtendedKey = { chainCode: DataView } -const BIP44_COIN_NANO = 165 -const BIP44_PURPOSE = 44 -const HARDENED_OFFSET = 0x80000000 -const SLIP10_ED25519 = 'ed25519 seed' +async function fn () { + const BIP44_COIN_NANO = 165 + const BIP44_PURPOSE = 44 + const HARDENED_OFFSET = 0x80000000 + const SLIP10_ED25519 = 'ed25519 seed' -/** -* 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`) + /** + * 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 { + console.log(`seed: ${seed}; index: ${index}`) + 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')), '') } - 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 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 }) -} - -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 ser32 (integer: number): Uint8Array { - if (typeof integer !== 'number') { - throw new TypeError(`Expected a number, received ${typeof integer}`) + 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 (integer > 0xffffffff) { - throw new RangeError(`Expected 32-bit integer, received ${integer.toString(2).length}-bit value: ${integer}`) + + 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 }) } - const view = new DataView(new ArrayBuffer(4)) - view.setUint32(0, integer, false) - return new Uint8Array(view.buffer) -} -function ser256 (integer: DataView): Uint8Array { - if (integer.constructor !== DataView) { - throw new TypeError(`Expected DataView, received ${typeof integer}`) + 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) } - if (integer.byteLength > 32) { - throw new RangeError(`Expected 32-byte integer, received ${integer.byteLength}-byte value: ${integer}`) + + 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) } - 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) + 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 default import.meta.url +export default `(${fn.toString()})()` diff --git a/src/lib/workers/ckdBlake2b.ts b/src/lib/workers/ckdBlake2b.ts index 6f27858..edb5b83 100644 --- a/src/lib/workers/ckdBlake2b.ts +++ b/src/lib/workers/ckdBlake2b.ts @@ -1,29 +1,31 @@ // SPDX-FileCopyrightText: 2024 Chris Duncan // SPDX-License-Identifier: GPL-3.0-or-later -import blake2b from 'blake2b' +async function fn () { + /** + * Listens for messages from a calling function. + */ + addEventListener('message', (message) => { + const { seed, index, blake2b } = message.data ?? message + ckdBlake2b(seed, index, blake2b).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, blake2b: string): Promise { + const blake = Function(`return ${blake2b}`) + console.log(blake) + 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 = blake(32).update(inputBytes).digest('hex') + return hash + } } -export default import.meta.url +export default `(${fn.toString()})()` -- 2.34.1