-
Notifications
You must be signed in to change notification settings - Fork 542
/
getEntropy.ts
191 lines (168 loc) · 5.49 KB
/
getEntropy.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
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 { ethErrors } from 'eth-rpc-errors';
import { Infer, literal, object, optional, string } from 'superstruct';
const HARDENED_VALUE = 0x80000000;
const targetKey = 'snap_getEntropy';
type GetEntropySpecificationBuilderOptions = {
allowedCaveats?: Readonly<NonEmptyArray<string>> | null;
methodHooks: GetEntropyHooks;
};
type GetEntropySpecification = ValidPermissionSpecification<{
permissionType: PermissionType.RestrictedMethod;
targetKey: typeof targetKey;
methodImplementation: ReturnType<typeof getEntropyImplementation>;
allowedCaveats: Readonly<NonEmptyArray<string>> | null;
}>;
export const GetEntropyArgsStruct = object({
version: literal(1),
salt: optional(string()),
});
/**
* @property version - The version of the `snap_getEntropy` method. This must be
* the numeric literal `1`.
* @property salt - A string to use as the salt when deriving the entropy. If
* omitted, the salt will be an empty string.
*/
export type GetEntropyArgs = Infer<typeof GetEntropyArgsStruct>;
const specificationBuilder: PermissionSpecificationBuilder<
PermissionType.RestrictedMethod,
GetEntropySpecificationBuilderOptions,
GetEntropySpecification
> = ({
allowedCaveats = null,
methodHooks,
}: GetEntropySpecificationBuilderOptions) => {
return {
permissionType: PermissionType.RestrictedMethod,
targetKey,
allowedCaveats,
methodImplementation: getEntropyImplementation(methodHooks),
};
};
export const getEntropyBuilder = Object.freeze({
targetKey,
specificationBuilder,
methodHooks: {
getMnemonic: true,
getUnlockPromise: true,
},
} as const);
export type GetEntropyHooks = {
/**
* @returns The mnemonic of the user's primary keyring.
*/
getMnemonic: () => Promise<string>;
/**
* Waits for the extension to be unlocked.
*
* @returns A promise that resolves once the extension is unlocked.
*/
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
* [SIP-6](https://metamask.github.io/SIPs/SIPS/sip-6).
*
* @param hooks - The RPC method hooks.
* @param hooks.getMnemonic - The method to get the mnemonic of the user's
* primary keyring.
* @param hooks.getUnlockPromise - The method to get a promise that resolves
* once the extension is unlocked.
* @returns The method implementation.
*/
function getEntropyImplementation({
getMnemonic,
getUnlockPromise,
}: GetEntropyHooks) {
return async function getEntropy(
options: RestrictedMethodOptions<GetEntropyArgs>,
): Promise<Hex> {
const {
params,
context: { origin },
} = options;
assertStruct(
params,
GetEntropyArgsStruct,
'Invalid "snap_getEntropy" parameters',
ethErrors.rpc.invalidParams,
);
await getUnlockPromise(true);
const mnemonicPhrase = await getMnemonic();
return deriveEntropy(origin, mnemonicPhrase, params.salt);
};
}