]> git.codecow.com Git - libnemo.git/commitdiff
Refactor BIP-39 mnemonic generation from strings to bitwise operations.
authorChris Duncan <chris@zoso.dev>
Tue, 29 Jul 2025 09:34:04 +0000 (02:34 -0700)
committerChris Duncan <chris@zoso.dev>
Tue, 29 Jul 2025 09:34:04 +0000 (02:34 -0700)
src/lib/bip39-mnemonic.ts

index 46a36d143d280dcd28d2e73ff5fc483a2766e4db..785f743883b0c537563a2c8731b21384718d6fb8 100644 (file)
@@ -3,7 +3,7 @@
 \r
 import { Bip39Words } from './bip39-wordlist'\r
 import { BIP39_ITERATIONS } from './constants'\r
-import { bin, bytes, dec, utf8 } from './convert'\r
+import { bin, bytes, dec, hex, utf8 } from './convert'\r
 import { Entropy } from './entropy'\r
 import { Key } from '#types'\r
 \r
@@ -54,16 +54,26 @@ 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: string): Promise<Bip39Mnemonic> {\r
-               const e = await Entropy.import(entropy)\r
-               const checksum = await this.checksum(e)\r
-               let concatenation = `${e.bits}${checksum}`\r
+       static async fromEntropy (entropy: Key): 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
+\r
+               let e = 0n\r
+               for (let i = 0; i < entropy.byteLength; i++) {\r
+                       e = e << 8n | BigInt(entropy[i])\r
+               }\r
+\r
+               let concatenation = (e << BigInt(entropy.byteLength) / 4n) | checksum\r
                const words: string[] = []\r
-               while (concatenation.length > 0) {\r
-                       const wordBits = concatenation.substring(0, 11)\r
-                       const wordIndex = parseInt(wordBits, 2)\r
-                       words.push(Bip39Words[wordIndex])\r
-                       concatenation = concatenation.substring(11)\r
+               for (let i = 0; i < phraseLength; i++) {\r
+                       const wordBits = concatenation & 2047n\r
+                       const wordIndex = Number(wordBits)\r
+                       words.unshift(Bip39Words[wordIndex])\r
+                       concatenation >>= 11n\r
                }\r
                const sentence = words.join(' ')\r
                return this.fromPhrase(sentence)\r
@@ -76,12 +86,10 @@ export class Bip39Mnemonic {
         * @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: Entropy): Promise<string> {\r
-               const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', entropy.bytes)\r
-               const hashBytes = new Uint8Array(hashBuffer)\r
-               const hashBits = bytes.toBin(hashBytes)\r
-               const checksumLength = entropy.bits.length / 32\r
-               const checksum = hashBits.substring(0, checksumLength)\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
@@ -131,9 +139,9 @@ export class Bip39Mnemonic {
                }\r
 \r
                const entropy = await Entropy.import(bin.toBytes(entropyBits))\r
-               const expectedChecksum = await this.checksum(entropy)\r
+               const expectedChecksum = await this.checksum(entropy.bytes)\r
 \r
-               if (expectedChecksum !== checksumBits) {\r
+               if (Number(expectedChecksum) !== parseInt(checksumBits, 2)) {\r
                        return false\r
                }\r
 \r