From: Chris Duncan Date: Wed, 30 Jul 2025 13:56:16 +0000 (-0700) Subject: Store mnemonic phrase as word array and convert to full string on-demand. Fix private... X-Git-Tag: v0.10.5~48^2~7 X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=303686da4d086e881e5b3f819fd7d70076624579;p=libnemo.git 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. --- 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)