From 5a1795da0440ad9d1491b9466f516bb4747d1d93 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Wed, 30 Jul 2025 08:27:17 -0700 Subject: [PATCH] Fix bit shifting when deriving blake seed from mnemonic. Reorder static methods. Remove unnecessary imports. --- src/lib/bip39-mnemonic.ts | 140 +++++++++++++++++++------------------- 1 file changed, 69 insertions(+), 71 deletions(-) diff --git a/src/lib/bip39-mnemonic.ts b/src/lib/bip39-mnemonic.ts index 3ca66e0..c03efcc 100644 --- a/src/lib/bip39-mnemonic.ts +++ b/src/lib/bip39-mnemonic.ts @@ -3,15 +3,77 @@ import { Bip39Words } from './bip39-wordlist' import { BIP39_ITERATIONS } from './constants' -import { bin, bytes, dec, hex, utf8 } from './convert' -import { Entropy } from './entropy' -import { Key } from '#types' +import { bytes, hex, utf8 } from './convert' /** * Represents a mnemonic phrase that identifies a wallet as defined by BIP-39. */ export class Bip39Mnemonic { static #isInternal: boolean = false + + /** + * SHA-256 hash of entropy that is appended to the entropy and subsequently + * used to generate the mnemonic phrase. + * + * @param {Entropy} entropy - Cryptographically strong pseudorandom data of length N bits + * @returns {Promise} First N/32 bits of the hash as a hexadecimal string + */ + static async #checksum (entropy: Uint8Array): Promise { + const sha256sum = new Uint8Array(await crypto.subtle.digest('SHA-256', entropy))[0] + const checksumBitLength = BigInt(entropy.byteLength) / 4n + const checksum = BigInt(sha256sum) >> (8n - checksumBitLength) + return checksum + } + + /** + * Validates a mnemonic phrase. + * + * @param {string} mnemonic - Mnemonic phrase to validate + * @returns {boolean} True if the mnemonic phrase is valid + */ + static async validate (mnemonic: string): Promise { + const words = mnemonic.normalize('NFKD').split(' ') + if (words.length % 3 !== 0) { + return false + } + let bits = 0n, bitLength = 0n + for (const word of words) { + const wordIndex = Bip39Words.indexOf(word) + if (wordIndex === -1) { + return false + } + bits = (bits << 11n) | BigInt(Bip39Words.indexOf(word)) + bitLength += 11n + } + if (Number(bitLength) % 33 !== 0) { + return false + } + const checksumLength = bitLength / 33n + const entropyLength = bitLength - checksumLength + const entropyBits = bits >> checksumLength + const checksumBits = bits & ((1n << checksumLength) - 1n) + if (entropyBits == null + || entropyBits < 0n + || entropyBits > (1n << 256n) - 1n + || entropyLength < 128n + || entropyLength > 256n + || Number(entropyLength) % 32 !== 0 + ) { + return false + } + const bytes = new Uint8Array(Number(entropyLength) / 8) + for (let i = 0; i < bytes.length; i++) { + const shift = entropyLength - (8n * BigInt(i + 1)) + const byte = (entropyBits >> shift) & 255n + bytes[i] = Number(byte) + } + const expectedChecksum = await this.#checksum(bytes) + if (expectedChecksum !== checksumBits) { + return false + } + return true + } + #bip39Seed?: Uint8Array #blake2bSeed?: Uint8Array #phrase?: string[] @@ -54,13 +116,13 @@ export class Bip39Mnemonic { * @param {string} entropy - Hexadecimal string * @returns {string} Mnemonic phrase created using the BIP-39 wordlist */ - static async fromEntropy (entropy: Key): Promise { + static async fromEntropy (entropy: string | Uint8Array): Promise { if (typeof entropy === 'string') entropy = hex.toBytes(entropy) if (![16, 20, 24, 28, 32].includes(entropy.byteLength)) { throw new RangeError('Invalid entropy byte length for BIP-39') } const phraseLength = 0.75 * entropy.byteLength - const checksum = await this.checksum(entropy) + const checksum = await this.#checksum(entropy) let e = 0n for (let i = 0; i < entropy.byteLength; i++) { @@ -79,20 +141,6 @@ export class Bip39Mnemonic { return this.fromPhrase(sentence) } - /** - * SHA-256 hash of entropy that is appended to the entropy and subsequently - * used to generate the mnemonic phrase. - * - * @param {Entropy} entropy - Cryptographically strong pseudorandom data of length N bits - * @returns {Promise} First N/32 bits of the hash as a hexadecimal string - */ - static async checksum (entropy: Uint8Array): Promise { - const sha256sum = new Uint8Array(await crypto.subtle.digest('SHA-256', entropy))[0] - const checksumBitLength = BigInt(entropy.byteLength) / 4n - const checksum = BigInt(sha256sum) >> (8n - checksumBitLength) - return checksum - } - /** * Erases seed bytes and releases variable references to allow garbage * collection. @@ -160,7 +208,7 @@ export class Bip39Mnemonic { * @returns {Promise} Promise for seed as hexadecimal string */ async toBlake2bSeed (format: 'hex'): Promise - 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') } @@ -177,62 +225,12 @@ export class Bip39Mnemonic { this.#blake2bSeed = new Uint8Array(32) for (let i = 31; i >= 0; i--) { this.#blake2bSeed[i] = Number(bits & 255n) - bits >> 8n + bits >>= 8n } } return format === 'hex' ? bytes.toHex(this.#blake2bSeed) : this.#blake2bSeed } - - /** - * Validates a mnemonic phrase. - * - * @param {string} mnemonic - Mnemonic phrase to validate - * @returns {boolean} True if the mnemonic phrase is valid - */ - static async validate (mnemonic: string): Promise { - const words = mnemonic.normalize('NFKD').split(' ') - if (words.length % 3 !== 0) { - return false - } - - let bits = 0n, bitLength = 0n - for (const word of words) { - const wordIndex = Bip39Words.indexOf(word) - if (wordIndex === -1) { - return false - } - bits = (bits << 11n) | BigInt(Bip39Words.indexOf(word)) - bitLength += 11n - } - if (Number(bitLength) % 33 !== 0) { - return false - } - - const checksumLength = bitLength / 33n - const entropyLength = Number(bitLength - checksumLength) - const entropyBits = bits >> checksumLength - const checksumBits = bits & ((1n << checksumLength) - 1n) - - if (entropyBits == null - || entropyBits < 0n - || entropyBits > (1n << 256n) - 1n - || entropyLength < 128 - || entropyLength > 256 - || entropyLength % 32 !== 0 - ) { - return false - } - - const entropyHex = entropyBits.toString(16).padStart(entropyLength / 4, '0') - const entropy = await Entropy.import(entropyHex) - const expectedChecksum = await this.checksum(entropy.bytes) - if (expectedChecksum !== checksumBits) { - return false - } - - return true - } } -- 2.47.3