From f887dfe5227408812da271632a4093ed1ec896c4 Mon Sep 17 00:00:00 2001 From: Tai Nguyen TT Date: Thu, 4 Jun 2026 13:17:26 +0700 Subject: [PATCH 1/9] feat(keyring): allow exportSeedPhrase with encryption key credentials --- .../KeyringController-method-action-types.ts | 3 +- .../src/KeyringController.test.ts | 81 +++++++++++++++++++ .../src/KeyringController.ts | 41 +++++++++- 3 files changed, 121 insertions(+), 4 deletions(-) diff --git a/packages/keyring-controller/src/KeyringController-method-action-types.ts b/packages/keyring-controller/src/KeyringController-method-action-types.ts index 1abd9cf97e..a7af3cf47b 100644 --- a/packages/keyring-controller/src/KeyringController-method-action-types.ts +++ b/packages/keyring-controller/src/KeyringController-method-action-types.ts @@ -82,7 +82,8 @@ export type KeyringControllerIsUnlockedAction = { /** * Gets the seed phrase of the HD keyring. * - * @param password - Password of the keyring. + * @param credentials - The wallet password, or an object holding either the + * `password` or the vault `encryptionKey`. * @param keyringId - The id of the keyring. * @returns Promise resolving to the seed phrase. */ diff --git a/packages/keyring-controller/src/KeyringController.test.ts b/packages/keyring-controller/src/KeyringController.test.ts index 0924cb581d..e66da72c69 100644 --- a/packages/keyring-controller/src/KeyringController.test.ts +++ b/packages/keyring-controller/src/KeyringController.test.ts @@ -891,6 +891,13 @@ describe('KeyringController', () => { }); }); + it('should export seed phrase with a password credential object', async () => { + await withController(async ({ controller }) => { + const seed = await controller.exportSeedPhrase({ password }); + expect(seed).not.toBe(''); + }); + }); + it('should throw error if keyringId is invalid', async () => { await withController(async ({ controller }) => { await expect( @@ -926,6 +933,42 @@ describe('KeyringController', () => { ); }); }); + + describe('when correct encryption key is provided', () => { + it('should export seed phrase with an encryption key credential', async () => { + await withController(async ({ controller }) => { + const encryptionKey = await controller.exportEncryptionKey(); + const seed = await controller.exportSeedPhrase({ encryptionKey }); + expect(seed).not.toBe(''); + }); + }); + + it('should export seed phrase with an encryption key and a valid keyringId', async () => { + await withController(async ({ controller, initialState }) => { + const keyringId = initialState.keyrings[0].metadata.id; + const encryptionKey = await controller.exportEncryptionKey(); + const seed = await controller.exportSeedPhrase( + { encryptionKey }, + keyringId, + ); + expect(seed).not.toBe(''); + }); + }); + }); + + describe('when wrong encryption key is provided', () => { + it('should throw the decryption error', async () => { + await withController(async ({ controller, encryptor }) => { + const encryptionKey = await controller.exportEncryptionKey(); + jest + .spyOn(encryptor, 'decryptWithKey') + .mockRejectedValueOnce(new Error('Invalid key')); + await expect( + controller.exportSeedPhrase({ encryptionKey }), + ).rejects.toThrow('Invalid key'); + }); + }); + }); }); it('should throw error when the controller is locked', async () => { @@ -3616,6 +3659,44 @@ describe('KeyringController', () => { }); }); + describe('verifyEncryptionKey', () => { + describe('when correct encryption key is provided', () => { + it('should not throw any error', async () => { + await withController(async ({ controller }) => { + const encryptionKey = await controller.exportEncryptionKey(); + expect( + await controller.verifyEncryptionKey(encryptionKey), + ).toBeUndefined(); + }); + }); + + it('should throw error if vault is missing', async () => { + await withController( + { skipVaultCreation: true }, + async ({ controller }) => { + await expect( + controller.verifyEncryptionKey('encryption-key'), + ).rejects.toThrow(KeyringControllerErrorMessage.VaultError); + }, + ); + }); + }); + + describe('when wrong encryption key is provided', () => { + it('should throw the decryption error', async () => { + await withController(async ({ controller, encryptor }) => { + const encryptionKey = await controller.exportEncryptionKey(); + jest + .spyOn(encryptor, 'decryptWithKey') + .mockRejectedValueOnce(new Error('Decryption failed')); + await expect( + controller.verifyEncryptionKey(encryptionKey), + ).rejects.toThrow('Decryption failed'); + }); + }); + }); + }); + describe('withKeyring', () => { it('should rollback if an error is thrown', async () => { await withController(async ({ controller, initialState }) => { diff --git a/packages/keyring-controller/src/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts index 35603b8395..05305458fd 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -1045,6 +1045,23 @@ export class KeyringController< await this.#encryptor.decrypt(password, this.state.vault); } + /** + * Method to verify a given encryption key validity. Throws an error if the + * encryption key is invalid, i.e. it cannot decrypt the vault. + * + * @param encryptionKey - Serialized vault encryption key. + */ + async verifyEncryptionKey(encryptionKey: string): Promise { + if (!this.state.vault) { + throw new KeyringControllerError( + KeyringControllerErrorMessage.VaultError, + ); + } + + const key = await this.#encryptor.importKey(encryptionKey); + await this.#encryptor.decryptWithKey(key, JSON.parse(this.state.vault)); + } + /** * Returns the status of the vault. * @@ -1057,16 +1074,34 @@ export class KeyringController< /** * Gets the seed phrase of the HD keyring. * - * @param password - Password of the keyring. + * The keyring can be re-authenticated with the wallet password (passed either + * as a bare string or as `{ password }`) or with the vault `{ encryptionKey }`. + * The bare-string form is kept for backwards compatibility. + * + * @param credentials - The wallet password, or an object holding either the + * `password` or the vault `encryptionKey`. * @param keyringId - The id of the keyring. * @returns Promise resolving to the seed phrase. */ async exportSeedPhrase( - password: string, + credentials: string | { password: string } | { encryptionKey: string }, keyringId?: string, ): Promise { this.#assertIsUnlocked(); - await this.verifyPassword(password); + + if (typeof credentials === 'string') { + await this.verifyPassword(credentials); + } else { + const { encryptionKey } = credentials as { encryptionKey?: string }; + if (typeof encryptionKey === 'string') { + await this.verifyEncryptionKey(encryptionKey); + } else { + await this.verifyPassword( + (credentials as { password: string }).password, + ); + } + } + const selectedKeyring = this.#getKeyringByIdOrDefault(keyringId); if (!selectedKeyring) { throw new KeyringControllerError('Keyring not found'); From 799d8a52eb4d3f7a281fba79c0509492309845d8 Mon Sep 17 00:00:00 2001 From: Tai Nguyen TT Date: Thu, 4 Jun 2026 15:25:11 +0700 Subject: [PATCH 2/9] feat(keyring): fix CI --- packages/keyring-controller/CHANGELOG.md | 4 ++++ .../src/KeyringController-method-action-types.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/packages/keyring-controller/CHANGELOG.md b/packages/keyring-controller/CHANGELOG.md index 9bb57e1f03..2ede2fe3b0 100644 --- a/packages/keyring-controller/CHANGELOG.md +++ b/packages/keyring-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Allow `exportSeedPhrase` to accept `{ encryptionKey }` credentials ([#8996](https://github.com/MetaMask/core/pull/8996)) + ### Fixed - Automatically remove and destroy non-primary keyrings whose last account is removed during a `withKeyring` or `withKeyringV2` callback ([#8951](https://github.com/MetaMask/core/pull/8951)) diff --git a/packages/keyring-controller/src/KeyringController-method-action-types.ts b/packages/keyring-controller/src/KeyringController-method-action-types.ts index a7af3cf47b..cbd56d701d 100644 --- a/packages/keyring-controller/src/KeyringController-method-action-types.ts +++ b/packages/keyring-controller/src/KeyringController-method-action-types.ts @@ -82,6 +82,10 @@ export type KeyringControllerIsUnlockedAction = { /** * Gets the seed phrase of the HD keyring. * + * The keyring can be re-authenticated with the wallet password (passed either + * as a bare string or as `{ password }`) or with the vault `{ encryptionKey }`. + * The bare-string form is kept for backwards compatibility. + * * @param credentials - The wallet password, or an object holding either the * `password` or the vault `encryptionKey`. * @param keyringId - The id of the keyring. From 986e59e9e5a4bec76e103512b8ded57201e0a62c Mon Sep 17 00:00:00 2001 From: Tai Nguyen TT Date: Thu, 4 Jun 2026 17:41:58 +0700 Subject: [PATCH 3/9] chore(keyring): refactor code --- packages/keyring-controller/src/KeyringController.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/keyring-controller/src/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts index 05305458fd..96283faa94 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -1091,15 +1091,10 @@ export class KeyringController< if (typeof credentials === 'string') { await this.verifyPassword(credentials); + } else if (hasProperty(credentials, 'password')) { + await this.verifyPassword(credentials.password as string); } else { - const { encryptionKey } = credentials as { encryptionKey?: string }; - if (typeof encryptionKey === 'string') { - await this.verifyEncryptionKey(encryptionKey); - } else { - await this.verifyPassword( - (credentials as { password: string }).password, - ); - } + await this.verifyEncryptionKey(credentials.encryptionKey); } const selectedKeyring = this.#getKeyringByIdOrDefault(keyringId); From 73b0252bde683a2bcdf76132e4cc124aadc67613 Mon Sep 17 00:00:00 2001 From: Tai Nguyen TT Date: Fri, 5 Jun 2026 15:45:05 +0700 Subject: [PATCH 4/9] feat(keyring): allow verification with encryption key when exporting account and seed phrase --- packages/keyring-controller/CHANGELOG.md | 4 +- .../KeyringController-method-action-types.ts | 12 +-- .../src/KeyringController.test.ts | 78 ++++++++++++++----- .../src/KeyringController.ts | 50 ++++++++---- packages/keyring-controller/src/types.ts | 8 ++ 5 files changed, 110 insertions(+), 42 deletions(-) diff --git a/packages/keyring-controller/CHANGELOG.md b/packages/keyring-controller/CHANGELOG.md index 2ede2fe3b0..f6d46c3dd6 100644 --- a/packages/keyring-controller/CHANGELOG.md +++ b/packages/keyring-controller/CHANGELOG.md @@ -7,9 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Added +### Changed -- Allow `exportSeedPhrase` to accept `{ encryptionKey }` credentials ([#8996](https://github.com/MetaMask/core/pull/8996)) +- **BREAKING:** `exportSeedPhrase` and `exportAccount` now take `VerificationCredentials` (`{ password }` | `{ encryptionKey }`) instead of a bare password string ([#8996](https://github.com/MetaMask/core/pull/8996)) ### Fixed diff --git a/packages/keyring-controller/src/KeyringController-method-action-types.ts b/packages/keyring-controller/src/KeyringController-method-action-types.ts index cbd56d701d..a996f7be8c 100644 --- a/packages/keyring-controller/src/KeyringController-method-action-types.ts +++ b/packages/keyring-controller/src/KeyringController-method-action-types.ts @@ -82,12 +82,11 @@ export type KeyringControllerIsUnlockedAction = { /** * Gets the seed phrase of the HD keyring. * - * The keyring can be re-authenticated with the wallet password (passed either - * as a bare string or as `{ password }`) or with the vault `{ encryptionKey }`. - * The bare-string form is kept for backwards compatibility. + * The keyring can be re-authenticated with the wallet `{ password }` or with + * the vault `{ encryptionKey }`. * - * @param credentials - The wallet password, or an object holding either the - * `password` or the vault `encryptionKey`. + * @param credentials - Object holding either the `password` or the vault + * `encryptionKey`. * @param keyringId - The id of the keyring. * @returns Promise resolving to the seed phrase. */ @@ -99,7 +98,8 @@ export type KeyringControllerExportSeedPhraseAction = { /** * Gets the private key from the keyring controlling an address. * - * @param password - Password of the keyring. + * @param credentials - Object holding either the `password` or the vault + * `encryptionKey`. * @param address - Address to export. * @returns Promise resolving to the private key for an address. */ diff --git a/packages/keyring-controller/src/KeyringController.test.ts b/packages/keyring-controller/src/KeyringController.test.ts index e66da72c69..4a3a25888b 100644 --- a/packages/keyring-controller/src/KeyringController.test.ts +++ b/packages/keyring-controller/src/KeyringController.test.ts @@ -605,7 +605,7 @@ describe('KeyringController', () => { { type: 'HD Key Tree' }, async ({ keyring }) => keyring.serialize(), ); - const currentSeedWord = await controller.exportSeedPhrase(password); + const currentSeedWord = await controller.exportSeedPhrase({ password }); await controller.createNewVaultAndRestore(password, currentSeedWord); @@ -680,7 +680,7 @@ describe('KeyringController', () => { await controller.createNewVaultAndKeychain(password); const currentSeedPhrase = - await controller.exportSeedPhrase(password); + await controller.exportSeedPhrase({ password }); expect(currentSeedPhrase.length).toBeGreaterThan(0); expect( @@ -794,13 +794,13 @@ describe('KeyringController', () => { describe('when there is an existing vault', () => { it('should not create a new vault or keychain', async () => { await withController(async ({ controller, initialState }) => { - const initialSeedWord = await controller.exportSeedPhrase(password); + const initialSeedWord = await controller.exportSeedPhrase({ password }); expect(initialSeedWord).toBeDefined(); const initialVault = controller.state.vault; await controller.createNewVaultAndKeychain(password); - const currentSeedWord = await controller.exportSeedPhrase(password); + const currentSeedWord = await controller.exportSeedPhrase({ password }); expect(initialState).toStrictEqual(controller.state); expect(initialSeedWord).toBe(currentSeedWord); expect(initialVault).toStrictEqual(controller.state.vault); @@ -867,7 +867,7 @@ describe('KeyringController', () => { primaryKeyring.mnemonic = ''; - await expect(controller.exportSeedPhrase(password)).rejects.toThrow( + await expect(controller.exportSeedPhrase({ password })).rejects.toThrow( "Can't get mnemonic bytes from keyring", ); }); @@ -878,7 +878,7 @@ describe('KeyringController', () => { describe('when correct password is provided', () => { it('should export seed phrase without keyringId', async () => { await withController(async ({ controller }) => { - const seed = await controller.exportSeedPhrase(password); + const seed = await controller.exportSeedPhrase({ password }); expect(seed).not.toBe(''); }); }); @@ -886,7 +886,7 @@ describe('KeyringController', () => { it('should export seed phrase with valid keyringId', async () => { await withController(async ({ controller, initialState }) => { const keyringId = initialState.keyrings[0].metadata.id; - const seed = await controller.exportSeedPhrase(password, keyringId); + const seed = await controller.exportSeedPhrase({ password }, keyringId); expect(seed).not.toBe(''); }); }); @@ -901,7 +901,7 @@ describe('KeyringController', () => { it('should throw error if keyringId is invalid', async () => { await withController(async ({ controller }) => { await expect( - controller.exportSeedPhrase(password, 'invalid-id'), + controller.exportSeedPhrase({ password }, 'invalid-id'), ).rejects.toThrow('Keyring not found'); }); }); @@ -913,7 +913,7 @@ describe('KeyringController', () => { jest .spyOn(encryptor, 'decrypt') .mockRejectedValueOnce(new Error('Invalid password')); - await expect(controller.exportSeedPhrase('')).rejects.toThrow( + await expect(controller.exportSeedPhrase({ password: '' })).rejects.toThrow( 'Invalid password', ); }); @@ -927,7 +927,7 @@ describe('KeyringController', () => { .spyOn(encryptor, 'decrypt') .mockRejectedValueOnce(new Error('Invalid password')); await expect( - controller.exportSeedPhrase('', keyringId), + controller.exportSeedPhrase({ password: '' }, keyringId), ).rejects.toThrow('Invalid password'); }, ); @@ -975,7 +975,7 @@ describe('KeyringController', () => { await withController(async ({ controller }) => { await controller.setLocked(); - await expect(controller.exportSeedPhrase(password)).rejects.toThrow( + await expect(controller.exportSeedPhrase({ password })).rejects.toThrow( KeyringControllerErrorMessage.ControllerLocked, ); }); @@ -990,7 +990,7 @@ describe('KeyringController', () => { await withController(async ({ controller, initialState }) => { const account = initialState.keyrings[0].accounts[0]; const newPrivateKey = await controller.exportAccount( - password, + { password }, account, ); expect(newPrivateKey).not.toBe(''); @@ -1002,7 +1002,7 @@ describe('KeyringController', () => { it('should throw error', async () => { await withController(async ({ controller }) => { await expect( - controller.exportAccount(password, ''), + controller.exportAccount({ password }, ''), ).rejects.toThrow(KeyringControllerErrorMessage.KeyringNotFound); }); }); @@ -1011,17 +1011,59 @@ describe('KeyringController', () => { describe('when wrong password is provided', () => { it('should throw error', async () => { - await withController(async ({ controller, encryptor }) => { + await withController(async ({ controller, initialState, encryptor }) => { + const account = initialState.keyrings[0].accounts[0]; jest .spyOn(encryptor, 'decrypt') .mockRejectedValueOnce(new Error('Invalid password')); - await expect(controller.exportSeedPhrase('')).rejects.toThrow( - 'Invalid password', + await expect( + controller.exportAccount({ password: '' }, account), + ).rejects.toThrow('Invalid password'); + }); + }); + }); + + describe('when correct encryption key is provided', () => { + it('should export account with an encryption key credential', async () => { + await withController(async ({ controller, initialState }) => { + const account = initialState.keyrings[0].accounts[0]; + const encryptionKey = await controller.exportEncryptionKey(); + const newPrivateKey = await controller.exportAccount( + { encryptionKey }, + account, ); + expect(newPrivateKey).not.toBe(''); + }); + }); + }); + + describe('when wrong encryption key is provided', () => { + it('should throw the decryption error', async () => { + await withController(async ({ controller, initialState, encryptor }) => { + const account = initialState.keyrings[0].accounts[0]; + const encryptionKey = await controller.exportEncryptionKey(); + jest + .spyOn(encryptor, 'decryptWithKey') + .mockRejectedValueOnce(new Error('Invalid key')); + await expect( + controller.exportAccount({ encryptionKey }, account), + ).rejects.toThrow('Invalid key'); }); }); }); }); + + it('should throw error when the controller is locked', async () => { + await withController(async ({ controller, initialState }) => { + const account = initialState.keyrings[0].accounts[0]; + await controller.setLocked(); + + await expect( + controller.exportAccount({ password }, account), + ).rejects.toThrow(KeyringControllerErrorMessage.ControllerLocked); + }); + }); + describe('when the keyring for the given address does not support exportAccount', () => { it('should throw error', async () => { const address = '0x5AC6D462f054690a373FABF8CC28e161003aEB19'; @@ -1032,7 +1074,7 @@ describe('KeyringController', () => { await controller.addNewKeyring(MockKeyring.type); await expect( - controller.exportAccount(password, address), + controller.exportAccount({ password }, address), ).rejects.toThrow( KeyringControllerErrorMessage.UnsupportedExportAccount, ); @@ -5767,7 +5809,7 @@ describe('KeyringController', () => { expect(controller.state).toStrictEqual(initialState); await expect( - controller.exportAccount(password, mockAddress), + controller.exportAccount({ password }, mockAddress), ).rejects.toThrow(KeyringControllerErrorMessage.KeyringNotFound); }, ); diff --git a/packages/keyring-controller/src/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts index 96283faa94..692e0219ea 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -48,6 +48,7 @@ import { KeyringControllerError } from './errors'; import type { KeyringControllerMethodActions } from './KeyringController-method-action-types'; import type { Eip7702AuthorizationParams, + VerificationCredentials, PersonalMessageParams, TypedMessageParams, } from './types'; @@ -1062,6 +1063,21 @@ export class KeyringController< await this.#encryptor.decryptWithKey(key, JSON.parse(this.state.vault)); } + /** + * Verifies export credentials by checking either the wallet password or the + * vault encryption key. + * + * @param credentials - Object holding either the `password` or the vault + * `encryptionKey`. + */ + async #verifyCredentials(credentials: VerificationCredentials): Promise { + if ('password' in credentials) { + await this.verifyPassword(credentials.password); + } else { + await this.verifyEncryptionKey(credentials.encryptionKey); + } + } + /** * Returns the status of the vault. * @@ -1074,28 +1090,21 @@ export class KeyringController< /** * Gets the seed phrase of the HD keyring. * - * The keyring can be re-authenticated with the wallet password (passed either - * as a bare string or as `{ password }`) or with the vault `{ encryptionKey }`. - * The bare-string form is kept for backwards compatibility. + * The keyring can be re-authenticated with the wallet `{ password }` or with + * the vault `{ encryptionKey }`. * - * @param credentials - The wallet password, or an object holding either the - * `password` or the vault `encryptionKey`. + * @param credentials - Object holding either the `password` or the vault + * `encryptionKey`. * @param keyringId - The id of the keyring. * @returns Promise resolving to the seed phrase. */ async exportSeedPhrase( - credentials: string | { password: string } | { encryptionKey: string }, + credentials: VerificationCredentials, keyringId?: string, ): Promise { this.#assertIsUnlocked(); - if (typeof credentials === 'string') { - await this.verifyPassword(credentials); - } else if (hasProperty(credentials, 'password')) { - await this.verifyPassword(credentials.password as string); - } else { - await this.verifyEncryptionKey(credentials.encryptionKey); - } + await this.#verifyCredentials(credentials); const selectedKeyring = this.#getKeyringByIdOrDefault(keyringId); if (!selectedKeyring) { @@ -1109,12 +1118,21 @@ export class KeyringController< /** * Gets the private key from the keyring controlling an address. * - * @param password - Password of the keyring. + * The keyring can be re-authenticated with the wallet `{ password }` or with + * the vault `{ encryptionKey }`. + * + * @param credentials - Object holding either the `password` or the vault + * `encryptionKey`. * @param address - Address to export. * @returns Promise resolving to the private key for an address. */ - async exportAccount(password: string, address: string): Promise { - await this.verifyPassword(password); + async exportAccount( + credentials: VerificationCredentials, + address: string, + ): Promise { + this.#assertIsUnlocked(); + + await this.#verifyCredentials(credentials); const keyring = (await this.getKeyringForAccount(address)) as EthKeyring; if (!keyring.exportAccount) { diff --git a/packages/keyring-controller/src/types.ts b/packages/keyring-controller/src/types.ts index f4595db90f..a4cf2b4bca 100644 --- a/packages/keyring-controller/src/types.ts +++ b/packages/keyring-controller/src/types.ts @@ -72,3 +72,11 @@ export type SignTypedDataMessageV3V4 = { export type TypedMessageParams = { data: Record[] | string | SignTypedDataMessageV3V4; } & AbstractMessageParams; + +/** + * Credentials for re-authenticating the keyring during sensitive operations + * such as `exportSeedPhrase` and `exportAccount`. + */ +export type VerificationCredentials = + | { password: string } + | { encryptionKey: string }; From 2de030d7383aa96efe8090d4f5fd9171523190f7 Mon Sep 17 00:00:00 2001 From: Tai Nguyen TT Date: Fri, 5 Jun 2026 15:57:39 +0700 Subject: [PATCH 5/9] chore(keyring): refactor code --- .../src/KeyringController-method-action-types.ts | 3 --- packages/keyring-controller/src/KeyringController.test.ts | 7 ------- packages/keyring-controller/src/KeyringController.ts | 6 ------ 3 files changed, 16 deletions(-) diff --git a/packages/keyring-controller/src/KeyringController-method-action-types.ts b/packages/keyring-controller/src/KeyringController-method-action-types.ts index a996f7be8c..b469bc643d 100644 --- a/packages/keyring-controller/src/KeyringController-method-action-types.ts +++ b/packages/keyring-controller/src/KeyringController-method-action-types.ts @@ -82,9 +82,6 @@ export type KeyringControllerIsUnlockedAction = { /** * Gets the seed phrase of the HD keyring. * - * The keyring can be re-authenticated with the wallet `{ password }` or with - * the vault `{ encryptionKey }`. - * * @param credentials - Object holding either the `password` or the vault * `encryptionKey`. * @param keyringId - The id of the keyring. diff --git a/packages/keyring-controller/src/KeyringController.test.ts b/packages/keyring-controller/src/KeyringController.test.ts index 4a3a25888b..cdd6f61d2c 100644 --- a/packages/keyring-controller/src/KeyringController.test.ts +++ b/packages/keyring-controller/src/KeyringController.test.ts @@ -891,13 +891,6 @@ describe('KeyringController', () => { }); }); - it('should export seed phrase with a password credential object', async () => { - await withController(async ({ controller }) => { - const seed = await controller.exportSeedPhrase({ password }); - expect(seed).not.toBe(''); - }); - }); - it('should throw error if keyringId is invalid', async () => { await withController(async ({ controller }) => { await expect( diff --git a/packages/keyring-controller/src/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts index 692e0219ea..1211d60ee5 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -1090,9 +1090,6 @@ export class KeyringController< /** * Gets the seed phrase of the HD keyring. * - * The keyring can be re-authenticated with the wallet `{ password }` or with - * the vault `{ encryptionKey }`. - * * @param credentials - Object holding either the `password` or the vault * `encryptionKey`. * @param keyringId - The id of the keyring. @@ -1118,9 +1115,6 @@ export class KeyringController< /** * Gets the private key from the keyring controlling an address. * - * The keyring can be re-authenticated with the wallet `{ password }` or with - * the vault `{ encryptionKey }`. - * * @param credentials - Object holding either the `password` or the vault * `encryptionKey`. * @param address - Address to export. From 034613f490d17041470cae75b79eda3b0f20f297 Mon Sep 17 00:00:00 2001 From: Tai Nguyen TT Date: Fri, 5 Jun 2026 16:04:30 +0700 Subject: [PATCH 6/9] fix(keyring): lint issue --- .../src/KeyringController.test.ts | 72 +++++++++++-------- .../src/KeyringController.ts | 4 +- 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/packages/keyring-controller/src/KeyringController.test.ts b/packages/keyring-controller/src/KeyringController.test.ts index cdd6f61d2c..c6ac13e6bb 100644 --- a/packages/keyring-controller/src/KeyringController.test.ts +++ b/packages/keyring-controller/src/KeyringController.test.ts @@ -679,8 +679,9 @@ describe('KeyringController', () => { async ({ controller }) => { await controller.createNewVaultAndKeychain(password); - const currentSeedPhrase = - await controller.exportSeedPhrase({ password }); + const currentSeedPhrase = await controller.exportSeedPhrase({ + password, + }); expect(currentSeedPhrase.length).toBeGreaterThan(0); expect( @@ -794,13 +795,17 @@ describe('KeyringController', () => { describe('when there is an existing vault', () => { it('should not create a new vault or keychain', async () => { await withController(async ({ controller, initialState }) => { - const initialSeedWord = await controller.exportSeedPhrase({ password }); + const initialSeedWord = await controller.exportSeedPhrase({ + password, + }); expect(initialSeedWord).toBeDefined(); const initialVault = controller.state.vault; await controller.createNewVaultAndKeychain(password); - const currentSeedWord = await controller.exportSeedPhrase({ password }); + const currentSeedWord = await controller.exportSeedPhrase({ + password, + }); expect(initialState).toStrictEqual(controller.state); expect(initialSeedWord).toBe(currentSeedWord); expect(initialVault).toStrictEqual(controller.state.vault); @@ -867,9 +872,9 @@ describe('KeyringController', () => { primaryKeyring.mnemonic = ''; - await expect(controller.exportSeedPhrase({ password })).rejects.toThrow( - "Can't get mnemonic bytes from keyring", - ); + await expect( + controller.exportSeedPhrase({ password }), + ).rejects.toThrow("Can't get mnemonic bytes from keyring"); }); }); }); @@ -886,7 +891,10 @@ describe('KeyringController', () => { it('should export seed phrase with valid keyringId', async () => { await withController(async ({ controller, initialState }) => { const keyringId = initialState.keyrings[0].metadata.id; - const seed = await controller.exportSeedPhrase({ password }, keyringId); + const seed = await controller.exportSeedPhrase( + { password }, + keyringId, + ); expect(seed).not.toBe(''); }); }); @@ -906,9 +914,9 @@ describe('KeyringController', () => { jest .spyOn(encryptor, 'decrypt') .mockRejectedValueOnce(new Error('Invalid password')); - await expect(controller.exportSeedPhrase({ password: '' })).rejects.toThrow( - 'Invalid password', - ); + await expect( + controller.exportSeedPhrase({ password: '' }), + ).rejects.toThrow('Invalid password'); }); }); @@ -1004,15 +1012,17 @@ describe('KeyringController', () => { describe('when wrong password is provided', () => { it('should throw error', async () => { - await withController(async ({ controller, initialState, encryptor }) => { - const account = initialState.keyrings[0].accounts[0]; - jest - .spyOn(encryptor, 'decrypt') - .mockRejectedValueOnce(new Error('Invalid password')); - await expect( - controller.exportAccount({ password: '' }, account), - ).rejects.toThrow('Invalid password'); - }); + await withController( + async ({ controller, initialState, encryptor }) => { + const account = initialState.keyrings[0].accounts[0]; + jest + .spyOn(encryptor, 'decrypt') + .mockRejectedValueOnce(new Error('Invalid password')); + await expect( + controller.exportAccount({ password: '' }, account), + ).rejects.toThrow('Invalid password'); + }, + ); }); }); @@ -1032,16 +1042,18 @@ describe('KeyringController', () => { describe('when wrong encryption key is provided', () => { it('should throw the decryption error', async () => { - await withController(async ({ controller, initialState, encryptor }) => { - const account = initialState.keyrings[0].accounts[0]; - const encryptionKey = await controller.exportEncryptionKey(); - jest - .spyOn(encryptor, 'decryptWithKey') - .mockRejectedValueOnce(new Error('Invalid key')); - await expect( - controller.exportAccount({ encryptionKey }, account), - ).rejects.toThrow('Invalid key'); - }); + await withController( + async ({ controller, initialState, encryptor }) => { + const account = initialState.keyrings[0].accounts[0]; + const encryptionKey = await controller.exportEncryptionKey(); + jest + .spyOn(encryptor, 'decryptWithKey') + .mockRejectedValueOnce(new Error('Invalid key')); + await expect( + controller.exportAccount({ encryptionKey }, account), + ).rejects.toThrow('Invalid key'); + }, + ); }); }); }); diff --git a/packages/keyring-controller/src/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts index 1211d60ee5..1474f6c06b 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -1070,7 +1070,9 @@ export class KeyringController< * @param credentials - Object holding either the `password` or the vault * `encryptionKey`. */ - async #verifyCredentials(credentials: VerificationCredentials): Promise { + async #verifyCredentials( + credentials: VerificationCredentials, + ): Promise { if ('password' in credentials) { await this.verifyPassword(credentials.password); } else { From 25c9fe1870f8822b1e36d652672a0f6bbb8f474e Mon Sep 17 00:00:00 2001 From: Tai Nguyen TT Date: Fri, 5 Jun 2026 16:16:54 +0700 Subject: [PATCH 7/9] fix(keyring): disable lint issue --- packages/keyring-controller/src/KeyringController.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/keyring-controller/src/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts index 1474f6c06b..8314e0337f 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -1073,6 +1073,7 @@ export class KeyringController< async #verifyCredentials( credentials: VerificationCredentials, ): Promise { + // eslint-disable-next-line no-restricted-syntax if ('password' in credentials) { await this.verifyPassword(credentials.password); } else { From 3ec1aa950a812f5c3deaa5ec437c3d2ccc865193 Mon Sep 17 00:00:00 2001 From: Tai Nguyen TT Date: Fri, 5 Jun 2026 19:31:04 +0700 Subject: [PATCH 8/9] refactor(keyring): make verifyEncryptionKey private and rename type --- .../src/KeyringController.test.ts | 38 ------------------- .../src/KeyringController.ts | 14 +++---- packages/keyring-controller/src/types.ts | 4 +- 3 files changed, 7 insertions(+), 49 deletions(-) diff --git a/packages/keyring-controller/src/KeyringController.test.ts b/packages/keyring-controller/src/KeyringController.test.ts index c6ac13e6bb..3bf16ef7e2 100644 --- a/packages/keyring-controller/src/KeyringController.test.ts +++ b/packages/keyring-controller/src/KeyringController.test.ts @@ -3706,44 +3706,6 @@ describe('KeyringController', () => { }); }); - describe('verifyEncryptionKey', () => { - describe('when correct encryption key is provided', () => { - it('should not throw any error', async () => { - await withController(async ({ controller }) => { - const encryptionKey = await controller.exportEncryptionKey(); - expect( - await controller.verifyEncryptionKey(encryptionKey), - ).toBeUndefined(); - }); - }); - - it('should throw error if vault is missing', async () => { - await withController( - { skipVaultCreation: true }, - async ({ controller }) => { - await expect( - controller.verifyEncryptionKey('encryption-key'), - ).rejects.toThrow(KeyringControllerErrorMessage.VaultError); - }, - ); - }); - }); - - describe('when wrong encryption key is provided', () => { - it('should throw the decryption error', async () => { - await withController(async ({ controller, encryptor }) => { - const encryptionKey = await controller.exportEncryptionKey(); - jest - .spyOn(encryptor, 'decryptWithKey') - .mockRejectedValueOnce(new Error('Decryption failed')); - await expect( - controller.verifyEncryptionKey(encryptionKey), - ).rejects.toThrow('Decryption failed'); - }); - }); - }); - }); - describe('withKeyring', () => { it('should rollback if an error is thrown', async () => { await withController(async ({ controller, initialState }) => { diff --git a/packages/keyring-controller/src/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts index 8314e0337f..c6f5528deb 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -48,7 +48,7 @@ import { KeyringControllerError } from './errors'; import type { KeyringControllerMethodActions } from './KeyringController-method-action-types'; import type { Eip7702AuthorizationParams, - VerificationCredentials, + Credentials, PersonalMessageParams, TypedMessageParams, } from './types'; @@ -1052,7 +1052,7 @@ export class KeyringController< * * @param encryptionKey - Serialized vault encryption key. */ - async verifyEncryptionKey(encryptionKey: string): Promise { + async #verifyEncryptionKey(encryptionKey: string): Promise { if (!this.state.vault) { throw new KeyringControllerError( KeyringControllerErrorMessage.VaultError, @@ -1070,14 +1070,12 @@ export class KeyringController< * @param credentials - Object holding either the `password` or the vault * `encryptionKey`. */ - async #verifyCredentials( - credentials: VerificationCredentials, - ): Promise { + async #verifyCredentials(credentials: Credentials): Promise { // eslint-disable-next-line no-restricted-syntax if ('password' in credentials) { await this.verifyPassword(credentials.password); } else { - await this.verifyEncryptionKey(credentials.encryptionKey); + await this.#verifyEncryptionKey(credentials.encryptionKey); } } @@ -1099,7 +1097,7 @@ export class KeyringController< * @returns Promise resolving to the seed phrase. */ async exportSeedPhrase( - credentials: VerificationCredentials, + credentials: Credentials, keyringId?: string, ): Promise { this.#assertIsUnlocked(); @@ -1124,7 +1122,7 @@ export class KeyringController< * @returns Promise resolving to the private key for an address. */ async exportAccount( - credentials: VerificationCredentials, + credentials: Credentials, address: string, ): Promise { this.#assertIsUnlocked(); diff --git a/packages/keyring-controller/src/types.ts b/packages/keyring-controller/src/types.ts index a4cf2b4bca..11487b8621 100644 --- a/packages/keyring-controller/src/types.ts +++ b/packages/keyring-controller/src/types.ts @@ -77,6 +77,4 @@ export type TypedMessageParams = { * Credentials for re-authenticating the keyring during sensitive operations * such as `exportSeedPhrase` and `exportAccount`. */ -export type VerificationCredentials = - | { password: string } - | { encryptionKey: string }; +export type Credentials = { password: string } | { encryptionKey: string }; From 834f9ac8f61ea4b929ecf6fd67f6645cb6580998 Mon Sep 17 00:00:00 2001 From: Tai Nguyen TT Date: Fri, 5 Jun 2026 19:47:35 +0700 Subject: [PATCH 9/9] test(keyring): add tests when vault is missing --- .../src/KeyringController.test.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/keyring-controller/src/KeyringController.test.ts b/packages/keyring-controller/src/KeyringController.test.ts index 3bf16ef7e2..4fad5575b9 100644 --- a/packages/keyring-controller/src/KeyringController.test.ts +++ b/packages/keyring-controller/src/KeyringController.test.ts @@ -970,6 +970,26 @@ describe('KeyringController', () => { }); }); }); + + describe('when vault is missing', () => { + it('should throw error', async () => { + await withController( + { + skipVaultCreation: true, + state: { + isUnlocked: true, + } as KeyringControllerState, + }, + async ({ controller }) => { + await expect( + controller.exportSeedPhrase({ + encryptionKey: 'encryption-key', + }), + ).rejects.toThrow(KeyringControllerErrorMessage.VaultError); + }, + ); + }); + }); }); it('should throw error when the controller is locked', async () => {