Skip to content

Commit 777121a

Browse files
authored
refactor!: KeyringController always derives encryption key (#7128)
1 parent 0720b9f commit 777121a

File tree

12 files changed

+378
-163
lines changed

12 files changed

+378
-163
lines changed

packages/keyring-controller/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
- **BREAKING:** The `KeyringController` constructor options now require an encryptor ([#7127](https://github.com/MetaMask/core/pull/7127))
1818
- The `encryptor` constructor option was previously optional and defaulted to an instance of `@metamask/browser-passworder`.
1919
- **BREAKING:** The `GenericEncryptor` and `ExportableKeyEncryptor` types have been merged into a single `Encryptor` type ([#7127](https://github.com/MetaMask/core/pull/7127))
20+
- **BREAKING:** The `Encryptor` type requires `exportKey`, `keyFromPassword` and `generateSalt` methods ([#7128](https://github.com/MetaMask/core/pull/7128))
2021

2122
### Removed
2223

packages/keyring-controller/jest.config.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ module.exports = merge(baseConfig, {
1717
// An object that configures minimum threshold enforcement for coverage results
1818
coverageThreshold: {
1919
global: {
20-
branches: 94.03,
20+
branches: 95.2,
2121
functions: 100,
22-
lines: 98.31,
23-
statements: 98.33,
22+
lines: 98.8,
23+
statements: 98.81,
2424
},
2525
},
2626

packages/keyring-controller/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"dependencies": {
5151
"@ethereumjs/util": "^9.1.0",
5252
"@metamask/base-controller": "^9.0.0",
53-
"@metamask/browser-passworder": "^4.3.0",
53+
"@metamask/browser-passworder": "^6.0.0",
5454
"@metamask/eth-hd-keyring": "^13.0.0",
5555
"@metamask/eth-sig-util": "^8.2.0",
5656
"@metamask/eth-simple-keyring": "^11.0.0",

packages/keyring-controller/src/KeyringController.test.ts

Lines changed: 142 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,23 @@ describe('KeyringController', () => {
614614
});
615615
});
616616

617+
it('should create new vault with a different password', async () => {
618+
await withController(async ({ controller, initialState }) => {
619+
const initialKeyrings = controller.state.keyrings;
620+
621+
await controller.createNewVaultAndRestore(
622+
'new-password',
623+
uint8ArraySeed,
624+
);
625+
626+
expect(controller.state).not.toBe(initialState);
627+
expect(controller.state.vault).toBeDefined();
628+
expect(controller.state.keyrings).toHaveLength(initialKeyrings.length);
629+
// new keyring metadata should be generated
630+
expect(controller.state.keyrings).not.toStrictEqual(initialKeyrings);
631+
});
632+
});
633+
617634
it('should throw error if creating new vault and restore without password', async () => {
618635
await withController(async ({ controller }) => {
619636
await expect(
@@ -2555,12 +2572,15 @@ describe('KeyringController', () => {
25552572
it('should encrypt the vault with the new password', async () => {
25562573
await withController(async ({ controller, encryptor }) => {
25572574
const newPassword = 'new-password';
2558-
const spiedEncryptionFn = jest.spyOn(encryptor, 'encryptWithDetail');
2575+
const keyFromPasswordSpy = jest.spyOn(encryptor, 'keyFromPassword');
25592576

25602577
await controller.changePassword(newPassword);
25612578

2562-
// we pick the first argument of the first call
2563-
expect(spiedEncryptionFn.mock.calls[0][0]).toBe(newPassword);
2579+
expect(keyFromPasswordSpy).toHaveBeenCalledWith(
2580+
newPassword,
2581+
controller.state.encryptionSalt,
2582+
true,
2583+
);
25642584
});
25652585
});
25662586

@@ -2672,6 +2692,24 @@ describe('KeyringController', () => {
26722692
);
26732693
});
26742694

2695+
it('should throw an error if the encryptor returns an undefined encryption key', async () => {
2696+
await withController(
2697+
{ skipVaultCreation: true, state: { vault: createVault() } },
2698+
async ({ controller, encryptor }) => {
2699+
jest.spyOn(encryptor, 'decryptWithDetail').mockResolvedValueOnce({
2700+
vault: defaultKeyrings,
2701+
// @ts-expect-error we are testing a broken encryptor
2702+
exportedKeyString: undefined,
2703+
salt: '',
2704+
});
2705+
2706+
await expect(controller.submitPassword(password)).rejects.toThrow(
2707+
KeyringControllerError.MissingCredentials,
2708+
);
2709+
},
2710+
);
2711+
});
2712+
26752713
it('should unlock succesfully when the controller is instantiated with an existing `keyringsMetadata`', async () => {
26762714
stubKeyringClassWithAccount(HdKeyring, '0x123');
26772715
await withController(
@@ -2710,27 +2748,27 @@ describe('KeyringController', () => {
27102748
});
27112749

27122750
it('should generate new metadata when there is no metadata in the vault', async () => {
2751+
const vault = createVault([
2752+
{
2753+
type: KeyringTypes.hd,
2754+
data: {
2755+
accounts: ['0x123'],
2756+
},
2757+
},
2758+
]);
27132759
const hdKeyringSerializeSpy = jest.spyOn(
27142760
HdKeyring.prototype,
27152761
'serialize',
27162762
);
27172763
await withController(
27182764
{
27192765
state: {
2720-
vault: createVault([
2721-
{
2722-
type: KeyringTypes.hd,
2723-
data: {
2724-
accounts: ['0x123'],
2725-
},
2726-
},
2727-
]),
2766+
vault,
27282767
},
27292768
skipVaultCreation: true,
27302769
},
27312770
async ({ controller, encryptor }) => {
27322771
const encryptWithKeySpy = jest.spyOn(encryptor, 'encryptWithKey');
2733-
jest.spyOn(encryptor, 'importKey').mockResolvedValue('imported key');
27342772
hdKeyringSerializeSpy.mockResolvedValue({
27352773
// @ts-expect-error we are assigning a mock value
27362774
accounts: ['0x123'],
@@ -2748,7 +2786,7 @@ describe('KeyringController', () => {
27482786
},
27492787
},
27502788
]);
2751-
expect(encryptWithKeySpy).toHaveBeenCalledWith('imported key', [
2789+
expect(encryptWithKeySpy).toHaveBeenCalledWith(defaultCredentials, [
27522790
{
27532791
type: KeyringTypes.hd,
27542792
data: {
@@ -2969,6 +3007,43 @@ describe('KeyringController', () => {
29693007
},
29703008
);
29713009
});
3010+
3011+
it('should throw an error if the password is not a string', async () => {
3012+
await withController(async ({ controller }) => {
3013+
await expect(
3014+
// @ts-expect-error we are testing wrong input
3015+
controller.submitPassword(123456),
3016+
).rejects.toThrow(KeyringControllerError.WrongPasswordType);
3017+
});
3018+
});
3019+
3020+
it('should siletly fail the key derivation params upgrade if it fails', async () => {
3021+
await withController(
3022+
{
3023+
skipVaultCreation: true,
3024+
state: {
3025+
vault: createVault([
3026+
{
3027+
type: KeyringTypes.hd,
3028+
data: {
3029+
accounts: ['0x123'],
3030+
},
3031+
},
3032+
]),
3033+
},
3034+
},
3035+
async ({ controller, encryptor }) => {
3036+
jest.spyOn(encryptor, 'isVaultUpdated').mockReturnValue(false);
3037+
jest
3038+
.spyOn(encryptor, 'exportKey')
3039+
.mockRejectedValue(new Error('Error'));
3040+
3041+
await controller.submitPassword(password);
3042+
3043+
expect(controller.state.isUnlocked).toBe(true);
3044+
},
3045+
);
3046+
});
29723047
});
29733048

29743049
describe('submitEncryptionKey', () => {
@@ -3057,6 +3132,57 @@ describe('KeyringController', () => {
30573132
);
30583133
});
30593134

3135+
it('should suppress errors if new metadata is created while unlocking and the vault update fails', async () => {
3136+
jest.spyOn(HdKeyring.prototype, 'serialize').mockResolvedValue({
3137+
// @ts-expect-error we are assigning a mock value
3138+
accounts: ['0x123'],
3139+
});
3140+
await withController(
3141+
{
3142+
skipVaultCreation: true,
3143+
state: {
3144+
vault: createVault([
3145+
{
3146+
type: KeyringTypes.hd,
3147+
data: '0x123',
3148+
},
3149+
]),
3150+
// @ts-expect-error we want to force the controller to have an
3151+
// encryption salt equal to the one in the vault
3152+
encryptionSalt: SALT,
3153+
},
3154+
},
3155+
async ({ controller, initialState, encryptor }) => {
3156+
const encryptWithKeySpy = jest.spyOn(encryptor, 'encryptWithKey');
3157+
jest
3158+
.spyOn(encryptor, 'encryptWithKey')
3159+
.mockRejectedValueOnce(new Error('Error'));
3160+
3161+
await controller.submitEncryptionKey(
3162+
MOCK_ENCRYPTION_KEY,
3163+
initialState.encryptionSalt as string,
3164+
);
3165+
3166+
expect(controller.state.isUnlocked).toBe(true);
3167+
expect(encryptWithKeySpy).toHaveBeenCalledWith(
3168+
JSON.parse(MOCK_ENCRYPTION_KEY),
3169+
[
3170+
{
3171+
type: KeyringTypes.hd,
3172+
data: {
3173+
accounts: ['0x123'],
3174+
},
3175+
metadata: {
3176+
id: expect.any(String),
3177+
name: '',
3178+
},
3179+
},
3180+
],
3181+
);
3182+
},
3183+
);
3184+
});
3185+
30603186
it('should throw error if vault unlocked has an unexpected shape', async () => {
30613187
await withController(async ({ controller, initialState, encryptor }) => {
30623188
jest.spyOn(encryptor, 'decryptWithKey').mockResolvedValueOnce([
@@ -3083,15 +3209,15 @@ describe('KeyringController', () => {
30833209
});
30843210

30853211
it('should throw error if encryptionKey is of an unexpected type', async () => {
3086-
await withController(async ({ controller, initialState }) => {
3212+
await withController(async ({ controller }) => {
30873213
await expect(
30883214
controller.submitEncryptionKey(
30893215
// @ts-expect-error we are testing the case of a user using
30903216
// the wrong encryptionKey type
30913217
12341234,
3092-
initialState.encryptionSalt as string,
3218+
SALT,
30933219
),
3094-
).rejects.toThrow(KeyringControllerError.WrongPasswordType);
3220+
).rejects.toThrow(KeyringControllerError.WrongEncryptionKeyType);
30953221
});
30963222
});
30973223
});

0 commit comments

Comments
 (0)