hd-wallet-wasm 2.1.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,368 @@
1
+ /**
2
+ * HD Wallet WASM - input validation shared by every wrapper
3
+ *
4
+ * The rule: no input reaches native code until its type, its length (exact,
5
+ * or one of an allowed set) and, for keys, its validity are known good. Native
6
+ * exports read fixed-size inputs (32-byte keys, 33-byte public keys, 64- or
7
+ * 96-byte signatures) without a length, so an input of the wrong size would
8
+ * otherwise be completed from, or written past, stale heap memory.
9
+ *
10
+ * Keys are checked by the module itself, never with curve arithmetic in JS:
11
+ * - hd_curve_validate_privkey: the curve's scalar size, and 0 < d < n for
12
+ * secp256k1, P-256 and P-384 (any 32 bytes are an Ed25519 seed or an X25519
13
+ * scalar).
14
+ * - hd_curve_validate_pubkey: SEC1 02/03/04 encodings decoding to a curve point
15
+ * (libsecp256k1 for secp256k1, OpenSSL for P-256/P-384); Ed25519 keys
16
+ * canonical, decodable and not of small order; X25519 keys 32 bytes.
17
+ *
18
+ * Operations throw on an invalid input. Verification throws only for a
19
+ * programmer error (an argument of the wrong type, an unknown curve or coin)
20
+ * and returns false for every other invalid input: a signature, key or hash of
21
+ * the wrong length, a key that is not a point, a malformed encoding.
22
+ *
23
+ * @module hd-wallet-wasm/input-validation
24
+ */
25
+
26
+ /** A UTF-16 surrogate without its partner. */
27
+ const UNPAIRED_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
28
+
29
+ /** Curve ids (match the Curve enum in index.mjs and the aligned schema). */
30
+ export const CURVE_ID = Object.freeze({
31
+ SECP256K1: 0,
32
+ ED25519: 1,
33
+ P256: 2,
34
+ P384: 3,
35
+ X25519: 4,
36
+ });
37
+
38
+ /** Error codes thrown here (match ErrorCode in index.mjs). */
39
+ export const VALIDATION_ERROR = Object.freeze({
40
+ INVALID_ARGUMENT: 2,
41
+ NOT_SUPPORTED: 3,
42
+ OUT_OF_MEMORY: 4,
43
+ INVALID_PRIVATE_KEY: 400,
44
+ INVALID_PUBLIC_KEY: 401,
45
+ INVALID_SIGNATURE: 402,
46
+ });
47
+
48
+ // The length a typed array really has, read from its internal slot through
49
+ // %TypedArray%.prototype's getter: a subclass getter or an own `length`
50
+ // property cannot change it. Captured at load.
51
+ const TYPED_ARRAY_PROTOTYPE = Object.getPrototypeOf(Uint8Array.prototype);
52
+ const TYPED_ARRAY_LENGTH = Object.getOwnPropertyDescriptor(TYPED_ARRAY_PROTOTYPE, 'length').get;
53
+ const TYPED_ARRAY_BUFFER = Object.getOwnPropertyDescriptor(TYPED_ARRAY_PROTOTYPE, 'buffer').get;
54
+ const BUFFER_RESIZABLE = Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, 'resizable')?.get;
55
+ const BUFFER_DETACHED = Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, 'detached')?.get;
56
+ const SHARED_BUFFER_GROWABLE = typeof SharedArrayBuffer === 'function'
57
+ ? Object.getOwnPropertyDescriptor(SharedArrayBuffer.prototype, 'growable')?.get
58
+ : undefined;
59
+
60
+ /** The internal length of a typed array, or undefined for anything else. */
61
+ function intrinsicLength(value) {
62
+ try {
63
+ return TYPED_ARRAY_LENGTH.call(value);
64
+ } catch {
65
+ return undefined;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Whether reading `value.length` reaches %TypedArray%.prototype's getter: no
71
+ * own `length` property and none on a subclass prototype, so no later read
72
+ * can answer differently from the check.
73
+ */
74
+ function hasIntrinsicLengthProperty(value) {
75
+ for (let object = value; object !== null && object !== TYPED_ARRAY_PROTOTYPE;
76
+ object = Object.getPrototypeOf(object)) {
77
+ if (Object.prototype.hasOwnProperty.call(object, 'length')) return false;
78
+ }
79
+ return true;
80
+ }
81
+
82
+ /** Whether a typed array's buffer is detached (transferred). */
83
+ function isDetached(value) {
84
+ const buffer = TYPED_ARRAY_BUFFER.call(value);
85
+ if (BUFFER_DETACHED) {
86
+ try {
87
+ return BUFFER_DETACHED.call(buffer);
88
+ } catch {
89
+ return false; // A SharedArrayBuffer cannot be detached.
90
+ }
91
+ }
92
+ if (TYPED_ARRAY_LENGTH.call(value) !== 0) return false;
93
+ try {
94
+ new Uint8Array(value);
95
+ return false;
96
+ } catch {
97
+ return true;
98
+ }
99
+ }
100
+
101
+ /** Whether a typed array's buffer can never change size (not resizable or growable). */
102
+ function hasFixedSize(value) {
103
+ const buffer = TYPED_ARRAY_BUFFER.call(value);
104
+ for (const getter of [BUFFER_RESIZABLE, SHARED_BUFFER_GROWABLE]) {
105
+ if (!getter) continue;
106
+ try {
107
+ if (getter.call(buffer)) return false;
108
+ } catch {
109
+ // Not that kind of buffer.
110
+ }
111
+ }
112
+ return true;
113
+ }
114
+
115
+ const CURVE_NAMES = Object.freeze(['secp256k1', 'ed25519', 'P-256', 'P-384', 'X25519']);
116
+ const PRIVATE_KEY_LENGTH = Object.freeze([32, 32, 32, 48, 32]);
117
+ const PUBLIC_KEY_LENGTHS = Object.freeze([
118
+ Object.freeze([33, 65]),
119
+ Object.freeze([32]),
120
+ Object.freeze([33, 65]),
121
+ Object.freeze([49, 97]),
122
+ Object.freeze([32]),
123
+ ]);
124
+
125
+ /**
126
+ * Create the validation helpers, throwing errors made by `makeError(code,
127
+ * message)` (HDWalletError for the main API, AlignedError for the aligned API).
128
+ *
129
+ * @param {(code: number, message: string) => Error} makeError
130
+ */
131
+ export function createInputValidation(makeError) {
132
+ const {
133
+ INVALID_ARGUMENT,
134
+ NOT_SUPPORTED,
135
+ OUT_OF_MEMORY,
136
+ INVALID_PRIVATE_KEY,
137
+ INVALID_PUBLIC_KEY,
138
+ } = VALIDATION_ERROR;
139
+
140
+ function fail(code, message) {
141
+ throw makeError(code, message);
142
+ }
143
+
144
+ function describeLengths(lengths) {
145
+ return typeof lengths === 'number' ? `${lengths}` : lengths.join(' or ');
146
+ }
147
+
148
+ /**
149
+ * True when `value` is a Uint8Array (Node Buffers included) whose `length`
150
+ * is its own (no subclass getter or own property can answer, then or later)
151
+ * over a buffer that is attached and cannot be resized. A DataView, an
152
+ * ArrayBuffer or another typed array is not binary data here.
153
+ */
154
+ function isBytes(value) {
155
+ if (intrinsicLength(value) === undefined || !(value instanceof Uint8Array)) return false;
156
+ return hasIntrinsicLengthProperty(value) && !isDetached(value) && hasFixedSize(value);
157
+ }
158
+
159
+ /**
160
+ * Binary data checked as by bytes(), returned as a copy made once from the
161
+ * array's own bytes: later reads of the caller's array cannot change it.
162
+ */
163
+ function snapshot(value, name = 'data', code = INVALID_ARGUMENT) {
164
+ bytes(value, name, code);
165
+ return new Uint8Array(value);
166
+ }
167
+
168
+ /**
169
+ * The byte length of binary data from the array itself, never from its
170
+ * `length` property. Throws for anything isBytes refuses.
171
+ */
172
+ function byteLength(value, name = 'data', code = INVALID_ARGUMENT) {
173
+ bytes(value, name, code);
174
+ return intrinsicLength(value);
175
+ }
176
+
177
+ /** True when the length of `bytes` is `lengths` (a number) or one of `lengths`. */
178
+ function hasLength(bytes, lengths) {
179
+ const length = intrinsicLength(bytes);
180
+ return typeof lengths === 'number' ? length === lengths : lengths.includes(length);
181
+ }
182
+
183
+ /** Require binary data (a Uint8Array) of any length. */
184
+ function bytes(value, name = 'data', code = INVALID_ARGUMENT) {
185
+ if (!isBytes(value)) fail(code, `${name} must be a Uint8Array`);
186
+ return value;
187
+ }
188
+
189
+ /** Require a Uint8Array whose length is `lengths` or one of `lengths`. */
190
+ function bytesOfLength(value, lengths, name = 'data', code = INVALID_ARGUMENT) {
191
+ bytes(value, name, code);
192
+ if (!hasLength(value, lengths)) fail(code, `${name} must be ${describeLengths(lengths)} bytes`);
193
+ return value;
194
+ }
195
+
196
+ /** Require a Uint8Array of min..max bytes. */
197
+ function bytesInRange(value, min, max, name = 'data', code = INVALID_ARGUMENT) {
198
+ bytes(value, name, code);
199
+ const length = intrinsicLength(value);
200
+ if (length < min || length > max) {
201
+ fail(code, `${name} must be ${min}-${max} bytes`);
202
+ }
203
+ return value;
204
+ }
205
+
206
+ /** Require a string. */
207
+ function string(value, name = 'value') {
208
+ if (typeof value !== 'string') fail(INVALID_ARGUMENT, `${name} must be a string`);
209
+ return value;
210
+ }
211
+
212
+ /**
213
+ * Require a string that reaches native code unchanged as a NUL-terminated
214
+ * UTF-8 C string: no NUL (native code would stop there, so 'pw\0extra'
215
+ * would act as 'pw') and no unpaired surrogate (which has no UTF-8 form;
216
+ * the encoder would merge it with the next character).
217
+ */
218
+ function cString(value, name = 'value') {
219
+ string(value, name);
220
+ if (value.includes('\0')) fail(INVALID_ARGUMENT, `${name} must not contain a NUL character`);
221
+ if (UNPAIRED_SURROGATE.test(value)) fail(INVALID_ARGUMENT, `${name} must be well-formed Unicode`);
222
+ return value;
223
+ }
224
+
225
+ /** Whether a string passes cString (for checks that answer false, not throw). */
226
+ function isCString(value) {
227
+ return typeof value === 'string' && !value.includes('\0') && !UNPAIRED_SURROGATE.test(value);
228
+ }
229
+
230
+ /** Require an integer number within [min, max]. */
231
+ function integer(value, name, min, max, code = INVALID_ARGUMENT) {
232
+ if (!Number.isInteger(value) || value < min || value > max) {
233
+ fail(code, `${name} must be an integer from ${min} to ${max}`);
234
+ }
235
+ return value;
236
+ }
237
+
238
+ /** Require an unsigned 32-bit integer. */
239
+ function uint32(value, name) {
240
+ return integer(value, name, 0, 0xffffffff);
241
+ }
242
+
243
+ /**
244
+ * Require a non-negative integer (number, bigint or decimal string) below
245
+ * 2^bits, returned as a bigint.
246
+ */
247
+ function unsignedBigInt(value, name, bits = 64) {
248
+ let n;
249
+ if (typeof value === 'bigint') {
250
+ n = value;
251
+ } else if (typeof value === 'number' && Number.isSafeInteger(value)) {
252
+ n = BigInt(value);
253
+ } else if (typeof value === 'string' && /^(0|[1-9][0-9]*|0x[0-9a-fA-F]+)$/.test(value)) {
254
+ n = BigInt(value);
255
+ } else {
256
+ fail(INVALID_ARGUMENT, `${name} must be a non-negative integer`);
257
+ }
258
+ if (n < 0n || n >= (1n << BigInt(bits))) {
259
+ fail(INVALID_ARGUMENT, `${name} must be a non-negative integer below 2^${bits}`);
260
+ }
261
+ return n;
262
+ }
263
+
264
+ /** Require one of the listed values. */
265
+ function oneOf(value, allowed, name) {
266
+ if (!allowed.includes(value)) fail(INVALID_ARGUMENT, `${name} must be one of ${allowed.join(', ')}`);
267
+ return value;
268
+ }
269
+
270
+ /** Require a known curve id, optionally one of `allowed` (NOT_SUPPORTED otherwise). */
271
+ function curve(value, allowed) {
272
+ if (!Number.isInteger(value) || value < 0 || value >= CURVE_NAMES.length ||
273
+ (allowed !== undefined && !allowed.includes(value))) {
274
+ const label = Number.isInteger(value) && value >= 0 && value < CURVE_NAMES.length
275
+ ? CURVE_NAMES[value]
276
+ : String(value);
277
+ fail(NOT_SUPPORTED, `Curve ${label} is not supported here`);
278
+ }
279
+ return value;
280
+ }
281
+
282
+ function privateKeyLength(curveId) {
283
+ return PRIVATE_KEY_LENGTH[curve(curveId)];
284
+ }
285
+
286
+ function publicKeyLengths(curveId) {
287
+ return PUBLIC_KEY_LENGTHS[curve(curveId)];
288
+ }
289
+
290
+ // Run a hd_curve_validate_* export on a copy of `value`; true when valid.
291
+ function nativeKeyCheck(wasm, exportName, value, curveId, secret) {
292
+ const check = wasm[exportName];
293
+ if (typeof check !== 'function') {
294
+ fail(NOT_SUPPORTED, `Function ${exportName} is not available in this build`);
295
+ }
296
+ const copy = new Uint8Array(value);
297
+ const pointer = wasm._hd_alloc(Math.max(1, copy.length));
298
+ if (!pointer) fail(OUT_OF_MEMORY, 'Out of memory');
299
+ try {
300
+ wasm.HEAPU8.set(copy, pointer);
301
+ return check(pointer, copy.length, curveId) === 0;
302
+ } finally {
303
+ if (secret) {
304
+ wasm._hd_secure_wipe(pointer, Math.max(1, copy.length));
305
+ copy.fill(0);
306
+ }
307
+ wasm._hd_dealloc(pointer);
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Require a valid private key of `curveId`: the scalar size, and 0 < d < n
313
+ * for the short-Weierstrass curves (checked by the module).
314
+ */
315
+ function privateKey(wasm, value, curveId, name = 'privateKey') {
316
+ const length = privateKeyLength(curveId);
317
+ bytesOfLength(value, length, name, INVALID_PRIVATE_KEY);
318
+ if (!nativeKeyCheck(wasm, '_hd_curve_validate_privkey', value, curveId, true)) {
319
+ fail(INVALID_PRIVATE_KEY, `${name} is not a valid ${CURVE_NAMES[curveId]} private key`);
320
+ }
321
+ return value;
322
+ }
323
+
324
+ /**
325
+ * True when `value` (which must be a Uint8Array: anything else is a
326
+ * programmer error and throws) is a valid public key of `curveId` in one of
327
+ * `lengths` (the curve's encodings by default).
328
+ */
329
+ function isPublicKey(wasm, value, curveId, lengths = publicKeyLengths(curveId)) {
330
+ curve(curveId);
331
+ bytes(value, 'publicKey', INVALID_PUBLIC_KEY);
332
+ if (!hasLength(value, lengths)) return false;
333
+ return nativeKeyCheck(wasm, '_hd_curve_validate_pubkey', value, curveId, false);
334
+ }
335
+
336
+ /** Require a valid public key of `curveId` in one of `lengths`. */
337
+ function publicKey(wasm, value, curveId, name = 'publicKey', lengths = publicKeyLengths(curveId)) {
338
+ curve(curveId);
339
+ bytesOfLength(value, lengths, name, INVALID_PUBLIC_KEY);
340
+ if (!nativeKeyCheck(wasm, '_hd_curve_validate_pubkey', value, curveId, false)) {
341
+ fail(INVALID_PUBLIC_KEY, `${name} is not a valid ${CURVE_NAMES[curveId]} public key`);
342
+ }
343
+ return value;
344
+ }
345
+
346
+ return Object.freeze({
347
+ isBytes,
348
+ byteLength,
349
+ snapshot,
350
+ hasLength,
351
+ bytes,
352
+ bytesOfLength,
353
+ bytesInRange,
354
+ string,
355
+ cString,
356
+ isCString,
357
+ integer,
358
+ uint32,
359
+ unsignedBigInt,
360
+ oneOf,
361
+ curve,
362
+ privateKeyLength,
363
+ publicKeyLengths,
364
+ privateKey,
365
+ publicKey,
366
+ isPublicKey,
367
+ });
368
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hd-wallet-wasm",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "Comprehensive HD Wallet implementation in WebAssembly - BIP-32/39/44, multi-curve, multi-chain support",
5
5
  "type": "module",
6
6
  "main": "./dist/runtime/index.mjs",
@@ -92,5 +92,5 @@
92
92
  "devDependencies": {
93
93
  "@types/node": "^20.0.0"
94
94
  },
95
- "gitHead": "62e752dfd9b7163619bfbe61a52b158532fc59c1"
95
+ "gitHead": "e862d42fcf505cdbc7e928d676bc1a180a167989"
96
96
  }