]> git.codecow.com Git - libnemo.git/commitdiff
Store mnemonic phrase as word array and convert to full string on-demand. Fix private...
authorChris Duncan <chris@zoso.dev>
Wed, 30 Jul 2025 13:56:16 +0000 (06:56 -0700)
committerChris Duncan <chris@zoso.dev>
Wed, 30 Jul 2025 13:56:16 +0000 (06:56 -0700)
src/lib/bip39-mnemonic.ts

index 785f743883b0c537563a2c8731b21384718d6fb8..608499b1c8c88acfafc9cb7bffbc121b79babb08 100644 (file)
@@ -12,10 +12,10 @@ import { Key } from '#types'
 */\r
 export class Bip39Mnemonic {\r
        static #isInternal: boolean = false\r
-       #bip44Seed?: Uint8Array<ArrayBuffer>\r
+       #bip39Seed?: Uint8Array<ArrayBuffer>\r
        #blake2bSeed?: Uint8Array<ArrayBuffer>\r
-       #phrase: string = ''\r
-       get phrase (): string { return this.#phrase.normalize('NFKD') }\r
+       #phrase?: string[]\r
+       get phrase (): string | undefined { return this.#phrase?.join(' ').normalize('NFKD') }\r
 \r
        private constructor () {\r
                if (!Bip39Mnemonic.#isInternal) {\r
@@ -38,7 +38,7 @@ export class Bip39Mnemonic {
                const self = new this()\r
                const isValid = await this.validate(phrase)\r
                if (isValid) {\r
-                       self.#phrase = phrase.normalize('NFKD')\r
+                       self.#phrase = phrase.normalize('NFKD').split(' ')\r
                        return self\r
                } else {\r
                        throw new Error('Invalid mnemonic phrase.')\r
@@ -98,11 +98,11 @@ export class Bip39Mnemonic {
        * collection.\r
        */\r
        destroy () {\r
-               bytes.erase(this.#bip44Seed)\r
+               bytes.erase(this.#bip39Seed)\r
                bytes.erase(this.#blake2bSeed)\r
-               this.#bip44Seed = undefined\r
+               this.#bip39Seed = undefined\r
                this.#blake2bSeed = undefined\r
-               this.#phrase = ''\r
+               this.#phrase = undefined\r
        }\r
 \r
        /**\r
@@ -160,7 +160,10 @@ export class Bip39Mnemonic {
        */\r
        async toBip39Seed (passphrase: string, format: 'hex'): Promise<string>\r
        async toBip39Seed (passphrase: string, format?: 'hex'): Promise<string | Uint8Array> {\r
-               if (this.#bip44Seed == null) {\r
+               if (this.phrase == null) {\r
+                       throw new Error('BIP-39 mnemonic phrase not found')\r
+               }\r
+               if (this.#bip39Seed == null) {\r
                        if (passphrase == null || typeof passphrase !== 'string') {\r
                                passphrase = ''\r
                        }\r
@@ -181,38 +184,45 @@ export class Bip39Mnemonic {
                        }\r
                        const seedKey = await globalThis.crypto.subtle.deriveKey(algorithm, phraseKey, derivedKeyType, true, ['sign'])\r
                        const seedBuffer = await globalThis.crypto.subtle.exportKey('raw', seedKey)\r
-                       this.#bip44Seed = new Uint8Array(seedBuffer)\r
+                       this.#bip39Seed = new Uint8Array(seedBuffer)\r
                }\r
                return format === 'hex'\r
-                       ? bytes.toHex(this.#bip44Seed)\r
-                       : this.#bip44Seed\r
+                       ? bytes.toHex(this.#bip39Seed)\r
+                       : this.#bip39Seed\r
        }\r
 \r
+       /**\r
+       * Converts the mnemonic phrase to a BLAKE2b seed.\r
+       *\r
+       * @returns {Promise<Uint8Array<ArrayBuffer>>} Promise for seed as bytes\r
+       */\r
        async toBlake2bSeed (): Promise<Uint8Array<ArrayBuffer>>\r
        /**\r
        * Converts the mnemonic phrase to a BLAKE2b seed.\r
        *\r
-       * @returns {string} Hexadecimal seed\r
+       * @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
+               if (this.#phrase?.length !== 24) {\r
+                       throw new Error('BIP-39 mnemonic phrase must be 24 words to convert to BLAKE2b seed')\r
+               }\r
                if (this.#blake2bSeed == null) {\r
-                       const wordArray = this.phrase.split(' ')\r
-                       const bits = wordArray.map((w: string) => {\r
-                               const wordIndex = Bip39Words.indexOf(w)\r
+                       let bits = 0n\r
+                       const words = this.#phrase\r
+                       for (const word of this.#phrase) {\r
+                               const wordIndex = Bip39Words.indexOf(word)\r
                                if (wordIndex === -1) {\r
-                                       return false\r
+                                       throw new RangeError('Word not found in BIP-39 list')\r
                                }\r
-                               return dec.toBin(wordIndex, 11)\r
-                       }).join('')\r
-\r
-                       const dividerIndex = Math.floor(bits.length / 33) * 32\r
-                       const entropyBits = bits.slice(0, dividerIndex)\r
-                       const entropyBytes = entropyBits.match(/(.{1,8})/g)?.map((bin: string) => parseInt(bin, 2))\r
-                       if (entropyBytes == null) {\r
-                               throw new Error('Invalid mnemonic phrase')\r
+                               bits = (bits << 11n) | BigInt(Bip39Words.indexOf(word))\r
+                       }\r
+                       bits >>= 8n\r
+                       this.#blake2bSeed = new Uint8Array(32)\r
+                       for (let i = 31; i >= 0; i--) {\r
+                               this.#blake2bSeed[i] = Number(bits & 255n)\r
+                               bits >> 8n\r
                        }\r
-                       this.#blake2bSeed = new Uint8Array(entropyBytes)\r
                }\r
                return format === 'hex'\r
                        ? bytes.toHex(this.#blake2bSeed)\r