diff --git a/packages/rpc-methods/jest.config.js b/packages/rpc-methods/jest.config.js index 65199e5199..8944434726 100644 --- a/packages/rpc-methods/jest.config.js +++ b/packages/rpc-methods/jest.config.js @@ -5,10 +5,10 @@ module.exports = deepmerge(baseConfig, { coveragePathIgnorePatterns: ['./src/index.ts'], coverageThreshold: { global: { - branches: 88.05, - functions: 89.65, - lines: 84.11, - statements: 84.11, + branches: 87.41, + functions: 87.27, + lines: 83.36, + statements: 83.36, }, }, testTimeout: 2500, diff --git a/packages/rpc-methods/src/restricted/getBip32Entropy.test.ts b/packages/rpc-methods/src/restricted/getBip32Entropy.test.ts index 4586a84e9c..51616694af 100644 --- a/packages/rpc-methods/src/restricted/getBip32Entropy.test.ts +++ b/packages/rpc-methods/src/restricted/getBip32Entropy.test.ts @@ -165,6 +165,26 @@ describe('getBip32EntropyCaveatSpecifications', () => { ).toBe('foo'); }); + it('ignores unknown fields', async () => { + const fn = jest.fn().mockImplementation(() => 'foo'); + + expect( + await getBip32EntropyCaveatSpecifications[ + SnapCaveatType.PermittedDerivationPaths + ].decorator(fn, { + type: SnapCaveatType.PermittedDerivationPaths, + value: [params], + // @ts-expect-error Missing other required properties. + })({ + params: { + path: ['m', "44'", "60'", "0'", '0', '1'], + curve: 'secp256k1', + compressed: true, + }, + }), + ).toBe('foo'); + }); + it('throws if the path is invalid', async () => { const fn = jest.fn().mockImplementation(() => 'foo'); diff --git a/packages/rpc-methods/src/restricted/getBip32PublicKey.test.ts b/packages/rpc-methods/src/restricted/getBip32PublicKey.test.ts index d7e7ddcd30..b9bbf7f838 100644 --- a/packages/rpc-methods/src/restricted/getBip32PublicKey.test.ts +++ b/packages/rpc-methods/src/restricted/getBip32PublicKey.test.ts @@ -1,7 +1,5 @@ -import { SnapCaveatType } from '@metamask/snaps-utils'; import { getBip32PublicKeyBuilder, - getBip32PublicKeyCaveatSpecifications, getBip32PublicKeyImplementation, } from './getBip32PublicKey'; @@ -45,73 +43,6 @@ describe('specificationBuilder', () => { }); }); -describe('getBip32PublicKeyCaveatSpecifications', () => { - describe('decorator', () => { - const params = { path: ['m', "44'", "60'"], curve: 'secp256k1' }; - - it('returns the result of the method implementation', async () => { - const fn = jest.fn().mockImplementation(() => 'foo'); - - expect( - await getBip32PublicKeyCaveatSpecifications[ - SnapCaveatType.PermittedDerivationPaths - ].decorator(fn, { - type: SnapCaveatType.PermittedDerivationPaths, - value: [params], - // @ts-expect-error Missing other required properties. - })({ params }), - ).toBe('foo'); - }); - - it('throws if the path is invalid', async () => { - const fn = jest.fn().mockImplementation(() => 'foo'); - - await expect( - getBip32PublicKeyCaveatSpecifications[ - SnapCaveatType.PermittedDerivationPaths - ].decorator(fn, { - type: SnapCaveatType.PermittedDerivationPaths, - value: [params], - // @ts-expect-error Missing other required properties. - })({ params: { ...params, path: [] } }), - ).rejects.toThrow( - 'Invalid BIP-32 public key path definition: At path: path -- Path must be a non-empty BIP-32 derivation path array.', - ); - }); - - it('throws if the path is not specified in the caveats', async () => { - const fn = jest.fn().mockImplementation(() => 'foo'); - - await expect( - getBip32PublicKeyCaveatSpecifications[ - SnapCaveatType.PermittedDerivationPaths - ].decorator(fn, { - type: SnapCaveatType.PermittedDerivationPaths, - value: [params], - // @ts-expect-error Missing other required properties. - })({ params: { ...params, path: ['m', "44'", "0'"] } }), - ).rejects.toThrow( - 'The requested path is not permitted. Allowed paths must be specified in the snap manifest.', - ); - }); - }); - - describe('validator', () => { - it('throws if the caveat values are invalid', () => { - expect(() => - getBip32PublicKeyCaveatSpecifications[ - SnapCaveatType.PermittedDerivationPaths - ].validator?.({ - type: SnapCaveatType.PermittedDerivationPaths, - value: [{ path: ['foo'], curve: 'secp256k1' }], - }), - ).toThrow( - 'Invalid BIP-32 public key caveat: At path: value.0.path -- Path must start with "m".', - ); - }); - }); -}); - describe('getBip32PublicKeyImplementation', () => { describe('getBip32PublicKey', () => { it('derives the public key from the path', async () => { diff --git a/packages/rpc-methods/src/restricted/getBip32PublicKey.ts b/packages/rpc-methods/src/restricted/getBip32PublicKey.ts index 234c64909e..5f93bcbc34 100644 --- a/packages/rpc-methods/src/restricted/getBip32PublicKey.ts +++ b/packages/rpc-methods/src/restricted/getBip32PublicKey.ts @@ -3,20 +3,20 @@ import { PermissionSpecificationBuilder, PermissionType, PermissionValidatorConstraint, - RestrictedMethodCaveatSpecificationConstraint, RestrictedMethodOptions, ValidPermissionSpecification, } from '@metamask/controllers'; import { BIP32Node, SLIP10Node } from '@metamask/key-tree'; import { - Bip32PublicKey, - Bip32PublicKeyStruct, + Bip32Entropy, + bip32entropy, + Bip32PathStruct, SnapCaveatType, + SnapGetBip32EntropyPermissionsStruct, } from '@metamask/snaps-utils'; import { NonEmptyArray, assertStruct } from '@metamask/utils'; import { ethErrors } from 'eth-rpc-errors'; -import { array, size, type } from 'superstruct'; -import { isEqual } from '../utils'; +import { boolean, enums, object, optional, type } from 'superstruct'; const targetKey = 'snap_getBip32PublicKey'; @@ -52,23 +52,13 @@ type GetBip32PublicKeyParameters = { compressed?: boolean; }; -/** - * Validate a caveat path object. The object must consist of a `path` array and - * a `curve` string. Paths must start with `m`, and must contain at - * least two indices. If `ed25519` is used, this checks if all the path indices - * are hardened. - * - * @param value - The value to validate. - * @throws If the value is invalid. - */ -function validatePath(value: unknown): asserts value is Bip32PublicKey { - assertStruct( - value, - Bip32PublicKeyStruct, - 'Invalid BIP-32 public key path definition', - ethErrors.rpc.invalidParams, - ); -} +export const Bip32PublicKeyArgsStruct = bip32entropy( + object({ + path: Bip32PathStruct, + curve: enums(['ed225519', 'secp256k1']), + compressed: optional(boolean()), + }), +); /** * Validate the path values associated with a caveat. This validates that the @@ -79,10 +69,10 @@ function validatePath(value: unknown): asserts value is Bip32PublicKey { */ export function validateCaveatPaths( caveat: Caveat, -): asserts caveat is Caveat { +): asserts caveat is Caveat { assertStruct( caveat, - type({ value: size(array(Bip32PublicKeyStruct), 1, Infinity) }), + type({ value: SnapGetBip32EntropyPermissionsStruct }), 'Invalid BIP-32 public key caveat', ethErrors.rpc.internal, ); @@ -129,43 +119,6 @@ export const getBip32PublicKeyBuilder = Object.freeze({ }, } as const); -export const getBip32PublicKeyCaveatSpecifications: Record< - SnapCaveatType.PermittedDerivationPaths, - RestrictedMethodCaveatSpecificationConstraint -> = { - [SnapCaveatType.PermittedDerivationPaths]: Object.freeze({ - type: SnapCaveatType.PermittedDerivationPaths, - decorator: ( - method, - caveat: Caveat< - SnapCaveatType.PermittedDerivationPaths, - GetBip32PublicKeyParameters[] - >, - ) => { - return async (args) => { - const { params } = args; - validatePath(params); - - const path = caveat.value.find( - (caveatPath) => - isEqual(params.path, caveatPath.path) && - caveatPath.curve === params.curve, - ); - - if (!path) { - throw ethErrors.provider.unauthorized({ - message: - 'The requested path is not permitted. Allowed paths must be specified in the snap manifest.', - }); - } - - return await method(args); - }; - }, - validator: (caveat) => validateCaveatPaths(caveat), - }), -}; - /** * Builds the method implementation for `snap_getBip32PublicKey`. * @@ -185,9 +138,14 @@ export function getBip32PublicKeyImplementation({ ): Promise { await getUnlockPromise(true); - // `args.params` is validated by the decorator, so it's safe to assert here. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const params = args.params!; + assertStruct( + args.params, + Bip32PublicKeyArgsStruct, + 'Invalid BIP-32 public key params', + ethErrors.rpc.invalidParams, + ); + + const { params } = args; const node = await SLIP10Node.fromDerivationPath({ curve: params.curve, diff --git a/packages/snaps-utils/src/manifest/validation.ts b/packages/snaps-utils/src/manifest/validation.ts index a407c35f1b..34f06dd6da 100644 --- a/packages/snaps-utils/src/manifest/validation.ts +++ b/packages/snaps-utils/src/manifest/validation.ts @@ -112,7 +112,7 @@ export const Bip32PathStruct = refine( }, ); -const bip32entropy = ( +export const bip32entropy = ( struct: Struct, ) => refine(struct, 'BIP-32 entropy', (value) => { @@ -128,7 +128,7 @@ const bip32entropy = ( // Used outside @metamask/snap-utils export const Bip32EntropyStruct = bip32entropy( - object({ + type({ path: Bip32PathStruct, curve: enums(['ed25519', 'secp256k1']), }), @@ -136,17 +136,13 @@ export const Bip32EntropyStruct = bip32entropy( export type Bip32Entropy = Infer; -export const Bip32PublicKeyStruct = bip32entropy( - object({ - path: Bip32PathStruct, - curve: enums(['ed225519', 'secp256k1']), - compressed: optional(boolean()), - }), +export const SnapGetBip32EntropyPermissionsStruct = size( + array(Bip32EntropyStruct), + 1, + Infinity, ); -export type Bip32PublicKey = Infer; - -const PermissionsStruct = type({ +export const PermissionsStruct = type({ 'endowment:long-running': optional(object({})), 'endowment:network-access': optional(object({})), 'endowment:transaction-insight': optional( @@ -160,10 +156,8 @@ const PermissionsStruct = type({ snap_confirm: optional(object({})), snap_manageState: optional(object({})), snap_notify: optional(object({})), - snap_getBip32Entropy: optional(size(array(Bip32EntropyStruct), 1, Infinity)), - snap_getBip32PublicKey: optional( - size(array(Bip32PublicKeyStruct), 1, Infinity), - ), + snap_getBip32Entropy: optional(SnapGetBip32EntropyPermissionsStruct), + snap_getBip32PublicKey: optional(SnapGetBip32EntropyPermissionsStruct), snap_getBip44Entropy: optional( size( array(object({ coinType: size(integer(), 0, 2 ** 32 - 1) })),