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 1abd9cf97e..cbd56d701d 100644 --- a/packages/keyring-controller/src/KeyringController-method-action-types.ts +++ b/packages/keyring-controller/src/KeyringController-method-action-types.ts @@ -82,7 +82,12 @@ export type KeyringControllerIsUnlockedAction = { /** * 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. */ 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..96283faa94 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,29 @@ 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 if (hasProperty(credentials, 'password')) { + await this.verifyPassword(credentials.password as string); + } else { + await this.verifyEncryptionKey(credentials.encryptionKey); + } + const selectedKeyring = this.#getKeyringByIdOrDefault(keyringId); if (!selectedKeyring) { throw new KeyringControllerError('Keyring not found');