Skip to content

Commit

Permalink
Move encryption logic to rpc-methods
Browse files Browse the repository at this point in the history
  • Loading branch information
Mrtenz committed Dec 14, 2022
1 parent ca9642b commit 29be72b
Show file tree
Hide file tree
Showing 12 changed files with 326 additions and 205 deletions.
6 changes: 3 additions & 3 deletions packages/rpc-methods/jest.config.js
Expand Up @@ -11,9 +11,9 @@ module.exports = deepmerge(baseConfig, {
coverageThreshold: {
global: {
branches: 75.18,
functions: 86.56,
lines: 87.27,
statements: 86.78,
functions: 87.14,
lines: 87.96,
statements: 87.57,
},
},
testTimeout: 2500,
Expand Down
1 change: 1 addition & 0 deletions packages/rpc-methods/package.json
Expand Up @@ -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
@@ -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
173 changes: 162 additions & 11 deletions packages/rpc-methods/src/restricted/manageState.test.ts
@@ -1,18 +1,44 @@
import { encrypt } from '@metamask/browser-passworder';
import {
deriveEntropy,
STATE_ENCRYPTION_MAGIC_VALUE,
} from '@metamask/snaps-utils';
import {
MOCK_LOCAL_SNAP_ID,
MOCK_SNAP_ID,
TEST_SECRET_RECOVERY_PHRASE,
} from '@metamask/snaps-utils/test-utils';
import { ethErrors } from 'eth-rpc-errors';

import {
getManageStateImplementation,
getValidatedParams,
ManageStateOperation,
specificationBuilder,
STATE_ENCRYPTION_SALT,
} from './manageState';

Object.defineProperty(global, 'crypto', {
value: {
/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
...require('crypto').webcrypto,
subtle: require('crypto').webcrypto.subtle,
/* eslint-enable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
getRandomValues: (input: Uint8Array) => input.fill(0),
},
});

describe('snap_manageState', () => {
const MOCK_SMALLER_STORAGE_SIZE_LIMIT = 10; // In bytes

describe('specification', () => {
it('builds specification', () => {
const methodHooks = {
clearSnapState: jest.fn(),
getSnapState: jest.fn(),
updateSnapState: jest.fn(),
getMnemonic: jest.fn(),
getUnlockPromise: jest.fn(),
};

expect(
Expand All @@ -31,28 +57,40 @@ describe('snap_manageState', () => {

describe('getManageStateImplementation', () => {
it('gets snap state', async () => {
const encryptionKey = await deriveEntropy({
input: MOCK_SNAP_ID,
salt: STATE_ENCRYPTION_SALT,
mnemonicPhrase: TEST_SECRET_RECOVERY_PHRASE,
magic: STATE_ENCRYPTION_MAGIC_VALUE,
});

const mockSnapState = {
some: {
data: 'for a snap state',
},
};

const mockEncryptedState = encrypt(encryptionKey, mockSnapState);

const clearSnapState = jest.fn().mockResolvedValueOnce(true);
const getSnapState = jest.fn().mockResolvedValueOnce(mockSnapState);
const getSnapState = jest.fn().mockResolvedValueOnce(mockEncryptedState);
const updateSnapState = jest.fn().mockResolvedValueOnce(true);

const manageStateImplementation = getManageStateImplementation({
clearSnapState,
getSnapState,
updateSnapState,
getMnemonic: jest.fn().mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE),
getUnlockPromise: jest.fn(),
});

const result = await manageStateImplementation({
context: { origin: 'snap-origin' },
context: { origin: MOCK_SNAP_ID },
method: 'snap_manageState',
params: { operation: ManageStateOperation.GetState },
});

expect(getSnapState).toHaveBeenCalledWith('snap-origin');
expect(getSnapState).toHaveBeenCalledWith(MOCK_SNAP_ID);
expect(result).toStrictEqual(mockSnapState);
});

Expand All @@ -65,18 +103,78 @@ describe('snap_manageState', () => {
clearSnapState,
getSnapState,
updateSnapState,
getMnemonic: jest.fn().mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE),
getUnlockPromise: jest.fn(),
});

await manageStateImplementation({
context: { origin: 'snap-origin' },
context: { origin: MOCK_SNAP_ID },
method: 'snap_manageState',
params: { operation: ManageStateOperation.ClearState },
});

expect(clearSnapState).toHaveBeenCalledWith('snap-origin');
expect(clearSnapState).toHaveBeenCalledWith(MOCK_SNAP_ID);
});

it('updates snap state', async () => {
const encryptionKey = await deriveEntropy({
input: MOCK_SNAP_ID,
salt: STATE_ENCRYPTION_SALT,
mnemonicPhrase: TEST_SECRET_RECOVERY_PHRASE,
magic: STATE_ENCRYPTION_MAGIC_VALUE,
});

const mockSnapState = {
some: {
data: 'for a snap state',
},
};

const mockEncryptedState = await encrypt(encryptionKey, mockSnapState);

const clearSnapState = jest.fn().mockResolvedValueOnce(true);
const getSnapState = jest.fn().mockResolvedValueOnce(true);
const updateSnapState = jest.fn().mockResolvedValueOnce(true);

const manageStateImplementation = getManageStateImplementation({
clearSnapState,
getSnapState,
updateSnapState,
getMnemonic: jest.fn().mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE),
getUnlockPromise: jest.fn(),
});

await manageStateImplementation({
context: { origin: MOCK_SNAP_ID },
method: 'snap_manageState',
params: {
operation: ManageStateOperation.UpdateState,
newState: mockSnapState,
},
});

expect(updateSnapState).toHaveBeenCalledWith(
MOCK_SNAP_ID,
mockEncryptedState,
);
});

it('uses different encryption for different snap IDs', async () => {
const encryptionKey = await deriveEntropy({
input: MOCK_SNAP_ID,
salt: STATE_ENCRYPTION_SALT,
mnemonicPhrase: TEST_SECRET_RECOVERY_PHRASE,
magic: STATE_ENCRYPTION_MAGIC_VALUE,
});

const mockSnapState = {
some: {
data: 'for a snap state',
},
};

const mockEncryptedState = await encrypt(encryptionKey, mockSnapState);

const clearSnapState = jest.fn().mockResolvedValueOnce(true);
const getSnapState = jest.fn().mockResolvedValueOnce(true);
const updateSnapState = jest.fn().mockResolvedValueOnce(true);
Expand All @@ -85,19 +183,66 @@ describe('snap_manageState', () => {
clearSnapState,
getSnapState,
updateSnapState,
getMnemonic: jest.fn().mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE),
getUnlockPromise: jest.fn(),
});

await manageStateImplementation({
context: { origin: MOCK_SNAP_ID },
method: 'snap_manageState',
params: {
operation: ManageStateOperation.UpdateState,
newState: mockSnapState,
},
});
const newState = { data: 'updated data' };

await manageStateImplementation({
context: { origin: 'snap-origin' },
context: { origin: MOCK_LOCAL_SNAP_ID },
method: 'snap_manageState',
params: {
operation: ManageStateOperation.UpdateState,
newState,
newState: mockSnapState,
},
});

expect(updateSnapState).toHaveBeenCalledWith('snap-origin', newState);
expect(updateSnapState).toHaveBeenCalledTimes(2);
expect(updateSnapState).toHaveBeenNthCalledWith(
1,
MOCK_SNAP_ID,
mockEncryptedState,
);

expect(updateSnapState).not.toHaveBeenNthCalledWith(
2,
MOCK_LOCAL_SNAP_ID,
mockEncryptedState,
);
});

it('throws an error if the state is corrupt', async () => {
const clearSnapState = jest.fn().mockResolvedValueOnce(true);
const getSnapState = jest.fn().mockResolvedValueOnce('foo');
const updateSnapState = jest.fn().mockResolvedValueOnce(true);

const manageStateImplementation = getManageStateImplementation({
clearSnapState,
getSnapState,
updateSnapState,
getMnemonic: jest.fn().mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE),
getUnlockPromise: jest.fn(),
});

await expect(
manageStateImplementation({
context: { origin: MOCK_SNAP_ID },
method: 'snap_manageState',
params: { operation: ManageStateOperation.GetState },
}),
).rejects.toThrow(
ethErrors.rpc.internal({
message: 'Failed to decrypt snap state, the state must be corrupted.',
}),
);
});

it('throws an error on update if the new state is not plain object', async () => {
Expand All @@ -109,14 +254,17 @@ describe('snap_manageState', () => {
clearSnapState,
getSnapState,
updateSnapState,
getMnemonic: jest.fn().mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE),
getUnlockPromise: jest.fn(),
});

const newState = (a: unknown) => {
return a;
};

await expect(
manageStateImplementation({
context: { origin: 'snap-origin' },
context: { origin: MOCK_SNAP_ID },
method: 'snap_manageState',
params: {
operation: ManageStateOperation.UpdateState,
Expand All @@ -129,7 +277,7 @@ describe('snap_manageState', () => {
'Invalid snap_manageState "updateState" parameter: The new state must be a plain object.',
);

expect(updateSnapState).not.toHaveBeenCalledWith('snap-origin', newState);
expect(updateSnapState).not.toHaveBeenCalledWith(MOCK_SNAP_ID, newState);
});

it('throws an error on update if the new state is not valid json serializable object', async () => {
Expand All @@ -141,7 +289,10 @@ describe('snap_manageState', () => {
clearSnapState,
getSnapState,
updateSnapState,
getMnemonic: jest.fn().mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE),
getUnlockPromise: jest.fn(),
});

const newState = {
something: {
something: {
Expand Down

0 comments on commit 29be72b

Please sign in to comment.