Skip to content

Commit

Permalink
BREAKING: Use SIP-6 algorithm for state encryption (MetaMask#1055)
Browse files Browse the repository at this point in the history
* Use SIP-6 algorithm for state encryption

* Make magic value non-optional

* Disallow deriving the state encryption magic value

* Fix iframe tests

* Remove unused files

* Move encryption logic to rpc-methods

* Remove unused parameter

* Move stuff back to rpc-methods

* Remove more stuff from controllers

* Revert controllers jest environment

* Improve snap_manageState test
  • Loading branch information
Mrtenz authored Dec 14, 2022
1 parent 1fe00c8 commit 08ac6fa
Show file tree
Hide file tree
Showing 23 changed files with 473 additions and 274 deletions.
14 changes: 9 additions & 5 deletions packages/rpc-methods/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ const deepmerge = require('deepmerge');
const baseConfig = require('../../jest.config.base');

module.exports = deepmerge(baseConfig, {
coveragePathIgnorePatterns: ['./src/index.ts'],
collectCoverageFrom: [
'./src/**/*.ts',
'!./src/**/*.test.ts',
'!./src/**/index.ts',
],
coverageThreshold: {
global: {
branches: 74.63,
functions: 83.33,
lines: 83.46,
statements: 82.44,
branches: 75.18,
functions: 87.5,
lines: 88.67,
statements: 88.32,
},
},
testTimeout: 2500,
Expand Down
3 changes: 2 additions & 1 deletion packages/rpc-methods/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
],
"scripts": {
"test": "jest && yarn posttest",
"posttest": "jest-it-up",
"posttest": "jest-it-up --margin 0.25",
"test:ci": "yarn test",
"lint:eslint": "eslint . --cache --ext js,ts",
"lint:misc": "prettier '**/*.json' '**/*.md' '!CHANGELOG.md' --ignore-path ../../.gitignore",
Expand All @@ -26,6 +26,7 @@
"publish:package": "../../scripts/publish-package.sh"
},
"dependencies": {
"@metamask/browser-passworder": "^4.0.2",
"@metamask/key-tree": "^6.0.0",
"@metamask/permission-controller": "^1.0.1",
"@metamask/snaps-utils": "^0.26.2",
Expand Down
4 changes: 1 addition & 3 deletions packages/rpc-methods/src/restricted/getBip32Entropy.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { SIP_6_MAGIC_VALUE, SnapCaveatType } from '@metamask/snaps-utils';
import { TEST_SECRET_RECOVERY_PHRASE } from '@metamask/snaps-utils/test-utils';

import {
getBip32EntropyBuilder,
Expand All @@ -8,9 +9,6 @@ import {
validateCaveatPaths,
} from './getBip32Entropy';

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

describe('validateCaveatPaths', () => {
it.each([[], null, undefined, 'foo'])(
'throws if the value is not an array or empty',
Expand Down
16 changes: 1 addition & 15 deletions packages/rpc-methods/src/restricted/getEntropy.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { PermissionType } from '@metamask/permission-controller';
import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils';

import { ENTROPY_VECTORS } from './__fixtures__';
import { deriveEntropy, getEntropyBuilder } from './getEntropy';
import { getEntropyBuilder } from './getEntropy';

const TEST_SECRET_RECOVERY_PHRASE =
'test test test test test test test test test test test ball';
Expand Down Expand Up @@ -36,19 +35,6 @@ describe('getEntropyBuilder', () => {
});
});

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
Expand Down
86 changes: 8 additions & 78 deletions packages/rpc-methods/src/restricted/getEntropy.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,15 @@
import { HardenedBIP32Node, SLIP10Node } from '@metamask/key-tree';
import {
PermissionSpecificationBuilder,
PermissionType,
RestrictedMethodOptions,
ValidPermissionSpecification,
} from '@metamask/permission-controller';
import { SIP_6_MAGIC_VALUE } from '@metamask/snaps-utils';
import {
add0x,
assert,
assertStruct,
concatBytes,
createDataView,
Hex,
NonEmptyArray,
stringToBytes,
} from '@metamask/utils';
import { keccak_256 as keccak256 } from '@noble/hashes/sha3';
import { assertStruct, Hex, NonEmptyArray } from '@metamask/utils';
import { ethErrors } from 'eth-rpc-errors';
import { Infer, literal, object, optional, string } from 'superstruct';

const HARDENED_VALUE = 0x80000000;
import { deriveEntropy } from '../utils';

const targetKey = 'snap_getEntropy';

Expand Down Expand Up @@ -88,70 +77,6 @@ export type GetEntropyHooks = {
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 = 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}'` 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:${SIP_6_MAGIC_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
Expand Down Expand Up @@ -186,6 +111,11 @@ function getEntropyImplementation({
await getUnlockPromise(true);
const mnemonicPhrase = await getMnemonic();

return deriveEntropy(origin, mnemonicPhrase, params.salt);
return deriveEntropy({
input: origin,
salt: params.salt,
mnemonicPhrase,
magic: SIP_6_MAGIC_VALUE,
});
};
}
Loading

0 comments on commit 08ac6fa

Please sign in to comment.