Skip to content

Commit

Permalink
[keyring-controller]: Add changePassword method (#4279)
Browse files Browse the repository at this point in the history
  • Loading branch information
mikesposito authored May 16, 2024
1 parent 0a6bb9e commit 75805e7
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 10 deletions.
5 changes: 5 additions & 0 deletions packages/keyring-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Added `changePassword` method ([#4279](https://github.com/MetaMask/core/pull/4279))
- This method can be used to change the password used to encrypt the vault

## [16.0.0]

### Added
Expand Down
6 changes: 3 additions & 3 deletions packages/keyring-controller/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ module.exports = merge(baseConfig, {
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
branches: 94.8,
branches: 95.51,
functions: 100,
lines: 98.87,
statements: 98.88,
lines: 99.08,
statements: 99.09,
},
},

Expand Down
62 changes: 61 additions & 1 deletion packages/keyring-controller/src/KeyringController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ describe('KeyringController', () => {
async ({ controller }) => {
await expect(
controller.createNewVaultAndRestore('', uint8ArraySeed),
).rejects.toThrow('Invalid password');
).rejects.toThrow(KeyringControllerError.InvalidEmptyPassword);
},
);
});
Expand Down Expand Up @@ -1948,6 +1948,66 @@ describe('KeyringController', () => {
});
});

describe('changePassword', () => {
[false, true].map((cacheEncryptionKey) =>
describe(`when cacheEncryptionKey is ${cacheEncryptionKey}`, () => {
it('should encrypt the vault with the new password', async () => {
await withController(
{ cacheEncryptionKey },
async ({ controller, encryptor }) => {
const newPassword = 'new-password';
const spiedEncryptionFn = jest.spyOn(
encryptor,
cacheEncryptionKey ? 'encryptWithDetail' : 'encrypt',
);

await controller.changePassword(newPassword);

// we pick the first argument of the first call
expect(spiedEncryptionFn.mock.calls[0][0]).toBe(newPassword);
},
);
});

it('should throw error if `isUnlocked` is false', async () => {
await withController(
{ cacheEncryptionKey },
async ({ controller }) => {
await controller.setLocked();

await expect(controller.changePassword('')).rejects.toThrow(
KeyringControllerError.MissingCredentials,
);
},
);
});

it('should throw error if the new password is an empty string', async () => {
await withController(
{ cacheEncryptionKey },
async ({ controller }) => {
await expect(controller.changePassword('')).rejects.toThrow(
KeyringControllerError.InvalidEmptyPassword,
);
},
);
});

it('should throw error if the new password is undefined', async () => {
await withController(
{ cacheEncryptionKey },
async ({ controller }) => {
await expect(
// @ts-expect-error we are testing wrong input
controller.changePassword(undefined),
).rejects.toThrow(KeyringControllerError.WrongPasswordType);
},
);
});
}),
);
});

describe('submitPassword', () => {
[false, true].map((cacheEncryptionKey) =>
describe(`when cacheEncryptionKey is ${cacheEncryptionKey}`, () => {
Expand Down
51 changes: 45 additions & 6 deletions packages/keyring-controller/src/KeyringController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,22 @@ function assertIsExportableKeyEncryptor(
}
}

/**
* Assert that the provided password is a valid non-empty string.
*
* @param password - The password to check.
* @throws If the password is not a valid string.
*/
function assertIsValidPassword(password: unknown): asserts password is string {
if (typeof password !== 'string') {
throw new Error(KeyringControllerError.WrongPasswordType);
}

if (!password || !password.length) {
throw new Error(KeyringControllerError.InvalidEmptyPassword);
}
}

/**
* Checks if the provided value is a serialized keyrings array.
*
Expand Down Expand Up @@ -686,9 +702,7 @@ export class KeyringController extends BaseController<
seed: Uint8Array,
): Promise<void> {
return this.#persistOrRollback(async () => {
if (!password || !password.length) {
throw new Error('Invalid password');
}
assertIsValidPassword(password);

await this.#createNewVaultWithKeyring(password, {
type: KeyringTypes.hd,
Expand Down Expand Up @@ -1224,6 +1238,33 @@ export class KeyringController extends BaseController<
return await keyring.signUserOperation(address, userOp, executionContext);
}

/**
* Changes the password used to encrypt the vault.
*
* @param password - The new password.
* @returns Promise resolving when the operation completes.
*/
changePassword(password: string): Promise<void> {
return this.#persistOrRollback(async () => {
if (!this.state.isUnlocked) {
throw new Error(KeyringControllerError.MissingCredentials);
}

assertIsValidPassword(password);

this.#password = password;
// We need to clear encryption key and salt from state
// to force the controller to re-encrypt the vault using
// the new password.
if (this.#cacheEncryptionKey) {
this.update((state) => {
delete state.encryptionKey;
delete state.encryptionSalt;
});
}
});
}

/**
* Attempts to decrypt the current vault and load its keyrings,
* using the given encryption key and salt.
Expand Down Expand Up @@ -1907,9 +1948,7 @@ export class KeyringController extends BaseController<
updatedState.encryptionKey = exportedKeyString;
}
} else {
if (typeof this.#password !== 'string') {
throw new TypeError(KeyringControllerError.WrongPasswordType);
}
assertIsValidPassword(this.#password);
updatedState.vault = await this.#encryptor.encrypt(
this.#password,
serializedKeyrings,
Expand Down
1 change: 1 addition & 0 deletions packages/keyring-controller/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export enum KeyringControllerError {
KeyringNotFound = 'KeyringController - Keyring not found.',
UnsafeDirectKeyringAccess = 'KeyringController - Returning keyring instances is unsafe',
WrongPasswordType = 'KeyringController - Password must be of type string.',
InvalidEmptyPassword = 'KeyringController - Password cannot be empty.',
NoFirstAccount = 'KeyringController - First Account not found.',
DuplicatedAccount = 'KeyringController - The account you are trying to import is a duplicate',
VaultError = 'KeyringController - Cannot unlock without a previous vault.',
Expand Down

0 comments on commit 75805e7

Please sign in to comment.