Skip to content

Commit

Permalink
Add snap_getEntropy JSON-RPC method
Browse files Browse the repository at this point in the history
  • Loading branch information
Mrtenz committed Nov 14, 2022
1 parent af98e50 commit 9dcd86c
Show file tree
Hide file tree
Showing 7 changed files with 314 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/rpc-methods/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@metamask/snaps-utils": "^0.23.0",
"@metamask/types": "^1.1.0",
"@metamask/utils": "^3.3.1",
"@noble/hashes": "^1.1.3",
"eth-rpc-errors": "^4.0.2",
"nanoid": "^3.1.31",
"superstruct": "^0.16.7"
Expand Down
35 changes: 35 additions & 0 deletions packages/rpc-methods/src/restricted/__fixtures__/entropy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* SIP-6 test vectors.
*/
export const ENTROPY_VECTORS = [
{
snapId: 'foo',
derivationPath:
"m/1399742832'/1323571613'/1848851859'/458888073'/1339050117'/513522582'/1371866341'/2121938770'/1014285256'",
entropy:
'0x8bbb59ec55a4a8dd5429268e367ebbbe54eee7467c0090ca835c64d45c33a155',
},
{
snapId: 'bar',
derivationPath:
"m/1399742832'/767024459'/1206550137'/1427647479'/1048031962'/1656784813'/1860822351'/1362389435'/2133253878'",
entropy:
'0xbdae5c0790d9189d8ae27fd4860b3b57bab420b6594c420ae9ae3a9f87c1ea14',
},
{
snapId: 'foo',
salt: 'bar',
derivationPath:
"m/1399742832'/2002032866'/301374032'/1159533269'/453247377'/187127851'/1859522268'/152471137'/187531423'",
entropy:
'0x59cbec1fa877ecb38d88c3a2326b23bff374954b39ad9482c9b082306ac4b3ad',
},
{
snapId: 'bar',
salt: 'baz',
derivationPath:
"m/1399742832'/734358031'/701613791'/1618075622'/1535938847'/1610213550'/18831365'/356906080'/2095933563'",
entropy:
'0x814c1f121eb4067d1e1d177246461e8a1cc6a1b1152756737aba7fa9c2161ba2',
},
];
1 change: 1 addition & 0 deletions packages/rpc-methods/src/restricted/__fixtures__/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './entropy';
83 changes: 83 additions & 0 deletions packages/rpc-methods/src/restricted/getEntropy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { PermissionType } from '@metamask/controllers';
import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils';
import { deriveEntropy, getEntropyBuilder } from './getEntropy';
import { ENTROPY_VECTORS } from './__fixtures__';

const TEST_SECRET_RECOVERY_PHRASE =
'test test test test test test test test test test test ball';

describe('getEntropyBuilder', () => {
it('has the expected shape', () => {
expect(getEntropyBuilder).toStrictEqual({
targetKey: 'snap_getEntropy',
specificationBuilder: expect.any(Function),
methodHooks: {
getMnemonic: true,
getUnlockPromise: true,
},
});
});

it('returns the expected specification', () => {
const methodHooks = {
getMnemonic: jest.fn(),
getUnlockPromise: jest.fn(),
};

expect(
getEntropyBuilder.specificationBuilder({ methodHooks }),
).toStrictEqual({
permissionType: PermissionType.RestrictedMethod,
targetKey: 'snap_getEntropy',
allowedCaveats: null,
methodImplementation: expect.any(Function),
});
});
});

describe('deriveEntropy', () => {
it.each(ENTROPY_VECTORS)(
'derives entropy from the given parameters',
async () => {
const { snapId, salt, entropy } = ENTROPY_VECTORS[0];

expect(
await deriveEntropy(snapId, TEST_SECRET_RECOVERY_PHRASE, salt ?? ''),
).toStrictEqual(entropy);
},
);
});

