@theqrl/wallet.js 1.0.4 → 1.0.6

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.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # wallet.js
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@theqrl/wallet.js.svg)](https://www.npmjs.com/package/@theqrl/wallet.js)
4
- ![test](https://github.com/theQRL/wallet.js/actions/workflows/test.yml/badge.svg)
4
+ ![test](https://github.com/theQRL/wallet.js/actions/workflows/release.yml/badge.svg)
5
5
  [![codecov](https://codecov.io/gh/theQRL/wallet.js/branch/main/graph/badge.svg?token=HHVBFBVGFR)](https://codecov.io/gh/theQRL/wallet.js)
6
6
 
7
7
  Quantum-resistant wallet library for The QRL using **ML-DSA-87** (FIPS 204).
@@ -6,7 +6,6 @@ var sha2_js = require('@noble/hashes/sha2.js');
6
6
  var utils = require('@noble/hashes/utils');
7
7
  var utils_js = require('@noble/hashes/utils.js');
8
8
 
9
- var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
10
9
  /**
11
10
  * Constants used across wallet components.
12
11
  * @module wallet/common/constants
@@ -260,10 +259,15 @@ function getDescriptorBytes(walletType, metadata = [0, 0]) {
260
259
  if (!isValidWalletType(walletType)) {
261
260
  throw new Error('Invalid wallet type in descriptor');
262
261
  }
262
+ const m0 = metadata?.[0] ?? 0;
263
+ const m1 = metadata?.[1] ?? 0;
264
+ if (!Number.isInteger(m0) || m0 < 0 || m0 > 255 || !Number.isInteger(m1) || m1 < 0 || m1 > 255) {
265
+ throw new Error('Descriptor metadata bytes must be in range [0, 255]');
266
+ }
263
267
  const out = new Uint8Array(DESCRIPTOR_SIZE);
264
268
  out[0] = walletType >>> 0;
265
- out[1] = (metadata?.[0] ?? 0) >>> 0;
266
- out[2] = (metadata?.[1] ?? 0) >>> 0;
269
+ out[1] = m0;
270
+ out[2] = m1;
267
271
  return out;
268
272
  }
269
273
 
@@ -298,6 +302,13 @@ class Seed {
298
302
  return this.bytes.slice();
299
303
  }
300
304
 
305
+ /**
306
+ * Best-effort zeroize internal seed bytes.
307
+ */
308
+ zeroize() {
309
+ this.bytes.fill(0);
310
+ }
311
+
301
312
  /**
302
313
  * Constructor: accepts hex string / Uint8Array / Buffer / number[].
303
314
  * @param {string|Uint8Array|Buffer|number[]} input
@@ -373,7 +384,11 @@ class ExtendedSeed {
373
384
  const out = new Uint8Array(EXTENDED_SEED_SIZE);
374
385
  out.set(desc.toBytes(), 0);
375
386
  out.set(seed.toBytes(), DESCRIPTOR_SIZE);
376
- return new ExtendedSeed(out);
387
+ try {
388
+ return new ExtendedSeed(out);
389
+ } finally {
390
+ out.fill(0);
391
+ }
377
392
  }
378
393
 
379
394
  /**
@@ -385,6 +400,13 @@ class ExtendedSeed {
385
400
  return new ExtendedSeed(toFixedU8(input, EXTENDED_SEED_SIZE, 'ExtendedSeed'));
386
401
  }
387
402
 
403
+ /**
404
+ * Best-effort zeroize internal extended seed bytes.
405
+ */
406
+ zeroize() {
407
+ this.bytes.fill(0);
408
+ }
409
+
388
410
  /**
389
411
  * Internal helper: construct without wallet type validation.
390
412
  * @param {string|Uint8Array|Buffer|number[]} input
@@ -414,61 +436,27 @@ function newMLDSA87Descriptor(metadata = [0, 0]) {
414
436
 
415
437
  /**
416
438
  * Secure random number generation for browser and Node.js environments.
439
+ * Requires Web Crypto API (globalThis.crypto.getRandomValues).
417
440
  * @module utils/random
418
441
  */
419
442
 
420
443
  const MAX_BYTES = 65536;
421
444
 
422
- function getGlobalScope() {
423
- if (typeof globalThis === 'object') return globalThis;
424
- // eslint-disable-next-line no-restricted-globals
425
- if (typeof self === 'object') return self;
426
- if (typeof window === 'object') return window;
427
- if (typeof global === 'object') return global;
428
- return {};
429
- }
430
-
431
445
  function getWebCrypto() {
432
- const scope = getGlobalScope();
433
- return scope.crypto || scope.msCrypto || null;
434
- }
435
-
436
- function getNodeRandomBytes() {
437
- /* c8 ignore next */
438
- const isNode = typeof process === 'object' && process !== null && process.versions && process.versions.node;
439
- if (!isNode) return null;
440
-
441
- let req = null;
442
- if (typeof module !== 'undefined' && module && typeof module.require === 'function') {
443
- req = module.require.bind(module);
444
- } else if (typeof module !== 'undefined' && module && typeof module.createRequire === 'function') {
445
- req = module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('wallet.js', document.baseURI).href)));
446
- } else if (typeof require === 'function') {
447
- req = require;
448
- }
449
- if (!req) return null;
450
-
451
- try {
452
- const nodeCrypto = req('crypto');
453
- if (nodeCrypto && typeof nodeCrypto.randomBytes === 'function') {
454
- return nodeCrypto.randomBytes;
455
- }
456
- } catch {
457
- return null;
458
- }
459
-
446
+ if (typeof globalThis === 'object' && globalThis.crypto) return globalThis.crypto;
460
447
  return null;
461
448
  }
462
449
 
463
450
  /**
464
451
  * Generate cryptographically secure random bytes.
465
452
  *
466
- * Uses Web Crypto API (getRandomValues) in browsers and crypto.randomBytes in Node.js.
453
+ * Uses Web Crypto API (getRandomValues) exclusively.
454
+ * Throws if Web Crypto API is unavailable.
467
455
  *
468
456
  * @param {number} size - Number of random bytes to generate
469
457
  * @returns {Uint8Array} Random bytes
470
458
  * @throws {RangeError} If size is invalid or too large
471
- * @throws {Error} If no secure random source is available
459
+ * @throws {Error} If no secure random source is available or RNG output is suspect
472
460
  */
473
461
  function randomBytes(size) {
474
462
  if (!Number.isSafeInteger(size) || size < 0) {
@@ -481,14 +469,14 @@ function randomBytes(size) {
481
469
  for (let i = 0; i < size; i += MAX_BYTES) {
482
470
  cryptoObj.getRandomValues(out.subarray(i, Math.min(size, i + MAX_BYTES)));
483
471
  }
472
+ {
473
+ let acc = 0;
474
+ for (let i = 0; i < 16; i++) acc |= out[i];
475
+ if (acc === 0) throw new Error('getRandomValues returned all zeros');
476
+ }
484
477
  return out;
485
478
  }
486
479
 
487
- const nodeRandomBytes = getNodeRandomBytes();
488
- if (nodeRandomBytes) {
489
- return nodeRandomBytes(size);
490
- }
491
-
492
480
  throw new Error('Secure random number generation is not supported by this environment');
493
481
  }
494
482
 
@@ -4698,8 +4686,12 @@ function keygen(seed) {
4698
4686
  const sk = new Uint8Array(mldsa87.CryptoSecretKeyBytes);
4699
4687
  // FIPS 204 requires 32-byte seed; hash 48-byte QRL seed to derive it
4700
4688
  const seedBytes = new Uint8Array(seed.hashSHA256());
4701
- mldsa87.cryptoSignKeypair(seedBytes, pk, sk);
4702
- return { pk, sk };
4689
+ try {
4690
+ mldsa87.cryptoSignKeypair(seedBytes, pk, sk);
4691
+ return { pk, sk };
4692
+ } finally {
4693
+ seedBytes.fill(0);
4694
+ }
4703
4695
  }
4704
4696
 
4705
4697
  /**
@@ -4792,9 +4784,13 @@ class Wallet {
4792
4784
  static newWallet(metadata = [0, 0]) {
4793
4785
  const descriptor = newMLDSA87Descriptor(metadata);
4794
4786
  const seedBytes = randomBytes(48);
4795
- const seed = new Seed(seedBytes);
4796
- const { pk, sk } = keygen(seed);
4797
- return new Wallet({ descriptor, seed, pk, sk });
4787
+ try {
4788
+ const seed = new Seed(seedBytes);
4789
+ const { pk, sk } = keygen(seed);
4790
+ return new Wallet({ descriptor, seed, pk, sk });
4791
+ } finally {
4792
+ seedBytes.fill(0);
4793
+ }
4798
4794
  }
4799
4795
 
4800
4796
  /**
@@ -4825,8 +4821,12 @@ class Wallet {
4825
4821
  */
4826
4822
  static newWalletFromMnemonic(mnemonic) {
4827
4823
  const bin = mnemonicToBin(mnemonic);
4828
- const extendedSeed = new ExtendedSeed(bin);
4829
- return this.newWalletFromExtendedSeed(extendedSeed);
4824
+ try {
4825
+ const extendedSeed = new ExtendedSeed(bin);
4826
+ return this.newWalletFromExtendedSeed(extendedSeed);
4827
+ } finally {
4828
+ bin.fill(0);
4829
+ }
4830
4830
  }
4831
4831
 
4832
4832
  /** @returns {Uint8Array} */
@@ -4874,7 +4874,13 @@ class Wallet {
4874
4874
  return this.pk.slice();
4875
4875
  }
4876
4876
 
4877
- /** @returns {Uint8Array} */
4877
+ /**
4878
+ * Returns a copy of the secret key.
4879
+ * @returns {Uint8Array}
4880
+ * @warning Caller is responsible for zeroing the returned buffer when done
4881
+ * (e.g. `sk.fill(0)`). The Wallet's `zeroize()` method cannot reach copies
4882
+ * returned by this method.
4883
+ */
4878
4884
  getSK() {
4879
4885
  return this.sk.slice();
4880
4886
  }
@@ -4911,11 +4917,11 @@ class Wallet {
4911
4917
  if (this.sk) {
4912
4918
  this.sk.fill(0);
4913
4919
  }
4914
- if (this.seed && this.seed.bytes) {
4915
- this.seed.bytes.fill(0);
4920
+ if (this.seed) {
4921
+ this.seed.zeroize();
4916
4922
  }
4917
- if (this.extendedSeed && this.extendedSeed.bytes) {
4918
- this.extendedSeed.bytes.fill(0);
4923
+ if (this.extendedSeed) {
4924
+ this.extendedSeed.zeroize();
4919
4925
  }
4920
4926
  }
4921
4927
  }
@@ -257,10 +257,15 @@ function getDescriptorBytes(walletType, metadata = [0, 0]) {
257
257
  if (!isValidWalletType(walletType)) {
258
258
  throw new Error('Invalid wallet type in descriptor');
259
259
  }
260
+ const m0 = metadata?.[0] ?? 0;
261
+ const m1 = metadata?.[1] ?? 0;
262
+ if (!Number.isInteger(m0) || m0 < 0 || m0 > 255 || !Number.isInteger(m1) || m1 < 0 || m1 > 255) {
263
+ throw new Error('Descriptor metadata bytes must be in range [0, 255]');
264
+ }
260
265
  const out = new Uint8Array(DESCRIPTOR_SIZE);
261
266
  out[0] = walletType >>> 0;
262
- out[1] = (metadata?.[0] ?? 0) >>> 0;
263
- out[2] = (metadata?.[1] ?? 0) >>> 0;
267
+ out[1] = m0;
268
+ out[2] = m1;
264
269
  return out;
265
270
  }
266
271
 
@@ -295,6 +300,13 @@ class Seed {
295
300
  return this.bytes.slice();
296
301
  }
297
302
 
303
+ /**
304
+ * Best-effort zeroize internal seed bytes.
305
+ */
306
+ zeroize() {
307
+ this.bytes.fill(0);
308
+ }
309
+
298
310
  /**
299
311
  * Constructor: accepts hex string / Uint8Array / Buffer / number[].
300
312
  * @param {string|Uint8Array|Buffer|number[]} input
@@ -370,7 +382,11 @@ class ExtendedSeed {
370
382
  const out = new Uint8Array(EXTENDED_SEED_SIZE);
371
383
  out.set(desc.toBytes(), 0);
372
384
  out.set(seed.toBytes(), DESCRIPTOR_SIZE);
373
- return new ExtendedSeed(out);
385
+ try {
386
+ return new ExtendedSeed(out);
387
+ } finally {
388
+ out.fill(0);
389
+ }
374
390
  }
375
391
 
376
392
  /**
@@ -382,6 +398,13 @@ class ExtendedSeed {
382
398
  return new ExtendedSeed(toFixedU8(input, EXTENDED_SEED_SIZE, 'ExtendedSeed'));
383
399
  }
384
400
 
401
+ /**
402
+ * Best-effort zeroize internal extended seed bytes.
403
+ */
404
+ zeroize() {
405
+ this.bytes.fill(0);
406
+ }
407
+
385
408
  /**
386
409
  * Internal helper: construct without wallet type validation.
387
410
  * @param {string|Uint8Array|Buffer|number[]} input
@@ -411,61 +434,27 @@ function newMLDSA87Descriptor(metadata = [0, 0]) {
411
434
 
412
435
  /**
413
436
  * Secure random number generation for browser and Node.js environments.
437
+ * Requires Web Crypto API (globalThis.crypto.getRandomValues).
414
438
  * @module utils/random
415
439
  */
416
440
 
417
441
  const MAX_BYTES = 65536;
418
442
 
419
- function getGlobalScope() {
420
- if (typeof globalThis === 'object') return globalThis;
421
- // eslint-disable-next-line no-restricted-globals
422
- if (typeof self === 'object') return self;
423
- if (typeof window === 'object') return window;
424
- if (typeof global === 'object') return global;
425
- return {};
426
- }
427
-
428
443
  function getWebCrypto() {
429
- const scope = getGlobalScope();
430
- return scope.crypto || scope.msCrypto || null;
431
- }
432
-
433
- function getNodeRandomBytes() {
434
- /* c8 ignore next */
435
- const isNode = typeof process === 'object' && process !== null && process.versions && process.versions.node;
436
- if (!isNode) return null;
437
-
438
- let req = null;
439
- if (typeof module !== 'undefined' && module && typeof module.require === 'function') {
440
- req = module.require.bind(module);
441
- } else if (typeof module !== 'undefined' && module && typeof module.createRequire === 'function') {
442
- req = module.createRequire(import.meta.url);
443
- } else if (typeof require === 'function') {
444
- req = require;
445
- }
446
- if (!req) return null;
447
-
448
- try {
449
- const nodeCrypto = req('crypto');
450
- if (nodeCrypto && typeof nodeCrypto.randomBytes === 'function') {
451
- return nodeCrypto.randomBytes;
452
- }
453
- } catch {
454
- return null;
455
- }
456
-
444
+ if (typeof globalThis === 'object' && globalThis.crypto) return globalThis.crypto;
457
445
  return null;
458
446
  }
459
447
 
460
448
  /**
461
449
  * Generate cryptographically secure random bytes.
462
450
  *
463
- * Uses Web Crypto API (getRandomValues) in browsers and crypto.randomBytes in Node.js.
451
+ * Uses Web Crypto API (getRandomValues) exclusively.
452
+ * Throws if Web Crypto API is unavailable.
464
453
  *
465
454
  * @param {number} size - Number of random bytes to generate
466
455
  * @returns {Uint8Array} Random bytes
467
456
  * @throws {RangeError} If size is invalid or too large
468
- * @throws {Error} If no secure random source is available
457
+ * @throws {Error} If no secure random source is available or RNG output is suspect
469
458
  */
470
459
  function randomBytes(size) {
471
460
  if (!Number.isSafeInteger(size) || size < 0) {
@@ -478,14 +467,14 @@ function randomBytes(size) {
478
467
  for (let i = 0; i < size; i += MAX_BYTES) {
479
468
  cryptoObj.getRandomValues(out.subarray(i, Math.min(size, i + MAX_BYTES)));
480
469
  }
470
+ {
471
+ let acc = 0;
472
+ for (let i = 0; i < 16; i++) acc |= out[i];
473
+ if (acc === 0) throw new Error('getRandomValues returned all zeros');
474
+ }
481
475
  return out;
482
476
  }
483
477
 
484
- const nodeRandomBytes = getNodeRandomBytes();
485
- if (nodeRandomBytes) {
486
- return nodeRandomBytes(size);
487
- }
488
-
489
478
  throw new Error('Secure random number generation is not supported by this environment');
490
479
  }
491
480
 
@@ -4695,8 +4684,12 @@ function keygen(seed) {
4695
4684
  const sk = new Uint8Array(CryptoSecretKeyBytes);
4696
4685
  // FIPS 204 requires 32-byte seed; hash 48-byte QRL seed to derive it
4697
4686
  const seedBytes = new Uint8Array(seed.hashSHA256());
4698
- cryptoSignKeypair(seedBytes, pk, sk);
4699
- return { pk, sk };
4687
+ try {
4688
+ cryptoSignKeypair(seedBytes, pk, sk);
4689
+ return { pk, sk };
4690
+ } finally {
4691
+ seedBytes.fill(0);
4692
+ }
4700
4693
  }
4701
4694
 
4702
4695
  /**
@@ -4789,9 +4782,13 @@ class Wallet {
4789
4782
  static newWallet(metadata = [0, 0]) {
4790
4783
  const descriptor = newMLDSA87Descriptor(metadata);
4791
4784
  const seedBytes = randomBytes(48);
4792
- const seed = new Seed(seedBytes);
4793
- const { pk, sk } = keygen(seed);
4794
- return new Wallet({ descriptor, seed, pk, sk });
4785
+ try {
4786
+ const seed = new Seed(seedBytes);
4787
+ const { pk, sk } = keygen(seed);
4788
+ return new Wallet({ descriptor, seed, pk, sk });
4789
+ } finally {
4790
+ seedBytes.fill(0);
4791
+ }
4795
4792
  }
4796
4793
 
4797
4794
  /**
@@ -4822,8 +4819,12 @@ class Wallet {
4822
4819
  */
4823
4820
  static newWalletFromMnemonic(mnemonic) {
4824
4821
  const bin = mnemonicToBin(mnemonic);
4825
- const extendedSeed = new ExtendedSeed(bin);
4826
- return this.newWalletFromExtendedSeed(extendedSeed);
4822
+ try {
4823
+ const extendedSeed = new ExtendedSeed(bin);
4824
+ return this.newWalletFromExtendedSeed(extendedSeed);
4825
+ } finally {
4826
+ bin.fill(0);
4827
+ }
4827
4828
  }
4828
4829
 
4829
4830
  /** @returns {Uint8Array} */
@@ -4871,7 +4872,13 @@ class Wallet {
4871
4872
  return this.pk.slice();
4872
4873
  }
4873
4874
 
4874
- /** @returns {Uint8Array} */
4875
+ /**
4876
+ * Returns a copy of the secret key.
4877
+ * @returns {Uint8Array}
4878
+ * @warning Caller is responsible for zeroing the returned buffer when done
4879
+ * (e.g. `sk.fill(0)`). The Wallet's `zeroize()` method cannot reach copies
4880
+ * returned by this method.
4881
+ */
4875
4882
  getSK() {
4876
4883
  return this.sk.slice();
4877
4884
  }
@@ -4908,11 +4915,11 @@ class Wallet {
4908
4915
  if (this.sk) {
4909
4916
  this.sk.fill(0);
4910
4917
  }
4911
- if (this.seed && this.seed.bytes) {
4912
- this.seed.bytes.fill(0);
4918
+ if (this.seed) {
4919
+ this.seed.zeroize();
4913
4920
  }
4914
- if (this.extendedSeed && this.extendedSeed.bytes) {
4915
- this.extendedSeed.bytes.fill(0);
4921
+ if (this.extendedSeed) {
4922
+ this.extendedSeed.zeroize();
4916
4923
  }
4917
4924
  }
4918
4925
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theqrl/wallet.js",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Quantum-resistant wallet library for The QRL using ML-DSA-87 (FIPS 204)",
5
5
  "type": "module",
6
6
  "main": "dist/cjs/wallet.js",
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@noble/hashes": "^1.8.0",
34
- "@theqrl/mldsa87": "^1.0.8"
34
+ "@theqrl/mldsa87": "^1.0.9"
35
35
  },
36
36
  "directories": {
37
37
  "lib": "src",
@@ -1,61 +1,27 @@
1
1
  /**
2
2
  * Secure random number generation for browser and Node.js environments.
3
+ * Requires Web Crypto API (globalThis.crypto.getRandomValues).
3
4
  * @module utils/random
4
5
  */
5
6
 
6
7
  const MAX_BYTES = 65536;
7
8
  const MAX_UINT32 = 0xffffffff;
8
9
 
9
- function getGlobalScope() {
10
- if (typeof globalThis === 'object') return globalThis;
11
- // eslint-disable-next-line no-restricted-globals
12
- if (typeof self === 'object') return self;
13
- if (typeof window === 'object') return window;
14
- if (typeof global === 'object') return global;
15
- return {};
16
- }
17
-
18
10
  function getWebCrypto() {
19
- const scope = getGlobalScope();
20
- return scope.crypto || scope.msCrypto || null;
21
- }
22
-
23
- function getNodeRandomBytes() {
24
- /* c8 ignore next */
25
- const isNode = typeof process === 'object' && process !== null && process.versions && process.versions.node;
26
- if (!isNode) return null;
27
-
28
- let req = null;
29
- if (typeof module !== 'undefined' && module && typeof module.require === 'function') {
30
- req = module.require.bind(module);
31
- } else if (typeof module !== 'undefined' && module && typeof module.createRequire === 'function') {
32
- req = module.createRequire(import.meta.url);
33
- } else if (typeof require === 'function') {
34
- req = require;
35
- }
36
- if (!req) return null;
37
-
38
- try {
39
- const nodeCrypto = req('crypto');
40
- if (nodeCrypto && typeof nodeCrypto.randomBytes === 'function') {
41
- return nodeCrypto.randomBytes;
42
- }
43
- } catch {
44
- return null;
45
- }
46
-
11
+ if (typeof globalThis === 'object' && globalThis.crypto) return globalThis.crypto;
47
12
  return null;
48
13
  }
49
14
 
50
15
  /**
51
16
  * Generate cryptographically secure random bytes.
52
17
  *
53
- * Uses Web Crypto API (getRandomValues) in browsers and crypto.randomBytes in Node.js.
18
+ * Uses Web Crypto API (getRandomValues) exclusively.
19
+ * Throws if Web Crypto API is unavailable.
54
20
  *
55
21
  * @param {number} size - Number of random bytes to generate
56
22
  * @returns {Uint8Array} Random bytes
57
23
  * @throws {RangeError} If size is invalid or too large
58
- * @throws {Error} If no secure random source is available
24
+ * @throws {Error} If no secure random source is available or RNG output is suspect
59
25
  */
60
26
  export function randomBytes(size) {
61
27
  if (!Number.isSafeInteger(size) || size < 0) {
@@ -72,13 +38,13 @@ export function randomBytes(size) {
72
38
  for (let i = 0; i < size; i += MAX_BYTES) {
73
39
  cryptoObj.getRandomValues(out.subarray(i, Math.min(size, i + MAX_BYTES)));
74
40
  }
41
+ if (size >= 16) {
42
+ let acc = 0;
43
+ for (let i = 0; i < 16; i++) acc |= out[i];
44
+ if (acc === 0) throw new Error('getRandomValues returned all zeros');
45
+ }
75
46
  return out;
76
47
  }
77
48
 
78
- const nodeRandomBytes = getNodeRandomBytes();
79
- if (nodeRandomBytes) {
80
- return nodeRandomBytes(size);
81
- }
82
-
83
49
  throw new Error('Secure random number generation is not supported by this environment');
84
50
  }
@@ -60,10 +60,15 @@ function getDescriptorBytes(walletType, metadata = [0, 0]) {
60
60
  if (!isValidWalletType(walletType)) {
61
61
  throw new Error('Invalid wallet type in descriptor');
62
62
  }
63
+ const m0 = metadata?.[0] ?? 0;
64
+ const m1 = metadata?.[1] ?? 0;
65
+ if (!Number.isInteger(m0) || m0 < 0 || m0 > 255 || !Number.isInteger(m1) || m1 < 0 || m1 > 255) {
66
+ throw new Error('Descriptor metadata bytes must be in range [0, 255]');
67
+ }
63
68
  const out = new Uint8Array(DESCRIPTOR_SIZE);
64
69
  out[0] = walletType >>> 0;
65
- out[1] = (metadata?.[0] ?? 0) >>> 0;
66
- out[2] = (metadata?.[1] ?? 0) >>> 0;
70
+ out[1] = m0;
71
+ out[2] = m1;
67
72
  return out;
68
73
  }
69
74
 
@@ -34,6 +34,13 @@ class Seed {
34
34
  return this.bytes.slice();
35
35
  }
36
36
 
37
+ /**
38
+ * Best-effort zeroize internal seed bytes.
39
+ */
40
+ zeroize() {
41
+ this.bytes.fill(0);
42
+ }
43
+
37
44
  /**
38
45
  * Constructor: accepts hex string / Uint8Array / Buffer / number[].
39
46
  * @param {string|Uint8Array|Buffer|number[]} input
@@ -109,7 +116,11 @@ class ExtendedSeed {
109
116
  const out = new Uint8Array(EXTENDED_SEED_SIZE);
110
117
  out.set(desc.toBytes(), 0);
111
118
  out.set(seed.toBytes(), DESCRIPTOR_SIZE);
112
- return new ExtendedSeed(out);
119
+ try {
120
+ return new ExtendedSeed(out);
121
+ } finally {
122
+ out.fill(0);
123
+ }
113
124
  }
114
125
 
115
126
  /**
@@ -121,6 +132,13 @@ class ExtendedSeed {
121
132
  return new ExtendedSeed(toFixedU8(input, EXTENDED_SEED_SIZE, 'ExtendedSeed'));
122
133
  }
123
134
 
135
+ /**
136
+ * Best-effort zeroize internal extended seed bytes.
137
+ */
138
+ zeroize() {
139
+ this.bytes.fill(0);
140
+ }
141
+
124
142
  /**
125
143
  * Internal helper: construct without wallet type validation.
126
144
  * @param {string|Uint8Array|Buffer|number[]} input
@@ -26,8 +26,12 @@ function keygen(seed) {
26
26
  const sk = new Uint8Array(CryptoSecretKeyBytes);
27
27
  // FIPS 204 requires 32-byte seed; hash 48-byte QRL seed to derive it
28
28
  const seedBytes = new Uint8Array(seed.hashSHA256());
29
- cryptoSignKeypair(seedBytes, pk, sk);
30
- return { pk, sk };
29
+ try {
30
+ cryptoSignKeypair(seedBytes, pk, sk);
31
+ return { pk, sk };
32
+ } finally {
33
+ seedBytes.fill(0);
34
+ }
31
35
  }
32
36
 
33
37
  /**
@@ -33,9 +33,13 @@ class Wallet {
33
33
  static newWallet(metadata = [0, 0]) {
34
34
  const descriptor = newMLDSA87Descriptor(metadata);
35
35
  const seedBytes = randomBytes(48);
36
- const seed = new Seed(seedBytes);
37
- const { pk, sk } = keygen(seed);
38
- return new Wallet({ descriptor, seed, pk, sk });
36
+ try {
37
+ const seed = new Seed(seedBytes);
38
+ const { pk, sk } = keygen(seed);
39
+ return new Wallet({ descriptor, seed, pk, sk });
40
+ } finally {
41
+ seedBytes.fill(0);
42
+ }
39
43
  }
40
44
 
41
45
  /**
@@ -66,8 +70,12 @@ class Wallet {
66
70
  */
67
71
  static newWalletFromMnemonic(mnemonic) {
68
72
  const bin = mnemonicToBin(mnemonic);
69
- const extendedSeed = new ExtendedSeed(bin);
70
- return this.newWalletFromExtendedSeed(extendedSeed);
73
+ try {
74
+ const extendedSeed = new ExtendedSeed(bin);
75
+ return this.newWalletFromExtendedSeed(extendedSeed);
76
+ } finally {
77
+ bin.fill(0);
78
+ }
71
79
  }
72
80
 
73
81
  /** @returns {Uint8Array} */
@@ -115,7 +123,13 @@ class Wallet {
115
123
  return this.pk.slice();
116
124
  }
117
125
 
118
- /** @returns {Uint8Array} */
126
+ /**
127
+ * Returns a copy of the secret key.
128
+ * @returns {Uint8Array}
129
+ * @warning Caller is responsible for zeroing the returned buffer when done
130
+ * (e.g. `sk.fill(0)`). The Wallet's `zeroize()` method cannot reach copies
131
+ * returned by this method.
132
+ */
119
133
  getSK() {
120
134
  return this.sk.slice();
121
135
  }
@@ -152,11 +166,11 @@ class Wallet {
152
166
  if (this.sk) {
153
167
  this.sk.fill(0);
154
168
  }
155
- if (this.seed && this.seed.bytes) {
156
- this.seed.bytes.fill(0);
169
+ if (this.seed) {
170
+ this.seed.zeroize();
157
171
  }
158
- if (this.extendedSeed && this.extendedSeed.bytes) {
159
- this.extendedSeed.bytes.fill(0);
172
+ if (this.extendedSeed) {
173
+ this.extendedSeed.zeroize();
160
174
  }
161
175
  }
162
176
  }