Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
8 changes: 4 additions & 4 deletions packages/snaps-rpc-methods/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ module.exports = deepmerge(baseConfig, {
],
coverageThreshold: {
global: {
branches: 95.2,
functions: 98.63,
lines: 98.83,
statements: 98.51,
branches: 94.98,
functions: 98.64,
lines: 98.76,
statements: 98.45,
},
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { SnapCaveatType } from '@metamask/snaps-utils';
import {
MOCK_SNAP_ID,
TEST_SECRET_RECOVERY_PHRASE_BYTES,
TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES,
} from '@metamask/snaps-utils/test-utils';
import { hmac } from '@noble/hashes/hmac';
import { sha512 } from '@noble/hashes/sha512';

import {
getBip32EntropyBuilder,
Expand All @@ -13,6 +16,7 @@ import {
describe('specificationBuilder', () => {
const methodHooks = {
getMnemonic: jest.fn(),
getMnemonicSeed: jest.fn(),
getUnlockPromise: jest.fn(),
getClientCryptography: jest.fn(),
};
Expand Down Expand Up @@ -66,12 +70,16 @@ describe('getBip32EntropyImplementation', () => {
const getMnemonic = jest
.fn()
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_BYTES);
const getMnemonicSeed = jest
.fn()
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES);
const getClientCryptography = jest.fn().mockReturnValue({});

expect(
await getBip32EntropyImplementation({
getUnlockPromise,
getMnemonic,
getMnemonicSeed,
getClientCryptography,
// @ts-expect-error Missing other required properties.
})({
Expand All @@ -97,12 +105,16 @@ describe('getBip32EntropyImplementation', () => {
const getMnemonic = jest
.fn()
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_BYTES);
const getMnemonicSeed = jest
.fn()
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES);
const getClientCryptography = jest.fn().mockReturnValue({});

expect(
await getBip32EntropyImplementation({
getUnlockPromise,
getMnemonic,
getMnemonicSeed,
getClientCryptography,
// @ts-expect-error Missing other required properties.
})({
Expand Down Expand Up @@ -131,13 +143,17 @@ describe('getBip32EntropyImplementation', () => {
const getMnemonic = jest
.fn()
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_BYTES);
const getMnemonicSeed = jest
.fn()
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES);

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

expect(
await getBip32EntropyImplementation({
getUnlockPromise,
getMnemonic,
getMnemonicSeed,
getClientCryptography,
// @ts-expect-error Missing other required properties.
})({
Expand Down Expand Up @@ -166,13 +182,17 @@ describe('getBip32EntropyImplementation', () => {
const getMnemonic = jest
.fn()
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_BYTES);
const getMnemonicSeed = jest
.fn()
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES);

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

expect(
await getBip32EntropyImplementation({
getUnlockPromise,
getMnemonic,
getMnemonicSeed,
getClientCryptography,
// @ts-expect-error Missing other required properties.
})({
Expand Down Expand Up @@ -200,6 +220,52 @@ describe('getBip32EntropyImplementation', () => {
const getMnemonic = jest
.fn()
.mockImplementation(() => TEST_SECRET_RECOVERY_PHRASE_BYTES);
const getMnemonicSeed = jest
.fn()
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES);

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

expect(
await getBip32EntropyImplementation({
getUnlockPromise,
getMnemonic,
getMnemonicSeed,
getClientCryptography,
})({
method: 'snap_getBip32Entropy',
context: { origin: MOCK_SNAP_ID },
params: {
path: ['m', "44'", "1'"],
curve: 'ed25519Bip32',
source: 'source-id',
},
}),
).toMatchInlineSnapshot(`
{
"chainCode": "0x25ebfafe30b0178f7b95b9a8580c386800344f4820697cc5e8cac4afb6d91d01",
"curve": "ed25519Bip32",
"depth": 2,
"index": 2147483649,
"masterFingerprint": 1587894111,
"network": "mainnet",
"parentFingerprint": 1429088440,
"privateKey": "0x98a1e11f532dc7c8130246d6bce0bea5412cb333862f7de2cf94cbaedfbc734e7c6bcc37dd7270a54709ff022dbae6b9fa416a25ac760ccfeff157a469eab0a3",
"publicKey": "0x5af28295d600e796ed0b4e51ec8a0f3d5b1f8a647d3ba7a56d7eb0941561d537",
}
`);

expect(getMnemonic).toHaveBeenCalledWith('source-id');
});

it('calls `getMnemonicSeed` with a different entropy source', async () => {
const getMnemonic = jest
.fn()
.mockImplementation(() => TEST_SECRET_RECOVERY_PHRASE_BYTES);
const getMnemonicSeed = jest
.fn()
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES);

const getUnlockPromise = jest.fn();
const getClientCryptography = jest.fn().mockReturnValue({});
Expand All @@ -208,6 +274,7 @@ describe('getBip32EntropyImplementation', () => {
await getBip32EntropyImplementation({
getUnlockPromise,
getMnemonic,
getMnemonicSeed,
getClientCryptography,
})({
method: 'snap_getBip32Entropy',
Expand All @@ -232,44 +299,52 @@ describe('getBip32EntropyImplementation', () => {
}
`);

expect(getMnemonic).toHaveBeenCalledWith('source-id');
expect(getMnemonicSeed).toHaveBeenCalledWith('source-id');
});

it('uses custom client cryptography functions', async () => {
const getUnlockPromise = jest.fn().mockResolvedValue(undefined);
const getMnemonic = jest
.fn()
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_BYTES);
const getMnemonicSeed = jest
.fn()
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES);

const pbkdf2Sha512 = jest.fn().mockResolvedValue(new Uint8Array(64));
const hmacSha512 = jest
.fn()
.mockImplementation((key: Uint8Array, data: Uint8Array) =>
hmac(sha512, key, data),
);
const getClientCryptography = jest.fn().mockReturnValue({
pbkdf2Sha512,
hmacSha512,
});

expect(
await getBip32EntropyImplementation({
getUnlockPromise,
getMnemonic,
getMnemonicSeed,
getClientCryptography,
// @ts-expect-error Missing other required properties.
})({
params: { path: ['m', "44'", "1'"], curve: 'secp256k1' },
}),
).toMatchInlineSnapshot(`
{
"chainCode": "0x8472428420c7fd8ef7280545bb6d2bde1d7c6b490556ccd59895f242716388d1",
"chainCode": "0x50ccfa58a885b48b5eed09486b3948e8454f34856fb81da5d7b8519d7997abd1",
"curve": "secp256k1",
"depth": 2,
"index": 2147483649,
"masterFingerprint": 3276136937,
"masterFingerprint": 1404659567,
"network": "mainnet",
"parentFingerprint": 1981505209,
"privateKey": "0x71d945aba22cd337ff26a107073ae2606dee5dbf7ecfe5c25870b8eaf62b9f1b",
"publicKey": "0x0491c4b234ca9b394f40d90f09092e04fd3bca2aa68c57e1311b25acfd972c5a6fc7ffd19e7812127473aa2bd827917b6ec7b57bec73cf022fc1f1fa0593f48770",
"parentFingerprint": 1829122711,
"privateKey": "0xc73cedb996e7294f032766853a8b7ba11ab4ce9755fc052f2f7b9000044c99af",
"publicKey": "0x048e129862c1de5ca86468add43b001d32fd34b8113de716ecd63fa355b7f1165f0e76f5dc6095100f9fdaa76ddf28aa3f21406ac5fda7c71ffbedb45634fe2ceb",
}
`);

expect(pbkdf2Sha512).toHaveBeenCalledTimes(1);
expect(hmacSha512).toHaveBeenCalledTimes(3);
});
});
});
40 changes: 37 additions & 3 deletions packages/snaps-rpc-methods/src/restricted/getBip32Entropy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import type { NonEmptyArray } from '@metamask/utils';
import { assert } from '@metamask/utils';

import type { MethodHooksObject } from '../utils';
import { getSecretRecoveryPhrase, getNode } from '../utils';
import {
getNodeFromMnemonic,
getNodeFromSeed,
getValueFromEntropySource,
} from '../utils';

const targetName = 'snap_getBip32Entropy';

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

/**
* Get the mnemonic seed of the provided source. If no source is provided, the
* mnemonic seed of the primary keyring will be returned.
*
* @param source - The optional ID of the source to get the mnemonic of.
* @returns The mnemonic seed of the provided source, or the default source if no
* source is provided.
*/
getMnemonicSeed: (source?: string | undefined) => Promise<Uint8Array>;

/**
* Waits for the extension to be unlocked.
*
Expand Down Expand Up @@ -95,6 +109,7 @@ const specificationBuilder: PermissionSpecificationBuilder<

const methodHooks: MethodHooksObject<GetBip32EntropyMethodHooks> = {
getMnemonic: true,
getMnemonicSeed: true,
getUnlockPromise: true,
getClientCryptography: true,
};
Expand All @@ -110,6 +125,7 @@ export const getBip32EntropyBuilder = Object.freeze({
*
* @param hooks - The RPC method hooks.
* @param hooks.getMnemonic - A function to retrieve the Secret Recovery Phrase of the user.
* @param hooks.getMnemonicSeed - A function to retrieve the BIP-39 seed of the user.
* @param hooks.getUnlockPromise - A function that resolves once the MetaMask extension is unlocked
* and prompts the user to unlock their MetaMask if it is locked.
* @param hooks.getClientCryptography - A function to retrieve the cryptographic
Expand All @@ -119,6 +135,7 @@ export const getBip32EntropyBuilder = Object.freeze({
*/
export function getBip32EntropyImplementation({
getMnemonic,
getMnemonicSeed,
getUnlockPromise,
getClientCryptography,
}: GetBip32EntropyMethodHooks) {
Expand All @@ -130,12 +147,29 @@ export function getBip32EntropyImplementation({
const { params } = args;
assert(params);

const secretRecoveryPhrase = await getSecretRecoveryPhrase(
// Using the seed is much faster, but we can only do it for these specific curves.
if (params.curve === 'secp256k1' || params.curve === 'ed25519') {
const seed = await getValueFromEntropySource(
getMnemonicSeed,
params.source,
);

const node = await getNodeFromSeed({
curve: params.curve,
path: params.path,
seed,
cryptographicFunctions: getClientCryptography(),
});

return node.toJSON();
}

const secretRecoveryPhrase = await getValueFromEntropySource(
getMnemonic,
params.source,
);

const node = await getNode({
const node = await getNodeFromMnemonic({
curve: params.curve,
path: params.path,
secretRecoveryPhrase,
Expand Down
Loading
Loading