]> git.codecow.com Git - libnemo.git/commitdiff
Fix bit shifting when deriving blake seed from mnemonic. Reorder static methods....
authorChris Duncan <chris@zoso.dev>
Wed, 30 Jul 2025 15:27:17 +0000 (08:27 -0700)
committerChris Duncan <chris@zoso.dev>
Wed, 30 Jul 2025 15:27:17 +0000 (08:27 -0700)
src/lib/bip39-mnemonic.ts

index 3ca66e072460f8a08ff7ac36a37020ca74b5cbea..c03efcc14807026597d919278b4308b69dcaff4d 100644 (file)
@@ -3,15 +3,77 @@
 \r
 import { Bip39Words } from './bip39-wordlist'\r
 import { BIP39_ITERATIONS } from './constants'\r
-import { bin, bytes, dec, hex, utf8 } from './convert'\r
-import { Entropy } from './entropy'\r
-import { Key } from '#types'\r
+import { bytes, hex, utf8 } from './convert'\r
 \r
 /**\r
 * Represents a mnemonic phrase that identifies a wallet as defined by BIP-39.\r
 */\r
 export class Bip39Mnemonic {\r
        static #isInternal: boolean = false\r
+\r
+       /**\r
+        * SHA-256 hash of entropy that is appended to the entropy and subsequently\r
+        * used to generate the mnemonic phrase.\r
+        *\r
+        * @param {Entropy} entropy - Cryptographically strong pseudorandom data of length N bits\r
+        * @returns {Promise<string>} First N/32 bits of the hash as a hexadecimal string\r
+        */\r
+       static async #checksum (entropy: Uint8Array<ArrayBuffer>): Promise<bigint> {\r
+               const sha256sum = new Uint8Array(await crypto.subtle.digest('SHA-256', entropy))[0]\r
+               const checksumBitLength = BigInt(entropy.byteLength) / 4n\r
+               const checksum = BigInt(sha256sum) >> (8n - checksumBitLength)\r
+               return checksum\r
+       }\r
+\r
+       /**\r
+       * Validates a mnemonic phrase.\r
+       *\r
+       * @param {string} mnemonic - Mnemonic phrase to validate\r
+       * @returns {boolean} True if the mnemonic phrase is valid\r
+       */\r
+       static async validate (mnemonic: string): Promise<boolean> {\r
+               const words = mnemonic.normalize('NFKD').split(' ')\r
+               if (words.length % 3 !== 0) {\r
+                       return false\r
+               }\r
+               let bits = 0n, bitLength = 0n\r
+               for (const word of words) {\r
+                       const wordIndex = Bip39Words.indexOf(word)\r
+                       if (wordIndex === -1) {\r
+                               return false\r
+                       }\r
+                       bits = (bits << 11n) | BigInt(Bip39Words.indexOf(word))\r
+                       bitLength += 11n\r
+               }\r
+               if (Number(bitLength) % 33 !== 0) {\r
+                       return false\r
+               }\r
+               const checksumLength = bitLength / 33n\r
+               const entropyLength = bitLength - checksumLength\r
+               const entropyBits = bits >> checksumLength\r
+               const checksumBits = bits & ((1n << checksumLength) - 1n)\r
+               if (entropyBits == null\r
+                       || entropyBits < 0n\r
+                       || entropyBits > (1n << 256n) - 1n\r
+                       || entropyLength < 128n\r
+                       || entropyLength > 256n\r
+                       || Number(entropyLength) % 32 !== 0\r
+               ) {\r
+                       return false\r
+               }\r
+               const bytes = new Uint8Array(Number(entropyLength) / 8)\r
+               for (let i = 0; i < bytes.length; i++) {\r
+                       const shift = entropyLength - (8n * BigInt(i + 1))\r
+                       const byte = (entropyBits >> shift) & 255n\r
+                       bytes[i] = Number(byte)\r
+               }\r
+               const expectedChecksum = await this.#checksum(bytes)\r
+               if (expectedChecksum !== checksumBits) {\r
+                       return false\r
+               }\r
+               return true\r
+       }\r
+\r
        #bip39Seed?: Uint8Array<ArrayBuffer>\r
        #blake2bSeed?: Uint8Array<ArrayBuffer>\r
        #phrase?: string[]\r
