Skip to content
Merged
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
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,53 @@ 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');
expect(getMnemonicSeed).not.toHaveBeenCalled();
});

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 +275,7 @@ describe('getBip32EntropyImplementation', () => {
await getBip32EntropyImplementation({
getUnlockPromise,
getMnemonic,
getMnemonicSeed,
getClientCryptography,
})({
method: 'snap_getBip32Entropy',
Expand All @@ -232,44 +300,53 @@ describe('getBip32EntropyImplementation', () => {
}
`);

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

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