@metamask/snaps-rpc-methods
Version:
MetaMask Snaps JSON-RPC method implementations
305 lines • 11.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getMnemonicSeed = exports.getMnemonic = exports.HD_KEYRING = exports.UI_PERMISSIONS = exports.getValueFromEntropySource = exports.StateKeyStruct = exports.isValidStateKey = exports.getNodeFromSeed = exports.getNodeFromMnemonic = exports.getPathPrefix = exports.deriveEntropyFromSeed = exports.FORBIDDEN_KEYS = void 0;
const key_tree_1 = require("@metamask/key-tree");
const rpc_errors_1 = require("@metamask/rpc-errors");
const superstruct_1 = require("@metamask/superstruct");
const utils_1 = require("@metamask/utils");
const sha3_1 = require("@noble/hashes/sha3");
const endowments_1 = require("./endowments/index.cjs");
const HARDENED_VALUE = 0x80000000;
exports.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 = (0, utils_1.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 = (0, utils_1.stringToBytes)(input);
const saltBytes = (0, utils_1.stringToBytes)(salt);
// Get the derivation path from the snap ID.
const hash = (0, sha3_1.keccak_256)((0, utils_1.concatBytes)([inputBytes, (0, sha3_1.keccak_256)(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.
*/
async function deriveEntropyFromSeed({ input, salt = '', seed, magic, cryptographicFunctions, }) {
const computedDerivationPath = getEntropyDerivationPath({
input,
salt,
magic,
});
// Derive the private key using BIP-32.
const { privateKey } = await key_tree_1.SLIP10Node.fromSeed({
derivationPath: [seed, ...computedDerivationPath],
curve: 'secp256k1',
}, cryptographicFunctions);
// This should never happen, but this keeps TypeScript happy.
(0, utils_1.assert)(privateKey, 'Failed to derive the entropy.');
return (0, utils_1.add0x)(privateKey);
}
exports.deriveEntropyFromSeed = deriveEntropyFromSeed;
/**
* 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`.
*/
function getPathPrefix(curve) {
switch (curve) {
case 'secp256k1':
return 'bip32';
case 'ed25519':
return 'slip10';
case 'ed25519Bip32':
return 'cip3';
default:
return (0, utils_1.assertExhaustive)(curve);
}
}
exports.getPathPrefix = getPathPrefix;
/**
* 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.
*/
async function getNodeFromMnemonic({ curve, secretRecoveryPhrase, path, cryptographicFunctions, }) {
const prefix = getPathPrefix(curve);
return await key_tree_1.SLIP10Node.fromDerivationPath({
curve,
derivationPath: [
secretRecoveryPhrase,
...path.slice(1).map((index) => `${prefix}:${index}`),
],
}, cryptographicFunctions);
}
exports.getNodeFromMnemonic = getNodeFromMnemonic;
/**
* 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.
*/
async function getNodeFromSeed({ curve, seed, path, cryptographicFunctions, }) {
const prefix = getPathPrefix(curve);
return await key_tree_1.SLIP10Node.fromSeed({
curve,
derivationPath: [
seed,
...path.slice(1).map((index) => `${prefix}:${index}`),
],
}, cryptographicFunctions);
}
exports.getNodeFromSeed = getNodeFromSeed;
/**
* Validate the key of a state object.
*
* @param key - The key to validate.
* @returns `true` if the key is valid, `false` otherwise.
*/
function isValidStateKey(key) {
if (key === undefined) {
return true;
}
return key.split('.').every((part) => part.length > 0);
}
exports.isValidStateKey = isValidStateKey;
exports.StateKeyStruct = (0, superstruct_1.refine)((0, superstruct_1.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.
*/
async function getValueFromEntropySource(hook, source) {
try {
return await hook(source);
}
catch (error) {
if (error instanceof Error) {
throw rpc_errors_1.rpcErrors.invalidParams({
message: error.message,
});
}
throw rpc_errors_1.rpcErrors.internal({
message: 'An unknown error occurred.',
data: {
error: error.toString(),
},
});
}
}
exports.getValueFromEntropySource = getValueFromEntropySource;
/**
* 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.
*/
exports.UI_PERMISSIONS = [
'snap_dialog',
'snap_notify',
endowments_1.SnapEndowments.HomePage,
endowments_1.SnapEndowments.SettingsPage,
endowments_1.SnapEndowments.TransactionInsight,
endowments_1.SnapEndowments.SignatureInsight,
];
exports.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.
*/
async function getMnemonic(messenger, source) {
if (!source) {
const mnemonic = (await messenger.call('KeyringController:withKeyringV2Unsafe', {
type: exports.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.
(0, utils_1.assert)(type === exports.HD_KEYRING && mnemonic);
return mnemonic;
}
catch {
throw new Error(`Entropy source with ID "${source}" not found.`);
}
}
exports.getMnemonic = getMnemonic;
/**
* 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.
*/
async function getMnemonicSeed(messenger, source) {
if (!source) {
const seed = (await messenger.call('KeyringController:withKeyringV2Unsafe', {
type: exports.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.
(0, utils_1.assert)(type === exports.HD_KEYRING && seed);
return seed;
}
catch {
throw new Error(`Entropy source with ID "${source}" not found.`);
}
}
exports.getMnemonicSeed = getMnemonicSeed;
//# sourceMappingURL=utils.cjs.map