Skip to content
4 changes: 4 additions & 0 deletions packages/keyring-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- **BREAKING:** `exportSeedPhrase` and `exportAccount` now take `VerificationCredentials` (`{ password }` | `{ encryptionKey }`) instead of a bare password string ([#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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ export type KeyringControllerIsUnlockedAction = {
/**
* Gets the seed phrase of the HD keyring.
*
* @param password - Password of the keyring.
* @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.
*/
Expand All @@ -94,7 +95,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.
*/
Expand Down
162 changes: 136 additions & 26 deletions packages/keyring-controller/src/KeyringController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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");
});
});
});
Expand All @@ -878,23 +883,26 @@ 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('');
});
});

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('');
});
});

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');
});
});
Expand All @@ -906,9 +914,9 @@ describe('KeyringController', () => {
jest
.spyOn(encryptor, 'decrypt')
.mockRejectedValueOnce(new Error('Invalid password'));
await expect(controller.exportSeedPhrase('')).rejects.toThrow(
'Invalid password',
);
await expect(
controller.exportSeedPhrase({ password: '' }),
).rejects.toThrow('Invalid password');
});
});

Expand All @@ -920,19 +928,75 @@ describe('KeyringController', () => {
.spyOn(encryptor, 'decrypt')
.mockRejectedValueOnce(new Error('Invalid password'));
await expect(
controller.exportSeedPhrase('', keyringId),
controller.exportSeedPhrase({ password: '' }, keyringId),
).rejects.toThrow('Invalid password');
},
);
});
});

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');
});
});
});

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 () => {
await withController(async ({ controller }) => {
await controller.setLocked();

await expect(controller.exportSeedPhrase(password)).rejects.toThrow(
await expect(controller.exportSeedPhrase({ password })).rejects.toThrow(
KeyringControllerErrorMessage.ControllerLocked,
);
});
Expand All @@ -947,7 +1011,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('');
Expand All @@ -959,7 +1023,7 @@ describe('KeyringController', () => {
it('should throw error', async () => {
await withController(async ({ controller }) => {
await expect(
controller.exportAccount(password, ''),
controller.exportAccount({ password }, ''),
).rejects.toThrow(KeyringControllerErrorMessage.KeyringNotFound);
});
});
Expand All @@ -968,17 +1032,63 @@ describe('KeyringController', () => {

describe('when wrong password is provided', () => {
it('should throw error', async () => {
await withController(async ({ controller, encryptor }) => {
jest
.spyOn(encryptor, 'decrypt')
.mockRejectedValueOnce(new Error('Invalid password'));
await expect(controller.exportSeedPhrase('')).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');
},
);
});
});

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';
Expand All @@ -989,7 +1099,7 @@ describe('KeyringController', () => {
await controller.addNewKeyring(MockKeyring.type);

await expect(
controller.exportAccount(password, address),
controller.exportAccount({ password }, address),
).rejects.toThrow(
KeyringControllerErrorMessage.UnsupportedExportAccount,
);
Expand Down Expand Up @@ -5686,7 +5796,7 @@ describe('KeyringController', () => {

expect(controller.state).toStrictEqual(initialState);
await expect(
controller.exportAccount(password, mockAddress),
controller.exportAccount({ password }, mockAddress),
).rejects.toThrow(KeyringControllerErrorMessage.KeyringNotFound);
},
);
Expand Down
Loading
Loading