@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 +1 -1
- package/dist/cjs/wallet.js +66 -60
- package/dist/mjs/wallet.js +66 -59
- package/package.json +2 -2
- package/src/utils/random.js +10 -44
- package/src/wallet/common/descriptor.js +7 -2
- package/src/wallet/common/seed.js +19 -1
- package/src/wallet/ml_dsa_87/crypto.js +6 -2
- package/src/wallet/ml_dsa_87/wallet.js +24 -10
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# wallet.js
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@theqrl/wallet.js)
|
|
4
|
-

|
|
5
5
|
[](https://codecov.io/gh/theQRL/wallet.js)
|
|
6
6
|
|
|
7
7
|
Quantum-resistant wallet library for The QRL using **ML-DSA-87** (FIPS 204).
|
package/dist/cjs/wallet.js
CHANGED
|
@@ -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] =
|
|
266
|
-
out[2] =
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
4702
|
-
|
|
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
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
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
|
-
|
|
4829
|
-
|
|
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
|
-
/**
|
|
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
|
|
4915
|
-
this.seed.
|
|
4920
|
+
if (this.seed) {
|
|
4921
|
+
this.seed.zeroize();
|
|
4916
4922
|
}
|
|
4917
|
-
if (this.extendedSeed
|
|
4918
|
-
this.extendedSeed.
|
|
4923
|
+
if (this.extendedSeed) {
|
|
4924
|
+
this.extendedSeed.zeroize();
|
|
4919
4925
|
}
|
|
4920
4926
|
}
|
|
4921
4927
|
}
|
package/dist/mjs/wallet.js
CHANGED
|
@@ -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] =
|
|
263
|
-
out[2] =
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
4699
|
-
|
|
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
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
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
|
-
|
|
4826
|
-
|
|
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
|
-
/**
|
|
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
|
|
4912
|
-
this.seed.
|
|
4918
|
+
if (this.seed) {
|
|
4919
|
+
this.seed.zeroize();
|
|
4913
4920
|
}
|
|
4914
|
-
if (this.extendedSeed
|
|
4915
|
-
this.extendedSeed.
|
|
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.
|
|
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.
|
|
34
|
+
"@theqrl/mldsa87": "^1.0.9"
|
|
35
35
|
},
|
|
36
36
|
"directories": {
|
|
37
37
|
"lib": "src",
|
package/src/utils/random.js
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
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] =
|
|
66
|
-
out[2] =
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
/**
|
|
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
|
|
156
|
-
this.seed.
|
|
169
|
+
if (this.seed) {
|
|
170
|
+
this.seed.zeroize();
|
|
157
171
|
}
|
|
158
|
-
if (this.extendedSeed
|
|
159
|
-
this.extendedSeed.
|
|
172
|
+
if (this.extendedSeed) {
|
|
173
|
+
this.extendedSeed.zeroize();
|
|
160
174
|
}
|
|
161
175
|
}
|
|
162
176
|
}
|