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
2 changes: 2 additions & 0 deletions packages/keyring-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **BREAKING:** The `data` property has been added to the `KeyringObject` type ([#8009](https://github.com/MetaMask/core/pull/8009))
- This change is also reflected in the `KeyringControllerState` type, via the `keyrings` property
- Bump `@metamask/keyring-api` from `^21.0.0` to `^21.5.0` ([#7857](https://github.com/MetaMask/core/pull/7857))
- Bump `@metamask/keyring-internal-api` from `^9.0.0` to `^10.0.0` ([#7857](https://github.com/MetaMask/core/pull/7857))

Expand Down
22 changes: 15 additions & 7 deletions packages/keyring-controller/src/KeyringController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1330,6 +1330,9 @@ describe('KeyringController', () => {
const newKeyring = {
accounts: [address],
type: 'Simple Key Pair',
data: [
'1e4e6a4c0c077f4ae8ddfbf372918e61dd0fb4a4cfa592cb16e7546d505e68fc',
],
};
const importedAccountAddress =
await controller.importAccountWithStrategy(
Expand Down Expand Up @@ -1422,6 +1425,9 @@ describe('KeyringController', () => {
const newKeyring = {
accounts: [address],
type: 'Simple Key Pair',
data: [
'62390c1fe2fe70071bf6dc7345a872de041dc9663b89b2e28e6ce920427169c7',
],
};
const modifiedState = {
...initialState,
Expand Down Expand Up @@ -2868,32 +2874,32 @@ describe('KeyringController', () => {
});

it('should unlock succesfully when the controller is instantiated with an existing `keyringsMetadata`', async () => {
stubKeyringClassWithAccount(HdKeyring, '0x123');
stubKeyringClassWithAccount(MockKeyring, '0x123');
await withController(
{
state: {
vault: createVault([
{
type: KeyringTypes.hd,
data: {
accounts: ['0x123'],
},
type: MockKeyring.type,
data: {},
metadata: {
id: '123',
name: '',
},
},
]),
},
keyringBuilders: [keyringBuilderFactory(MockKeyring)],
skipVaultCreation: true,
},
async ({ controller }) => {
await controller.submitPassword(password);

expect(controller.state.keyrings).toStrictEqual([
{
type: KeyringTypes.hd,
type: MockKeyring.type,
accounts: ['0x123'],
data: {},
metadata: {
id: '123',
name: '',
Expand Down Expand Up @@ -2937,6 +2943,9 @@ describe('KeyringController', () => {
{
type: KeyringTypes.hd,
accounts: expect.any(Array),
data: {
accounts: ['0x123'],
},
metadata: {
id: expect.any(String),
name: '',
Expand Down Expand Up @@ -4406,7 +4415,6 @@ describe('KeyringController', () => {
).toMatchInlineSnapshot(`
{
"isUnlocked": false,
"keyrings": [],
}
`);
},
Expand Down
8 changes: 7 additions & 1 deletion packages/keyring-controller/src/KeyringController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,10 @@ export type KeyringObject = {
* Keyring type.
*/
type: string;
/**
* Keyring serialized data.
*/
data: Json;
/**
* Additional data associated with the keyring.
*/
Expand Down Expand Up @@ -662,6 +666,7 @@ async function displayForKeyring({
// Cast to `string[]` here is safe here because `accounts` has no nullish
// values, and `normalize` returns `string` unless given a nullish value
accounts: accounts.map(normalize) as string[],
data: cloneDeep(await keyring.serialize()),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Serialized keyring secrets exposed via controller state

High Severity

displayForKeyring now puts await keyring.serialize() into state.keyrings[*].data, and keyrings is marked usedInUi: true. Since keyring serialize() commonly includes sensitive material (e.g., mnemonics/private keys), this exposes secrets to any consumer of KeyringController:getState / KeyringController:stateChange (including UI code).

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should worry about KeyringController consumers being able to read secrets, because that's already possible via withKeyring. But perhaps we should consider restricting this property in the UI, to avoid accidental leaks.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

State updates now repeatedly serialize keyrings

Medium Severity

displayForKeyring now calls keyring.serialize() (and cloneDeep) when building state.keyrings. This makes common operations that refresh state (e.g., unlock, vault updates) perform full keyring serialization even when only accounts/type/metadata are needed, which can significantly increase latency for large keyrings or expensive serializers.

Fix in Cursor Fix in Web

metadata,
};
}
Expand Down Expand Up @@ -770,7 +775,7 @@ export class KeyringController<
usedInUi: true,
},
keyrings: {
includeInStateLogs: true,
includeInStateLogs: false,
Copy link
Member Author

@mikesposito mikesposito Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keyrings array has historically provided useful insights into state logs. If we want to keep this array exposed, we can add an separate serializedKeyrings state property where we can store serialized keyring objects safely, instead of having a data property in the objects included in keyrings

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keyrings removed from state logs unexpectedly

Medium Severity

Setting keyrings.includeInStateLogs to false removes keyrings from deriveStateFromMetadata(..., 'includeInStateLogs'), reducing observability in state logs. This can break existing diagnostics/support workflows that relied on seeing the (non-sensitive) keyrings array shape/types in logs, especially since the PR discussion notes historical reliance on it.

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

persist: false,
includeInDebugSnapshot: false,
usedInUi: true,
Expand Down Expand Up @@ -2303,6 +2308,7 @@ export class KeyringController<
const updatedKeyrings = await this.#getUpdatedKeyrings();

this.update((state) => {
// @ts-expect-error The `Json` type causes the error `Type instantiation is excessively deep and possibly infinite`
state.keyrings = updatedKeyrings;
state.encryptionKey = encryptionKey;
state.encryptionSalt = this.#encryptionKey?.salt;
Expand Down
Loading