Skip to content
Draft
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 13 additions & 4 deletions packages/snaps-rpc-methods/src/permissions.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -13,7 +14,8 @@
caveatMappers,
restrictedMethodPermissionBuilders,
} from './restricted';
import { selectHooks } from './utils';

Check failure on line 17 in packages/snaps-rpc-methods/src/permissions.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

There should be at least one empty line between import groups
import { Messenger } from '@metamask/messenger';

Check failure on line 18 in packages/snaps-rpc-methods/src/permissions.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

All imports in the declaration are only used as types. Use `import type`

Check failure on line 18 in packages/snaps-rpc-methods/src/permissions.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

`@metamask/messenger` import should occur before import of `@metamask/permission-controller`

/**
* Map initial permissions as defined in a Snap manifest to something that can
Expand Down Expand Up @@ -64,17 +66,24 @@
export const buildSnapRestrictedMethodSpecifications = (
excludedPermissions: string[],
hooks: Record<string, unknown>,
messenger: Messenger<string>,
) =>
Object.values(restrictedMethodPermissionBuilders).reduce<
Record<string, PermissionSpecificationConstraint>
>((specifications, { targetName, specificationBuilder, methodHooks }) => {
// @ts-expect-error TypeScript not convinced actionNames exists.

Check failure on line 73 in packages/snaps-rpc-methods/src/permissions.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

Insert `·`
>((specifications, { targetName, specificationBuilder, methodHooks, actionNames }) => {

Check failure on line 74 in packages/snaps-rpc-methods/src/permissions.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

Replace `(specifications,·{·targetName,·specificationBuilder,·methodHooks,·actionNames·}` with `⏎····(⏎······specifications,⏎······{·targetName,·specificationBuilder,·methodHooks,·actionNames·},⏎····`
if (!excludedPermissions.includes(targetName)) {

Check failure on line 75 in packages/snaps-rpc-methods/src/permissions.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

Insert `··`
specifications[targetName] = specificationBuilder({

Check failure on line 76 in packages/snaps-rpc-methods/src/permissions.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

Insert `··`
// @ts-expect-error The selectHooks type is wonky

Check failure on line 77 in packages/snaps-rpc-methods/src/permissions.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

Insert `··`
methodHooks: selectHooks<typeof hooks, keyof typeof methodHooks>(

Check failure on line 78 in packages/snaps-rpc-methods/src/permissions.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

Insert `··`
hooks,

Check failure on line 79 in packages/snaps-rpc-methods/src/permissions.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

Insert `··`
methodHooks,
) as Pick<typeof hooks, keyof typeof methodHooks>,
messenger: createRestrictedMethodMessenger({
namespace: targetName,
rootMessenger: messenger,
actionNames,
})
});
}
return specifications;
Expand Down
58 changes: 22 additions & 36 deletions packages/snaps-rpc-methods/src/restricted/getBip32Entropy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<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 All @@ -62,8 +45,11 @@ export type GetBip32EntropyMethodHooks = {
getClientCryptography: () => CryptographicFunctions | undefined;
};

export type GetBip32EntropyMessengerActions = never;

type GetBip32EntropySpecificationBuilderOptions = {
methodHooks: GetBip32EntropyMethodHooks;
messenger: Messenger<string, GetBip32EntropyMessengerActions>;
};

type GetBip32EntropySpecification = ValidPermissionSpecification<{
Expand All @@ -80,19 +66,20 @@ 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.
*/
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 ||
Expand All @@ -108,8 +95,6 @@ const specificationBuilder: PermissionSpecificationBuilder<
};

const methodHooks: MethodHooksObject<GetBip32EntropyMethodHooks> = {
getMnemonic: true,
getMnemonicSeed: true,
getUnlockPromise: true,
getClientCryptography: true,
};
Expand Down Expand Up @@ -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<GetBip32EntropyParams>,
): Promise<GetBip32EntropyResult> {
Expand All @@ -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,
);

Expand All @@ -220,7 +206,7 @@ export function getBip32EntropyImplementation({
}

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

Expand Down
13 changes: 13 additions & 0 deletions packages/snaps-rpc-methods/src/types.ts
Original file line number Diff line number Diff line change
@@ -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<unknown>) => Promise<unknown>;
};
119 changes: 119 additions & 0 deletions packages/snaps-rpc-methods/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<string, KeyringControllerWithKeyringAction, never>,
source?: string | undefined,
): Promise<Uint8Array> {
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<string, KeyringControllerWithKeyringAction, never>,
source?: string | undefined,
): Promise<Uint8Array> {
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.`);
}
}
45 changes: 25 additions & 20 deletions packages/snaps-simulation/src/methods/specifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
};
}

Expand Down
Loading
Loading