From 303686da4d086e881e5b3f819fd7d70076624579 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Wed, 30 Jul 2025 06:56:16 -0700 Subject: [PATCH] Store mnemonic phrase as word array and convert to full string on-demand. Fix private member name for accuracy. Check for null. Disallow blake seeds from phrases shorter than 24 words per Nano spec. Improve performance by deriving blake seed using bitwise operations on bigints instead of string manipulation. --- src/lib/bip39-mnemonic.ts | 60 +++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/src/lib/bip39-mnemonic.ts b/src/lib/bip39-mnemonic.ts index 785f743..608499b 100644 --- a/src/lib/bip39-mnemonic.ts +++ b/src/lib/bip39-mnemonic.ts @@ -12,10 +12,10 @@ import { Key } from '#types' */ export class Bip39Mnemonic { static #isInternal: boolean = false - #bip44Seed?: Uint8Array + #bip39Seed?: Uint8Array #blake2bSeed?: Uint8Array - #phrase: string = '' - get phrase (): string { return this.#phrase.normalize('NFKD') } + #phrase?: string[] + get phrase (): string | undefined { return this.#phrase?.join(' ').normalize('NFKD') } private constructor () { if (!Bip39Mnemonic.#isInternal) { @@ -38,7 +38,7 @@ export class Bip39Mnemonic { const self = new this() const isValid = await this.validate(phrase) if (isValid) { - self.#phrase = phrase.normalize('NFKD') + self.#phrase = phrase.normalize('NFKD').split(' ') return self } else { throw new Error('Invalid mnemonic phrase.') @@ -98,11 +98,11 @@ export class Bip39Mnemonic { * collection. */ destroy () { - bytes.erase(this.#bip44Seed) + bytes.erase(this.#bip39Seed) bytes.erase(this.#blake2bSeed) - this.#bip44Seed = undefined + this.#bip39Seed = undefined this.#blake2bSeed = undefined - this.#phrase = '' + this.#phrase = undefined } /** @@ -160,7 +160,10 @@ export class Bip39Mnemonic { */ async toBip39Seed (passphrase: string, format: 'hex'): Promise async toBip39Seed (passphrase: string, format?: 'hex'): Promise { - if (this.#bip44Seed == null) { + if (this.phrase == null) { + throw new Error('BIP-39 mnemonic phrase not found') + } + if (this.#bip39Seed == null) { if (passphrase == null || typeof passphrase !== 'string') { passphrase = '' } @@ -181,38 +184,45 @@ export class Bip39Mnemonic { } const seedKey = await globalThis.crypto.subtle.deriveKey(algorithm, phraseKey, derivedKeyType, true, ['sign']) const seedBuffer = await globalThis.crypto.subtle.exportKey('raw', seedKey) - this.#bip44Seed = new Uint8Array(seedBuffer) + this.#bip39Seed = new Uint8Array(seedBuffer) } return format === 'hex' - ? bytes.toHex(this.#bip44Seed) - : this.#bip44Seed + ? bytes.toHex(this.#bip39Seed) + : this.#bip39Seed } + /** + * Converts the mnemonic phrase to a BLAKE2b seed. + * + * @returns {Promise>} Promise for seed as bytes + */ async toBlake2bSeed (): Promise> /** * Converts the mnemonic phrase to a BLAKE2b seed. * - * @returns {string} Hexadecimal seed + * @returns {Promise} Promise for seed as hexadecimal string */ async toBlake2bSeed (format: 'hex'): Promise async toBlake2bSeed (format?: 'hex'): Promise { + if (this.#phrase?.length !== 24) { + throw new Error('BIP-39 mnemonic phrase must be 24 words to convert to BLAKE2b seed') + } if (this.#blake2bSeed == null) { - const wordArray = this.phrase.split(' ') - const bits = wordArray.map((w: string) => { - const wordIndex = Bip39Words.indexOf(w) + let bits = 0n + const words = this.#phrase + for (const word of this.#phrase) { + const wordIndex = Bip39Words.indexOf(word) if (wordIndex === -1) { - return false + throw new RangeError('Word not found in BIP-39 list') } - return dec.toBin(wordIndex, 11) - }).join('') - - const dividerIndex = Math.floor(bits.length / 33) * 32 - const entropyBits = bits.slice(0, dividerIndex) - const entropyBytes = entropyBits.match(/(.{1,8})/g)?.map((bin: string) => parseInt(bin, 2)) - if (entropyBytes == null) { - throw new Error('Invalid mnemonic phrase') + bits = (bits << 11n) | BigInt(Bip39Words.indexOf(word)) + } + bits >>= 8n + this.#blake2bSeed = new Uint8Array(32) + for (let i = 31; i >= 0; i--) { + this.#blake2bSeed[i] = Number(bits & 255n) + bits >> 8n } - this.#blake2bSeed = new Uint8Array(entropyBytes) } return format === 'hex' ? bytes.toHex(this.#blake2bSeed) -- 2.47.3