Skip to content

Commit 1f5ae17

Browse files
perf!: Use mnemonic seed for entropy RPC methods (#3220)
This PR adjusts the implementations of the entropy RPC methods to use `getMnemonicSeed` if the curve being used is either `secp256k1` or `ed25519`. This let's us skip the most computationally heavy part of the key derivation by re-using the already derived BIP-39 seed. Closes #3219 **Breaking changes** - A `getMnemonicSeed` hook is now required to support all entropy RPC methods. - Note that `getMnemonic` can not be removed as that is still also in use.
1 parent 74a2526 commit 1f5ae17

File tree

16 files changed

+527
-181
lines changed

16 files changed

+527
-181
lines changed

packages/snaps-rpc-methods/jest.config.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ module.exports = deepmerge(baseConfig, {
1010
],
1111
coverageThreshold: {
1212
global: {
13-
branches: 95.2,
14-
functions: 98.63,
15-
lines: 98.83,
16-
statements: 98.51,
13+
branches: 94.98,
14+
functions: 98.64,
15+
lines: 98.76,
16+
statements: 98.45,
1717
},
1818
},
1919
});

packages/snaps-rpc-methods/src/restricted/getBip32Entropy.test.ts

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { SnapCaveatType } from '@metamask/snaps-utils';
33
import {
44
MOCK_SNAP_ID,
55
TEST_SECRET_RECOVERY_PHRASE_BYTES,
6+
TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES,
67
} from '@metamask/snaps-utils/test-utils';
8+
import { hmac } from '@noble/hashes/hmac';
9+
import { sha512 } from '@noble/hashes/sha512';
710

