@metamask/snaps-rpc-methods
Version:
MetaMask Snaps JSON-RPC method implementations
294 lines • 10.7 kB
JavaScript
import { SLIP10Node } from "@metamask/key-tree";
import { rpcErrors } from "@metamask/rpc-errors";
import { refine, string } from "@metamask/superstruct";
import { assertExhaustive, add0x, assert, concatBytes, createDataView, stringToBytes } from "@metamask/utils";
import { keccak_256 as keccak256 } from "@noble/hashes/sha3";
import { SnapEndowments } from "./endowments/index.mjs";
const HARDENED_VALUE = 0x80000000;
export const FORBIDDEN_KEYS = ['constructor', '__proto__', 'prototype'];
/**
* 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) {
const array = [];
const view = createDataView(hash);
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}'`);
}
return array;
}
/**
* Get the derivation path to use for entropy derivation.
*
* 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.magic - A hardened BIP-32 index, which is used to derive the
* root key from the mnemonic phrase.
* @returns The derivation path to be used for entropy key derivation.
*/
function getEntropyDerivationPath({ input, salt, magic, }) {
const inputBytes = stringToBytes(input);
const saltBytes = stringToBytes(salt);
// Get the derivation path from the snap ID.
const hash = keccak256(concatBytes([inputBytes, keccak256(saltBytes)]));
const computedDerivationPath = getDerivationPathArray(hash);
return [`bip32:${magic}`, ...computedDerivationPath];
}
/**
* Derive entropy from the given mnemonic seed 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.seed - The mnemonic seed 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 deriveEntropyFromSeed({ input, salt = '', seed, magic, cryptographicFunctions, }) {
const computedDerivationPath = getEntropyDerivationPath({
input,
salt,
magic,
});
// Derive the private key using BIP-32.
const { privateKey } = await SLIP10Node.fromSeed({
derivationPath: [seed, ...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:
*
* - The Secp256k1 curve always uses the BIP-32 specification.
* - The Ed25519 curve always uses the SLIP-10 specification.
* - The BIP-32-Ed25519 curve always uses the CIP-3 specification.
*
* While this does not matter in most situations (no known case at the time of
* writing), `key-tree` requires a specific specification to be used.
*
* @param curve - The curve to get the path prefix for. The curve is NOT
* validated by this function.
* @returns The path prefix, i.e., `bip32` or `slip10`.
*/
export function getPathPrefix(curve) {
switch (curve) {
case 'secp256k1':
return 'bip32';
case 'ed25519':
return 'slip10';
case 'ed25519Bip32':
return 'cip3';
default:
return assertExhaustive(curve);
}
}
/**
* 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.secretRecoveryPhrase - The secret recovery phrase 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 getNodeFromMnemonic({ curve, secretRecoveryPhrase, path, cryptographicFunctions, }) {
const prefix = getPathPrefix(curve);
return await SLIP10Node.fromDerivationPath({
curve,
derivationPath: [
secretRecoveryPhrase,
...path.slice(1).map((index) => `${prefix}:${index}`),
],
}, cryptographicFunctions);
}
/**
* 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, }) {
const prefix = getPathPrefix(curve);
return await SLIP10Node.fromSeed({
curve,
derivationPath: [
seed,
...path.slice(1).map((index) => `${prefix}:${index}`),
],
}, cryptographicFunctions);
}
/**
* Validate the key of a state object.
*
* @param key - The key to validate.
* @returns `true` if the key is valid, `false` otherwise.
*/
export function isValidStateKey(key) {
if (key === undefined) {
return true;
}
return key.split('.').every((part) => part.length > 0);
}
export const StateKeyStruct = refine(string(), 'state key', (value) => {
if (!isValidStateKey(value)) {
return 'Invalid state key. Each part of the key must be non-empty.';
}
return true;
});
/**
* 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 hook - The hook.
* @param source - The entropy source to use.
* @returns The secret recovery phrase.
*/
export async function getValueFromEntropySource(hook, source) {
try {
return await hook(source);
}
catch (error) {
if (error instanceof Error) {
throw rpcErrors.invalidParams({
message: error.message,
});
}
throw rpcErrors.internal({
message: 'An unknown error occurred.',
data: {
error: error.toString(),
},
});
}
}
/**
* The permissions that allow a Snap to show UI. Snaps must have at least one
* of these permissions to use the interface management RPC methods.
*/
export const UI_PERMISSIONS = [
'snap_dialog',
'snap_notify',
SnapEndowments.HomePage,
SnapEndowments.SettingsPage,
SnapEndowments.TransactionInsight,
SnapEndowments.SignatureInsight,
];
export 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, source) {
if (!source) {
const mnemonic = (await messenger.call('KeyringController:withKeyringV2Unsafe', {
type: HD_KEYRING,
index: 0,
}, async ({ keyring }) => keyring.mnemonic));
if (!mnemonic) {
throw new Error('Primary keyring mnemonic unavailable.');
}
return mnemonic;
}
try {
const keyringData = await messenger.call('KeyringController:withKeyringV2Unsafe', {
id: source,
}, async ({ keyring }) => ({
type: keyring.type,
mnemonic: keyring.mnemonic,
}));
const { type, mnemonic } = keyringData;
// 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.
assert(type === HD_KEYRING && mnemonic);
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, source) {
if (!source) {
const seed = (await messenger.call('KeyringController:withKeyringV2Unsafe', {
type: HD_KEYRING,
index: 0,
}, async ({ keyring }) => keyring.seed));
if (!seed) {
throw new Error('Primary keyring mnemonic unavailable.');
}
return seed;
}
try {
const keyringData = await messenger.call('KeyringController:withKeyringV2Unsafe', {
id: source,
}, async ({ keyring }) => ({
type: keyring.type,
seed: keyring.seed,
}));
const { type, seed } = keyringData;
// 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.
assert(type === HD_KEYRING && seed);
return seed;
}
catch {
throw new Error(`Entropy source with ID "${source}" not found.`);
}
}
//# sourceMappingURL=utils.mjs.map