describe('getEntropyImplementation', () => {
it('returns the expected result', async () => {
const getMnemonic = jest
.fn()
.mockImplementation(() => TEST_SECRET_RECOVERY_PHRASE);

const getUnlockPromise = jest.fn();

const methodHooks = {
getMnemonic,
getUnlockPromise,
};

const implementation = getEntropyBuilder.specificationBuilder({
methodHooks,
}).methodImplementation;

const result = await implementation({
method: 'snap_getEntropy',
params: {
version: 1,
salt: 'foo',
},
context: {
origin: MOCK_SNAP_ID,
},
});

expect(result).toStrictEqual(
'0x6d8e92de419401c7da3cedd5f60ce5635b26059c2a4a8003877fec83653a4921',
);
});
});
190 changes: 190 additions & 0 deletions packages/rpc-methods/src/restricted/getEntropy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { ethErrors } from 'eth-rpc-errors';
import { Infer, literal, object, optional, string } from 'superstruct';
import {
add0x,
assert,
assertStruct,
concatBytes,
Hex,
NonEmptyArray,
stringToBytes,
} from '@metamask/utils';
import { HardenedBIP32Node, SLIP10Node } from '@metamask/key-tree';
import {
PermissionSpecificationBuilder,
PermissionType,
RestrictedMethodOptions,
ValidPermissionSpecification,
} from '@metamask/controllers';
import { keccak_256 as keccak256 } from '@noble/hashes/sha3';

const MAGIC_VALUE = 0xd36e6170;
const HARDENED_VALUE = 0x80000000;

const targetKey = 'snap_getEntropy';

type GetEntropySpecificationBuilderOptions = {
allowedCaveats?: Readonly<NonEmptyArray<string>> | null;
methodHooks: GetEntropyHooks;
};

type GetEntropySpecification = ValidPermissionSpecification<{
permissionType: PermissionType.RestrictedMethod;
targetKey: typeof targetKey;
methodImplementation: ReturnType<typeof getEntropyImplementation>;
allowedCaveats: Readonly<NonEmptyArray<string>> | null;
}>;

export const GetEntropyArgsStruct = object({
version: literal(1),
salt: optional(string()),
});

/**
* @property version - The version of the `snap_getEntropy` method. This must be
* the numeric literal `1`.
* @property salt - A string to use as the salt when deriving the entropy. If
* omitted, the salt will be an empty string.
*/
export type GetEntropyArgs = Infer<typeof GetEntropyArgsStruct>;

const specificationBuilder: PermissionSpecificationBuilder<
PermissionType.RestrictedMethod,
GetEntropySpecificationBuilderOptions,
GetEntropySpecification
> = ({
allowedCaveats = null,
methodHooks,
}: GetEntropySpecificationBuilderOptions) => {
return {
permissionType: PermissionType.RestrictedMethod,
targetKey,
allowedCaveats,
methodImplementation: getEntropyImplementation(methodHooks),
};
};

export const getEntropyBuilder = Object.freeze({
targetKey,
specificationBuilder,
methodHooks: {
getMnemonic: true,
getUnlockPromise: true,
},
} as const);

export type GetEntropyHooks = {
/**
* @returns The mnemonic of the user's primary keyring.
*/
getMnemonic: () => Promise<string>;

/**
* Waits for the extension to be unlocked.
*
* @returns A promise that resolves once the extension is unlocked.
*/
getUnlockPromise: (shouldShowUnlockRequest: boolean) => Promise<void>;
};

/**
* Get a BIP-32 derivation path array from a hash, which is compatible with
* `@metamask/key-tree`. The hash is assumed to be 32 bytes long.
*
* @param hash - The hash to derive indices from.
* @returns The derived indices as a {@link HardenedBIP32Node} array.
*/
function getDerivationPathArray(hash: Uint8Array): HardenedBIP32Node[] {
const array: HardenedBIP32Node[] = [];
const view = new DataView(hash.buffer, hash.byteOffset, hash.byteLength);

for (let index = 0; index < 8; index++) {
const uint32 = view.getUint32(index * 4);

// This is essentially `index | 0x80000000`. Because JavaScript numbers are
// signed, we use the bitwise unsigned right shift operator to ensure that
// the result is a positive number.
// eslint-disable-next-line no-bitwise
const pathIndex = (uint32 | HARDENED_VALUE) >>> 0;
array.push(`bip32:${pathIndex - HARDENED_VALUE}'` as const);
}

return array;
}

