diff --git a/packages/snaps-rpc-methods/jest.config.js b/packages/snaps-rpc-methods/jest.config.js index f9f0fb5aa3..906d4228db 100644 --- a/packages/snaps-rpc-methods/jest.config.js +++ b/packages/snaps-rpc-methods/jest.config.js @@ -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, }, }, }); diff --git a/packages/snaps-rpc-methods/src/restricted/getBip32Entropy.test.ts b/packages/snaps-rpc-methods/src/restricted/getBip32Entropy.test.ts index 075ddeb50f..d584035b1f 100644 --- a/packages/snaps-rpc-methods/src/restricted/getBip32Entropy.test.ts +++ b/packages/snaps-rpc-methods/src/restricted/getBip32Entropy.test.ts @@ -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, @@ -13,6 +16,7 @@ import { describe('specificationBuilder', () => { const methodHooks = { getMnemonic: jest.fn(), + getMnemonicSeed: jest.fn(), getUnlockPromise: jest.fn(), getClientCryptography: jest.fn(), }; @@ -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. })({ @@ -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. })({ @@ -131,6 +143,9 @@ 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({}); @@ -138,6 +153,7 @@ describe('getBip32EntropyImplementation', () => { await getBip32EntropyImplementation({ getUnlockPromise, getMnemonic, + getMnemonicSeed, getClientCryptography, // @ts-expect-error Missing other required properties. })({ @@ -166,6 +182,9 @@ 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({}); @@ -173,6 +192,7 @@ describe('getBip32EntropyImplementation', () => { await getBip32EntropyImplementation({ getUnlockPromise, getMnemonic, + getMnemonicSeed, getClientCryptography, // @ts-expect-error Missing other required properties. })({ @@ -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({}); @@ -208,6 +275,7 @@ describe('getBip32EntropyImplementation', () => { await getBip32EntropyImplementation({ getUnlockPromise, getMnemonic, + getMnemonicSeed, getClientCryptography, })({ method: 'snap_getBip32Entropy', @@ -232,7 +300,8 @@ describe('getBip32EntropyImplementation', () => { } `); - expect(getMnemonic).toHaveBeenCalledWith('source-id'); + expect(getMnemonicSeed).toHaveBeenCalledWith('source-id'); + expect(getMnemonic).not.toHaveBeenCalled(); }); it('uses custom client cryptography functions', async () => { @@ -240,16 +309,24 @@ describe('getBip32EntropyImplementation', () => { 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. })({ @@ -257,19 +334,19 @@ describe('getBip32EntropyImplementation', () => { }), ).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); }); }); }); diff --git a/packages/snaps-rpc-methods/src/restricted/getBip32Entropy.ts b/packages/snaps-rpc-methods/src/restricted/getBip32Entropy.ts index 2a725e6686..ecb67f6727 100644 --- a/packages/snaps-rpc-methods/src/restricted/getBip32Entropy.ts +++ b/packages/snaps-rpc-methods/src/restricted/getBip32Entropy.ts @@ -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'; @@ -31,6 +35,16 @@ export type GetBip32EntropyMethodHooks = { */ getMnemonic: (source?: string | undefined) => Promise; + /** + * 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; + /** * Waits for the extension to be unlocked. * @@ -95,6 +109,7 @@ const specificationBuilder: PermissionSpecificationBuilder< const methodHooks: MethodHooksObject = { getMnemonic: true, + getMnemonicSeed: true, getUnlockPromise: true, getClientCryptography: true, }; @@ -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 @@ -119,6 +135,7 @@ export const getBip32EntropyBuilder = Object.freeze({ */ export function getBip32EntropyImplementation({ getMnemonic, + getMnemonicSeed, getUnlockPromise, getClientCryptography, }: GetBip32EntropyMethodHooks) { @@ -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, diff --git a/packages/snaps-rpc-methods/src/restricted/getBip32PublicKey.test.ts b/packages/snaps-rpc-methods/src/restricted/getBip32PublicKey.test.ts index b77501a220..fef25b3e89 100644 --- a/packages/snaps-rpc-methods/src/restricted/getBip32PublicKey.test.ts +++ b/packages/snaps-rpc-methods/src/restricted/getBip32PublicKey.test.ts @@ -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 { getBip32PublicKeyBuilder, @@ -13,6 +16,7 @@ import { describe('specificationBuilder', () => { const methodHooks = { getMnemonic: jest.fn(), + getMnemonicSeed: jest.fn(), getUnlockPromise: jest.fn(), getClientCryptography: jest.fn(), }; @@ -66,12 +70,16 @@ describe('getBip32PublicKeyImplementation', () => { 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 getBip32PublicKeyImplementation({ getUnlockPromise, getMnemonic, + getMnemonicSeed, getClientCryptography, // @ts-expect-error Missing other required properties. })({ @@ -90,12 +98,16 @@ describe('getBip32PublicKeyImplementation', () => { 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 getBip32PublicKeyImplementation({ getUnlockPromise, getMnemonic, + getMnemonicSeed, getClientCryptography, // @ts-expect-error Missing other required properties. })({ @@ -114,12 +126,16 @@ describe('getBip32PublicKeyImplementation', () => { 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 getBip32PublicKeyImplementation({ getUnlockPromise, getMnemonic, + getMnemonicSeed, getClientCryptography, // @ts-expect-error Missing other required properties. })({ @@ -138,12 +154,16 @@ describe('getBip32PublicKeyImplementation', () => { 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 getBip32PublicKeyImplementation({ getUnlockPromise, getMnemonic, + getMnemonicSeed, getClientCryptography, // @ts-expect-error Missing other required properties. })({ @@ -162,6 +182,9 @@ describe('getBip32PublicKeyImplementation', () => { 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({}); @@ -170,6 +193,41 @@ describe('getBip32PublicKeyImplementation', () => { await getBip32PublicKeyImplementation({ getUnlockPromise, getMnemonic, + getMnemonicSeed, + getClientCryptography, + })({ + method: 'snap_getBip32PublicKey', + context: { origin: MOCK_SNAP_ID }, + params: { + path: ['m', "44'", "1'", '1', '2', '3'], + curve: 'ed25519Bip32', + source: 'source-id', + }, + }), + ).toMatchInlineSnapshot( + `"0x03303da49ddfafc90587b7559eacdd5523028e75be81f2a9f158733fee1211a6"`, + ); + + 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({}); + + expect( + await getBip32PublicKeyImplementation({ + getUnlockPromise, + getMnemonic, + getMnemonicSeed, getClientCryptography, })({ method: 'snap_getBip32PublicKey', @@ -184,7 +242,8 @@ describe('getBip32PublicKeyImplementation', () => { `"0x042de17487a660993177ce2a85bb73b6cd9ad436184d57bdf5a93f5db430bea914f7c31d378fe68f4723b297a04e49ef55fbf490605c4a3f9ca947a4af4f06526a"`, ); - expect(getMnemonic).toHaveBeenCalledWith('source-id'); + expect(getMnemonicSeed).toHaveBeenCalledWith('source-id'); + expect(getMnemonic).not.toHaveBeenCalled(); }); it('uses custom client cryptography functions', async () => { @@ -192,16 +251,24 @@ describe('getBip32PublicKeyImplementation', () => { 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 getBip32PublicKeyImplementation({ getUnlockPromise, getMnemonic, + getMnemonicSeed, getClientCryptography, // @ts-expect-error Missing other required properties. })({ @@ -212,10 +279,10 @@ describe('getBip32PublicKeyImplementation', () => { }, }), ).toMatchInlineSnapshot( - `"0x03102d63c39b6dda3f9aa06b247c50653cd9d01a91efce00ccc8735e9714058a01"`, + `"0x022de17487a660993177ce2a85bb73b6cd9ad436184d57bdf5a93f5db430bea914"`, ); - expect(pbkdf2Sha512).toHaveBeenCalledTimes(1); + expect(hmacSha512).toHaveBeenCalledTimes(6); }); }); }); diff --git a/packages/snaps-rpc-methods/src/restricted/getBip32PublicKey.ts b/packages/snaps-rpc-methods/src/restricted/getBip32PublicKey.ts index c0ff3d1c4e..5340ec7f6f 100644 --- a/packages/snaps-rpc-methods/src/restricted/getBip32PublicKey.ts +++ b/packages/snaps-rpc-methods/src/restricted/getBip32PublicKey.ts @@ -22,7 +22,11 @@ import type { NonEmptyArray } from '@metamask/utils'; import { assertStruct } from '@metamask/utils'; import type { MethodHooksObject } from '../utils'; -import { getSecretRecoveryPhrase, getNode } from '../utils'; +import { + getValueFromEntropySource, + getNodeFromMnemonic, + getNodeFromSeed, +} from '../utils'; const targetName = 'snap_getBip32PublicKey'; @@ -37,6 +41,16 @@ export type GetBip32PublicKeyMethodHooks = { */ getMnemonic: (source?: string | undefined) => Promise; + /** + * 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; + /** * Waits for the extension to be unlocked. * @@ -110,6 +124,7 @@ const specificationBuilder: PermissionSpecificationBuilder< const methodHooks: MethodHooksObject = { getMnemonic: true, + getMnemonicSeed: true, getUnlockPromise: true, getClientCryptography: true, }; @@ -125,6 +140,7 @@ export const getBip32PublicKeyBuilder = 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 @@ -134,6 +150,7 @@ export const getBip32PublicKeyBuilder = Object.freeze({ */ export function getBip32PublicKeyImplementation({ getMnemonic, + getMnemonicSeed, getUnlockPromise, getClientCryptography, }: GetBip32PublicKeyMethodHooks) { @@ -150,12 +167,34 @@ export function getBip32PublicKeyImplementation({ ); const { params } = args; - 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(), + }); + + if (params.compressed) { + return node.compressedPublicKey; + } + + return node.publicKey; + } + + const secretRecoveryPhrase = await getValueFromEntropySource( getMnemonic, params.source, ); - const node = await getNode({ + const node = await getNodeFromMnemonic({ curve: params.curve, path: params.path, secretRecoveryPhrase, diff --git a/packages/snaps-rpc-methods/src/restricted/getBip44Entropy.test.ts b/packages/snaps-rpc-methods/src/restricted/getBip44Entropy.test.ts index 6ba3a52322..bfcd794948 100644 --- a/packages/snaps-rpc-methods/src/restricted/getBip44Entropy.test.ts +++ b/packages/snaps-rpc-methods/src/restricted/getBip44Entropy.test.ts @@ -2,8 +2,10 @@ import { SubjectType, PermissionType } from '@metamask/permission-controller'; 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 { getBip44EntropyBuilder, @@ -12,7 +14,7 @@ import { describe('specificationBuilder', () => { const methodHooks = { - getMnemonic: jest.fn(), + getMnemonicSeed: jest.fn(), getUnlockPromise: jest.fn(), getClientCryptography: jest.fn(), }; @@ -63,15 +65,15 @@ describe('getBip44EntropyImplementation', () => { describe('getBip44Entropy', () => { it('derives the entropy from the path', async () => { const getUnlockPromise = jest.fn().mockResolvedValue(undefined); - const getMnemonic = jest + const getMnemonicSeed = jest .fn() - .mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_BYTES); + .mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES); const getClientCryptography = jest.fn().mockReturnValue({}); expect( await getBip44EntropyImplementation({ getUnlockPromise, - getMnemonic, + getMnemonicSeed, getClientCryptography, // @ts-expect-error Missing other required properties. })({ @@ -94,9 +96,9 @@ describe('getBip44EntropyImplementation', () => { }); it('calls `getMnemonic` with a different entropy source', async () => { - const getMnemonic = jest + const getMnemonicSeed = jest .fn() - .mockImplementation(() => TEST_SECRET_RECOVERY_PHRASE_BYTES); + .mockImplementation(() => TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES); const getUnlockPromise = jest.fn(); const getClientCryptography = jest.fn().mockReturnValue({}); @@ -104,7 +106,7 @@ describe('getBip44EntropyImplementation', () => { expect( await getBip44EntropyImplementation({ getUnlockPromise, - getMnemonic, + getMnemonicSeed, getClientCryptography, })({ method: 'snap_getBip44Entropy', @@ -126,24 +128,28 @@ describe('getBip44EntropyImplementation', () => { } `); - 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 + const getMnemonicSeed = jest .fn() - .mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_BYTES); + .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 getBip44EntropyImplementation({ getUnlockPromise, - getMnemonic, + getMnemonicSeed, getClientCryptography, // @ts-expect-error Missing other required properties. })({ @@ -151,20 +157,20 @@ describe('getBip44EntropyImplementation', () => { }), ).toMatchInlineSnapshot(` { - "chainCode": "0x8472428420c7fd8ef7280545bb6d2bde1d7c6b490556ccd59895f242716388d1", + "chainCode": "0x50ccfa58a885b48b5eed09486b3948e8454f34856fb81da5d7b8519d7997abd1", "coin_type": 1, "depth": 2, "index": 2147483649, - "masterFingerprint": 3276136937, + "masterFingerprint": 1404659567, "network": "mainnet", - "parentFingerprint": 1981505209, + "parentFingerprint": 1829122711, "path": "m / bip32:44' / bip32:1'", - "privateKey": "0x71d945aba22cd337ff26a107073ae2606dee5dbf7ecfe5c25870b8eaf62b9f1b", - "publicKey": "0x0491c4b234ca9b394f40d90f09092e04fd3bca2aa68c57e1311b25acfd972c5a6fc7ffd19e7812127473aa2bd827917b6ec7b57bec73cf022fc1f1fa0593f48770", + "privateKey": "0xc73cedb996e7294f032766853a8b7ba11ab4ce9755fc052f2f7b9000044c99af", + "publicKey": "0x048e129862c1de5ca86468add43b001d32fd34b8113de716ecd63fa355b7f1165f0e76f5dc6095100f9fdaa76ddf28aa3f21406ac5fda7c71ffbedb45634fe2ceb", } `); - expect(pbkdf2Sha512).toHaveBeenCalledTimes(1); + expect(hmacSha512).toHaveBeenCalledTimes(3); }); }); }); diff --git a/packages/snaps-rpc-methods/src/restricted/getBip44Entropy.ts b/packages/snaps-rpc-methods/src/restricted/getBip44Entropy.ts index 709e195fff..990a8e4b49 100644 --- a/packages/snaps-rpc-methods/src/restricted/getBip44Entropy.ts +++ b/packages/snaps-rpc-methods/src/restricted/getBip44Entropy.ts @@ -16,20 +16,20 @@ import { SnapCaveatType } from '@metamask/snaps-utils'; import type { NonEmptyArray } from '@metamask/utils'; import type { MethodHooksObject } from '../utils'; -import { getSecretRecoveryPhrase } from '../utils'; +import { getValueFromEntropySource } from '../utils'; const targetName = 'snap_getBip44Entropy'; export type GetBip44EntropyMethodHooks = { /** - * Get the mnemonic of the provided source. If no source is provided, the - * mnemonic of the primary keyring will be returned. + * 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 of the provided source, or the default source if no + * @returns The mnemonic seed of the provided source, or the default source if no * source is provided. */ - getMnemonic: (source?: string | undefined) => Promise; + getMnemonicSeed: (source?: string | undefined) => Promise; /** * Waits for the extension to be unlocked. @@ -95,7 +95,7 @@ const specificationBuilder: PermissionSpecificationBuilder< }; const methodHooks: MethodHooksObject = { - getMnemonic: true, + getMnemonicSeed: true, getUnlockPromise: true, getClientCryptography: true, }; @@ -110,7 +110,7 @@ export const getBip44EntropyBuilder = Object.freeze({ * Builds the method implementation for `snap_getBip44Entropy`. * * @param hooks - The RPC method hooks. - * @param hooks.getMnemonic - A function to retrieve the Secret Recovery Phrase + * @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 @@ -121,7 +121,7 @@ export const getBip44EntropyBuilder = Object.freeze({ * @throws If the params are invalid. */ export function getBip44EntropyImplementation({ - getMnemonic, + getMnemonicSeed, getUnlockPromise, getClientCryptography, }: GetBip44EntropyMethodHooks) { @@ -132,14 +132,16 @@ export function getBip44EntropyImplementation({ // `args.params` is validated by the decorator, so it's safe to assert here. const params = args.params as GetBip44EntropyParams; - const secretRecoveryPhrase = await getSecretRecoveryPhrase( - getMnemonic, + const seed = await getValueFromEntropySource( + getMnemonicSeed, params.source, ); - const node = await BIP44CoinTypeNode.fromDerivationPath( - [secretRecoveryPhrase, `bip32:44'`, `bip32:${params.coinType}'`], - 'mainnet', + const node = await BIP44CoinTypeNode.fromSeed( + { + derivationPath: [seed, `bip32:44'`, `bip32:${params.coinType}'`], + network: 'mainnet', + }, getClientCryptography(), ); diff --git a/packages/snaps-rpc-methods/src/restricted/getEntropy.test.ts b/packages/snaps-rpc-methods/src/restricted/getEntropy.test.ts index 5edc4c266b..b666d5b98e 100644 --- a/packages/snaps-rpc-methods/src/restricted/getEntropy.test.ts +++ b/packages/snaps-rpc-methods/src/restricted/getEntropy.test.ts @@ -1,8 +1,10 @@ import { PermissionType, SubjectType } from '@metamask/permission-controller'; 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 { getEntropyBuilder } from './getEntropy'; @@ -12,7 +14,7 @@ describe('getEntropyBuilder', () => { targetName: 'snap_getEntropy', specificationBuilder: expect.any(Function), methodHooks: { - getMnemonic: true, + getMnemonicSeed: true, getUnlockPromise: true, getClientCryptography: true, }, @@ -21,7 +23,7 @@ describe('getEntropyBuilder', () => { it('returns the expected specification', () => { const methodHooks = { - getMnemonic: jest.fn(), + getMnemonicSeed: jest.fn(), getUnlockPromise: jest.fn(), getClientCryptography: jest.fn(), }; @@ -40,15 +42,15 @@ describe('getEntropyBuilder', () => { describe('getEntropyImplementation', () => { it('returns the expected result', async () => { - const getMnemonic = jest + const getMnemonicSeed = jest .fn() - .mockImplementation(() => TEST_SECRET_RECOVERY_PHRASE_BYTES); + .mockImplementation(() => TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES); const getUnlockPromise = jest.fn(); const getClientCryptography = jest.fn().mockReturnValue({}); const methodHooks = { - getMnemonic, + getMnemonicSeed, getUnlockPromise, getClientCryptography, }; @@ -74,15 +76,15 @@ describe('getEntropyImplementation', () => { }); it('calls `getMnemonic` with a different entropy source', async () => { - const getMnemonic = jest + const getMnemonicSeed = jest .fn() - .mockImplementation(() => TEST_SECRET_RECOVERY_PHRASE_BYTES); + .mockImplementation(() => TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES); const getUnlockPromise = jest.fn(); const getClientCryptography = jest.fn().mockReturnValue({}); const methodHooks = { - getMnemonic, + getMnemonicSeed, getUnlockPromise, getClientCryptography, }; @@ -107,22 +109,26 @@ describe('getEntropyImplementation', () => { '0x6d8e92de419401c7da3cedd5f60ce5635b26059c2a4a8003877fec83653a4921', ); - 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 + const getMnemonicSeed = jest .fn() - .mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_BYTES); + .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, }); const methodHooks = { - getMnemonic, + getMnemonicSeed, getUnlockPromise, getClientCryptography, }; @@ -143,9 +149,9 @@ describe('getEntropyImplementation', () => { }); expect(result).toBe( - '0x9bea47f2180fd874147f2f455a5ccc779826cfeff005605190cf0c568b3de7b5', + '0x6d8e92de419401c7da3cedd5f60ce5635b26059c2a4a8003877fec83653a4921', ); - expect(pbkdf2Sha512).toHaveBeenCalledTimes(1); + expect(hmacSha512).toHaveBeenCalledTimes(10); }); }); diff --git a/packages/snaps-rpc-methods/src/restricted/getEntropy.ts b/packages/snaps-rpc-methods/src/restricted/getEntropy.ts index f5969ecb8f..5146dcd6b0 100644 --- a/packages/snaps-rpc-methods/src/restricted/getEntropy.ts +++ b/packages/snaps-rpc-methods/src/restricted/getEntropy.ts @@ -14,7 +14,7 @@ import type { NonEmptyArray } from '@metamask/utils'; import { assertStruct } from '@metamask/utils'; import type { MethodHooksObject } from '../utils'; -import { getSecretRecoveryPhrase, deriveEntropyFromMnemonic } from '../utils'; +import { getValueFromEntropySource, deriveEntropyFromSeed } from '../utils'; const targetName = 'snap_getEntropy'; @@ -62,7 +62,7 @@ const specificationBuilder: PermissionSpecificationBuilder< }; const methodHooks: MethodHooksObject = { - getMnemonic: true, + getMnemonicSeed: true, getUnlockPromise: true, getClientCryptography: true, }; @@ -75,14 +75,14 @@ export const getEntropyBuilder = Object.freeze({ export type GetEntropyHooks = { /** - * Get the mnemonic of the provided source. If no source is provided, the - * mnemonic of the primary keyring will be returned. + * 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 of the provided source, or the default source if no + * @returns The mnemonic seed of the provided source, or the default source if no * source is provided. */ - getMnemonic: (source?: string | undefined) => Promise; + getMnemonicSeed: (source?: string | undefined) => Promise; /** * Waits for the extension to be unlocked. @@ -107,8 +107,8 @@ export type GetEntropyHooks = { * [SIP-6](https://metamask.github.io/SIPs/SIPS/sip-6). * * @param hooks - The RPC method hooks. - * @param hooks.getMnemonic - The method to get the mnemonic of the user's - * primary keyring. + * @param hooks.getMnemonicSeed - A function to retrieve the BIP-39 seed + * of the user. * @param hooks.getUnlockPromise - The method to get a promise that resolves * once the extension is unlocked. * @param hooks.getClientCryptography - A function to retrieve the cryptographic @@ -116,7 +116,7 @@ export type GetEntropyHooks = { * @returns The method implementation. */ function getEntropyImplementation({ - getMnemonic, + getMnemonicSeed, getUnlockPromise, getClientCryptography, }: GetEntropyHooks) { @@ -136,15 +136,15 @@ function getEntropyImplementation({ ); await getUnlockPromise(true); - const mnemonicPhrase = await getSecretRecoveryPhrase( - getMnemonic, + const seed = await getValueFromEntropySource( + getMnemonicSeed, params.source, ); - return deriveEntropyFromMnemonic({ + return deriveEntropyFromSeed({ input: origin, salt: params.salt, - mnemonicPhrase, + seed, magic: SIP_6_MAGIC_VALUE, cryptographicFunctions: getClientCryptography(), }); diff --git a/packages/snaps-rpc-methods/src/utils.test.ts b/packages/snaps-rpc-methods/src/utils.test.ts index dfc4f967a0..0436b90f12 100644 --- a/packages/snaps-rpc-methods/src/utils.test.ts +++ b/packages/snaps-rpc-methods/src/utils.test.ts @@ -8,32 +8,15 @@ import { create, is } from '@metamask/superstruct'; import { ENTROPY_VECTORS } from './__fixtures__'; import { - deriveEntropyFromMnemonic, deriveEntropyFromSeed, - getNode, + getNodeFromMnemonic, + getNodeFromSeed, getPathPrefix, - getSecretRecoveryPhrase, + getValueFromEntropySource, isValidStateKey, StateKeyStruct, } from './utils'; -describe('deriveEntropyFromMnemonic', () => { - it.each(ENTROPY_VECTORS)( - 'derives entropy from the given parameters', - async ({ snapId, salt, entropy }) => { - expect( - await deriveEntropyFromMnemonic({ - input: snapId, - salt, - mnemonicPhrase: TEST_SECRET_RECOVERY_PHRASE_BYTES, - magic: SIP_6_MAGIC_VALUE, - cryptographicFunctions: {}, - }), - ).toStrictEqual(entropy); - }, - ); -}); - describe('deriveEntropyFromSeed', () => { it.each(ENTROPY_VECTORS)( 'derives entropy from the given parameters', @@ -72,9 +55,9 @@ describe('getPathPrefix', () => { }); }); -describe('getNode', () => { +describe('getNodeFromMnemonic', () => { it('returns a secp256k1 node', async () => { - const node = await getNode({ + const node = await getNodeFromMnemonic({ curve: 'secp256k1', path: ['m', "44'", "1'"], secretRecoveryPhrase: TEST_SECRET_RECOVERY_PHRASE_BYTES, @@ -97,7 +80,7 @@ describe('getNode', () => { }); it('returns an ed25519 node', async () => { - const node = await getNode({ + const node = await getNodeFromMnemonic({ curve: 'ed25519', path: ['m', "44'", "1'"], secretRecoveryPhrase: TEST_SECRET_RECOVERY_PHRASE_BYTES, @@ -120,6 +103,54 @@ describe('getNode', () => { }); }); +describe('getNodeFromSeed', () => { + it('returns a secp256k1 node', async () => { + const node = await getNodeFromSeed({ + curve: 'secp256k1', + path: ['m', "44'", "1'"], + seed: TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES, + cryptographicFunctions: {}, + }); + + expect(node).toMatchInlineSnapshot(` + { + "chainCode": "0x50ccfa58a885b48b5eed09486b3948e8454f34856fb81da5d7b8519d7997abd1", + "curve": "secp256k1", + "depth": 2, + "index": 2147483649, + "masterFingerprint": 1404659567, + "network": "mainnet", + "parentFingerprint": 1829122711, + "privateKey": "0xc73cedb996e7294f032766853a8b7ba11ab4ce9755fc052f2f7b9000044c99af", + "publicKey": "0x048e129862c1de5ca86468add43b001d32fd34b8113de716ecd63fa355b7f1165f0e76f5dc6095100f9fdaa76ddf28aa3f21406ac5fda7c71ffbedb45634fe2ceb", + } + `); + }); + + it('returns an ed25519 node', async () => { + const node = await getNodeFromSeed({ + curve: 'ed25519', + path: ['m', "44'", "1'"], + seed: TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES, + cryptographicFunctions: {}, + }); + + expect(node).toMatchInlineSnapshot(` + { + "chainCode": "0xcecf799c541108016e8febb5956379533702574d509b52e1078df95fbc6ae054", + "curve": "ed25519", + "depth": 2, + "index": 2147483649, + "masterFingerprint": 650419359, + "network": "mainnet", + "parentFingerprint": 4080844380, + "privateKey": "0x9dee85af06f9b94d2451549f5a9b0a3bbba9e2513daebc793ca5c9a13e80cafa", + "publicKey": "0x00c9aaf347832dc3b1dbb7aab4f41e5e04c64446b819c0761571c27b9f90eacb27", + } + `); + }); +}); + describe('isValidStateKey', () => { it.each(['foo', 'foo.bar', 'foo.bar.baz'])( 'returns `true` for "%s"', @@ -151,13 +182,13 @@ describe('StateKeyStruct', () => { ); }); -describe('getSecretRecoveryPhrase', () => { +describe('getValueFromEntropySource', () => { it('returns the secret recovery phrase', async () => { const getMnemonic = jest .fn() .mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_BYTES); - const secretRecoveryPhrase = await getSecretRecoveryPhrase( + const secretRecoveryPhrase = await getValueFromEntropySource( getMnemonic, 'foo', ); @@ -169,7 +200,7 @@ describe('getSecretRecoveryPhrase', () => { it('throws an invalid params error if `getMnemonic` throws with an error', async () => { const getMnemonic = jest.fn().mockRejectedValue(new Error('foo')); - await expect(getSecretRecoveryPhrase(getMnemonic)).rejects.toThrow( + await expect(getValueFromEntropySource(getMnemonic)).rejects.toThrow( rpcErrors.invalidParams({ message: 'foo', }), @@ -179,7 +210,7 @@ describe('getSecretRecoveryPhrase', () => { it('throws an internal error if `getMnemonic` throws with a non-error', async () => { const getMnemonic = jest.fn().mockRejectedValue('foo'); - await expect(getSecretRecoveryPhrase(getMnemonic)).rejects.toThrow( + await expect(getValueFromEntropySource(getMnemonic)).rejects.toThrow( rpcErrors.internal({ message: 'An unknown error occurred.', data: { diff --git a/packages/snaps-rpc-methods/src/utils.ts b/packages/snaps-rpc-methods/src/utils.ts index 37f58ff2a7..203fe8f642 100644 --- a/packages/snaps-rpc-methods/src/utils.ts +++ b/packages/snaps-rpc-methods/src/utils.ts @@ -9,7 +9,6 @@ import { SLIP10Node } from '@metamask/key-tree'; import { rpcErrors } from '@metamask/rpc-errors'; import type { MagicValue } from '@metamask/snaps-utils'; import { refine, string } from '@metamask/superstruct'; -import type { Hex } from '@metamask/utils'; import { assertExhaustive, add0x, @@ -120,13 +119,6 @@ type SeedDeriveEntropyOptions = BaseDeriveEntropyOptions & { seed: Uint8Array; }; -type MnemonicDeriveEntropyOptions = BaseDeriveEntropyOptions & { - /** - * The mnemonic phrase to use for entropy derivation. - */ - mnemonicPhrase: Uint8Array; -}; - /** * Get the derivation path to use for entropy derivation. * @@ -202,51 +194,6 @@ export async function deriveEntropyFromSeed({ return add0x(privateKey); } -/** - * Derive entropy from the given mnemonic phrase and salt. - * - * This is based on the reference implementation of - * [SIP-6](https://metamask.github.io/SIPs/SIPS/sip-6). - * - * @param options - The options for entropy derivation. - * @param options.input - The input value to derive entropy from. - * @param options.salt - An optional salt to use when deriving entropy. - * @param options.mnemonicPhrase - The mnemonic phrase to use for entropy - * derivation. - * @param options.magic - A hardened BIP-32 index, which is used to derive the - * root key from the mnemonic phrase. - * @param options.cryptographicFunctions - The cryptographic functions to use - * for the derivation. - * @returns The derived entropy. - */ -export async function deriveEntropyFromMnemonic({ - input, - salt = '', - mnemonicPhrase, - magic, - cryptographicFunctions, -}: MnemonicDeriveEntropyOptions): Promise { - const computedDerivationPath = getEntropyDerivationPath({ - input, - salt, - magic, - }); - - // Derive the private key using BIP-32. - const { privateKey } = await SLIP10Node.fromDerivationPath( - { - derivationPath: [mnemonicPhrase, ...computedDerivationPath], - curve: 'secp256k1', - }, - cryptographicFunctions, - ); - - // This should never happen, but this keeps TypeScript happy. - assert(privateKey, 'Failed to derive the entropy.'); - - return add0x(privateKey); -} - /** * Get the path prefix to use for key derivation in `key-tree`. This assumes the * following: @@ -277,13 +224,20 @@ export function getPathPrefix( } } -type GetNodeArgs = { +type BaseGetNodeArgs = { curve: SupportedCurve; - secretRecoveryPhrase: Uint8Array; path: string[]; cryptographicFunctions: CryptographicFunctions | undefined; }; +type GetNodeArgsMnemonic = BaseGetNodeArgs & { + secretRecoveryPhrase: Uint8Array; +}; + +type GetNodeArgsSeed = BaseGetNodeArgs & { + seed: Uint8Array; +}; + /** * Get a `key-tree`-compatible node. * @@ -300,12 +254,12 @@ type GetNodeArgs = { * for the node. * @returns The `key-tree` SLIP-10 node. */ -export async function getNode({ +export async function getNodeFromMnemonic({ curve, secretRecoveryPhrase, path, cryptographicFunctions, -}: GetNodeArgs) { +}: GetNodeArgsMnemonic) { const prefix = getPathPrefix(curve); return await SLIP10Node.fromDerivationPath( @@ -322,6 +276,44 @@ export async function getNode({ ); } +/** + * Get a `key-tree`-compatible node. + * + * Note: This function assumes that all the parameters have been validated + * beforehand. + * + * @param options - The derivation options. + * @param options.curve - The curve to use for derivation. + * @param options.seed - The BIP-39 to use for + * derivation. + * @param options.path - The derivation path to use as array, starting with an + * "m" as the first item. + * @param options.cryptographicFunctions - The cryptographic functions to use + * for the node. + * @returns The `key-tree` SLIP-10 node. + */ +export async function getNodeFromSeed({ + curve, + seed, + path, + cryptographicFunctions, +}: GetNodeArgsSeed) { + const prefix = getPathPrefix(curve); + + return await SLIP10Node.fromSeed( + { + curve, + derivationPath: [ + seed, + ...(path.slice(1).map((index) => `${prefix}:${index}`) as + | BIP32Node[] + | SLIP10PathNode[]), + ], + }, + cryptographicFunctions, + ); +} + /** * Validate the key of a state object. * @@ -345,19 +337,20 @@ export const StateKeyStruct = refine(string(), 'state key', (value) => { }); /** - * Get the secret recovery phrase of the user. This calls the `getMnemonic` hook - * and handles any errors that occur, throwing formatted JSON-RPC errors. + * Get a value using the entropy source hooks: getMnemonic or getMnemonicSeed. + * This function calls the passed hook and handles any errors that occur, + * throwing formatted JSON-RPC errors. * - * @param getMnemonic - The `getMnemonic` hook. + * @param hook - The hook. * @param source - The entropy source to use. * @returns The secret recovery phrase. */ -export async function getSecretRecoveryPhrase( - getMnemonic: (source?: string | undefined) => Promise, +export async function getValueFromEntropySource( + hook: (source?: string | undefined) => Promise, source?: string | undefined, ): Promise { try { - return await getMnemonic(source); + return await hook(source); } catch (error) { if (error instanceof Error) { throw rpcErrors.invalidParams({ diff --git a/packages/snaps-simulation/src/methods/hooks/get-mnemonic-seed.test.ts b/packages/snaps-simulation/src/methods/hooks/get-mnemonic-seed.test.ts new file mode 100644 index 0000000000..691a655f65 --- /dev/null +++ b/packages/snaps-simulation/src/methods/hooks/get-mnemonic-seed.test.ts @@ -0,0 +1,49 @@ +import { TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES } from '@metamask/snaps-utils/test-utils'; + +import { getGetMnemonicSeedImplementation } from './get-mnemonic-seed'; +import { DEFAULT_ALTERNATIVE_SRP } from '../../constants'; + +describe('getGetMnemonicSeedImplementation', () => { + const alternativeSeedBytes = new Uint8Array([ + 94, 176, 11, 189, 220, 240, 105, 8, 72, 137, 168, 171, 145, 85, 86, 129, + 101, 245, 196, 83, 204, 184, 94, 112, 129, 26, 174, 214, 246, 218, 95, 193, + 154, 90, 196, 11, 56, 156, 211, 112, 208, 134, 32, 109, 236, 138, 166, 196, + 61, 174, 166, 105, 15, 32, 173, 61, 141, 72, 178, 210, 206, 158, 56, 228, + ]); + + it('returns the default mnemonic seed', async () => { + const getMnemonicSeed = getGetMnemonicSeedImplementation(); + expect(await getMnemonicSeed()).toStrictEqual( + TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES, + ); + + expect(await getMnemonicSeed('default')).toStrictEqual( + TEST_SECRET_RECOVERY_PHRASE_SEED_BYTES, + ); + }); + + it('returns the seed of the provided default mnemonic phrase', async () => { + const getMnemonicSeed = getGetMnemonicSeedImplementation( + DEFAULT_ALTERNATIVE_SRP, + ); + expect(await getMnemonicSeed()).toStrictEqual(alternativeSeedBytes); + + expect(await getMnemonicSeed('default')).toStrictEqual( + alternativeSeedBytes, + ); + }); + + it('returns the alternative mnemonic seed', async () => { + const getMnemonicSeed = getGetMnemonicSeedImplementation(); + expect(await getMnemonicSeed('alternative')).toStrictEqual( + alternativeSeedBytes, + ); + }); + + it('throws an error for an unknown entropy source', async () => { + const getMnemonicSeed = getGetMnemonicSeedImplementation(); + await expect(getMnemonicSeed('unknown')).rejects.toThrow( + 'Entropy source with ID "unknown" not found.', + ); + }); +}); diff --git a/packages/snaps-simulation/src/methods/hooks/get-mnemonic-seed.ts b/packages/snaps-simulation/src/methods/hooks/get-mnemonic-seed.ts new file mode 100644 index 0000000000..be5453f030 --- /dev/null +++ b/packages/snaps-simulation/src/methods/hooks/get-mnemonic-seed.ts @@ -0,0 +1,29 @@ +import { mnemonicToSeed } from '@metamask/key-tree'; + +import { DEFAULT_ALTERNATIVE_SRP, DEFAULT_SRP } from '../../constants'; + +/** + * Get the implementation of the `getMnemonicSeed` method. + * + * @param defaultSecretRecoveryPhrase - The default secret recovery phrase to + * use. + * @returns The implementation of the `getMnemonicSeed` method. + */ +export function getGetMnemonicSeedImplementation( + defaultSecretRecoveryPhrase: string = DEFAULT_SRP, +) { + return async (source?: string | undefined): Promise => { + if (!source) { + return mnemonicToSeed(defaultSecretRecoveryPhrase); + } + + switch (source) { + case 'default': + return mnemonicToSeed(defaultSecretRecoveryPhrase); + case 'alternative': + return mnemonicToSeed(DEFAULT_ALTERNATIVE_SRP); + default: + throw new Error(`Entropy source with ID "${source}" not found.`); + } + }; +} diff --git a/packages/snaps-simulation/src/simulation.ts b/packages/snaps-simulation/src/simulation.ts index 99f1da4e19..e04be1e572 100644 --- a/packages/snaps-simulation/src/simulation.ts +++ b/packages/snaps-simulation/src/simulation.ts @@ -44,6 +44,7 @@ import { getGetEntropySourcesImplementation, getGetMnemonicImplementation, } from './methods/hooks'; +import { getGetMnemonicSeedImplementation } from './methods/hooks/get-mnemonic-seed'; import { createJsonRpcEngine } from './middleware'; import type { SimulationOptions, SimulationUserOptions } from './options'; import { getOptions } from './options'; @@ -108,6 +109,14 @@ export type RestrictedMiddlewareHooks = { */ getMnemonic: (source?: string | undefined) => Promise; + /** + * A hook that returns the seed derived from the user's secret recovery phrase. + * + * @param source - The entropy source to get the seed from. + * @returns The seed. + */ + getMnemonicSeed: (source?: string | undefined) => Promise; + /** * A hook that returns whether the client is locked or not. * @@ -382,6 +391,9 @@ export function getRestrictedHooks( ): RestrictedMiddlewareHooks { return { getMnemonic: getGetMnemonicImplementation(options.secretRecoveryPhrase), + getMnemonicSeed: getGetMnemonicSeedImplementation( + options.secretRecoveryPhrase, + ), getIsLocked: () => false, getClientCryptography: () => ({}), }; diff --git a/packages/snaps-simulator/jest.config.js b/packages/snaps-simulator/jest.config.js index 2b1fb34a33..1bf2add3f6 100644 --- a/packages/snaps-simulator/jest.config.js +++ b/packages/snaps-simulator/jest.config.js @@ -8,9 +8,9 @@ module.exports = deepmerge(baseConfig, { coverageThreshold: { global: { branches: 54.33, - functions: 60.59, - lines: 80.54, - statements: 80.83, + functions: 60.43, + lines: 80.49, + statements: 80.79, }, }, setupFiles: ['./jest.setup.js'], diff --git a/packages/snaps-simulator/src/features/simulation/sagas.ts b/packages/snaps-simulator/src/features/simulation/sagas.ts index 2ea6963501..c5ad17c914 100644 --- a/packages/snaps-simulator/src/features/simulation/sagas.ts +++ b/packages/snaps-simulator/src/features/simulation/sagas.ts @@ -2,7 +2,7 @@ import { Messenger } from '@metamask/base-controller'; import { createFetchMiddleware } from '@metamask/eth-json-rpc-middleware'; import { JsonRpcEngine } from '@metamask/json-rpc-engine'; import { createEngineStream } from '@metamask/json-rpc-middleware-stream'; -import { mnemonicPhraseToBytes } from '@metamask/key-tree'; +import { mnemonicPhraseToBytes, mnemonicToSeed } from '@metamask/key-tree'; import type { GenericPermissionController } from '@metamask/permission-controller'; import { PermissionController, @@ -122,6 +122,7 @@ export function* initSaga({ payload }: PayloadAction) { const sharedHooks = { getMnemonic: async () => mnemonicPhraseToBytes(srp), + getMnemonicSeed: async () => mnemonicToSeed(srp), }; const permissionSpecifications = {