811
import {
912
getBip32EntropyBuilder,
@@ -13,6 +16,7 @@ import {
1316
describe('specificationBuilder', () => {
1417
const methodHooks = {
1518
getMnemonic: jest.fn(),
19+
getMnemonicSeed: jest.fn(),
1620
getUnlockPromise: jest.fn(),
1721
getClientCryptography: jest.fn(),
1822
};
@@ -66,12 +70,16 @@ describe('getBip32EntropyImplementation', () => {
6670
const getMnemonic = jest
6771
.fn()
6872
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_BYTES);
73+
const getMnemonicSeed = jest
74+
.fn()
75+
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES);
6976
const getClientCryptography = jest.fn().mockReturnValue({});
7077

7178
expect(
7279
await getBip32EntropyImplementation({
7380
getUnlockPromise,
7481
getMnemonic,
82+
getMnemonicSeed,
7583
getClientCryptography,
7684
// @ts-expect-error Missing other required properties.
7785
})({
@@ -97,12 +105,16 @@ describe('getBip32EntropyImplementation', () => {
97105
const getMnemonic = jest
98106
.fn()
99107
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_BYTES);
108+
const getMnemonicSeed = jest
109+
.fn()
110+
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES);
100111
const getClientCryptography = jest.fn().mockReturnValue({});
101112

102113
expect(
103114
await getBip32EntropyImplementation({
104115
getUnlockPromise,
105116
getMnemonic,
117+
getMnemonicSeed,
106118
getClientCryptography,
107119
// @ts-expect-error Missing other required properties.
108120
})({
@@ -131,13 +143,17 @@ describe('getBip32EntropyImplementation', () => {
131143
const getMnemonic = jest
132144
.fn()
133145
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_BYTES);
146+
const getMnemonicSeed = jest
147+
.fn()
148+
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES);
134149

135150
const getClientCryptography = jest.fn().mockReturnValue({});
136151

137152
expect(
138153
await getBip32EntropyImplementation({
139154
getUnlockPromise,
140155
getMnemonic,
156+
getMnemonicSeed,
141157
getClientCryptography,
142158
// @ts-expect-error Missing other required properties.
143159
})({
@@ -166,13 +182,17 @@ describe('getBip32EntropyImplementation', () => {
166182
const getMnemonic = jest
167183
.fn()
168184
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_BYTES);
185+
const getMnemonicSeed = jest
186+
.fn()
187+
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES);
169188

170189
const getClientCryptography = jest.fn().mockReturnValue({});
171190

172191
expect(
173192
await getBip32EntropyImplementation({
174193
getUnlockPromise,
175194
getMnemonic,
195+
getMnemonicSeed,
176196
getClientCryptography,
177197
// @ts-expect-error Missing other required properties.
178198
})({
@@ -200,6 +220,53 @@ describe('getBip32EntropyImplementation', () => {
200220
const getMnemonic = jest
201221
.fn()
202222
.mockImplementation(() => TEST_SECRET_RECOVERY_PHRASE_BYTES);
223+
const getMnemonicSeed = jest
224+
.fn()
225+
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES);
226+
227+
const getUnlockPromise = jest.fn();
228+
const getClientCryptography = jest.fn().mockReturnValue({});
229+
230+
expect(
231+
await getBip32EntropyImplementation({
232+
getUnlockPromise,
233+
getMnemonic,
234+
getMnemonicSeed,
235+
getClientCryptography,
236+
})({
237+
method: 'snap_getBip32Entropy',
238+
context: { origin: MOCK_SNAP_ID },
239+
params: {
240+
path: ['m', "44'", "1'"],
241+
curve: 'ed25519Bip32',
242+
source: 'source-id',
243+
},
244+
}),
245+
).toMatchInlineSnapshot(`
246+
{
247+
"chainCode": "0x25ebfafe30b0178f7b95b9a8580c386800344f4820697cc5e8cac4afb6d91d01",
248+
"curve": "ed25519Bip32",
249+
"depth": 2,
250+
"index": 2147483649,
251+
"masterFingerprint": 1587894111,
252+
"network": "mainnet",
253+
"parentFingerprint": 1429088440,
254+
"privateKey": "0x98a1e11f532dc7c8130246d6bce0bea5412cb333862f7de2cf94cbaedfbc734e7c6bcc37dd7270a54709ff022dbae6b9fa416a25ac760ccfeff157a469eab0a3",
255+
"publicKey": "0x5af28295d600e796ed0b4e51ec8a0f3d5b1f8a647d3ba7a56d7eb0941561d537",
256+
}
257+
`);
258+
259+
expect(getMnemonic).toHaveBeenCalledWith('source-id');
260+
expect(getMnemonicSeed).not.toHaveBeenCalled();
261+
});
262+
263+
it('calls `getMnemonicSeed` with a different entropy source', async () => {
264+
const getMnemonic = jest
265+
.fn()
266+
.mockImplementation(() => TEST_SECRET_RECOVERY_PHRASE_BYTES);
267+
const getMnemonicSeed = jest
268+
.fn()
269+
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES);
203270

204271
const getUnlockPromise = jest.fn();
205272
const getClientCryptography = jest.fn().mockReturnValue({});
@@ -208,6 +275,7 @@ describe('getBip32EntropyImplementation', () => {
208275
await getBip32EntropyImplementation({
209276
getUnlockPromise,
210277
getMnemonic,
278+
getMnemonicSeed,
211279
getClientCryptography,
212280
})({
213281
method: 'snap_getBip32Entropy',
@@ -232,44 +300,53 @@ describe('getBip32EntropyImplementation', () => {
232300
}
233301
`);
234302

235-
expect(getMnemonic).toHaveBeenCalledWith('source-id');
303+
expect(getMnemonicSeed).toHaveBeenCalledWith('source-id');
304+
expect(getMnemonic).not.toHaveBeenCalled();
236305
});
237306

238307
it('uses custom client cryptography functions', async () => {
239308
const getUnlockPromise = jest.fn().mockResolvedValue(undefined);
240309
const getMnemonic = jest
241310
.fn()
242311
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_BYTES);
312+
const getMnemonicSeed = jest
313+
.fn()
314+
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES);
243315

244-
const pbkdf2Sha512 = jest.fn().mockResolvedValue(new Uint8Array(64));
316+
const hmacSha512 = jest
317+
.fn()
318+
.mockImplementation((key: Uint8Array, data: Uint8Array) =>
319+
hmac(sha512, key, data),
320+
);
245321
const getClientCryptography = jest.fn().mockReturnValue({
246-
pbkdf2Sha512,
322+
hmacSha512,
247323
});
248324

249325
expect(
250326
await getBip32EntropyImplementation({
251327
getUnlockPromise,
252328
getMnemonic,
329+
getMnemonicSeed,
253330
getClientCryptography,
254331
// @ts-expect-error Missing other required properties.
255332
})({
256333
params: { path: ['m', "44'", "1'"], curve: 'secp256k1' },
257334
}),
258335
).toMatchInlineSnapshot(`
259336
{
260-
"chainCode": "0x8472428420c7fd8ef7280545bb6d2bde1d7c6b490556ccd59895f242716388d1",
337+
"chainCode": "0x50ccfa58a885b48b5eed09486b3948e8454f34856fb81da5d7b8519d7997abd1",
261338
"curve": "secp256k1",
262339
"depth": 2,
263340
"index": 2147483649,
264-
"masterFingerprint": 3276136937,
341+
"masterFingerprint": 1404659567,
265342
"network": "mainnet",
266-
"parentFingerprint": 1981505209,
267-
"privateKey": "0x71d945aba22cd337ff26a107073ae2606dee5dbf7ecfe5c25870b8eaf62b9f1b",
268-
"publicKey": "0x0491c4b234ca9b394f40d90f09092e04fd3bca2aa68c57e1311b25acfd972c5a6fc7ffd19e7812127473aa2bd827917b6ec7b57bec73cf022fc1f1fa0593f48770",
343+
"parentFingerprint": 1829122711,
344+
"privateKey": "0xc73cedb996e7294f032766853a8b7ba11ab4ce9755fc052f2f7b9000044c99af",
345+
"publicKey": "0x048e129862c1de5ca86468add43b001d32fd34b8113de716ecd63fa355b7f1165f0e76f5dc6095100f9fdaa76ddf28aa3f21406ac5fda7c71ffbedb45634fe2ceb",
269346
}
270347
`);
271348

272-
expect(pbkdf2Sha512).toHaveBeenCalledTimes(1);
349+
expect(hmacSha512).toHaveBeenCalledTimes(3);
273350
});
274351
});
275352
});

packages/snaps-rpc-methods/src/restricted/getBip32Entropy.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ import type { NonEmptyArray } from '@metamask/utils';
1616
import { assert } from '@metamask/utils';
1717

1818
import type { MethodHooksObject } from '../utils';
19-
import { getSecretRecoveryPhrase, getNode } from '../utils';
19+
import {
20+
getNodeFromMnemonic,
21+
getNodeFromSeed,
22+
getValueFromEntropySource,
23+
} from '../utils';
2024

2125
const targetName = 'snap_getBip32Entropy';
2226

@@ -31,6 +35,16 @@ export type GetBip32EntropyMethodHooks = {
3135
*/
3236
getMnemonic: (source?: string | undefined) => Promise<Uint8Array>;
3337

38+
/**
39+
* Get the mnemonic seed of the provided source. If no source is provided, the
40+
* mnemonic seed of the primary keyring will be returned.
41+
*
42+
* @param source - The optional ID of the source to get the mnemonic of.
43+
* @returns The mnemonic seed of the provided source, or the default source if no
44+
* source is provided.
45+
*/
46+
getMnemonicSeed: (source?: string | undefined) => Promise<Uint8Array>;
47+
3448
/**
3549
* Waits for the extension to be unlocked.
3650
*
@@ -95,6 +109,7 @@ const specificationBuilder: PermissionSpecificationBuilder<
95109

96110
const methodHooks: MethodHooksObject<GetBip32EntropyMethodHooks> = {
97111
getMnemonic: true,
112+
getMnemonicSeed: true,
98113
getUnlockPromise: true,
99114
getClientCryptography: true,
100115
};
@@ -110,6 +125,7 @@ export const getBip32EntropyBuilder = Object.freeze({
110125
*
111126
* @param hooks - The RPC method hooks.
112127
* @param hooks.getMnemonic - A function to retrieve the Secret Recovery Phrase of the user.
128+
* @param hooks.getMnemonicSeed - A function to retrieve the BIP-39 seed of the user.
113129
* @param hooks.getUnlockPromise - A function that resolves once the MetaMask extension is unlocked
114130
* and prompts the user to unlock their MetaMask if it is locked.
115131
* @param hooks.getClientCryptography - A function to retrieve the cryptographic
@@ -119,6 +135,7 @@ export const getBip32EntropyBuilder = Object.freeze({
119135
*/
120136
export function getBip32EntropyImplementation({
121137
getMnemonic,
138+
getMnemonicSeed,
122139
getUnlockPromise,
123140
getClientCryptography,
124141
}: GetBip32EntropyMethodHooks) {
@@ -130,12 +147,29 @@ export function getBip32EntropyImplementation({
130147
const { params } = args;
131148
assert(params);
132149

133-
const secretRecoveryPhrase = await getSecretRecoveryPhrase(
150+
// Using the seed is much faster, but we can only do it for these specific curves.
151+
if (params.curve === 'secp256k1' || params.curve === 'ed25519') {
152+
const seed = await getValueFromEntropySource(
153+
getMnemonicSeed,
154+
params.source,
155+
);
156+
157+
const node = await getNodeFromSeed({
158+
curve: params.curve,
159+
path: params.path,
160+
seed,
161+
cryptographicFunctions: getClientCryptography(),
162+
});
163+
164+
return node.toJSON();
165+
}
166+
167+
const secretRecoveryPhrase = await getValueFromEntropySource(
134168
getMnemonic,
135169
params.source,
136170
);
137171

138-
const node = await getNode({
172+
const node = await getNodeFromMnemonic({
139173
curve: params.curve,
140174
path: params.path,
141175
secretRecoveryPhrase,

0 commit comments

Comments
 (0)