/**
* 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 snapId - The snap ID to derive entropy for.
* @param mnemonicPhrase - The mnemonic phrase to derive entropy from.
* @param salt - The salt to use when deriving entropy.
* @returns The derived entropy.
*/
export async function deriveEntropy(
snapId: string,
mnemonicPhrase: string,
salt = '',
): Promise<Hex> {
const snapIdBytes = stringToBytes(snapId);
const saltBytes = stringToBytes(salt);

// Get the derivation path from the snap ID.
const hash = keccak256(concatBytes([snapIdBytes, keccak256(saltBytes)]));
const computedDerivationPath = getDerivationPathArray(hash);

// Derive the private key using BIP-32.
const { privateKey } = await SLIP10Node.fromDerivationPath({
derivationPath: [
`bip39:${mnemonicPhrase}`,
`bip32:${MAGIC_VALUE - HARDENED_VALUE}'`,
...computedDerivationPath,
],
curve: 'secp256k1',
});

// This should never happen, but this keeps TypeScript happy.
assert(privateKey, 'Failed to derive the entropy.');

return add0x(privateKey);
}

/**
* Builds the method implementation for `snap_getEntropy`. The implementation
* is based on the reference implementation of
* [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.getUnlockPromise - The method to get a promise that resolves
* once the extension is unlocked.
* @returns The method implementation.
*/
function getEntropyImplementation({
getMnemonic,
getUnlockPromise,
}: GetEntropyHooks) {
return async function getEntropy(
options: RestrictedMethodOptions<GetEntropyArgs>,
): Promise<Hex> {
const {
params,
context: { origin },
} = options;

assertStruct(
params,
GetEntropyArgsStruct,
'Invalid "snap_getEntropy" parameters',
ethErrors.rpc.invalidParams,
);

await getUnlockPromise(true);
const mnemonicPhrase = await getMnemonic();

return deriveEntropy(origin, mnemonicPhrase, params.salt);
};
}
3 changes: 3 additions & 0 deletions packages/rpc-methods/src/restricted/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
getBip32PublicKeyBuilder,
GetBip32PublicKeyMethodHooks,
} from './getBip32PublicKey';
import { getEntropyBuilder, GetEntropyHooks } from './getEntropy';

export {
AlertFields,
Expand All @@ -38,6 +39,7 @@ export type RestrictedMethodHooks = ConfirmMethodHooks &
GetBip32EntropyMethodHooks &
GetBip32PublicKeyMethodHooks &
GetBip44EntropyMethodHooks &
GetEntropyHooks &
InvokeSnapMethodHooks &
ManageStateMethodHooks &
NotifyMethodHooks;
Expand All @@ -48,6 +50,7 @@ export const restrictedMethodPermissionBuilders = {
[getBip32EntropyBuilder.targetKey]: getBip32EntropyBuilder,
[getBip32PublicKeyBuilder.targetKey]: getBip32PublicKeyBuilder,
[getBip44EntropyBuilder.targetKey]: getBip44EntropyBuilder,
[getEntropyBuilder.targetKey]: getEntropyBuilder,
[invokeSnapBuilder.targetKey]: invokeSnapBuilder,
[manageStateBuilder.targetKey]: manageStateBuilder,
[notifyBuilder.targetKey]: notifyBuilder,
Expand Down
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2871,6 +2871,7 @@ __metadata:
"@metamask/snaps-utils": ^0.23.0
"@metamask/types": ^1.1.0
"@metamask/utils": ^3.3.1
"@noble/hashes": ^1.1.3
"@types/node": ^14.14.25
deepmerge: ^4.2.2
eslint: ^7.30.0
Expand Down

0 comments on commit 9dcd86c

Please sign in to comment.