From 3925e16e3283901efce44359f617cc26ab909c56 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Fri, 30 Jan 2026 23:28:34 -0300 Subject: [PATCH 01/15] chore: add mnemonic validation method --- packages/keyring-eth-hd/package.json | 5 +- .../keyring-eth-hd/src/hd-keyring.test.ts | 81 ++++++++++++++++++- packages/keyring-eth-hd/src/hd-keyring.ts | 34 ++++++++ yarn.lock | 67 ++++++++++++++- 4 files changed, 183 insertions(+), 4 deletions(-) diff --git a/packages/keyring-eth-hd/package.json b/packages/keyring-eth-hd/package.json index 0658f4ecb..1ed147474 100644 --- a/packages/keyring-eth-hd/package.json +++ b/packages/keyring-eth-hd/package.json @@ -45,6 +45,7 @@ "dependencies": { "@ethereumjs/tx": "^5.4.0", "@ethereumjs/util": "^9.1.0", + "@ethersproject/hdnode": "^5.8.0", "@metamask/eth-sig-util": "^8.2.0", "@metamask/key-tree": "^10.0.2", "@metamask/keyring-api": "workspace:^", @@ -77,7 +78,9 @@ "allowScripts": { "@lavamoat/preinstall-always-fail": false, "@metamask/eth-hd-keyring>ethereumjs-util>ethereum-cryptography>keccak": false, - "@metamask/eth-hd-keyring>ethereumjs-util>ethereum-cryptography>secp256k1": false + "@metamask/eth-hd-keyring>ethereumjs-util>ethereum-cryptography>secp256k1": false, + "old-hd-keyring>ethereumjs-util>ethereum-cryptography>keccak": false, + "old-hd-keyring>ethereumjs-util>ethereum-cryptography>secp256k1": false } } } diff --git a/packages/keyring-eth-hd/src/hd-keyring.test.ts b/packages/keyring-eth-hd/src/hd-keyring.test.ts index 6d410fa09..f65c188e0 100644 --- a/packages/keyring-eth-hd/src/hd-keyring.test.ts +++ b/packages/keyring-eth-hd/src/hd-keyring.test.ts @@ -250,7 +250,7 @@ describe('hd-keyring', () => { expect(cryptographicFunctions.pbkdf2Sha512).toHaveBeenCalledTimes(1); }); - it('throws on invalid mnemonic', async () => { + it('throws on invalid mnemonic with wrong number of words', async () => { const keyring = new HdKeyring(); await expect( @@ -259,10 +259,87 @@ describe('hd-keyring', () => { numberOfAccounts: 2, }), ).rejects.toThrow( - 'Invalid mnemonic phrase: The mnemonic phrase must consist of 12, 15, 18, 21, or 24 words.', + 'Eth-Hd-Keyring: Invalid secret recovery phrase provided', ); }); + it('throws on mnemonic with invalid words', async () => { + const keyring = new HdKeyring(); + + // 12 words but invalid (not in BIP39 wordlist) + await expect( + keyring.deserialize({ + mnemonic: + 'invalid words that are not in the bip39 wordlist at all here', + numberOfAccounts: 1, + }), + ).rejects.toThrow( + 'Eth-Hd-Keyring: Invalid secret recovery phrase provided', + ); + }); + + it('throws on mnemonic with invalid checksum', async () => { + const keyring = new HdKeyring(); + + // Valid BIP39 words but invalid checksum (last word changed from 'mango' to 'abandon') + await expect( + keyring.deserialize({ + mnemonic: + 'finish oppose decorate face calm tragic certain desk hour urge dinosaur abandon', + numberOfAccounts: 1, + }), + ).rejects.toThrow( + 'Eth-Hd-Keyring: Invalid secret recovery phrase provided', + ); + }); + + it('throws on invalid mnemonic passed as Buffer', async () => { + const keyring = new HdKeyring(); + + // Invalid mnemonic as raw Buffer + await expect( + keyring.deserialize({ + // @ts-expect-error testing Buffer mnemonic directly + mnemonic: Buffer.from('invalid mnemonic phrase here', 'utf8'), + numberOfAccounts: 1, + }), + ).rejects.toThrow( + 'Eth-Hd-Keyring: Invalid secret recovery phrase provided', + ); + }); + + it('deserializes valid mnemonic passed as Buffer', async () => { + const keyring = new HdKeyring(); + + await keyring.deserialize({ + // @ts-expect-error testing Buffer mnemonic directly + mnemonic: Buffer.from(sampleMnemonic, 'utf8'), + numberOfAccounts: 1, + }); + + const accounts = await keyring.getAccounts(); + expect(accounts[0]).toStrictEqual(firstAcct); + }); + + it('validates mnemonic passed as Uint8Array with valid mnemonic', async () => { + const keyring = new HdKeyring(); + + // Serialize a valid keyring to get the Uint8Array format + const tempKeyring = new HdKeyring(); + await tempKeyring.deserialize({ mnemonic: sampleMnemonic }); + const { mnemonic } = tempKeyring; + assert(mnemonic, 'Mnemonic should be defined'); + + await keyring.deserialize({ + // @ts-expect-error testing Uint8Array mnemonic directly + mnemonic, + numberOfAccounts: 1, + }); + + const accounts = await keyring.getAccounts(); + expect(accounts[0]).toStrictEqual(firstAcct); + }); + it('throws when numberOfAccounts is passed with no mnemonic', async () => { const keyring = new HdKeyring(); diff --git a/packages/keyring-eth-hd/src/hd-keyring.ts b/packages/keyring-eth-hd/src/hd-keyring.ts index a4b09b827..ce30195b6 100644 --- a/packages/keyring-eth-hd/src/hd-keyring.ts +++ b/packages/keyring-eth-hd/src/hd-keyring.ts @@ -1,5 +1,6 @@ import type { TypedTransaction } from '@ethereumjs/tx'; import { privateToPublic, publicToAddress, ecsign } from '@ethereumjs/util'; +import { isValidMnemonic } from '@ethersproject/hdnode'; import { concatSig, decrypt, @@ -610,6 +611,7 @@ export class HdKeyring implements Keyring { ); } + this.#validateMnemonic(mnemonic); this.mnemonic = this.#mnemonicToUint8Array(mnemonic); this.seed = await mnemonicToSeed( @@ -644,4 +646,36 @@ export class HdKeyring implements Keyring { assert(normalized, 'Expected address to be set'); return add0x(normalized); } + + /** + * Validate the mnemonic seed phrase. + * + * @param mnemonic - The mnemonic seed phrase to validate. + * @throws If the mnemonic is invalid. + */ + #validateMnemonic( + mnemonic: string | number[] | SerializedBuffer | Buffer | Uint8Array, + ): void { + let mnemonicString: string; + + if (typeof mnemonic === 'string') { + mnemonicString = mnemonic; + } else if (Array.isArray(mnemonic)) { + mnemonicString = Buffer.from(mnemonic).toString(); + } else if (isSerializedBuffer(mnemonic)) { + mnemonicString = Buffer.from(mnemonic.data).toString(); + } else if (Buffer.isBuffer(mnemonic)) { + mnemonicString = mnemonic.toString(); + } else if (mnemonic instanceof Uint8Array) { + mnemonicString = this.#uint8ArrayToString(mnemonic); + } else { + throw new Error('Eth-Hd-Keyring: Invalid mnemonic format'); + } + + if (!isValidMnemonic(mnemonicString)) { + throw new Error( + 'Eth-Hd-Keyring: Invalid secret recovery phrase provided', + ); + } + } } diff --git a/yarn.lock b/yarn.lock index 81a8e5f45..dddf87b3c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -673,6 +673,16 @@ __metadata: languageName: node linkType: hard +"@ethersproject/basex@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/basex@npm:5.8.0" + dependencies: + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/properties": "npm:^5.8.0" + checksum: 10/1a8d48a9397461ea42ec43b69a15a0d13ba0b9192695713750d9d391503c55b258cca435fa78a4014d23a813053f1a471593b89c7c0d89351639a78d50a12ef2 + languageName: node + linkType: hard + "@ethersproject/bignumber@npm:^5.7.0, @ethersproject/bignumber@npm:^5.8.0": version: 5.8.0 resolution: "@ethersproject/bignumber@npm:5.8.0" @@ -702,7 +712,7 @@ __metadata: languageName: node linkType: hard -"@ethersproject/hash@npm:^5.7.0": +"@ethersproject/hash@npm:^5.7.0, @ethersproject/hash@npm:^5.8.0": version: 5.8.0 resolution: "@ethersproject/hash@npm:5.8.0" dependencies: @@ -719,6 +729,26 @@ __metadata: languageName: node linkType: hard +"@ethersproject/hdnode@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/hdnode@npm:5.8.0" + dependencies: + "@ethersproject/abstract-signer": "npm:^5.8.0" + "@ethersproject/basex": "npm:^5.8.0" + "@ethersproject/bignumber": "npm:^5.8.0" + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/logger": "npm:^5.8.0" + "@ethersproject/pbkdf2": "npm:^5.8.0" + "@ethersproject/properties": "npm:^5.8.0" + "@ethersproject/sha2": "npm:^5.8.0" + "@ethersproject/signing-key": "npm:^5.8.0" + "@ethersproject/strings": "npm:^5.8.0" + "@ethersproject/transactions": "npm:^5.8.0" + "@ethersproject/wordlists": "npm:^5.8.0" + checksum: 10/55b35cf30f0dd40e2d5ecd4b2f005ebea82a85a440717a61d4a483074f652d2c7063e9c704272b894bfdd500f7883aa36692931c6808591f702c1da7107ebb61 + languageName: node + linkType: hard + "@ethersproject/keccak256@npm:^5.7.0, @ethersproject/keccak256@npm:^5.8.0": version: 5.8.0 resolution: "@ethersproject/keccak256@npm:5.8.0" @@ -745,6 +775,16 @@ __metadata: languageName: node linkType: hard +"@ethersproject/pbkdf2@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/pbkdf2@npm:5.8.0" + dependencies: + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/sha2": "npm:^5.8.0" + checksum: 10/203bb992eec3042256702f4c8259a37202af7b341cc6e370614cdc52541042fc3b795fb040592bd6be8b67376a798c45312ca1e6d5d179c3e8eb7431882f1fd1 + languageName: node + linkType: hard + "@ethersproject/properties@npm:^5.7.0, @ethersproject/properties@npm:^5.8.0": version: 5.8.0 resolution: "@ethersproject/properties@npm:5.8.0" @@ -764,6 +804,17 @@ __metadata: languageName: node linkType: hard +"@ethersproject/sha2@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/sha2@npm:5.8.0" + dependencies: + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/logger": "npm:^5.8.0" + hash.js: "npm:1.1.7" + checksum: 10/ef8916e3033502476fba9358ba1993722ac3bb99e756d5681e4effa3dfa0f0bf0c29d3fa338662830660b45dd359cccb06ba40bc7b62cfd44f4a177b25829404 + languageName: node + linkType: hard + "@ethersproject/signing-key@npm:^5.8.0": version: 5.8.0 resolution: "@ethersproject/signing-key@npm:5.8.0" @@ -819,6 +870,19 @@ __metadata: languageName: node linkType: hard +"@ethersproject/wordlists@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/wordlists@npm:5.8.0" + dependencies: + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/hash": "npm:^5.8.0" + "@ethersproject/logger": "npm:^5.8.0" + "@ethersproject/properties": "npm:^5.8.0" + "@ethersproject/strings": "npm:^5.8.0" + checksum: 10/b8e6aa7d2195bb568847f360f6525ddc3d145404fbd4553e2e05daf4a95f58167591feb69e16e3398a28114ea85e1895fc8f5bd1c0cbf8b578123d7c1d21c32d + languageName: node + linkType: hard + "@fivebinaries/coin-selection@npm:3.0.0": version: 3.0.0 resolution: "@fivebinaries/coin-selection@npm:3.0.0" @@ -1684,6 +1748,7 @@ __metadata: dependencies: "@ethereumjs/tx": "npm:^5.4.0" "@ethereumjs/util": "npm:^9.1.0" + "@ethersproject/hdnode": "npm:^5.8.0" "@lavamoat/allow-scripts": "npm:^3.2.1" "@lavamoat/preinstall-always-fail": "npm:^2.1.0" "@metamask/account-api": "workspace:^" From 3b0d05d528baaf4e8aea27d225e963ea286a6f4a Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Fri, 30 Jan 2026 23:34:05 -0300 Subject: [PATCH 02/15] chore: update CHANGELOG --- packages/keyring-eth-hd/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/keyring-eth-hd/CHANGELOG.md b/packages/keyring-eth-hd/CHANGELOG.md index 92f6ebf13..6aaa3ed0c 100644 --- a/packages/keyring-eth-hd/CHANGELOG.md +++ b/packages/keyring-eth-hd/CHANGELOG.md @@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `HdKeyringV2` class implementing `KeyringV2` interface ([#398](https://github.com/MetaMask/accounts/pull/398)), ([#404](https://github.com/MetaMask/accounts/pull/404)), ([#410](https://github.com/MetaMask/accounts/pull/410)), ([#413](https://github.com/MetaMask/accounts/pull/413)) - Wraps legacy `HdKeyring` to expose accounts via the unified `KeyringV2` API and the `KeyringAccount` type. - Extends `EthKeyringWrapper` for common Ethereum logic. +- Add mnemonic validation using `@ethersproject/hdnode` + - Validates mnemonics against BIP39 specification (word count, wordlist, checksum) before use. + - Throws for invalid mnemonics. ## [13.0.0] From 7adb29bbfef783b6899a7d90845db6eb4d04b2f2 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Sat, 31 Jan 2026 12:35:33 -0300 Subject: [PATCH 03/15] refactor: use validateMnemonic from @metamask/scure-bip39 --- packages/keyring-eth-hd/CHANGELOG.md | 4 ++-- packages/keyring-eth-hd/package.json | 1 - packages/keyring-eth-hd/src/hd-keyring.ts | 9 ++++----- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/keyring-eth-hd/CHANGELOG.md b/packages/keyring-eth-hd/CHANGELOG.md index 6aaa3ed0c..5abcab410 100644 --- a/packages/keyring-eth-hd/CHANGELOG.md +++ b/packages/keyring-eth-hd/CHANGELOG.md @@ -12,9 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `HdKeyringV2` class implementing `KeyringV2` interface ([#398](https://github.com/MetaMask/accounts/pull/398)), ([#404](https://github.com/MetaMask/accounts/pull/404)), ([#410](https://github.com/MetaMask/accounts/pull/410)), ([#413](https://github.com/MetaMask/accounts/pull/413)) - Wraps legacy `HdKeyring` to expose accounts via the unified `KeyringV2` API and the `KeyringAccount` type. - Extends `EthKeyringWrapper` for common Ethereum logic. -- Add mnemonic validation using `@ethersproject/hdnode` +- Add mnemonic validation using `validateMnemonic` from `@metamask/scure-bip39` - Validates mnemonics against BIP39 specification (word count, wordlist, checksum) before use. - - Throws for invalid mnemonics. + - Throws `'Eth-Hd-Keyring: Invalid secret recovery phrase provided'` for invalid mnemonics. ## [13.0.0] diff --git a/packages/keyring-eth-hd/package.json b/packages/keyring-eth-hd/package.json index 1ed147474..4c29dcb9a 100644 --- a/packages/keyring-eth-hd/package.json +++ b/packages/keyring-eth-hd/package.json @@ -45,7 +45,6 @@ "dependencies": { "@ethereumjs/tx": "^5.4.0", "@ethereumjs/util": "^9.1.0", - "@ethersproject/hdnode": "^5.8.0", "@metamask/eth-sig-util": "^8.2.0", "@metamask/key-tree": "^10.0.2", "@metamask/keyring-api": "workspace:^", diff --git a/packages/keyring-eth-hd/src/hd-keyring.ts b/packages/keyring-eth-hd/src/hd-keyring.ts index ce30195b6..b55f6577f 100644 --- a/packages/keyring-eth-hd/src/hd-keyring.ts +++ b/packages/keyring-eth-hd/src/hd-keyring.ts @@ -1,6 +1,5 @@ import type { TypedTransaction } from '@ethereumjs/tx'; import { privateToPublic, publicToAddress, ecsign } from '@ethereumjs/util'; -import { isValidMnemonic } from '@ethersproject/hdnode'; import { concatSig, decrypt, @@ -21,7 +20,7 @@ import { mnemonicToSeed, } from '@metamask/key-tree'; import type { Keyring } from '@metamask/keyring-utils'; -import { generateMnemonic } from '@metamask/scure-bip39'; +import { generateMnemonic, validateMnemonic } from '@metamask/scure-bip39'; import { wordlist } from '@metamask/scure-bip39/dist/wordlists/english'; import { add0x, @@ -611,7 +610,7 @@ export class HdKeyring implements Keyring { ); } - this.#validateMnemonic(mnemonic); + this.#isValidMnemonic(mnemonic); this.mnemonic = this.#mnemonicToUint8Array(mnemonic); this.seed = await mnemonicToSeed( @@ -653,7 +652,7 @@ export class HdKeyring implements Keyring { * @param mnemonic - The mnemonic seed phrase to validate. * @throws If the mnemonic is invalid. */ - #validateMnemonic( + #isValidMnemonic( mnemonic: string | number[] | SerializedBuffer | Buffer | Uint8Array, ): void { let mnemonicString: string; @@ -672,7 +671,7 @@ export class HdKeyring implements Keyring { throw new Error('Eth-Hd-Keyring: Invalid mnemonic format'); } - if (!isValidMnemonic(mnemonicString)) { + if (!validateMnemonic(mnemonicString, wordlist)) { throw new Error( 'Eth-Hd-Keyring: Invalid secret recovery phrase provided', ); From 1ce9033d23321746e68bb9edcc03f933f7451b09 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Sat, 31 Jan 2026 14:24:46 -0300 Subject: [PATCH 04/15] fix: handle mnemonic as object --- .../keyring-eth-hd/src/hd-keyring.test.ts | 23 +++++++ packages/keyring-eth-hd/src/hd-keyring.ts | 17 +++-- yarn.lock | 67 +------------------ 3 files changed, 35 insertions(+), 72 deletions(-) diff --git a/packages/keyring-eth-hd/src/hd-keyring.test.ts b/packages/keyring-eth-hd/src/hd-keyring.test.ts index f65c188e0..8edd327e2 100644 --- a/packages/keyring-eth-hd/src/hd-keyring.test.ts +++ b/packages/keyring-eth-hd/src/hd-keyring.test.ts @@ -340,6 +340,29 @@ describe('hd-keyring', () => { expect(accounts[0]).toStrictEqual(firstAcct); }); + it('validates mnemonic passed as plain object (simulating encryption/decryption cycle)', async () => { + const keyring = new HdKeyring(); + + const tempKeyring = new HdKeyring(); + await tempKeyring.deserialize({ mnemonic: sampleMnemonic }); + const { mnemonic } = tempKeyring; + assert(mnemonic, 'Mnemonic should be defined'); + + const mnemonicAsPlainObject: Record = {}; + mnemonic.forEach((value, index) => { + mnemonicAsPlainObject[index] = value; + }); + + await keyring.deserialize({ + // @ts-expect-error testing plain object mnemonic directly + mnemonic: mnemonicAsPlainObject, + numberOfAccounts: 1, + }); + + const accounts = await keyring.getAccounts(); + expect(accounts[0]).toStrictEqual(firstAcct); + }); + it('throws when numberOfAccounts is passed with no mnemonic', async () => { const keyring = new HdKeyring(); diff --git a/packages/keyring-eth-hd/src/hd-keyring.ts b/packages/keyring-eth-hd/src/hd-keyring.ts index b55f6577f..1daec7bf8 100644 --- a/packages/keyring-eth-hd/src/hd-keyring.ts +++ b/packages/keyring-eth-hd/src/hd-keyring.ts @@ -38,6 +38,8 @@ import { keccak256 } from 'ethereum-cryptography/keccak'; const hdPathString = `m/44'/60'/0'/0`; const type = 'HD Key Tree'; +type Mnemonic = string | number[] | SerializedBuffer | Buffer | Uint8Array; + export type HDKeyringOptions = { cryptographicFunctions?: CryptographicFunctions; }; @@ -601,9 +603,7 @@ export class HdKeyring implements Keyring { * as a string, an array of UTF-8 bytes, or a Buffer. Mnemonic input * passed as type buffer or array of UTF-8 bytes must be NFKD normalized. */ - async #initFromMnemonic( - mnemonic: string | number[] | SerializedBuffer | Buffer | Uint8Array, - ): Promise { + async #initFromMnemonic(mnemonic: Mnemonic): Promise { if (this.root) { throw new Error( 'Eth-Hd-Keyring: Secret recovery phrase already provided', @@ -652,9 +652,7 @@ export class HdKeyring implements Keyring { * @param mnemonic - The mnemonic seed phrase to validate. * @throws If the mnemonic is invalid. */ - #isValidMnemonic( - mnemonic: string | number[] | SerializedBuffer | Buffer | Uint8Array, - ): void { + #isValidMnemonic(mnemonic: Mnemonic): void { let mnemonicString: string; if (typeof mnemonic === 'string') { @@ -667,6 +665,13 @@ export class HdKeyring implements Keyring { mnemonicString = mnemonic.toString(); } else if (mnemonic instanceof Uint8Array) { mnemonicString = this.#uint8ArrayToString(mnemonic); + } else if (typeof mnemonic === 'object') { + // When encrypted/decrypted, Uint8Arrays can become plain objects like {0: ..., 1: ..., ...} + // Convert back to Uint8Array first, then to mnemonic string + const mnemonicAsUint8Array = Uint8Array.from( + Object.values(mnemonic as Record), + ); + mnemonicString = this.#uint8ArrayToString(mnemonicAsUint8Array); } else { throw new Error('Eth-Hd-Keyring: Invalid mnemonic format'); } diff --git a/yarn.lock b/yarn.lock index dddf87b3c..81a8e5f45 100644 --- a/yarn.lock +++ b/yarn.lock @@ -673,16 +673,6 @@ __metadata: languageName: node linkType: hard -"@ethersproject/basex@npm:^5.8.0": - version: 5.8.0 - resolution: "@ethersproject/basex@npm:5.8.0" - dependencies: - "@ethersproject/bytes": "npm:^5.8.0" - "@ethersproject/properties": "npm:^5.8.0" - checksum: 10/1a8d48a9397461ea42ec43b69a15a0d13ba0b9192695713750d9d391503c55b258cca435fa78a4014d23a813053f1a471593b89c7c0d89351639a78d50a12ef2 - languageName: node - linkType: hard - "@ethersproject/bignumber@npm:^5.7.0, @ethersproject/bignumber@npm:^5.8.0": version: 5.8.0 resolution: "@ethersproject/bignumber@npm:5.8.0" @@ -712,7 +702,7 @@ __metadata: languageName: node linkType: hard -"@ethersproject/hash@npm:^5.7.0, @ethersproject/hash@npm:^5.8.0": +"@ethersproject/hash@npm:^5.7.0": version: 5.8.0 resolution: "@ethersproject/hash@npm:5.8.0" dependencies: @@ -729,26 +719,6 @@ __metadata: languageName: node linkType: hard -"@ethersproject/hdnode@npm:^5.8.0": - version: 5.8.0 - resolution: "@ethersproject/hdnode@npm:5.8.0" - dependencies: - "@ethersproject/abstract-signer": "npm:^5.8.0" - "@ethersproject/basex": "npm:^5.8.0" - "@ethersproject/bignumber": "npm:^5.8.0" - "@ethersproject/bytes": "npm:^5.8.0" - "@ethersproject/logger": "npm:^5.8.0" - "@ethersproject/pbkdf2": "npm:^5.8.0" - "@ethersproject/properties": "npm:^5.8.0" - "@ethersproject/sha2": "npm:^5.8.0" - "@ethersproject/signing-key": "npm:^5.8.0" - "@ethersproject/strings": "npm:^5.8.0" - "@ethersproject/transactions": "npm:^5.8.0" - "@ethersproject/wordlists": "npm:^5.8.0" - checksum: 10/55b35cf30f0dd40e2d5ecd4b2f005ebea82a85a440717a61d4a483074f652d2c7063e9c704272b894bfdd500f7883aa36692931c6808591f702c1da7107ebb61 - languageName: node - linkType: hard - "@ethersproject/keccak256@npm:^5.7.0, @ethersproject/keccak256@npm:^5.8.0": version: 5.8.0 resolution: "@ethersproject/keccak256@npm:5.8.0" @@ -775,16 +745,6 @@ __metadata: languageName: node linkType: hard -"@ethersproject/pbkdf2@npm:^5.8.0": - version: 5.8.0 - resolution: "@ethersproject/pbkdf2@npm:5.8.0" - dependencies: - "@ethersproject/bytes": "npm:^5.8.0" - "@ethersproject/sha2": "npm:^5.8.0" - checksum: 10/203bb992eec3042256702f4c8259a37202af7b341cc6e370614cdc52541042fc3b795fb040592bd6be8b67376a798c45312ca1e6d5d179c3e8eb7431882f1fd1 - languageName: node - linkType: hard - "@ethersproject/properties@npm:^5.7.0, @ethersproject/properties@npm:^5.8.0": version: 5.8.0 resolution: "@ethersproject/properties@npm:5.8.0" @@ -804,17 +764,6 @@ __metadata: languageName: node linkType: hard -"@ethersproject/sha2@npm:^5.8.0": - version: 5.8.0 - resolution: "@ethersproject/sha2@npm:5.8.0" - dependencies: - "@ethersproject/bytes": "npm:^5.8.0" - "@ethersproject/logger": "npm:^5.8.0" - hash.js: "npm:1.1.7" - checksum: 10/ef8916e3033502476fba9358ba1993722ac3bb99e756d5681e4effa3dfa0f0bf0c29d3fa338662830660b45dd359cccb06ba40bc7b62cfd44f4a177b25829404 - languageName: node - linkType: hard - "@ethersproject/signing-key@npm:^5.8.0": version: 5.8.0 resolution: "@ethersproject/signing-key@npm:5.8.0" @@ -870,19 +819,6 @@ __metadata: languageName: node linkType: hard -"@ethersproject/wordlists@npm:^5.8.0": - version: 5.8.0 - resolution: "@ethersproject/wordlists@npm:5.8.0" - dependencies: - "@ethersproject/bytes": "npm:^5.8.0" - "@ethersproject/hash": "npm:^5.8.0" - "@ethersproject/logger": "npm:^5.8.0" - "@ethersproject/properties": "npm:^5.8.0" - "@ethersproject/strings": "npm:^5.8.0" - checksum: 10/b8e6aa7d2195bb568847f360f6525ddc3d145404fbd4553e2e05daf4a95f58167591feb69e16e3398a28114ea85e1895fc8f5bd1c0cbf8b578123d7c1d21c32d - languageName: node - linkType: hard - "@fivebinaries/coin-selection@npm:3.0.0": version: 3.0.0 resolution: "@fivebinaries/coin-selection@npm:3.0.0" @@ -1748,7 +1684,6 @@ __metadata: dependencies: "@ethereumjs/tx": "npm:^5.4.0" "@ethereumjs/util": "npm:^9.1.0" - "@ethersproject/hdnode": "npm:^5.8.0" "@lavamoat/allow-scripts": "npm:^3.2.1" "@lavamoat/preinstall-always-fail": "npm:^2.1.0" "@metamask/account-api": "workspace:^" From a130994d10c5f19503a4ca08cec0ada0b5b72ca2 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Sat, 31 Jan 2026 14:26:45 -0300 Subject: [PATCH 05/15] chore: package.json --- packages/keyring-eth-hd/package.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/keyring-eth-hd/package.json b/packages/keyring-eth-hd/package.json index 4c29dcb9a..4558909e5 100644 --- a/packages/keyring-eth-hd/package.json +++ b/packages/keyring-eth-hd/package.json @@ -76,10 +76,7 @@ "lavamoat": { "allowScripts": { "@lavamoat/preinstall-always-fail": false, - "@metamask/eth-hd-keyring>ethereumjs-util>ethereum-cryptography>keccak": false, - "@metamask/eth-hd-keyring>ethereumjs-util>ethereum-cryptography>secp256k1": false, - "old-hd-keyring>ethereumjs-util>ethereum-cryptography>keccak": false, - "old-hd-keyring>ethereumjs-util>ethereum-cryptography>secp256k1": false + "@metamask/eth-hd-keyring>ethereumjs-util>ethereum-cryptography>keccak": false } } } From 81affefb63390a4922f96535b17aa20b05de8e5b Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Sat, 31 Jan 2026 16:07:43 -0300 Subject: [PATCH 06/15] chore: package.json --- packages/keyring-eth-hd/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/keyring-eth-hd/package.json b/packages/keyring-eth-hd/package.json index 4558909e5..50de58142 100644 --- a/packages/keyring-eth-hd/package.json +++ b/packages/keyring-eth-hd/package.json @@ -76,7 +76,7 @@ "lavamoat": { "allowScripts": { "@lavamoat/preinstall-always-fail": false, - "@metamask/eth-hd-keyring>ethereumjs-util>ethereum-cryptography>keccak": false - } + "@metamask/eth-hd-keyring>ethereumjs-util>ethereum-cryptography>keccak": false, + "@metamask/eth-hd-keyring>ethereumjs-util>ethereum-cryptography>secp256k1": false } } } From b32937bced4ee6d3e6c74b72ba048547a1b6b04c Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Sat, 31 Jan 2026 16:08:22 -0300 Subject: [PATCH 07/15] chore: package.json --- packages/keyring-eth-hd/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/keyring-eth-hd/package.json b/packages/keyring-eth-hd/package.json index 50de58142..0658f4ecb 100644 --- a/packages/keyring-eth-hd/package.json +++ b/packages/keyring-eth-hd/package.json @@ -77,6 +77,7 @@ "allowScripts": { "@lavamoat/preinstall-always-fail": false, "@metamask/eth-hd-keyring>ethereumjs-util>ethereum-cryptography>keccak": false, - "@metamask/eth-hd-keyring>ethereumjs-util>ethereum-cryptography>secp256k1": false } + "@metamask/eth-hd-keyring>ethereumjs-util>ethereum-cryptography>secp256k1": false + } } } From 5abe574b11f9f576850c1770cdf1d42539e987ca Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Sat, 31 Jan 2026 16:13:01 -0300 Subject: [PATCH 08/15] refactor: isValidMnemonic --- packages/keyring-eth-hd/src/hd-keyring.ts | 27 ++++------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/packages/keyring-eth-hd/src/hd-keyring.ts b/packages/keyring-eth-hd/src/hd-keyring.ts index 1daec7bf8..4b7228057 100644 --- a/packages/keyring-eth-hd/src/hd-keyring.ts +++ b/packages/keyring-eth-hd/src/hd-keyring.ts @@ -484,7 +484,7 @@ export class HdKeyring implements Keyring { * @returns The Uint8Array mnemonic. */ #mnemonicToUint8Array( - mnemonic: Buffer | SerializedBuffer | string | Uint8Array | number[], + mnemonic: Mnemonic, ): Uint8Array { let mnemonicData: unknown = mnemonic; // When using `Buffer.toJSON()`, the Buffer is serialized into an object @@ -653,28 +653,9 @@ export class HdKeyring implements Keyring { * @throws If the mnemonic is invalid. */ #isValidMnemonic(mnemonic: Mnemonic): void { - let mnemonicString: string; - - if (typeof mnemonic === 'string') { - mnemonicString = mnemonic; - } else if (Array.isArray(mnemonic)) { - mnemonicString = Buffer.from(mnemonic).toString(); - } else if (isSerializedBuffer(mnemonic)) { - mnemonicString = Buffer.from(mnemonic.data).toString(); - } else if (Buffer.isBuffer(mnemonic)) { - mnemonicString = mnemonic.toString(); - } else if (mnemonic instanceof Uint8Array) { - mnemonicString = this.#uint8ArrayToString(mnemonic); - } else if (typeof mnemonic === 'object') { - // When encrypted/decrypted, Uint8Arrays can become plain objects like {0: ..., 1: ..., ...} - // Convert back to Uint8Array first, then to mnemonic string - const mnemonicAsUint8Array = Uint8Array.from( - Object.values(mnemonic as Record), - ); - mnemonicString = this.#uint8ArrayToString(mnemonicAsUint8Array); - } else { - throw new Error('Eth-Hd-Keyring: Invalid mnemonic format'); - } + // Reuse #mnemonicToUint8Array for format conversion to avoid duplication + const mnemonicAsUint8Array = this.#mnemonicToUint8Array(mnemonic); + const mnemonicString = this.#uint8ArrayToString(mnemonicAsUint8Array); if (!validateMnemonic(mnemonicString, wordlist)) { throw new Error( From 7e31ce24e793309bc47c4d56285427a07b1f5cef Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Sat, 31 Jan 2026 16:20:36 -0300 Subject: [PATCH 09/15] refactor: isValidMnemonic --- packages/keyring-eth-hd/src/hd-keyring.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/keyring-eth-hd/src/hd-keyring.ts b/packages/keyring-eth-hd/src/hd-keyring.ts index 4b7228057..f1fba8cea 100644 --- a/packages/keyring-eth-hd/src/hd-keyring.ts +++ b/packages/keyring-eth-hd/src/hd-keyring.ts @@ -483,9 +483,7 @@ export class HdKeyring implements Keyring { * @param mnemonic - The mnemonic seed phrase. * @returns The Uint8Array mnemonic. */ - #mnemonicToUint8Array( - mnemonic: Mnemonic, - ): Uint8Array { + #mnemonicToUint8Array(mnemonic: Mnemonic): Uint8Array { let mnemonicData: unknown = mnemonic; // When using `Buffer.toJSON()`, the Buffer is serialized into an object // with the structure `{ type: 'Buffer', data: [...] }` @@ -610,8 +608,8 @@ export class HdKeyring implements Keyring { ); } - this.#isValidMnemonic(mnemonic); this.mnemonic = this.#mnemonicToUint8Array(mnemonic); + this.#isValidMnemonic(this.mnemonic); this.seed = await mnemonicToSeed( this.mnemonic, @@ -652,11 +650,8 @@ export class HdKeyring implements Keyring { * @param mnemonic - The mnemonic seed phrase to validate. * @throws If the mnemonic is invalid. */ - #isValidMnemonic(mnemonic: Mnemonic): void { - // Reuse #mnemonicToUint8Array for format conversion to avoid duplication - const mnemonicAsUint8Array = this.#mnemonicToUint8Array(mnemonic); - const mnemonicString = this.#uint8ArrayToString(mnemonicAsUint8Array); - + #isValidMnemonic(mnemonic: Uint8Array): void { + const mnemonicString = this.#uint8ArrayToString(mnemonic); if (!validateMnemonic(mnemonicString, wordlist)) { throw new Error( 'Eth-Hd-Keyring: Invalid secret recovery phrase provided', From 9b685f0b87537214897112928363707e3c492b56 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Sat, 31 Jan 2026 16:26:18 -0300 Subject: [PATCH 10/15] chore: update CHANGELOG --- packages/keyring-eth-hd/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/keyring-eth-hd/CHANGELOG.md b/packages/keyring-eth-hd/CHANGELOG.md index 5abcab410..91e7183d6 100644 --- a/packages/keyring-eth-hd/CHANGELOG.md +++ b/packages/keyring-eth-hd/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Extends `EthKeyringWrapper` for common Ethereum logic. - Add mnemonic validation using `validateMnemonic` from `@metamask/scure-bip39` - Validates mnemonics against BIP39 specification (word count, wordlist, checksum) before use. - - Throws `'Eth-Hd-Keyring: Invalid secret recovery phrase provided'` for invalid mnemonics. + - Throws for invalid mnemonics. ## [13.0.0] From 49a7c6cf3887088f3e57e26f0a868bcd7b467305 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Sun, 1 Feb 2026 21:15:36 -0300 Subject: [PATCH 11/15] fix: state error --- packages/keyring-eth-hd/src/hd-keyring.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/keyring-eth-hd/src/hd-keyring.ts b/packages/keyring-eth-hd/src/hd-keyring.ts index f1fba8cea..df8167296 100644 --- a/packages/keyring-eth-hd/src/hd-keyring.ts +++ b/packages/keyring-eth-hd/src/hd-keyring.ts @@ -608,8 +608,11 @@ export class HdKeyring implements Keyring { ); } - this.mnemonic = this.#mnemonicToUint8Array(mnemonic); - this.#isValidMnemonic(this.mnemonic); + // Convert and validate before assigning to instance property + // to avoid inconsistent state if validation fails + const mnemonicAsUint8Array = this.#mnemonicToUint8Array(mnemonic); + this.#isValidMnemonic(mnemonicAsUint8Array); + this.mnemonic = mnemonicAsUint8Array; this.seed = await mnemonicToSeed( this.mnemonic, From 409afcb4fdf105eb58bc2f373b6c3384349f97e1 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Sun, 1 Feb 2026 22:03:53 -0300 Subject: [PATCH 12/15] chore: change method name --- packages/keyring-eth-hd/src/hd-keyring.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/keyring-eth-hd/src/hd-keyring.ts b/packages/keyring-eth-hd/src/hd-keyring.ts index df8167296..3a0e8f985 100644 --- a/packages/keyring-eth-hd/src/hd-keyring.ts +++ b/packages/keyring-eth-hd/src/hd-keyring.ts @@ -611,7 +611,7 @@ export class HdKeyring implements Keyring { // Convert and validate before assigning to instance property // to avoid inconsistent state if validation fails const mnemonicAsUint8Array = this.#mnemonicToUint8Array(mnemonic); - this.#isValidMnemonic(mnemonicAsUint8Array); + this.#assertValidMnemonic(mnemonicAsUint8Array); this.mnemonic = mnemonicAsUint8Array; this.seed = await mnemonicToSeed( @@ -648,12 +648,13 @@ export class HdKeyring implements Keyring { } /** - * Validate the mnemonic seed phrase. + * Assert that the mnemonic seed phrase is valid. + * Throws an error if the mnemonic is not a valid BIP39 phrase. * - * @param mnemonic - The mnemonic seed phrase to validate. - * @throws If the mnemonic is invalid. + * @param mnemonic - The mnemonic seed phrase to validate (as Uint8Array). + * @throws If the mnemonic is not a valid BIP39 secret recovery phrase. */ - #isValidMnemonic(mnemonic: Uint8Array): void { + #assertValidMnemonic(mnemonic: Uint8Array): void { const mnemonicString = this.#uint8ArrayToString(mnemonic); if (!validateMnemonic(mnemonicString, wordlist)) { throw new Error( From fa5cf4e097713eb4e12b26d83602f91dc0772314 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Mon, 2 Feb 2026 07:44:17 -0300 Subject: [PATCH 13/15] chore: update CHANGELOG --- packages/keyring-eth-hd/CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/keyring-eth-hd/CHANGELOG.md b/packages/keyring-eth-hd/CHANGELOG.md index 91e7183d6..cc0af24ba 100644 --- a/packages/keyring-eth-hd/CHANGELOG.md +++ b/packages/keyring-eth-hd/CHANGELOG.md @@ -12,7 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `HdKeyringV2` class implementing `KeyringV2` interface ([#398](https://github.com/MetaMask/accounts/pull/398)), ([#404](https://github.com/MetaMask/accounts/pull/404)), ([#410](https://github.com/MetaMask/accounts/pull/410)), ([#413](https://github.com/MetaMask/accounts/pull/413)) - Wraps legacy `HdKeyring` to expose accounts via the unified `KeyringV2` API and the `KeyringAccount` type. - Extends `EthKeyringWrapper` for common Ethereum logic. -- Add mnemonic validation using `validateMnemonic` from `@metamask/scure-bip39` + +### Fixed + +- Enforce mnemonics validation ([#450](https://github.com/MetaMask/accounts/pull/450)) - Validates mnemonics against BIP39 specification (word count, wordlist, checksum) before use. - Throws for invalid mnemonics. From 178169c10757537a3d84559a8f16784cfb036503 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Mon, 2 Feb 2026 07:45:52 -0300 Subject: [PATCH 14/15] chore: update DeserializableHDKeyringState type --- packages/keyring-eth-hd/src/hd-keyring.test.ts | 3 --- packages/keyring-eth-hd/src/hd-keyring.ts | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/keyring-eth-hd/src/hd-keyring.test.ts b/packages/keyring-eth-hd/src/hd-keyring.test.ts index 8edd327e2..b6f6915a7 100644 --- a/packages/keyring-eth-hd/src/hd-keyring.test.ts +++ b/packages/keyring-eth-hd/src/hd-keyring.test.ts @@ -299,7 +299,6 @@ describe('hd-keyring', () => { // Invalid mnemonic as raw Buffer await expect( keyring.deserialize({ - // @ts-expect-error testing Buffer mnemonic directly mnemonic: Buffer.from('invalid mnemonic phrase here', 'utf8'), numberOfAccounts: 1, }), @@ -312,7 +311,6 @@ describe('hd-keyring', () => { const keyring = new HdKeyring(); await keyring.deserialize({ - // @ts-expect-error testing Buffer mnemonic directly mnemonic: Buffer.from(sampleMnemonic, 'utf8'), numberOfAccounts: 1, }); @@ -331,7 +329,6 @@ describe('hd-keyring', () => { assert(mnemonic, 'Mnemonic should be defined'); await keyring.deserialize({ - // @ts-expect-error testing Uint8Array mnemonic directly mnemonic, numberOfAccounts: 1, }); diff --git a/packages/keyring-eth-hd/src/hd-keyring.ts b/packages/keyring-eth-hd/src/hd-keyring.ts index 3a0e8f985..06a5848a7 100644 --- a/packages/keyring-eth-hd/src/hd-keyring.ts +++ b/packages/keyring-eth-hd/src/hd-keyring.ts @@ -69,7 +69,7 @@ export type DeserializableHDKeyringState = Omit< SerializedHDKeyringState, 'mnemonic' > & { - mnemonic: number[] | SerializedBuffer | string; + mnemonic: Mnemonic; }; /** From 5075bae74bd5f8c10b2301b6f0622c48c05f04a4 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Mon, 2 Feb 2026 08:43:15 -0300 Subject: [PATCH 15/15] Revert "chore: update DeserializableHDKeyringState type" This reverts commit 178169c10757537a3d84559a8f16784cfb036503. --- packages/keyring-eth-hd/src/hd-keyring.test.ts | 3 +++ packages/keyring-eth-hd/src/hd-keyring.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/keyring-eth-hd/src/hd-keyring.test.ts b/packages/keyring-eth-hd/src/hd-keyring.test.ts index b6f6915a7..8edd327e2 100644 --- a/packages/keyring-eth-hd/src/hd-keyring.test.ts +++ b/packages/keyring-eth-hd/src/hd-keyring.test.ts @@ -299,6 +299,7 @@ describe('hd-keyring', () => { // Invalid mnemonic as raw Buffer await expect( keyring.deserialize({ + // @ts-expect-error testing Buffer mnemonic directly mnemonic: Buffer.from('invalid mnemonic phrase here', 'utf8'), numberOfAccounts: 1, }), @@ -311,6 +312,7 @@ describe('hd-keyring', () => { const keyring = new HdKeyring(); await keyring.deserialize({ + // @ts-expect-error testing Buffer mnemonic directly mnemonic: Buffer.from(sampleMnemonic, 'utf8'), numberOfAccounts: 1, }); @@ -329,6 +331,7 @@ describe('hd-keyring', () => { assert(mnemonic, 'Mnemonic should be defined'); await keyring.deserialize({ + // @ts-expect-error testing Uint8Array mnemonic directly mnemonic, numberOfAccounts: 1, }); diff --git a/packages/keyring-eth-hd/src/hd-keyring.ts b/packages/keyring-eth-hd/src/hd-keyring.ts index 06a5848a7..3a0e8f985 100644 --- a/packages/keyring-eth-hd/src/hd-keyring.ts +++ b/packages/keyring-eth-hd/src/hd-keyring.ts @@ -69,7 +69,7 @@ export type DeserializableHDKeyringState = Omit< SerializedHDKeyringState, 'mnemonic' > & { - mnemonic: Mnemonic; + mnemonic: number[] | SerializedBuffer | string; }; /**