hd-wallet-wasm 2.0.21 → 2.0.26

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,928 @@
1
+ const MAX_OUTPUT_BYTES = 131072;
2
+ const MAX_INPUT_JSON_BYTES = 131072;
3
+ const MAX_AAD_BYTES = 4096;
4
+ const MAX_MODERN_CREDENTIAL_BYTES = 256;
5
+ const MAX_LEGACY_CREDENTIAL_BYTES = 4096;
6
+ const MAX_MNEMONIC_BYTES = 1024;
7
+ const MIN_REMEMBERED_CIPHERTEXT_BYTES = 16;
8
+ const MAX_REMEMBERED_CIPHERTEXT_BYTES = 1024;
9
+ const MAX_NATIVE_SLOT = 16n;
10
+ const UINT64_MAX = (1n << 64n) - 1n;
11
+
12
+ const ERROR_CODES = Object.freeze([
13
+ 'INVALID_USERNAME',
14
+ 'INVALID_PASSWORD',
15
+ 'COMMON_PASSWORD',
16
+ 'KDF_FAILURE',
17
+ 'INVALID_MNEMONIC',
18
+ 'INVALID_ACCOUNT',
19
+ 'STALE_HANDLE',
20
+ 'OPERATION_NOT_ALLOWED',
21
+ 'INVALID_REQUEST',
22
+ 'AUTHENTICATION_FAILED',
23
+ 'CAPACITY_EXCEEDED',
24
+ 'CRYPTO_FAILURE',
25
+ 'OUT_OF_MEMORY',
26
+ 'FIPS_NOT_ALLOWED',
27
+ ]);
28
+
29
+ const RAW_ENTRYPOINTS = Object.freeze([
30
+ '_hd_sdn_derive_password_identity',
31
+ '_hd_sdn_derive_legacy_password_identity',
32
+ '_hd_sdn_import_legacy_mnemonic_identity',
33
+ '_hd_sdn_import_remembered_identity',
34
+ '_hd_sdn_sign_login_v1',
35
+ '_hd_sdn_sign_login_v2',
36
+ '_hd_sdn_sign_asset_review_authority_activation',
37
+ '_hd_sdn_sign_asset_review_decision',
38
+ '_hd_sdn_seal_remembered_identity',
39
+ '_hd_sdn_destroy_identity',
40
+ ]);
41
+
42
+ const textEncoder = new TextEncoder();
43
+ const textDecoder = new TextDecoder('utf-8', { fatal: true });
44
+ const SafeUint8Array = Uint8Array;
45
+ const typedArrayPrototype = Object.getPrototypeOf(SafeUint8Array.prototype);
46
+ const typedArrayTag = Object.getOwnPropertyDescriptor(
47
+ typedArrayPrototype,
48
+ Symbol.toStringTag,
49
+ ).get;
50
+ const typedArrayByteLength = Object.getOwnPropertyDescriptor(
51
+ typedArrayPrototype,
52
+ 'byteLength',
53
+ ).get;
54
+ const intrinsicFill = SafeUint8Array.prototype.fill;
55
+ const intrinsicSet = SafeUint8Array.prototype.set;
56
+
57
+ class SdnWalletError extends Error {
58
+ constructor(code) {
59
+ super(code);
60
+ this.name = 'SdnWalletError';
61
+ this.code = code;
62
+ }
63
+
64
+ toJSON() {
65
+ return { name: this.name, code: this.code };
66
+ }
67
+ }
68
+
69
+ function fail(code) {
70
+ throw new SdnWalletError(code);
71
+ }
72
+
73
+ function statusCode(status) {
74
+ return Number.isInteger(status) && status >= 1 && status <= ERROR_CODES.length
75
+ ? ERROR_CODES[status - 1]
76
+ : 'CRYPTO_FAILURE';
77
+ }
78
+
79
+ function throwStatus(status) {
80
+ fail(statusCode(status));
81
+ }
82
+
83
+ function isObject(value) {
84
+ return value !== null && typeof value === 'object';
85
+ }
86
+
87
+ function exactKeys(value, keys) {
88
+ if (!isObject(value) || Array.isArray(value)) return false;
89
+ const actual = Object.keys(value).sort();
90
+ const expected = [...keys].sort();
91
+ return actual.length === expected.length &&
92
+ actual.every((key, index) => key === expected[index]);
93
+ }
94
+
95
+ function requireExactObject(value, keys) {
96
+ if (!exactKeys(value, keys)) fail('INVALID_REQUEST');
97
+ return value;
98
+ }
99
+
100
+ function requireBytes(value, code = 'INVALID_REQUEST') {
101
+ let tag;
102
+ try {
103
+ tag = typedArrayTag.call(value);
104
+ } catch {
105
+ fail(code);
106
+ }
107
+ if (tag !== 'Uint8Array') fail(code);
108
+ return value;
109
+ }
110
+
111
+ function copyBytes(value, code = 'INVALID_REQUEST') {
112
+ const source = requireBytes(value, code);
113
+ let length;
114
+ try {
115
+ length = typedArrayByteLength.call(source);
116
+ } catch {
117
+ fail(code);
118
+ }
119
+ let copy;
120
+ try {
121
+ copy = new SafeUint8Array(length);
122
+ } catch {
123
+ fail('OUT_OF_MEMORY');
124
+ }
125
+ try {
126
+ intrinsicSet.call(copy, source);
127
+ } catch {
128
+ intrinsicFill.call(copy, 0);
129
+ fail(code);
130
+ }
131
+ return copy;
132
+ }
133
+
134
+ function copySecretAndWipeCaller(value, code) {
135
+ const caller = requireBytes(value, code);
136
+ const copy = copyBytes(caller, code);
137
+ if (wipeAll([caller])) {
138
+ wipeAll([copy]);
139
+ fail('CRYPTO_FAILURE');
140
+ }
141
+ return copy;
142
+ }
143
+
144
+ function isUint8Array(value) {
145
+ try {
146
+ return typedArrayTag.call(value) === 'Uint8Array';
147
+ } catch {
148
+ return false;
149
+ }
150
+ }
151
+
152
+ function wipeAll(values) {
153
+ let failed = false;
154
+ for (const value of values) {
155
+ if (!isUint8Array(value)) continue;
156
+ try {
157
+ intrinsicFill.call(value, 0);
158
+ } catch {
159
+ failed = true;
160
+ }
161
+ }
162
+ return failed;
163
+ }
164
+
165
+ function onceWipe(values) {
166
+ let complete = false;
167
+ let failed = false;
168
+ return () => {
169
+ if (!complete) {
170
+ complete = true;
171
+ failed = wipeAll(values());
172
+ }
173
+ return failed;
174
+ };
175
+ }
176
+
177
+ function validUtf16(value) {
178
+ if (typeof value !== 'string') return false;
179
+ for (let index = 0; index < value.length; index += 1) {
180
+ const unit = value.charCodeAt(index);
181
+ if (unit >= 0xd800 && unit <= 0xdbff) {
182
+ if (index + 1 >= value.length) return false;
183
+ const next = value.charCodeAt(index + 1);
184
+ if (next < 0xdc00 || next > 0xdfff) return false;
185
+ index += 1;
186
+ } else if (unit >= 0xdc00 && unit <= 0xdfff) {
187
+ return false;
188
+ }
189
+ }
190
+ return true;
191
+ }
192
+
193
+ function encodeString(value, maximum = MAX_INPUT_JSON_BYTES) {
194
+ if (!validUtf16(value)) fail('INVALID_REQUEST');
195
+ const bytes = textEncoder.encode(value);
196
+ if (bytes.length > maximum) fail('INVALID_REQUEST');
197
+ return bytes;
198
+ }
199
+
200
+ function encodeRequest(value) {
201
+ let serialized;
202
+ try {
203
+ serialized = JSON.stringify(value);
204
+ } catch {
205
+ fail('INVALID_REQUEST');
206
+ }
207
+ if (typeof serialized !== 'string') fail('INVALID_REQUEST');
208
+ return encodeString(serialized, MAX_INPUT_JSON_BYTES);
209
+ }
210
+
211
+ function requireAccount(value) {
212
+ if (value !== 0 && value !== 1) fail('INVALID_ACCOUNT');
213
+ return value;
214
+ }
215
+
216
+ function validNativeHandle(value) {
217
+ if (typeof value !== 'bigint' || value <= 0n || value > UINT64_MAX) return false;
218
+ const slot = value & 0xffffffffn;
219
+ const generation = value >> 32n;
220
+ return generation !== 0n && slot !== 0n && slot <= MAX_NATIVE_SLOT;
221
+ }
222
+
223
+ function validLowerHex(value, length) {
224
+ return typeof value === 'string' && value.length === length &&
225
+ /^[0-9a-f]+$/.test(value);
226
+ }
227
+
228
+ function validKeyId(value) {
229
+ return typeof value === 'string' && /^sha256:[0-9a-f]{64}$/.test(value);
230
+ }
231
+
232
+ function copyDescriptor(value, scheme, profile, expected) {
233
+ const keys = [
234
+ 'purpose', 'identityScheme', 'seedProfile', 'signatureProfile', 'curve',
235
+ 'derivation', 'path', 'encoding', 'publicKeyHex', 'bip32Fingerprint', 'keyId',
236
+ ];
237
+ if (!exactKeys(value, keys) ||
238
+ value.purpose !== expected.purpose ||
239
+ value.identityScheme !== scheme ||
240
+ value.seedProfile !== profile ||
241
+ value.signatureProfile !== expected.signatureProfile ||
242
+ value.curve !== expected.curve ||
243
+ value.derivation !== expected.derivation ||
244
+ value.path !== expected.path ||
245
+ value.encoding !== 'raw' ||
246
+ !validLowerHex(value.publicKeyHex, 64) ||
247
+ value.bip32Fingerprint !== null ||
248
+ !validKeyId(value.keyId)) {
249
+ fail('CRYPTO_FAILURE');
250
+ }
251
+ return Object.freeze({
252
+ purpose: value.purpose,
253
+ identityScheme: value.identityScheme,
254
+ seedProfile: value.seedProfile,
255
+ signatureProfile: value.signatureProfile,
256
+ curve: value.curve,
257
+ derivation: value.derivation,
258
+ path: value.path,
259
+ encoding: value.encoding,
260
+ publicKeyHex: value.publicKeyHex,
261
+ bip32Fingerprint: null,
262
+ keyId: value.keyId,
263
+ });
264
+ }
265
+
266
+ function copyIdentity(value) {
267
+ const keys = [
268
+ 'schemaVersion', 'identityScheme', 'seedProfile', 'accountIndex',
269
+ 'accountLabel', 'accountXpub', 'accountPeerId', 'accountFingerprint', 'keys',
270
+ ];
271
+ if (!exactKeys(value, keys) || value.schemaVersion !== 1 ||
272
+ (value.accountIndex !== 0 && value.accountIndex !== 1) ||
273
+ value.accountLabel !== null || typeof value.accountXpub !== 'string' ||
274
+ value.accountXpub.length === 0 || typeof value.accountPeerId !== 'string' ||
275
+ value.accountPeerId.length === 0 || !validLowerHex(value.accountFingerprint, 8) ||
276
+ !Array.isArray(value.keys)) {
277
+ fail('CRYPTO_FAILURE');
278
+ }
279
+
280
+ const prefix = `m/44'/0'/${value.accountIndex}'/`;
281
+ let expected;
282
+ if (value.identityScheme === 'sdn-bip32-slip10-purpose-v1' &&
283
+ value.seedProfile === 'password-scrypt-v2') {
284
+ expected = [
285
+ {
286
+ purpose: 'asset-review-approval',
287
+ signatureProfile: 'ed25519-over-sha256-jcs-v1',
288
+ curve: 'ed25519',
289
+ derivation: 'slip10',
290
+ path: `${prefix}2'/0'`,
291
+ },
292
+ {
293
+ purpose: 'contact-encryption',
294
+ signatureProfile: null,
295
+ curve: 'x25519',
296
+ derivation: 'slip10',
297
+ path: `${prefix}1'/0'`,
298
+ },
299
+ {
300
+ purpose: 'sdn-authentication',
301
+ signatureProfile: 'ed25519-over-sha256-jcs-v1',
302
+ curve: 'ed25519',
303
+ derivation: 'slip10',
304
+ path: `${prefix}0'/0'`,
305
+ },
306
+ ];
307
+ } else if (
308
+ (value.identityScheme === 'sdn-fast-password-auth-v1-legacy' &&
309
+ value.seedProfile === 'password-fast-v1-legacy') ||
310
+ (value.identityScheme === 'sdn-bip39-auth-v1-legacy' &&
311
+ value.seedProfile === 'bip39-mnemonic-v1-legacy')
312
+ ) {
313
+ expected = [{
314
+ purpose: 'sdn-authentication',
315
+ signatureProfile: 'ed25519-raw-32-v1',
316
+ curve: 'ed25519',
317
+ derivation: 'bip32-scalar-as-ed25519-seed',
318
+ path: `${prefix}0/0`,
319
+ }];
320
+ } else {
321
+ fail('CRYPTO_FAILURE');
322
+ }
323
+ if (value.keys.length !== expected.length) fail('CRYPTO_FAILURE');
324
+ const descriptors = value.keys.map((descriptor, index) =>
325
+ copyDescriptor(descriptor, value.identityScheme, value.seedProfile, expected[index]));
326
+ return Object.freeze({
327
+ schemaVersion: 1,
328
+ identityScheme: value.identityScheme,
329
+ seedProfile: value.seedProfile,
330
+ accountIndex: value.accountIndex,
331
+ accountLabel: null,
332
+ accountXpub: value.accountXpub,
333
+ accountPeerId: value.accountPeerId,
334
+ accountFingerprint: value.accountFingerprint,
335
+ keys: Object.freeze(descriptors),
336
+ });
337
+ }
338
+
339
+ function parseIdentity(bytes) {
340
+ let value;
341
+ try {
342
+ value = JSON.parse(textDecoder.decode(bytes));
343
+ } catch {
344
+ fail('CRYPTO_FAILURE');
345
+ }
346
+ return copyIdentity(value);
347
+ }
348
+
349
+ function copySignature(value, canonical) {
350
+ const common = [
351
+ 'schemaVersion', 'keyId', 'identityScheme', 'algorithm', 'encoding',
352
+ 'signatureProfile', 'signatureHex',
353
+ ];
354
+ const keys = canonical
355
+ ? [...common, 'canonicalEnvelope', 'signedDigestSha256']
356
+ : common;
357
+ const validScheme = canonical
358
+ ? value?.identityScheme === 'sdn-bip32-slip10-purpose-v1'
359
+ : value?.identityScheme === 'sdn-fast-password-auth-v1-legacy' ||
360
+ value?.identityScheme === 'sdn-bip39-auth-v1-legacy';
361
+ if (!exactKeys(value, keys) || value.schemaVersion !== 1 ||
362
+ !validKeyId(value.keyId) || !validScheme || value.algorithm !== 'ed25519' ||
363
+ value.encoding !== 'raw' || !validLowerHex(value.signatureHex, 128) ||
364
+ value.signatureProfile !== (canonical
365
+ ? 'ed25519-over-sha256-jcs-v1'
366
+ : 'ed25519-raw-32-v1')) {
367
+ fail('CRYPTO_FAILURE');
368
+ }
369
+ if (canonical && (typeof value.canonicalEnvelope !== 'string' ||
370
+ value.canonicalEnvelope.length === 0 ||
371
+ !validLowerHex(value.signedDigestSha256, 64))) {
372
+ fail('CRYPTO_FAILURE');
373
+ }
374
+ const result = {
375
+ schemaVersion: 1,
376
+ keyId: value.keyId,
377
+ identityScheme: value.identityScheme,
378
+ algorithm: 'ed25519',
379
+ encoding: 'raw',
380
+ signatureProfile: value.signatureProfile,
381
+ };
382
+ if (canonical) {
383
+ result.canonicalEnvelope = value.canonicalEnvelope;
384
+ result.signedDigestSha256 = value.signedDigestSha256;
385
+ }
386
+ result.signatureHex = value.signatureHex;
387
+ return Object.freeze(result);
388
+ }
389
+
390
+ function parseSignature(bytes, canonical) {
391
+ let value;
392
+ try {
393
+ value = JSON.parse(textDecoder.decode(bytes));
394
+ } catch {
395
+ fail('CRYPTO_FAILURE');
396
+ }
397
+ return copySignature(value, canonical);
398
+ }
399
+
400
+ function checkedHeap(wasm) {
401
+ let heap;
402
+ try {
403
+ heap = wasm.HEAPU8;
404
+ } catch {
405
+ fail('CRYPTO_FAILURE');
406
+ }
407
+ if (!(heap instanceof Uint8Array)) fail('CRYPTO_FAILURE');
408
+ return heap;
409
+ }
410
+
411
+ function createAllocationScope(wasm) {
412
+ const allocations = [];
413
+ let cleaned = false;
414
+ let cleanupFailed = false;
415
+ return {
416
+ allocate(bytesOrSize, secret = false) {
417
+ const bytes = typeof bytesOrSize === 'number' ? null : bytesOrSize;
418
+ const requested = bytes === null ? bytesOrSize : bytes.length;
419
+ if (!Number.isSafeInteger(requested) || requested < 0 || requested > 0xffffffff) {
420
+ fail('OUT_OF_MEMORY');
421
+ }
422
+ const size = Math.max(1, requested);
423
+ let pointer;
424
+ try {
425
+ pointer = wasm._malloc(size);
426
+ } catch {
427
+ fail('OUT_OF_MEMORY');
428
+ }
429
+ if (!Number.isInteger(pointer) || pointer <= 0 || pointer > 0xffffffff) {
430
+ fail('OUT_OF_MEMORY');
431
+ }
432
+ allocations.push({ pointer, size, secret });
433
+ let heap;
434
+ try {
435
+ heap = wasm.HEAPU8;
436
+ if (!(heap instanceof Uint8Array) || pointer + size > heap.byteLength ||
437
+ pointer + size <= pointer) {
438
+ fail('OUT_OF_MEMORY');
439
+ }
440
+ heap.fill(0, pointer, pointer + size);
441
+ if (bytes !== null && bytes.length !== 0) heap.set(bytes, pointer);
442
+ } catch {
443
+ fail('OUT_OF_MEMORY');
444
+ }
445
+ return pointer;
446
+ },
447
+ view() {
448
+ return new DataView(checkedHeap(wasm).buffer);
449
+ },
450
+ bytes(pointer, length) {
451
+ const heap = checkedHeap(wasm);
452
+ if (!Number.isInteger(length) || length <= 0 || length > MAX_OUTPUT_BYTES ||
453
+ pointer <= 0 || pointer + length > heap.byteLength) {
454
+ fail('CRYPTO_FAILURE');
455
+ }
456
+ return heap.slice(pointer, pointer + length);
457
+ },
458
+ cleanup() {
459
+ if (cleaned) return cleanupFailed;
460
+ cleaned = true;
461
+ for (let index = allocations.length - 1; index >= 0; index -= 1) {
462
+ const { pointer, size, secret } = allocations[index];
463
+ if (secret) {
464
+ try {
465
+ const heap = checkedHeap(wasm);
466
+ if (pointer + size <= heap.byteLength) {
467
+ heap.fill(0, pointer, pointer + size);
468
+ } else {
469
+ cleanupFailed = true;
470
+ }
471
+ } catch {
472
+ cleanupFailed = true;
473
+ }
474
+ }
475
+ try {
476
+ wasm._free(pointer);
477
+ } catch {
478
+ cleanupFailed = true;
479
+ }
480
+ }
481
+ allocations.length = 0;
482
+ return cleanupFailed;
483
+ },
484
+ };
485
+ }
486
+
487
+ function readHandle(scope, pointer) {
488
+ try {
489
+ return scope.view().getBigUint64(pointer, true);
490
+ } catch {
491
+ fail('CRYPTO_FAILURE');
492
+ }
493
+ }
494
+
495
+ function readRequired(scope, pointer) {
496
+ try {
497
+ return scope.view().getUint32(pointer, true);
498
+ } catch {
499
+ fail('CRYPTO_FAILURE');
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Internal per-WASM-instance factory. This is intentionally not a package
505
+ * export subpath; the wallet-origin wrapper constructs it inside createModule.
506
+ */
507
+ export function createSdnTypedCapabilities(wasm) {
508
+ if (!isObject(wasm) || typeof wasm._malloc !== 'function' ||
509
+ typeof wasm._free !== 'function' ||
510
+ RAW_ENTRYPOINTS.some((name) => typeof wasm[name] !== 'function')) {
511
+ fail('CRYPTO_FAILURE');
512
+ }
513
+
514
+ const issuedHandles = new WeakMap();
515
+ const unpublishedRollbackQueue = new Set();
516
+
517
+ function rollback(nativeValue) {
518
+ if (typeof nativeValue !== 'bigint' || nativeValue === 0n) return;
519
+ unpublishedRollbackQueue.add(nativeValue);
520
+ try {
521
+ wasm._hd_sdn_destroy_identity(nativeValue);
522
+ unpublishedRollbackQueue.delete(nativeValue);
523
+ } catch {
524
+ // Keep unpublished native authority retryable. A later derive/import
525
+ // must drain this exact value before it can allocate another slot.
526
+ }
527
+ }
528
+
529
+ function drainRollbackQueue() {
530
+ for (const nativeValue of unpublishedRollbackQueue) {
531
+ try {
532
+ wasm._hd_sdn_destroy_identity(nativeValue);
533
+ } catch {
534
+ fail('CRYPTO_FAILURE');
535
+ }
536
+ unpublishedRollbackQueue.delete(nativeValue);
537
+ }
538
+ }
539
+
540
+ function isDestroyPendingHandle(handle) {
541
+ if (!isObject(handle)) return false;
542
+ return issuedHandles.get(handle)?.state === 'destroy-pending';
543
+ }
544
+
545
+ function recordFor(handle) {
546
+ if (!isObject(handle)) fail('STALE_HANDLE');
547
+ const record = issuedHandles.get(handle);
548
+ if (!record || record.state !== 'active') fail('STALE_HANDLE');
549
+ return record;
550
+ }
551
+
552
+ function invokeDerive(
553
+ rawName,
554
+ rawArguments,
555
+ inputs,
556
+ expectedIdentityScheme,
557
+ expectedSeedProfile,
558
+ expectedAccountIndex,
559
+ cleanupSecrets,
560
+ ) {
561
+ drainRollbackQueue();
562
+ const scope = createAllocationScope(wasm);
563
+ let pending = 0n;
564
+ let completed = false;
565
+ try {
566
+ const pointers = inputs.map(({ bytes, secret }) => scope.allocate(bytes, secret));
567
+ const outHandle = scope.allocate(8, true);
568
+ const outRequired = scope.allocate(4);
569
+ const outJson = scope.allocate(MAX_OUTPUT_BYTES);
570
+ let status;
571
+ let rawThrew = false;
572
+ try {
573
+ status = wasm[rawName].apply(wasm, [
574
+ ...rawArguments(pointers),
575
+ outHandle,
576
+ outJson,
577
+ MAX_OUTPUT_BYTES,
578
+ outRequired,
579
+ ]);
580
+ } catch {
581
+ rawThrew = true;
582
+ }
583
+ pending = readHandle(scope, outHandle);
584
+ if (rawThrew) fail('CRYPTO_FAILURE');
585
+ if (status !== 0) throwStatus(status);
586
+ if (!validNativeHandle(pending)) fail('CRYPTO_FAILURE');
587
+ const required = readRequired(scope, outRequired);
588
+ if (required <= 0 || required > MAX_OUTPUT_BYTES) fail('CRYPTO_FAILURE');
589
+ const identity = parseIdentity(scope.bytes(outJson, required));
590
+ if (identity.identityScheme !== expectedIdentityScheme ||
591
+ identity.seedProfile !== expectedSeedProfile ||
592
+ identity.accountIndex !== expectedAccountIndex) {
593
+ fail('CRYPTO_FAILURE');
594
+ }
595
+ if (scope.cleanup()) fail('CRYPTO_FAILURE');
596
+ if (cleanupSecrets()) fail('CRYPTO_FAILURE');
597
+ const handle = Object.freeze(Object.create(null));
598
+ const record = { nativeValue: pending, state: 'active' };
599
+ issuedHandles.set(handle, record);
600
+ const result = Object.freeze({ handle, identity });
601
+ completed = true;
602
+ pending = 0n;
603
+ return result;
604
+ } finally {
605
+ if (!completed && pending !== 0n) rollback(pending);
606
+ scope.cleanup();
607
+ }
608
+ }
609
+
610
+ function invokeSign(rawName, record, inputBytes, rawArguments, canonical) {
611
+ const scope = createAllocationScope(wasm);
612
+ try {
613
+ const input = scope.allocate(inputBytes);
614
+ const outRequired = scope.allocate(4);
615
+ const outJson = scope.allocate(MAX_OUTPUT_BYTES);
616
+ let status;
617
+ try {
618
+ status = wasm[rawName].apply(wasm, rawArguments(
619
+ record.nativeValue,
620
+ input,
621
+ inputBytes.length,
622
+ outJson,
623
+ outRequired,
624
+ ));
625
+ } catch {
626
+ fail('CRYPTO_FAILURE');
627
+ }
628
+ if (status !== 0) throwStatus(status);
629
+ const required = readRequired(scope, outRequired);
630
+ if (required <= 0 || required > MAX_OUTPUT_BYTES) fail('CRYPTO_FAILURE');
631
+ return parseSignature(scope.bytes(outJson, required), canonical);
632
+ } finally {
633
+ if (scope.cleanup()) fail('CRYPTO_FAILURE');
634
+ }
635
+ }
636
+
637
+ const capabilities = {
638
+ async derivePasswordIdentity(input) {
639
+ let callerPassword;
640
+ let password;
641
+ const cleanupSecrets = onceWipe(() => [password, callerPassword]);
642
+ try {
643
+ if (isObject(input)) callerPassword = input.passwordUtf8;
644
+ requireExactObject(input, ['usernameUtf8', 'passwordUtf8', 'accountIndex']);
645
+ const username = copyBytes(input.usernameUtf8, 'INVALID_USERNAME');
646
+ password = copySecretAndWipeCaller(callerPassword, 'INVALID_PASSWORD');
647
+ if (username.length > MAX_MODERN_CREDENTIAL_BYTES) fail('INVALID_USERNAME');
648
+ if (password.length > MAX_MODERN_CREDENTIAL_BYTES) fail('INVALID_PASSWORD');
649
+ const account = requireAccount(input.accountIndex);
650
+ return invokeDerive(
651
+ '_hd_sdn_derive_password_identity',
652
+ ([usernamePointer, passwordPointer]) => [
653
+ usernamePointer, username.length,
654
+ passwordPointer, password.length,
655
+ account,
656
+ ],
657
+ [{ bytes: username }, { bytes: password, secret: true }],
658
+ 'sdn-bip32-slip10-purpose-v1',
659
+ 'password-scrypt-v2',
660
+ account,
661
+ cleanupSecrets,
662
+ );
663
+ } finally {
664
+ if (cleanupSecrets()) fail('CRYPTO_FAILURE');
665
+ }
666
+ },
667
+
668
+ async deriveLegacyPasswordIdentity(input) {
669
+ let callerPassword;
670
+ let password;
671
+ const cleanupSecrets = onceWipe(() => [password, callerPassword]);
672
+ try {
673
+ if (isObject(input)) callerPassword = input.passwordUtf8;
674
+ requireExactObject(input, ['usernameUtf8', 'passwordUtf8', 'accountIndex']);
675
+ const username = copyBytes(input.usernameUtf8, 'INVALID_USERNAME');
676
+ password = copySecretAndWipeCaller(callerPassword, 'INVALID_PASSWORD');
677
+ if (username.length > MAX_LEGACY_CREDENTIAL_BYTES) fail('INVALID_USERNAME');
678
+ if (password.length > MAX_LEGACY_CREDENTIAL_BYTES) fail('INVALID_PASSWORD');
679
+ const account = requireAccount(input.accountIndex);
680
+ return invokeDerive(
681
+ '_hd_sdn_derive_legacy_password_identity',
682
+ ([usernamePointer, passwordPointer]) => [
683
+ usernamePointer, username.length,
684
+ passwordPointer, password.length,
685
+ account,
686
+ ],
687
+ [{ bytes: username }, { bytes: password, secret: true }],
688
+ 'sdn-fast-password-auth-v1-legacy',
689
+ 'password-fast-v1-legacy',
690
+ account,
691
+ cleanupSecrets,
692
+ );
693
+ } finally {
694
+ if (cleanupSecrets()) fail('CRYPTO_FAILURE');
695
+ }
696
+ },
697
+
698
+ async importLegacyMnemonicIdentity(input) {
699
+ let callerMnemonic;
700
+ let mnemonic;
701
+ const cleanupSecrets = onceWipe(() => [mnemonic, callerMnemonic]);
702
+ try {
703
+ if (isObject(input)) callerMnemonic = input.mnemonicUtf8;
704
+ requireExactObject(input, ['mnemonicUtf8', 'accountIndex']);
705
+ mnemonic = copySecretAndWipeCaller(callerMnemonic, 'INVALID_MNEMONIC');
706
+ if (mnemonic.length > MAX_MNEMONIC_BYTES) fail('INVALID_MNEMONIC');
707
+ const account = requireAccount(input.accountIndex);
708
+ return invokeDerive(
709
+ '_hd_sdn_import_legacy_mnemonic_identity',
710
+ ([mnemonicPointer]) => [mnemonicPointer, mnemonic.length, account],
711
+ [{ bytes: mnemonic, secret: true }],
712
+ 'sdn-bip39-auth-v1-legacy',
713
+ 'bip39-mnemonic-v1-legacy',
714
+ account,
715
+ cleanupSecrets,
716
+ );
717
+ } finally {
718
+ if (cleanupSecrets()) fail('CRYPTO_FAILURE');
719
+ }
720
+ },
721
+
722
+ importRememberedIdentity(input) {
723
+ let callerPrf;
724
+ let prf;
725
+ const cleanupSecrets = onceWipe(() => [prf, callerPrf]);
726
+ try {
727
+ if (isObject(input)) callerPrf = input.prfOutput;
728
+ requireExactObject(input, [
729
+ 'ciphertextAndTag', 'prfOutput', 'hkdfSalt', 'nonce',
730
+ 'canonicalUsernameUtf8', 'canonicalAad',
731
+ ]);
732
+ const ciphertext = copyBytes(input.ciphertextAndTag);
733
+ const salt = copyBytes(input.hkdfSalt);
734
+ const nonce = copyBytes(input.nonce);
735
+ const username = copyBytes(input.canonicalUsernameUtf8, 'INVALID_USERNAME');
736
+ const aad = encodeString(input.canonicalAad, MAX_AAD_BYTES);
737
+ prf = copySecretAndWipeCaller(callerPrf, 'INVALID_REQUEST');
738
+ if (prf.length !== 32 || salt.length !== 32 || nonce.length !== 12 ||
739
+ ciphertext.length < MIN_REMEMBERED_CIPHERTEXT_BYTES ||
740
+ ciphertext.length > MAX_REMEMBERED_CIPHERTEXT_BYTES ||
741
+ username.length > MAX_MODERN_CREDENTIAL_BYTES) {
742
+ fail('INVALID_REQUEST');
743
+ }
744
+ return invokeDerive(
745
+ '_hd_sdn_import_remembered_identity',
746
+ (pointers) => [
747
+ pointers[0], ciphertext.length,
748
+ pointers[1], prf.length,
749
+ pointers[2], salt.length,
750
+ pointers[3], nonce.length,
751
+ pointers[4], username.length,
752
+ pointers[5], aad.length,
753
+ ],
754
+ [
755
+ { bytes: ciphertext }, { bytes: prf, secret: true }, { bytes: salt },
756
+ { bytes: nonce }, { bytes: username }, { bytes: aad },
757
+ ],
758
+ 'sdn-bip32-slip10-purpose-v1',
759
+ 'password-scrypt-v2',
760
+ 0,
761
+ cleanupSecrets,
762
+ );
763
+ } finally {
764
+ if (cleanupSecrets()) fail('CRYPTO_FAILURE');
765
+ }
766
+ },
767
+
768
+ signSdnLoginV1(handle, challenge) {
769
+ const record = recordFor(handle);
770
+ const challengeBytes = copyBytes(challenge);
771
+ if (challengeBytes.length !== 32) fail('INVALID_REQUEST');
772
+ return invokeSign(
773
+ '_hd_sdn_sign_login_v1',
774
+ record,
775
+ challengeBytes,
776
+ (native, input, length, output, required) => [
777
+ native, input, length, output, MAX_OUTPUT_BYTES, required,
778
+ ],
779
+ false,
780
+ );
781
+ },
782
+
783
+ signSdnLoginV2(handle, request, registryRow) {
784
+ const record = recordFor(handle);
785
+ if (registryRow !== 'sdn-node-console-v2') fail('INVALID_REQUEST');
786
+ const requestBytes = encodeRequest(request);
787
+ return invokeSign(
788
+ '_hd_sdn_sign_login_v2',
789
+ record,
790
+ requestBytes,
791
+ (native, input, length, output, required) => [
792
+ native, input, length, 1, output, MAX_OUTPUT_BYTES, required,
793
+ ],
794
+ true,
795
+ );
796
+ },
797
+
798
+ signAssetReviewAuthorityActivation(handle, request, registryRow) {
799
+ const record = recordFor(handle);
800
+ if (registryRow !== 'asset-review-authority-activation-v1') {
801
+ fail('INVALID_REQUEST');
802
+ }
803
+ const requestBytes = encodeRequest(request);
804
+ return invokeSign(
805
+ '_hd_sdn_sign_asset_review_authority_activation',
806
+ record,
807
+ requestBytes,
808
+ (native, input, length, output, required) => [
809
+ native, input, length, 2, output, MAX_OUTPUT_BYTES, required,
810
+ ],
811
+ true,
812
+ );
813
+ },
814
+
815
+ signAssetReviewDecision(handle, request, registryRow) {
816
+ const record = recordFor(handle);
817
+ if (registryRow !== 'asset-review-decision-v1') fail('INVALID_REQUEST');
818
+ const requestBytes = encodeRequest(request);
819
+ return invokeSign(
820
+ '_hd_sdn_sign_asset_review_decision',
821
+ record,
822
+ requestBytes,
823
+ (native, input, length, output, required) => [
824
+ native, input, length, 3, output, MAX_OUTPUT_BYTES, required,
825
+ ],
826
+ true,
827
+ );
828
+ },
829
+
830
+ sealRememberedIdentity(handle, input) {
831
+ let callerPassword;
832
+ let callerPrf;
833
+ let password;
834
+ let prf;
835
+ let authorityEstablished = false;
836
+ const rejectInputBeforeRead = isDestroyPendingHandle(handle);
837
+ const scope = createAllocationScope(wasm);
838
+ try {
839
+ const record = recordFor(handle);
840
+ authorityEstablished = true;
841
+ if (!rejectInputBeforeRead && isObject(input)) {
842
+ callerPassword = input.passwordUtf8;
843
+ callerPrf = input.prfOutput;
844
+ }
845
+ requireExactObject(input, [
846
+ 'passwordUtf8', 'prfOutput', 'hkdfSalt', 'nonce', 'canonicalAad',
847
+ ]);
848
+ const salt = copyBytes(input.hkdfSalt);
849
+ const nonce = copyBytes(input.nonce);
850
+ const aad = encodeString(input.canonicalAad, MAX_AAD_BYTES);
851
+ password = copySecretAndWipeCaller(callerPassword, 'INVALID_PASSWORD');
852
+ prf = copySecretAndWipeCaller(callerPrf, 'INVALID_REQUEST');
853
+ if (password.length > MAX_MODERN_CREDENTIAL_BYTES || prf.length !== 32 ||
854
+ salt.length !== 32 || nonce.length !== 12) {
855
+ fail('INVALID_REQUEST');
856
+ }
857
+ const passwordPointer = scope.allocate(password, true);
858
+ const prfPointer = scope.allocate(prf, true);
859
+ const saltPointer = scope.allocate(salt);
860
+ const noncePointer = scope.allocate(nonce);
861
+ const aadPointer = scope.allocate(aad);
862
+ const outRequired = scope.allocate(4);
863
+ const outBytes = scope.allocate(MAX_OUTPUT_BYTES);
864
+ let status;
865
+ try {
866
+ status = wasm._hd_sdn_seal_remembered_identity(
867
+ record.nativeValue,
868
+ passwordPointer, password.length,
869
+ prfPointer, prf.length,
870
+ saltPointer, salt.length,
871
+ noncePointer, nonce.length,
872
+ aadPointer, aad.length,
873
+ outBytes, MAX_OUTPUT_BYTES, outRequired,
874
+ );
875
+ } catch {
876
+ fail('CRYPTO_FAILURE');
877
+ }
878
+ if (status !== 0) throwStatus(status);
879
+ const required = readRequired(scope, outRequired);
880
+ if (required <= 16 || required > MAX_REMEMBERED_CIPHERTEXT_BYTES) {
881
+ fail('CRYPTO_FAILURE');
882
+ }
883
+ return scope.bytes(outBytes, required);
884
+ } finally {
885
+ const cleanupFailed = scope.cleanup();
886
+ if (!rejectInputBeforeRead && isObject(input)) {
887
+ if (!isUint8Array(callerPassword)) {
888
+ try {
889
+ callerPassword = input.passwordUtf8;
890
+ } catch {
891
+ // Preserve the handle/validation error while attempting cleanup.
892
+ }
893
+ }
894
+ if (!isUint8Array(callerPrf)) {
895
+ try {
896
+ callerPrf = input.prfOutput;
897
+ } catch {
898
+ // Preserve the handle/validation error while attempting cleanup.
899
+ }
900
+ }
901
+ }
902
+ const secretCleanupFailed = wipeAll([
903
+ password, prf, callerPassword, callerPrf,
904
+ ]);
905
+ if (authorityEstablished && (cleanupFailed || secretCleanupFailed)) {
906
+ fail('CRYPTO_FAILURE');
907
+ }
908
+ }
909
+ },
910
+
911
+ destroySdnIdentity(handle) {
912
+ if (!isObject(handle)) fail('STALE_HANDLE');
913
+ const record = issuedHandles.get(handle);
914
+ if (!record) fail('STALE_HANDLE');
915
+ if (record.state === 'destroyed') return;
916
+ record.state = 'destroy-pending';
917
+ try {
918
+ wasm._hd_sdn_destroy_identity(record.nativeValue);
919
+ } catch {
920
+ fail('CRYPTO_FAILURE');
921
+ }
922
+ record.nativeValue = 0n;
923
+ record.state = 'destroyed';
924
+ },
925
+ };
926
+
927
+ return Object.freeze(capabilities);
928
+ }