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

### Changed

- **BREAKING:** Derive auth and user-storage message-signing keys natively via SIP-6 from HD keyring seeds instead of calling `@metamask/message-signing-snap` through `SnapController`. `AuthenticationController` and `UserStorageController` now require `KeyringController:withKeyringV2Unsafe` and no longer call `SnapController:handleRequest`. The message-signing snap remains for Portfolio / external origins ([#9824](https://github.com/MetaMask/core/pull/9824))
- Derive native SIP-6 keys with `@noble/hashes` HMAC-SHA-512 instead of Web Crypto, so auth works on React Native, whose SubtleCrypto cannot HMAC.
- Resolve HD entropy source IDs from `KeyringController` instead of the message-signing snap (`getBearerToken` primary ID, `performSignIn` SRP enumeration) ([#9794](https://github.com/MetaMask/core/pull/9794))
- Bump `@metamask/keyring-controller` from `^27.1.0` to `^27.1.1` ([#9791](https://github.com/MetaMask/core/pull/9791))
- Add `@metamask/key-tree` and `@noble/curves`; remove unused `@metamask/snaps-controllers`, `@metamask/snaps-sdk`, and `@metamask/snaps-utils` dependencies ([#9824](https://github.com/MetaMask/core/pull/9824))

## [29.0.0]

Expand Down
6 changes: 3 additions & 3 deletions packages/profile-sync-controller/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ module.exports = merge(baseConfig, {
coverageThreshold: {
global: {
branches: 85.03,
functions: 92.75,
lines: 94.98,
statements: 95.03,
functions: 93.03,
lines: 95.15,
statements: 95.19,
},
},

Expand Down
6 changes: 3 additions & 3 deletions packages/profile-sync-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,13 @@
"dependencies": {
"@metamask/address-book-controller": "^7.1.2",
"@metamask/base-controller": "^9.1.0",
"@metamask/key-tree": "^10.1.1",
"@metamask/keyring-controller": "^27.1.1",
"@metamask/messenger": "^2.0.0",
"@metamask/seedless-onboarding-controller": "^10.1.1",
"@metamask/snaps-controllers": "^19.0.0",
"@metamask/snaps-sdk": "^11.0.0",
"@metamask/snaps-utils": "^12.1.2",
"@metamask/utils": "^11.11.0",
"@noble/ciphers": "^1.3.0",
"@noble/curves": "^1.9.2",
"@noble/hashes": "^1.8.0",
"immer": "^9.0.6",
"loglevel": "^1.8.1",
Expand All @@ -127,6 +126,7 @@
"@lavamoat/allow-scripts": "^3.0.4",
"@lavamoat/preinstall-always-fail": "^2.1.0",
"@metamask/auto-changelog": "^6.1.0",
"@metamask/eth-hd-keyring": "^15.0.0",
Comment thread
cursor[bot] marked this conversation as resolved.
"@metamask/keyring-api": "^24.0.0",
"@metamask/keyring-internal-api": "^12.0.0",
"@metamask/providers": "^22.1.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import {
MOCK_ACCESS_JWT,
MOCK_USER_PROFILE_LINEAGE_RESPONSE,
} from '../../sdk/mocks/auth.js';
import {
getMessageSigningPublicKey,
signMessageWithMessageSigningKey,
} from '../../shared/utils/message-signing.js';
import { AuthenticationController } from './AuthenticationController.js';
import type {
AuthenticationControllerMessenger,
Expand All @@ -25,6 +29,16 @@ import {
MOCK_OATH_TOKEN_RESPONSE,
} from './mocks/mockResponses.js';

jest.mock('../../shared/utils/message-signing.js', () => ({
MESSAGE_SIGNING_SNAP_ID: 'npm:@metamask/message-signing-snap',
getMessageSigningPublicKey: jest.fn(async () => 'MOCK_PUBLIC_KEY'),
signMessageWithMessageSigningKey: jest.fn(async () => 'MOCK_SIGNED_MESSAGE'),
deriveMessageSigningPrivateKey: jest.fn(),
deriveSip6PrivateKey: jest.fn(),
}));

const MOCK_HD_SEED = new Uint8Array(64).fill(1);

const MOCK_ENTROPY_SOURCE_IDS = [
'MOCK_ENTROPY_SOURCE_ID',
'MOCK_ENTROPY_SOURCE_ID2',
Expand Down Expand Up @@ -138,7 +152,7 @@ describe('AuthenticationController', () => {
it('should create access token(s) and update state', async () => {
const metametrics = createMockAuthMetaMetrics();
const mockEndpoints = arrangeAuthAPIs();
const { messenger, mockSnapGetPublicKey, mockSnapSignMessage } =
const { messenger, mockGetPublicKey, mockSignMessage } =
createMockAuthenticationMessenger();

const controller = new AuthenticationController({
Expand All @@ -147,11 +161,11 @@ describe('AuthenticationController', () => {
});

const result = await controller.performSignIn();
// SRP enumeration uses KeyringController; snap is only needed for
// SRP enumeration uses KeyringController; native SIP-6 is used for
// getPublicKey / signMessage during cold login.
expect(mockSnapGetPublicKey).toHaveBeenCalledTimes(2);
expect(mockGetPublicKey).toHaveBeenCalledTimes(2);
// Primary and secondary tags produce distinct messages, so both are signed.
expect(mockSnapSignMessage).toHaveBeenCalledTimes(2);
expect(mockSignMessage).toHaveBeenCalledTimes(2);
mockEndpoints.mockNonceUrl.done();
mockEndpoints.mockSrpLoginUrl.done();
mockEndpoints.mockOAuth2TokenUrl.done();
Expand All @@ -166,10 +180,10 @@ describe('AuthenticationController', () => {
}
});

it('leverages the _snapSignMessageCache', async () => {
it('leverages the signMessage cache', async () => {
const metametrics = createMockAuthMetaMetrics();
const mockEndpoints = arrangeAuthAPIs();
const { messenger, mockSnapSignMessage } =
const { messenger, mockSignMessage } =
createMockAuthenticationMessenger();

const controller = new AuthenticationController({
Expand All @@ -181,7 +195,7 @@ describe('AuthenticationController', () => {
controller.performSignOut();
await controller.performSignIn();
// Both tagged login messages are cached across sign-out / sign-in.
expect(mockSnapSignMessage).toHaveBeenCalledTimes(2);
expect(mockSignMessage).toHaveBeenCalledTimes(2);
mockEndpoints.mockNonceUrl.done();
mockEndpoints.mockSrpLoginUrl.done();
mockEndpoints.mockOAuth2TokenUrl.done();
Expand All @@ -194,7 +208,7 @@ describe('AuthenticationController', () => {
it('signs primary and secondary login tags for multi-SRP wallets', async () => {
const metametrics = createMockAuthMetaMetrics();
arrangeAuthAPIs();
const { messenger, mockSnapSignMessage } =
const { messenger, mockSignMessage } =
createMockAuthenticationMessenger();

const controller = new AuthenticationController({
Expand All @@ -204,8 +218,8 @@ describe('AuthenticationController', () => {

await controller.performSignIn();

const signedMessages = mockSnapSignMessage.mock.calls.map(
(call) => (call[0] as { message: string }).message,
const signedMessages = mockSignMessage.mock.calls.map(
(call) => call[0] as string,
);
expect(signedMessages).toStrictEqual(
expect.arrayContaining([
Expand All @@ -220,7 +234,7 @@ describe('AuthenticationController', () => {
arrangeAuthAPIs();
const {
messenger,
mockSnapSignMessage,
mockSignMessage,
mockKeyringControllerGetState,
mockSeedlessOnboardingGetState,
} = createMockAuthenticationMessenger();
Expand All @@ -240,10 +254,9 @@ describe('AuthenticationController', () => {

await controller.performSignIn();

expect(mockSnapSignMessage).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringMatching(/^metamask:[^:]+:[^:]+:primary$/u),
}),
expect(mockSignMessage).toHaveBeenCalledWith(
expect.stringMatching(/^metamask:[^:]+:[^:]+:primary$/u),
MOCK_HD_SEED,
);
});

Expand Down Expand Up @@ -1052,9 +1065,9 @@ describe('AuthenticationController', () => {
expect(resultUndefined).toBe(resultExplicit);
});

it('resolves primary entropySourceId from the HD keyring without the snap', async () => {
it('resolves primary entropySourceId from the HD keyring without signing', async () => {
const metametrics = createMockAuthMetaMetrics();
const { messenger, mockSnapGetPublicKey, mockKeyringControllerGetState } =
const { messenger, mockGetPublicKey, mockKeyringControllerGetState } =
createMockAuthenticationMessenger();
const originalState = mockSignedInState();
const controller = new AuthenticationController({
Expand All @@ -1067,8 +1080,8 @@ describe('AuthenticationController', () => {
await controller.getBearerToken();
await controller.getBearerToken();

// Cached session: no snap identify/sign; only keyring for primary ID.
expect(mockSnapGetPublicKey).not.toHaveBeenCalled();
// Cached session: no identify/sign; only keyring for primary ID.
expect(mockGetPublicKey).not.toHaveBeenCalled();
expect(mockKeyringControllerGetState).toHaveBeenCalled();
});

Expand Down Expand Up @@ -1649,7 +1662,7 @@ function createAuthenticationMessenger(): {
messenger,
actions: [
'KeyringController:getState',
'SnapController:handleRequest',
'KeyringController:withKeyringV2Unsafe',
'SeedlessOnboardingController:getState',
],
events: ['KeyringController:lock', 'KeyringController:unlock'],
Expand All @@ -1666,50 +1679,58 @@ function createAuthenticationMessenger(): {
function createMockAuthenticationMessenger(): {
messenger: AuthenticationControllerMessenger;
baseMessenger: RootMessenger;
mockSnapGetPublicKey: jest.Mock;
mockSnapSignMessage: jest.Mock;
mockGetPublicKey: jest.Mock;
mockSignMessage: jest.Mock;
mockKeyringControllerGetState: jest.Mock;
mockWithKeyringV2Unsafe: jest.Mock;
mockSeedlessOnboardingGetState: jest.Mock;
} {
const { baseMessenger, messenger } = createAuthenticationMessenger();

const mockCall = jest.spyOn(messenger, 'call');
const mockSnapGetPublicKey = jest.fn().mockResolvedValue('MOCK_PUBLIC_KEY');
const mockSnapSignMessage = jest
.fn()
.mockResolvedValue('MOCK_SIGNED_MESSAGE');
const mockGetPublicKey = jest.mocked(getMessageSigningPublicKey);
const mockSignMessage = jest.mocked(signMessageWithMessageSigningKey);
mockGetPublicKey.mockReset().mockResolvedValue('MOCK_PUBLIC_KEY');
mockSignMessage.mockReset().mockResolvedValue('MOCK_SIGNED_MESSAGE');

const mockKeyringControllerGetState = jest.fn().mockReturnValue({
isUnlocked: true,
keyrings: MOCK_HD_KEYRINGS,
});

const mockWithKeyringV2Unsafe = jest
.fn()
.mockImplementation(
async (
_selector: { id: string },
operation: (context: {
keyring: { type: string; seed?: Uint8Array };
metadata: { id: string; name: string };
}) => Promise<unknown>,
) => {
return operation({
keyring: { type: 'hd', seed: MOCK_HD_SEED },
metadata: { id: 'mock', name: '' },
});
},
);

const mockSeedlessOnboardingGetState = jest
.fn()
.mockReturnValue({ vault: null });

mockCall.mockImplementation((...args) => {
const [actionType, params] = args;
if (actionType === 'SnapController:handleRequest') {
if (typeof params === 'string') {
throw new Error(
`MOCK_FAIL - unsupported SnapController:handleRequest call: ${params}`,
);
}

if (params?.request.method === 'getPublicKey') {
return mockSnapGetPublicKey();
}

if (params?.request.method === 'signMessage') {
return mockSnapSignMessage(params.request.params);
}

throw new Error(
`MOCK_FAIL - unsupported SnapController:handleRequest call: ${
params?.request.method as string
}`,
);
mockCall.mockImplementation((...args: unknown[]) => {
const [actionType] = args;
if (actionType === 'KeyringController:withKeyringV2Unsafe') {
const [, selector, operation] = args as [
typeof actionType,
{ id: string },
(context: {
keyring: { type: string; seed?: Uint8Array };
metadata: { id: string; name: string };
}) => Promise<unknown>,
];
return mockWithKeyringV2Unsafe(selector, operation);
}

if (actionType === 'KeyringController:getState') {
Expand All @@ -1728,9 +1749,10 @@ function createMockAuthenticationMessenger(): {
return {
messenger,
baseMessenger,
mockSnapGetPublicKey,
mockSnapSignMessage,
mockGetPublicKey,
mockSignMessage,
mockKeyringControllerGetState,
mockWithKeyringV2Unsafe,
mockSeedlessOnboardingGetState,
};
}
Expand All @@ -1745,13 +1767,7 @@ function createMockAuthenticationMessenger(): {
function mockAuthenticationFlowEndpoints(params?: {
endpointFail: 'nonce' | 'login' | 'token' | 'lineage' | 'customerService';
}): ReturnType<typeof arrangeAuthAPIs> {
const {
mockNonceUrl,
mockOAuth2TokenUrl,
mockSrpLoginUrl,
mockUserProfileLineageUrl,
mockCustomerServiceTokenUrl,
} = arrangeAuthAPIs({
return arrangeAuthAPIs({
mockNonceUrl:
params?.endpointFail === 'nonce' ? { status: 500 } : undefined,
mockSrpLoginUrl:
Expand All @@ -1763,14 +1779,6 @@ function mockAuthenticationFlowEndpoints(params?: {
mockCustomerServiceTokenUrl:
params?.endpointFail === 'customerService' ? { status: 500 } : undefined,
});

return {
mockNonceUrl,
mockOAuth2TokenUrl,
mockSrpLoginUrl,
mockUserProfileLineageUrl,
mockCustomerServiceTokenUrl,
};
}

/**
Expand Down
Loading