Skip to content

Commit 59ee984

Browse files
perf: Use mnemonic seed for entropy RPC methods
1 parent 56637c4 commit 59ee984

File tree

10 files changed

+421
-174
lines changed

10 files changed

+421
-174
lines changed

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

Lines changed: 84 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,52 @@ 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+
});
261+
262+
it('calls `getMnemonicSeed` with a different entropy source', async () => {
263+
const getMnemonic = jest
264+
.fn()
265+
.mockImplementation(() => TEST_SECRET_RECOVERY_PHRASE_BYTES);
266+
const getMnemonicSeed = jest
267+
.fn()
268+
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES);
203269

204270
const getUnlockPromise = jest.fn();
205271
const getClientCryptography = jest.fn().mockReturnValue({});
@@ -208,6 +274,7 @@ describe('getBip32EntropyImplementation', () => {
208274
await getBip32EntropyImplementation({
209275
getUnlockPromise,
210276
getMnemonic,
277+
getMnemonicSeed,
211278
getClientCryptography,
212279
})({
213280
method: 'snap_getBip32Entropy',
@@ -232,44 +299,52 @@ describe('getBip32EntropyImplementation', () => {
232299
}
233300
`);
234301

235-
expect(getMnemonic).toHaveBeenCalledWith('source-id');
302+
expect(getMnemonicSeed).toHaveBeenCalledWith('source-id');
236303
});
237304

238305
it('uses custom client cryptography functions', async () => {
239306
const getUnlockPromise = jest.fn().mockResolvedValue(undefined);
240307
const getMnemonic = jest
241308
.fn()
242309
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_BYTES);
310+
const getMnemonicSeed = jest
311+
.fn()
312+
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES);
243313

244-
const pbkdf2Sha512 = jest.fn().mockResolvedValue(new Uint8Array(64));
314+
const hmacSha512 = jest
315+
.fn()
316+
.mockImplementation((key: Uint8Array, data: Uint8Array) =>
317+
hmac(sha512, key, data),
318+
);
245319
const getClientCryptography = jest.fn().mockReturnValue({
246-
pbkdf2Sha512,
320+
hmacSha512,
247321
});
248322

249323
expect(
250324
await getBip32EntropyImplementation({
251325
getUnlockPromise,
252326
getMnemonic,
327+
getMnemonicSeed,
253328
getClientCryptography,
254329
// @ts-expect-error Missing other required properties.
255330
})({
256331
params: { path: ['m', "44'", "1'"], curve: 'secp256k1' },
257332
}),
258333
).toMatchInlineSnapshot(`
259334
{
260-
"chainCode": "0x8472428420c7fd8ef7280545bb6d2bde1d7c6b490556ccd59895f242716388d1",
335+
"chainCode": "0x50ccfa58a885b48b5eed09486b3948e8454f34856fb81da5d7b8519d7997abd1",
261336
"curve": "secp256k1",
262337
"depth": 2,
263338
"index": 2147483649,
264-
"masterFingerprint": 3276136937,
339+
"masterFingerprint": 1404659567,
265340
"network": "mainnet",
266-
"parentFingerprint": 1981505209,
267-
"privateKey": "0x71d945aba22cd337ff26a107073ae2606dee5dbf7ecfe5c25870b8eaf62b9f1b",
268-
"publicKey": "0x0491c4b234ca9b394f40d90f09092e04fd3bca2aa68c57e1311b25acfd972c5a6fc7ffd19e7812127473aa2bd827917b6ec7b57bec73cf022fc1f1fa0593f48770",
341+
"parentFingerprint": 1829122711,
342+
"privateKey": "0xc73cedb996e7294f032766853a8b7ba11ab4ce9755fc052f2f7b9000044c99af",
343+
"publicKey": "0x048e129862c1de5ca86468add43b001d32fd34b8113de716ecd63fa355b7f1165f0e76f5dc6095100f9fdaa76ddf28aa3f21406ac5fda7c71ffbedb45634fe2ceb",
269344
}
270345
`);
271346

272-
expect(pbkdf2Sha512).toHaveBeenCalledTimes(1);
347+
expect(hmacSha512).toHaveBeenCalledTimes(3);
273348
});
274349
});
275350
});

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)