@@ -54,13 +116,13 @@ export class Bip39Mnemonic {
        * @param {string} entropy - Hexadecimal string\r
        * @returns {string} Mnemonic phrase created using the BIP-39 wordlist\r
        */\r
-       static async fromEntropy (entropy: Key): Promise<Bip39Mnemonic> {\r
+       static async fromEntropy (entropy: string | Uint8Array<ArrayBuffer>): Promise<Bip39Mnemonic> {\r
                if (typeof entropy === 'string') entropy = hex.toBytes(entropy)\r
                if (![16, 20, 24, 28, 32].includes(entropy.byteLength)) {\r
                        throw new RangeError('Invalid entropy byte length for BIP-39')\r
                }\r
                const phraseLength = 0.75 * entropy.byteLength\r
-               const checksum = await this.checksum(entropy)\r
+               const checksum = await this.#checksum(entropy)\r
 \r
                let e = 0n\r
                for (let i = 0; i < entropy.byteLength; i++) {\r
@@ -79,20 +141,6 @@ export class Bip39Mnemonic {
                return this.fromPhrase(sentence)\r
        }\r
 \r
-       /**\r
-        * SHA-256 hash of entropy that is appended to the entropy and subsequently\r
-        * used to generate the mnemonic phrase.\r
-        *\r
-        * @param {Entropy} entropy - Cryptographically strong pseudorandom data of length N bits\r
-        * @returns {Promise<string>} First N/32 bits of the hash as a hexadecimal string\r
-        */\r
-       static async checksum (entropy: Uint8Array<ArrayBuffer>): Promise<bigint> {\r
-               const sha256sum = new Uint8Array(await crypto.subtle.digest('SHA-256', entropy))[0]\r
-               const checksumBitLength = BigInt(entropy.byteLength) / 4n\r
-               const checksum = BigInt(sha256sum) >> (8n - checksumBitLength)\r
-               return checksum\r
-       }\r
-\r
        /**\r
        * Erases seed bytes and releases variable references to allow garbage\r
        * collection.\r
@@ -160,7 +208,7 @@ export class Bip39Mnemonic {
        * @returns {Promise<string>} Promise for seed as hexadecimal string\r
        */\r
        async toBlake2bSeed (format: 'hex'): Promise<string>\r
-       async toBlake2bSeed (format?: 'hex'): Promise<Key> {\r
+       async toBlake2bSeed (format?: 'hex'): Promise<string | Uint8Array<ArrayBuffer>> {\r
                if (this.#phrase?.length !== 24) {\r
                        throw new Error('BIP-39 mnemonic phrase must be 24 words to convert to BLAKE2b seed')\r
                }\r
@@ -177,62 +225,12 @@ export class Bip39Mnemonic {
                        this.#blake2bSeed = new Uint8Array(32)\r
                        for (let i = 31; i >= 0; i--) {\r
                                this.#blake2bSeed[i] = Number(bits & 255n)\r
-                               bits >> 8n\r
+                               bits >>= 8n\r
                        }\r
                }\r
                return format === 'hex'\r
                        ? bytes.toHex(this.#blake2bSeed)\r
                        : this.#blake2bSeed\r
        }\r
-\r
-       /**\r
-       * Validates a mnemonic phrase.\r
-       *\r
-       * @param {string} mnemonic - Mnemonic phrase to validate\r
-       * @returns {boolean} True if the mnemonic phrase is valid\r
-       */\r
-       static async validate (mnemonic: string): Promise<boolean> {\r
-               const words = mnemonic.normalize('NFKD').split(' ')\r
-               if (words.length % 3 !== 0) {\r
-                       return false\r
-               }\r
-\r
-               let bits = 0n, bitLength = 0n\r
-               for (const word of words) {\r
-                       const wordIndex = Bip39Words.indexOf(word)\r
-                       if (wordIndex === -1) {\r
-                               return false\r
-                       }\r
-                       bits = (bits << 11n) | BigInt(Bip39Words.indexOf(word))\r
-                       bitLength += 11n\r
-               }\r
-               if (Number(bitLength) % 33 !== 0) {\r
-                       return false\r
-               }\r
-\r
-               const checksumLength = bitLength / 33n\r
-               const entropyLength = Number(bitLength - checksumLength)\r
-               const entropyBits = bits >> checksumLength\r
-               const checksumBits = bits & ((1n << checksumLength) - 1n)\r
-\r
-               if (entropyBits == null\r
-                       || entropyBits < 0n\r
-                       || entropyBits > (1n << 256n) - 1n\r
-                       || entropyLength < 128\r
-                       || entropyLength > 256\r
-                       || entropyLength % 32 !== 0\r
-               ) {\r
-                       return false\r
-               }\r
-\r
-               const entropyHex = entropyBits.toString(16).padStart(entropyLength / 4, '0')\r
-               const entropy = await Entropy.import(entropyHex)\r
-               const expectedChecksum = await this.checksum(entropy.bytes)\r
-               if (expectedChecksum !== checksumBits) {\r
-                       return false\r
-               }\r
-\r
-               return true\r
-       }\r
 }\r
 \r