Skip to content

Commit d998da1

Browse files
committed
Validate keys
1 parent 0bf3de8 commit d998da1

File tree

4 files changed

+75
-9
lines changed

4 files changed

+75
-9
lines changed

packages/snaps-rpc-methods/src/permitted/getState.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ import {
88
create,
99
object,
1010
optional,
11-
string,
1211
StructError,
1312
} from '@metamask/superstruct';
1413
import type { PendingJsonRpcResponse, JsonRpcRequest } from '@metamask/utils';
1514
import { hasProperty, isPlainObject, type Json } from '@metamask/utils';
1615

1716
import { manageStateBuilder } from '../restricted/manageState';
1817
import type { MethodHooksObject } from '../utils';
18+
import { StateKeyStruct } from '../utils';
1919

2020
const hookNames: MethodHooksObject<GetStateHooks> = {
2121
hasPermission: true,
@@ -64,7 +64,7 @@ export type GetStateHooks = {
6464
};
6565

6666
const GetStateParametersStruct = object({
67-
key: optional(string()),
67+
key: optional(StateKeyStruct),
6868
encrypted: optional(boolean()),
6969
});
7070

packages/snaps-rpc-methods/src/permitted/setState.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,21 @@ import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine';
22
import type { PermittedHandlerExport } from '@metamask/permission-controller';
33
import { providerErrors, rpcErrors } from '@metamask/rpc-errors';
44
import type { SetStateParams, SetStateResult } from '@metamask/snaps-sdk';
5+
import type { JsonObject } from '@metamask/snaps-sdk/jsx';
56
import { type InferMatching } from '@metamask/snaps-utils';
67
import {
78
boolean,
89
create,
910
object as objectStruct,
1011
optional,
11-
string,
1212
StructError,
1313
} from '@metamask/superstruct';
1414
import type { PendingJsonRpcResponse, JsonRpcRequest } from '@metamask/utils';
15-
import { JsonStruct, isPlainObject, type Json } from '@metamask/utils';
15+
import { assert, JsonStruct, isPlainObject, type Json } from '@metamask/utils';
1616

1717
import { manageStateBuilder } from '../restricted/manageState';
1818
import type { MethodHooksObject } from '../utils';
19+
import { StateKeyStruct } from '../utils';
1920

2021
const hookNames: MethodHooksObject<SetStateHooks> = {
2122
hasPermission: true,
@@ -80,7 +81,7 @@ export type SetStateHooks = {
8081
};
8182

8283
const SetStateParametersStruct = objectStruct({
83-
key: optional(string()),
84+
key: optional(StateKeyStruct),
8485
value: JsonStruct,
8586
encrypted: optional(boolean()),
8687
});
@@ -193,8 +194,9 @@ export function set(
193194
object: Record<string, Json> | null,
194195
key: string | undefined,
195196
value: Json,
196-
): Json {
197-
if (key === undefined || key === '') {
197+
): JsonObject {
198+
if (key === undefined) {
199+
assert(isPlainObject(value));
198200
return value;
199201
}
200202

@@ -223,5 +225,5 @@ export function set(
223225
}
224226

225227
// This should never be reached.
226-
return null;
228+
return {};
227229
}

packages/snaps-rpc-methods/src/utils.test.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import { SIP_6_MAGIC_VALUE } from '@metamask/snaps-utils';
22
import { TEST_SECRET_RECOVERY_PHRASE_BYTES } from '@metamask/snaps-utils/test-utils';
3+
import { create, is } from '@metamask/superstruct';
34

45
import { ENTROPY_VECTORS } from './__fixtures__';
5-
import { deriveEntropy, getNode, getPathPrefix } from './utils';
6+
import {
7+
deriveEntropy,
8+
getNode,
9+
getPathPrefix,
10+
isValidStateKey,
11+
StateKeyStruct,
12+
} from './utils';
613

714
describe('deriveEntropy', () => {
815
it.each(ENTROPY_VECTORS)(
@@ -14,6 +21,7 @@ describe('deriveEntropy', () => {
1421
salt,
1522
mnemonicPhrase: TEST_SECRET_RECOVERY_PHRASE_BYTES,
1623
magic: SIP_6_MAGIC_VALUE,
24+
cryptographicFunctions: {},
1725
}),
1826
).toStrictEqual(entropy);
1927
},
@@ -47,6 +55,7 @@ describe('getNode', () => {
4755
curve: 'secp256k1',
4856
path: ['m', "44'", "1'"],
4957
secretRecoveryPhrase: TEST_SECRET_RECOVERY_PHRASE_BYTES,
58+
cryptographicFunctions: {},
5059
});
5160

5261
expect(node).toMatchInlineSnapshot(`
@@ -69,6 +78,7 @@ describe('getNode', () => {
6978
curve: 'ed25519',
7079
path: ['m', "44'", "1'"],
7180
secretRecoveryPhrase: TEST_SECRET_RECOVERY_PHRASE_BYTES,
81+
cryptographicFunctions: {},
7282
});
7383

7484
expect(node).toMatchInlineSnapshot(`
@@ -86,3 +96,34 @@ describe('getNode', () => {
8696
`);
8797
});
8898
});
99+
100+
describe('isValidStateKey', () => {
101+
it.each(['foo', 'foo.bar', 'foo.bar.baz'])(
102+
'returns `true` for "%s"',
103+
(key) => {
104+
expect(isValidStateKey(key)).toBe(true);
105+
},
106+
);
107+
108+
it.each(['', '.', '..', 'foo.', 'foo..bar', 'foo.bar.', 'foo.bar..baz'])(
109+
'returns `false` for "%s"',
110+
(key) => {
111+
expect(isValidStateKey(key)).toBe(false);
112+
},
113+
);
114+
});
115+
116+
describe('StateKeyStruct', () => {
117+
it.each(['foo', 'foo.bar', 'foo.bar.baz'])('accepts "%s"', (key) => {
118+
expect(is(key, StateKeyStruct)).toBe(true);
119+
});
120+
121+
it.each(['', '.', '..', 'foo.', 'foo..bar', 'foo.bar.', 'foo.bar..baz'])(
122+
'does not accept "%s"',
123+
(key) => {
124+
expect(() => create(key, StateKeyStruct)).toThrow(
125+
'Invalid state key. Each part of the key must be non-empty.',
126+
);
127+
},
128+
);
129+
});

packages/snaps-rpc-methods/src/utils.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
} from '@metamask/key-tree';
88
import { SLIP10Node } from '@metamask/key-tree';
99
import type { MagicValue } from '@metamask/snaps-utils';
10+
import { refine, string } from '@metamask/superstruct';
1011
import type { Hex } from '@metamask/utils';
1112
import {
1213
assertExhaustive,
@@ -238,3 +239,25 @@ export async function getNode({
238239
cryptographicFunctions,
239240
);
240241
}
242+
243+
/**
244+
* Validate the key of a state object.
245+
*
246+
* @param key - The key to validate.
247+
* @returns `true` if the key is valid, `false` otherwise.
248+
*/
249+
export function isValidStateKey(key: string | undefined) {
250+
if (key === undefined) {
251+
return true;
252+
}
253+
254+
return key.split('.').every((part) => part.length > 0);
255+
}
256+
257+
export const StateKeyStruct = refine(string(), 'state key', (value) => {
258+
if (!isValidStateKey(value)) {
259+
return 'Invalid state key. Each part of the key must be non-empty.';
260+
}
261+
262+
return true;
263+
});

0 commit comments

Comments
 (0)