Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]

### 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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
81 changes: 81 additions & 0 deletions packages/keyring-controller/src/KeyringController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 }) => {
Expand Down
36 changes: 33 additions & 3 deletions packages/keyring-controller/src/KeyringController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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.
*
Expand All @@ -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<Uint8Array> {
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');
Expand Down
Loading