-
-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathderivation.ts
More file actions
executable file
·246 lines (220 loc) · 7.8 KB
/
derivation.ts
File metadata and controls
executable file
·246 lines (220 loc) · 7.8 KB
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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
import { assert } from '@metamask/utils';
import { BIP44CoinTypeNode } from './BIP44CoinTypeNode';
import { BIP44Node } from './BIP44Node';
import type { Network, SLIP10Path } from './constants';
import {
BIP_32_PATH_REGEX,
BIP_39_PATH_REGEX,
MIN_BIP_44_DEPTH,
SLIP_10_PATH_REGEX,
CIP_3_PATH_REGEX,
} from './constants';
import type { CryptographicFunctions } from './cryptography';
import type { SupportedCurve } from './curves';
import { getCurveByName } from './curves';
import type { Deriver } from './derivers';
import { derivers } from './derivers';
import { SLIP10Node } from './SLIP10Node';
/**
* Ethereum default seed path: "m/44'/60'/0'/0/{account_index}"
* Multipath: "bip32:44'/bip32:60'/bip32:0'/bip32:0/bip32:{account_index}"
*
* m: { privateKey, chainCode } = sha512Hmac("Bitcoin seed", masterSeed)
* 44': { privateKey, chainCode } = parentKey.privateKey + sha512Hmac(parentKey.chainCode, [0x00, parentKey.privateKey, index + HARDENED_OFFSET])
* 60': { privateKey, chainCode } = parentKey.privateKey + sha512Hmac(parentKey.chainCode, [0x00, parentKey.privateKey, index + HARDENED_OFFSET])
* 0': { privateKey, chainCode } = parentKey.privateKey + sha512Hmac(parentKey.chainCode, [0x00, parentKey.privateKey, index + HARDENED_OFFSET])
* 0: { privateKey, chainCode } = parentKey.privateKey + sha512Hmac(parentKey.chainCode, [parentKey.publicKey, index])
* 0: { privateKey, chainCode } = parentKey.privateKey + sha512Hmac(parentKey.chainCode, [parentKey.publicKey, index])
*/
type BaseDeriveKeyFromPathArgs = {
path: SLIP10Path;
depth?: number;
};
type DeriveKeyFromPathNodeArgs = BaseDeriveKeyFromPathArgs & {
node?: SLIP10Node | BIP44Node | BIP44CoinTypeNode;
};
type DeriveKeyFromPathCurveArgs = BaseDeriveKeyFromPathArgs & {
network?: Network | undefined;
curve: SupportedCurve;
};
type DeriveKeyFromPathArgs =
| DeriveKeyFromPathNodeArgs
| DeriveKeyFromPathCurveArgs;
/**
* Takes a full or partial HD path string and returns the key corresponding to
* the given path, with the following constraints:
*
* - If the path starts with a BIP-32 node, a parent key must be provided.
* - If the path starts with a BIP-39 node, a parent key must NOT be provided.
* - The path cannot exceed 5 BIP-32 nodes in length, optionally preceded by
* a single BIP-39 node.
*
* WARNING: It is the consumer's responsibility to ensure that the path is valid
* relative to its parent key.
*
* @param args - The arguments for deriving a key from a path.
* @param args.path - A full or partial HD path, e.g.:
* `bip39:SEED_PHRASE/bip32:44'/bip32:60'/bip32:0'/bip32:0/bip32:0`.
* BIP-39 seed phrases must be lowercase, space-delimited, and 12-24 words long.
* @param args.node - The node to derive from.
* @param args.depth - The depth of the segment.
* @param cryptographicFunctions - The cryptographic functions to use. If
* provided, these will be used instead of the built-in implementations.
* @returns The derived key.
*/
export async function deriveKeyFromPath(
args: DeriveKeyFromPathArgs,
cryptographicFunctions?: CryptographicFunctions,
): Promise<SLIP10Node> {
const { path, depth = path.length } = args;
const node = 'node' in args ? args.node : undefined;
const network = 'network' in args ? args.network : node?.network;
const curve = 'curve' in args ? args.curve : node?.curve;
if (
node &&
!(node instanceof SLIP10Node) &&
!(node instanceof BIP44Node) &&
!(node instanceof BIP44CoinTypeNode)
) {
throw new Error(
'Invalid arguments: Node must be a SLIP-10 node or a BIP-44 node when provided.',
);
}
if (!curve) {
throw new Error(
'Invalid arguments: Must specify either a parent node or curve.',
);
}
validatePathSegment(
path,
Boolean(node?.privateKey) || Boolean(node?.publicKey),
depth,
);
// Derive through each part of path. `pathSegment` needs to be cast because
// `HDPathTuple.reduce()` doesn't work. Note that the first element of the
// path can be a Uint8Array.
return await (path as readonly [Uint8Array | string, ...string[]]).reduce<
Promise<SLIP10Node>
>(
async (promise, pathNode, index) => {
const derivedNode = await promise;
if (typeof pathNode === 'string') {
const [pathType, pathPart] = pathNode.split(':');
assert(pathType);
assert(pathPart);
assert(hasDeriver(pathType), `Unknown derivation type: "${pathType}".`);
const deriver = derivers[pathType] as Deriver;
return await deriver.deriveChildKey(
{
path: pathPart,
node: derivedNode,
curve: getCurveByName(curve),
network,
},
cryptographicFunctions,
);
}
// Only the first path segment can be a Uint8Array.
assert(index === 0, getMalformedError());
return await derivers.bip39.deriveChildKey(
{
path: pathNode,
node: derivedNode,
curve: getCurveByName(curve),
network,
},
cryptographicFunctions,
);
},
Promise.resolve(node as SLIP10Node),
);
}
/**
* Check if the given path type is a valid deriver.
*
* @param pathType - The path type to check.
* @returns Whether the path type is a valid deriver.
*/
function hasDeriver(pathType: string): pathType is keyof typeof derivers {
return pathType in derivers;
}
/**
* The path segment must be one of the following:
* - A lone BIP-32 path node.
* - A lone BIP-39 path node.
* - A multipath.
*
* @param path - The path segment string to validate.
* @param hasKey - Whether the path segment has a key.
* @param depth - The depth of the segment.
*/
export function validatePathSegment(
path: SLIP10Path,
hasKey: boolean,
depth?: number,
): void {
if ((path as any).length === 0) {
throw new Error(`Invalid HD path segment: The segment must not be empty.`);
}
let startsWithBip39 = false;
path.forEach((node, index) => {
if (index === 0) {
startsWithBip39 =
node instanceof Uint8Array || BIP_39_PATH_REGEX.test(node);
if (
// TypeScript is unable to infer that `node` is a string here, so we
// need to explicitly check it again.
!(node instanceof Uint8Array) &&
!startsWithBip39 &&
!BIP_32_PATH_REGEX.test(node) &&
!SLIP_10_PATH_REGEX.test(node) &&
!CIP_3_PATH_REGEX.test(node)
) {
throw getMalformedError();
}
} else if (
node instanceof Uint8Array ||
(!BIP_32_PATH_REGEX.test(node) &&
!SLIP_10_PATH_REGEX.test(node) &&
!CIP_3_PATH_REGEX.test(node))
) {
throw getMalformedError();
}
});
if (depth === MIN_BIP_44_DEPTH && (!startsWithBip39 || path.length !== 1)) {
throw new Error(
`Invalid HD path segment: The segment must consist of a single BIP-39 node for depths of ${MIN_BIP_44_DEPTH}. Received: "${String(
path,
)}".`,
);
}
if (!hasKey && !startsWithBip39) {
throw new Error(
'Invalid derivation parameters: Must specify parent key if the first node of the path segment is not a BIP-39 node.',
);
}
if (hasKey && startsWithBip39) {
throw new Error(
'Invalid derivation parameters: May not specify parent key if the path segment starts with a BIP-39 node.',
);
}
const pathWithoutKey = (startsWithBip39 ? path.slice(1) : path) as string[];
if (pathWithoutKey.length > 0) {
const firstSegmentType = pathWithoutKey[0]?.split(':')[0];
assert(firstSegmentType);
assert(
pathWithoutKey.every((segment) =>
segment.startsWith(`${firstSegmentType}:`),
),
`Invalid HD path segment: Cannot mix 'bip32' and 'slip10' path segments.`,
);
}
}
/**
* Get the error for a malformed path segment.
*
* @returns The error.
*/
function getMalformedError(): Error {
return new Error('Invalid HD path segment: The path segment is malformed.');
}