diff --git a/package.json b/package.json index 7fa864e4f1..b0e2067b17 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,8 @@ "luxon@^3.2.1": "patch:luxon@npm%3A3.3.0#./.yarn/patches/luxon-npm-3.3.0-bdbae9bfd5.patch", "tsconfig-paths@^3.11.0": "patch:tsconfig-paths@npm%3A3.14.2#./.yarn/patches/tsconfig-paths-npm-3.14.2-90ce75420d.patch", "tsconfig-paths@^3.14.1": "patch:tsconfig-paths@npm%3A3.14.2#./.yarn/patches/tsconfig-paths-npm-3.14.2-90ce75420d.patch", - "tsconfig-paths@^4.1.2": "patch:tsconfig-paths@npm%3A3.14.2#./.yarn/patches/tsconfig-paths-npm-3.14.2-90ce75420d.patch" + "tsconfig-paths@^4.1.2": "patch:tsconfig-paths@npm%3A3.14.2#./.yarn/patches/tsconfig-paths-npm-3.14.2-90ce75420d.patch", + "@metamask/permission-controller": "npm:@metamask-previews/permission-controller@12.3.0-preview-1e2fe74a0" }, "devDependencies": { "@lavamoat/allow-scripts": "^4.0.0", diff --git a/packages/snaps-rpc-methods/src/permissions.ts b/packages/snaps-rpc-methods/src/permissions.ts index 6c0e6d43dc..5bd7666fad 100644 --- a/packages/snaps-rpc-methods/src/permissions.ts +++ b/packages/snaps-rpc-methods/src/permissions.ts @@ -1,6 +1,7 @@ -import type { - PermissionConstraint, - PermissionSpecificationConstraint, +import { + createRestrictedMethodMessenger, + type PermissionConstraint, + type PermissionSpecificationConstraint, } from '@metamask/permission-controller'; import type { SnapPermissions } from '@metamask/snaps-utils'; import { hasProperty } from '@metamask/utils'; @@ -14,6 +15,7 @@ import { restrictedMethodPermissionBuilders, } from './restricted'; import { selectHooks } from './utils'; +import { Messenger } from '@metamask/messenger'; /** * Map initial permissions as defined in a Snap manifest to something that can @@ -64,10 +66,12 @@ export const buildSnapEndowmentSpecifications = ( export const buildSnapRestrictedMethodSpecifications = ( excludedPermissions: string[], hooks: Record, + messenger: Messenger, ) => Object.values(restrictedMethodPermissionBuilders).reduce< Record - >((specifications, { targetName, specificationBuilder, methodHooks }) => { + // @ts-expect-error TypeScript not convinced actionNames exists. + >((specifications, { targetName, specificationBuilder, methodHooks, actionNames }) => { if (!excludedPermissions.includes(targetName)) { specifications[targetName] = specificationBuilder({ // @ts-expect-error The selectHooks type is wonky @@ -75,6 +79,11 @@ export const buildSnapRestrictedMethodSpecifications = ( hooks, methodHooks, ) as Pick, + messenger: createRestrictedMethodMessenger({ + namespace: targetName, + rootMessenger: messenger, + actionNames, + }) }); } return specifications; diff --git a/packages/snaps-rpc-methods/src/restricted/getBip32Entropy.ts b/packages/snaps-rpc-methods/src/restricted/getBip32Entropy.ts index ae16964e7b..e0c1766a59 100644 --- a/packages/snaps-rpc-methods/src/restricted/getBip32Entropy.ts +++ b/packages/snaps-rpc-methods/src/restricted/getBip32Entropy.ts @@ -17,34 +17,17 @@ import { assert } from '@metamask/utils'; import type { MethodHooksObject } from '../utils'; import { + getMnemonic, + getMnemonicSeed, getNodeFromMnemonic, getNodeFromSeed, getValueFromEntropySource, } from '../utils'; +import { Messenger } from '@metamask/messenger'; const targetName = 'snap_getBip32Entropy'; export type GetBip32EntropyMethodHooks = { - /** - * Get the mnemonic of the provided source. If no source is provided, the - * mnemonic 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 - * source is provided. - */ - 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. * @@ -62,8 +45,11 @@ export type GetBip32EntropyMethodHooks = { getClientCryptography: () => CryptographicFunctions | undefined; }; +export type GetBip32EntropyMessengerActions = never; + type GetBip32EntropySpecificationBuilderOptions = { methodHooks: GetBip32EntropyMethodHooks; + messenger: Messenger; }; type GetBip32EntropySpecification = ValidPermissionSpecification<{ @@ -80,6 +66,7 @@ type GetBip32EntropySpecification = ValidPermissionSpecification<{ * BIP-32 node. * * @param options - The specification builder options. + * @param options.messenger - The messenger. * @param options.methodHooks - The RPC method hooks needed by the method implementation. * @returns The specification for the `snap_getBip32Entropy` permission. */ @@ -87,12 +74,12 @@ const specificationBuilder: PermissionSpecificationBuilder< PermissionType.RestrictedMethod, GetBip32EntropySpecificationBuilderOptions, GetBip32EntropySpecification -> = ({ methodHooks }: GetBip32EntropySpecificationBuilderOptions) => { +> = ({ methodHooks, messenger }: GetBip32EntropySpecificationBuilderOptions) => { return { permissionType: PermissionType.RestrictedMethod, targetName, allowedCaveats: [SnapCaveatType.PermittedDerivationPaths], - methodImplementation: getBip32EntropyImplementation(methodHooks), + methodImplementation: getBip32EntropyImplementation({ methodHooks, messenger }), validator: ({ caveats }) => { if ( caveats?.length !== 1 || @@ -108,8 +95,6 @@ const specificationBuilder: PermissionSpecificationBuilder< }; const methodHooks: MethodHooksObject = { - getMnemonic: true, - getMnemonicSeed: true, getUnlockPromise: true, getClientCryptography: true, }; @@ -173,27 +158,28 @@ export const getBip32EntropyBuilder = Object.freeze({ targetName, specificationBuilder, methodHooks, + actionNames: ['KeyringController:withKeyring'], } as const); /** * Builds the method implementation for `snap_getBip32Entropy`. * - * @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 + * @param options - The options. + * @param options.messenger - The messenger. + * @param options.methodHooks - The RPC method hooks. + * @param options.methodHooks.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 + * @param options.methodHooks.getClientCryptography - A function to retrieve the cryptographic * functions to use for the client. * @returns The method implementation which returns a `JsonSLIP10Node`. * @throws If the params are invalid. */ export function getBip32EntropyImplementation({ - getMnemonic, - getMnemonicSeed, - getUnlockPromise, - getClientCryptography, -}: GetBip32EntropyMethodHooks) { + methodHooks: { + getUnlockPromise, + getClientCryptography, + }, messenger +}: GetBip32EntropySpecificationBuilderOptions) { return async function getBip32Entropy( args: RestrictedMethodOptions, ): Promise { @@ -205,7 +191,7 @@ export function getBip32EntropyImplementation({ // 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, + getMnemonicSeed.bind(null, messenger), params.source, ); @@ -220,7 +206,7 @@ export function getBip32EntropyImplementation({ } const secretRecoveryPhrase = await getValueFromEntropySource( - getMnemonic, + getMnemonic.bind(null, messenger), params.source, ); diff --git a/packages/snaps-rpc-methods/src/types.ts b/packages/snaps-rpc-methods/src/types.ts new file mode 100644 index 0000000000..89401ed8fe --- /dev/null +++ b/packages/snaps-rpc-methods/src/types.ts @@ -0,0 +1,13 @@ +export type HdKeyring = { + type: 'hd', + seed?: Uint8Array; + mnemonic?: Uint8Array; +} + +export type KeyringControllerWithKeyringAction = { + type: 'KeyringController:withKeyring'; + handler: (selector: { + type: string; + index?: number; + } | { id: string }, operation: (args: { keyring: HdKeyring }) => Promise) => Promise; +}; \ No newline at end of file diff --git a/packages/snaps-rpc-methods/src/utils.ts b/packages/snaps-rpc-methods/src/utils.ts index 84b91a21e5..1d0fa5ccec 100644 --- a/packages/snaps-rpc-methods/src/utils.ts +++ b/packages/snaps-rpc-methods/src/utils.ts @@ -18,8 +18,10 @@ import { stringToBytes, } from '@metamask/utils'; import { keccak_256 as keccak256 } from '@noble/hashes/sha3'; +import { Messenger } from '@metamask/messenger'; import { SnapEndowments } from './endowments'; +import type { HdKeyring, KeyringControllerWithKeyringAction } from './types'; const HARDENED_VALUE = 0x80000000; @@ -381,3 +383,120 @@ export const UI_PERMISSIONS = [ SnapEndowments.TransactionInsight, SnapEndowments.SignatureInsight, ] as const; + +const HD_KEYRING = 'hd'; + +/** + * Get the mnemonic for a given entropy source. If no source is + * provided, the primary HD keyring's mnemonic will be returned. + * + * @param messenger - The messenger. + * @param source - The ID of the entropy source keyring. + * @returns The mnemonic. + */ +export async function getMnemonic( + messenger: Messenger, + source?: string | undefined, +): Promise { + if (!source) { + const mnemonic = (await messenger.call( + 'KeyringController:withKeyring', + { + type: HD_KEYRING, + index: 0, + }, + async ({ keyring }) => (keyring as HdKeyring).mnemonic, + )) as Uint8Array | null; + + if (!mnemonic) { + throw new Error('Primary keyring mnemonic unavailable.'); + } + + return mnemonic; + } + + try { + const keyringData = await messenger.call( + 'KeyringController:withKeyring', + { + id: source, + }, + async ({ keyring }) => ({ + type: keyring.type, + mnemonic: (keyring as HdKeyring).mnemonic, + }), + ); + + const { type, mnemonic } = keyringData as { + type: string; + mnemonic?: Uint8Array; + }; + + if (type !== HD_KEYRING || !mnemonic) { + // The keyring isn't guaranteed to have a mnemonic (e.g., + // hardware wallets, which can't be used as entropy sources), + // so we throw an error if it doesn't. + throw new Error(`Entropy source with ID "${source}" not found.`); + } + + return mnemonic; + } catch { + throw new Error(`Entropy source with ID "${source}" not found.`); + } +} + +/** + * Get the mnemonic seed for a given entropy source. If no source is + * provided, the primary HD keyring's mnemonic seed will be returned. + * + * @param messenger - The messenger. + * @param source - The ID of the entropy source keyring. + * @returns The mnemonic seed. + */ +export async function getMnemonicSeed( + messenger: Messenger, + source?: string | undefined, +): Promise { + if (!source) { + const seed = (await messenger.call( + 'KeyringController:withKeyring', + { + type: HD_KEYRING, + index: 0, + }, + async ({ keyring }) => (keyring as HdKeyring).seed, + )) as Uint8Array | null; + + if (!seed) { + throw new Error('Primary keyring mnemonic unavailable.'); + } + + return seed; + } + + try { + const keyringData = await messenger.call( + 'KeyringController:withKeyring', + { + id: source, + }, + async ({ keyring }) => ({ + type: keyring.type, + seed: (keyring as HdKeyring).seed, + }), + ); + + const { type, seed } = keyringData as { type: string; seed?: Uint8Array }; + + if (type !== HD_KEYRING || !seed) { + // The keyring isn't guaranteed to have a mnemonic (e.g., + // hardware wallets, which can't be used as entropy sources), + // so we throw an error if it doesn't. + throw new Error(`Entropy source with ID "${source}" not found.`); + } + + return seed; + } catch { + throw new Error(`Entropy source with ID "${source}" not found.`); + } +} diff --git a/packages/snaps-simulation/src/methods/specifications.ts b/packages/snaps-simulation/src/methods/specifications.ts index 406fbefe56..037cfddf89 100644 --- a/packages/snaps-simulation/src/methods/specifications.ts +++ b/packages/snaps-simulation/src/methods/specifications.ts @@ -86,29 +86,34 @@ export function getPermissionSpecifications({ [caip25EndowmentBuilder.targetName]: caip25EndowmentBuilder.specificationBuilder({}), ...buildSnapEndowmentSpecifications(EXCLUDED_SNAP_ENDOWMENTS), - ...buildSnapRestrictedMethodSpecifications(EXCLUDED_SNAP_PERMISSIONS, { - // Shared hooks. - ...hooks, + ...buildSnapRestrictedMethodSpecifications( + EXCLUDED_SNAP_PERMISSIONS, + { + // Shared hooks. + ...hooks, - // Snaps-specific hooks. - clearSnapState: getClearSnapStateMethodImplementation(runSaga), - getPreferences: getGetPreferencesMethodImplementation(options), - getSnapState: getGetSnapStateMethodImplementation(runSaga), - getUnlockPromise: asyncResolve(true), + // Snaps-specific hooks. + clearSnapState: getClearSnapStateMethodImplementation(runSaga), + getPreferences: getGetPreferencesMethodImplementation(options), + getSnapState: getGetSnapStateMethodImplementation(runSaga), + getUnlockPromise: asyncResolve(true), - // TODO: Allow the user to specify the result of this function. - isOnPhishingList: resolve(false), + // TODO: Allow the user to specify the result of this function. + isOnPhishingList: resolve(false), - maybeUpdatePhishingList: asyncResolve(), - requestUserApproval: getRequestUserApprovalImplementation(runSaga), - showInAppNotification: getShowInAppNotificationImplementation(runSaga), - showNativeNotification: getShowNativeNotificationImplementation(runSaga), - updateSnapState: getUpdateSnapStateMethodImplementation(runSaga), - createInterface: getCreateInterfaceImplementation(controllerMessenger), - getInterface: getGetInterfaceImplementation(controllerMessenger), - setInterfaceDisplayed: - getSetInterfaceDisplayedImplementation(controllerMessenger), - }), + maybeUpdatePhishingList: asyncResolve(), + requestUserApproval: getRequestUserApprovalImplementation(runSaga), + showInAppNotification: getShowInAppNotificationImplementation(runSaga), + showNativeNotification: + getShowNativeNotificationImplementation(runSaga), + updateSnapState: getUpdateSnapStateMethodImplementation(runSaga), + createInterface: getCreateInterfaceImplementation(controllerMessenger), + getInterface: getGetInterfaceImplementation(controllerMessenger), + setInterfaceDisplayed: + getSetInterfaceDisplayedImplementation(controllerMessenger), + }, + controllerMessenger, + ), }; } diff --git a/packages/snaps-simulation/src/simulation.ts b/packages/snaps-simulation/src/simulation.ts index 23d955d171..d403889712 100644 --- a/packages/snaps-simulation/src/simulation.ts +++ b/packages/snaps-simulation/src/simulation.ts @@ -11,6 +11,7 @@ import { PermissionDoesNotExistError, type Caveat, type RequestedPermissions, + createPermissionMiddleware, } from '@metamask/permission-controller'; import type { ExecutionService } from '@metamask/snaps-controllers'; import { @@ -454,7 +455,8 @@ export async function installSnap< options, }); - const permissionMiddleware = permissionController.createPermissionMiddleware({ + const permissionMiddleware = createPermissionMiddleware({ + messenger: controllerMessenger, origin: snapId, }); diff --git a/yarn.lock b/yarn.lock index bd319c211b..00b430db3b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2905,14 +2905,14 @@ __metadata: languageName: unknown linkType: soft -"@metamask/base-controller@npm:^9.0.1": - version: 9.0.1 - resolution: "@metamask/base-controller@npm:9.0.1" +"@metamask/base-controller@npm:^9.0.1, @metamask/base-controller@npm:^9.1.0": + version: 9.1.0 + resolution: "@metamask/base-controller@npm:9.1.0" dependencies: - "@metamask/messenger": "npm:^1.0.0" + "@metamask/messenger": "npm:^1.1.1" "@metamask/utils": "npm:^11.9.0" immer: "npm:^9.0.6" - checksum: 10/bc5052c9a38c21a52003e9a79de1f609ff127d939c87eb7b9ebe01cdf05ce2a9ee8e4635dd96f193e9951983e9554d9381af303fbadaae740445ffb2424698e8 + checksum: 10/752b70b35026fdf31ea8feef638f06286dbbde8d17e1ba804085fdbedbb076d900d2d7b6db3d2af284b3aa8fa84c95a0ceea6a3a46ee7b7e29433f037e9afc69 languageName: node linkType: hard @@ -3048,7 +3048,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/controller-utils@npm:^11.19.0": +"@metamask/controller-utils@npm:^11.19.0, @metamask/controller-utils@npm:^11.20.0": version: 11.20.0 resolution: "@metamask/controller-utils@npm:11.20.0" dependencies: @@ -3920,22 +3920,22 @@ __metadata: languageName: node linkType: hard -"@metamask/permission-controller@npm:^12.2.1, @metamask/permission-controller@npm:^12.3.0": - version: 12.3.0 - resolution: "@metamask/permission-controller@npm:12.3.0" +"@metamask/permission-controller@npm:@metamask-previews/permission-controller@12.3.0-preview-1e2fe74a0": + version: 12.3.0-preview-1e2fe74a0 + resolution: "@metamask-previews/permission-controller@npm:12.3.0-preview-1e2fe74a0" dependencies: "@metamask/approval-controller": "npm:^9.0.1" - "@metamask/base-controller": "npm:^9.0.1" - "@metamask/controller-utils": "npm:^11.19.0" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/controller-utils": "npm:^11.20.0" "@metamask/json-rpc-engine": "npm:^10.2.4" - "@metamask/messenger": "npm:^1.0.0" + "@metamask/messenger": "npm:^1.1.1" "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/utils": "npm:^11.9.0" "@types/deep-freeze-strict": "npm:^1.1.0" deep-freeze-strict: "npm:^1.1.1" immer: "npm:^9.0.6" nanoid: "npm:^3.3.8" - checksum: 10/a5fe9f2bab8c2d41cd829cd6c1af970e71da97eac42de17071c10f90d975e9135a4e6987ed6b2f3ea2209b1c6c51b822508f800225fda2207cdc598c16ea77dd + checksum: 10/082815b35b19e5587127e128e5723d25e7842ed64a8c5aae6b5f8226b4070359220161ac5721c1315b7aee1e1a11d5eb1cbdb67d596aa8a6087bd9b5cc05151e languageName: node linkType: hard