@theqrl/wallet.js 1.0.2 → 1.0.3

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
@@ -116,6 +116,10 @@ const wallet = newWalletFromExtendedSeed('0x01000000...'); // 51-byte hex
116
116
 
117
117
  ### Address Utilities
118
118
 
119
+ **Address Format:** `Q` prefix + 40 lowercase hex characters (41 chars total).
120
+ - Output is always lowercase; input parsing is case-insensitive
121
+ - No checksum encoding (unlike EIP-55)
122
+
119
123
  ```javascript
120
124
  import {
121
125
  addressToString,
@@ -126,8 +130,9 @@ import {
126
130
  // Convert bytes to string
127
131
  const addrStr = addressToString(addressBytes); // 'Qabc...'
128
132
 
129
- // Convert string to bytes
133
+ // Convert string to bytes (case-insensitive)
130
134
  const addrBytes = stringToAddress('Qabc123...');
135
+ const same = stringToAddress('QABC123...'); // Also valid
131
136
 
132
137
  // Validate address format
133
138
  if (isValidAddress(userInput)) {
@@ -4,9 +4,9 @@ var sha3 = require('@noble/hashes/sha3');
4
4
  var mldsa87 = require('@theqrl/mldsa87');
5
5
  var sha2_js = require('@noble/hashes/sha2.js');
6
6
  var utils = require('@noble/hashes/utils');
7
- var randomBytes = require('randombytes');
8
7
  var utils_js = require('@noble/hashes/utils.js');
9
8
 
9
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
10
10
  /**
11
11
  * Constants used across wallet components.
12
12
  * @module wallet/common/constants
@@ -27,6 +27,13 @@ const EXTENDED_SEED_SIZE = DESCRIPTOR_SIZE + SEED_SIZE;
27
27
  /**
28
28
  * Address helpers.
29
29
  * @module wallet/common/address
30
+ *
31
+ * Address Format:
32
+ * - String form: "Q" prefix followed by 40 lowercase hex characters (41 chars total)
33
+ * - Byte form: 20-byte SHAKE-256 hash of (descriptor || public key)
34
+ * - Output is always lowercase hex; input parsing is case-insensitive for both
35
+ * the "Q"/"q" prefix and hex characters
36
+ * - Unlike EIP-55, no checksum encoding is used in the address itself
30
37
  */
31
38
 
32
39
 
@@ -405,6 +412,86 @@ function newMLDSA87Descriptor(metadata = [0, 0]) {
405
412
  return new Descriptor(getDescriptorBytes(WalletType.ML_DSA_87, metadata));
406
413
  }
407
414
 
415
+ /**
416
+ * Secure random number generation for browser and Node.js environments.
417
+ * @module utils/random
418
+ */
419
+
420
+ const MAX_BYTES = 65536;
421
+
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
+ 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
+
460
+ return null;
461
+ }
462
+
463
+ /**
464
+ * Generate cryptographically secure random bytes.
465
+ *
466
+ * Uses Web Crypto API (getRandomValues) in browsers and crypto.randomBytes in Node.js.
467
+ *
468
+ * @param {number} size - Number of random bytes to generate
469
+ * @returns {Uint8Array} Random bytes
470
+ * @throws {RangeError} If size is invalid or too large
471
+ * @throws {Error} If no secure random source is available
472
+ */
473
+ function randomBytes(size) {
474
+ if (!Number.isSafeInteger(size) || size < 0) {
475
+ throw new RangeError('size must be a non-negative integer');
476
+ }
477
+
478
+ const cryptoObj = getWebCrypto();
479
+ if (cryptoObj && typeof cryptoObj.getRandomValues === 'function') {
480
+ const out = new Uint8Array(size);
481
+ for (let i = 0; i < size; i += MAX_BYTES) {
482
+ cryptoObj.getRandomValues(out.subarray(i, Math.min(size, i + MAX_BYTES)));
483
+ }
484
+ return out;
485
+ }
486
+
487
+ const nodeRandomBytes = getNodeRandomBytes();
488
+ if (nodeRandomBytes) {
489
+ return nodeRandomBytes(size);
490
+ }
491
+
492
+ throw new Error('Secure random number generation is not supported by this environment');
493
+ }
494
+
408
495
  /**
409
496
  * Mnemonic word list used by encoding/decoding utilities.
410
497
  * @module qrl/wordlist
@@ -4553,8 +4640,12 @@ function binToMnemonic(input) {
4553
4640
  * Decode spaced hex mnemonic to bytes.
4554
4641
  * @param {string} mnemonic
4555
4642
  * @returns {Uint8Array}
4643
+ *
4644
+ * Note: Mnemonic words are normalized to lowercase for user convenience.
4645
+ * This is by design to reduce errors from capitalization differences.
4556
4646
  */
4557
4647
  function mnemonicToBin(mnemonic) {
4648
+ // Normalize to lowercase for user-friendly input (case-insensitive matching)
4558
4649
  const mnemonicWords = mnemonic.trim().toLowerCase().split(/\s+/);
4559
4650
  if (mnemonicWords.length % 2 !== 0) throw new Error('word count must be even');
4560
4651
 
@@ -4594,11 +4685,18 @@ function mnemonicToBin(mnemonic) {
4594
4685
 
4595
4686
  /**
4596
4687
  * Generate a keypair.
4688
+ *
4689
+ * Note: ML-DSA-87 (FIPS 204) requires a 32-byte seed for key generation.
4690
+ * QRL uses a 48-byte seed for mnemonic compatibility across wallet types.
4691
+ * SHA-256 hashing reduces the 48-byte seed to the required 32 bytes per spec.
4692
+ * This matches go-qrllib behavior for cross-implementation compatibility.
4693
+ *
4597
4694
  * @returns {{ pk: Uint8Array, sk: Uint8Array }}
4598
4695
  */
4599
4696
  function keygen(seed) {
4600
4697
  const pk = new Uint8Array(mldsa87.CryptoPublicKeyBytes);
4601
4698
  const sk = new Uint8Array(mldsa87.CryptoSecretKeyBytes);
4699
+ // FIPS 204 requires 32-byte seed; hash 48-byte QRL seed to derive it
4602
4700
  const seedBytes = new Uint8Array(seed.hashSHA256());
4603
4701
  mldsa87.cryptoSignKeypair(seedBytes, pk, sk);
4604
4702
  return { pk, sk };
@@ -2,7 +2,6 @@ import { shake256 } from '@noble/hashes/sha3';
2
2
  import { CryptoPublicKeyBytes, cryptoSignKeypair, CryptoSecretKeyBytes, cryptoSign, CryptoBytes, cryptoSignVerify } from '@theqrl/mldsa87';
3
3
  import { sha256 } from '@noble/hashes/sha2.js';
4
4
  import { hexToBytes } from '@noble/hashes/utils';
5
- import randomBytes from 'randombytes';
6
5
  import { bytesToHex } from '@noble/hashes/utils.js';
7
6
 
8
7
  /**
@@ -25,6 +24,13 @@ const EXTENDED_SEED_SIZE = DESCRIPTOR_SIZE + SEED_SIZE;
25
24
  /**
26
25
  * Address helpers.
27
26
  * @module wallet/common/address
27
+ *
28
+ * Address Format:
29
+ * - String form: "Q" prefix followed by 40 lowercase hex characters (41 chars total)
30
+ * - Byte form: 20-byte SHAKE-256 hash of (descriptor || public key)
31
+ * - Output is always lowercase hex; input parsing is case-insensitive for both
32
+ * the "Q"/"q" prefix and hex characters
33
+ * - Unlike EIP-55, no checksum encoding is used in the address itself
28
34
  */
29
35
 
30
36
 
@@ -403,6 +409,86 @@ function newMLDSA87Descriptor(metadata = [0, 0]) {
403
409
  return new Descriptor(getDescriptorBytes(WalletType.ML_DSA_87, metadata));
404
410
  }
405
411
 
412
+ /**
413
+ * Secure random number generation for browser and Node.js environments.
414
+ * @module utils/random
415
+ */
416
+
417
+ const MAX_BYTES = 65536;
418
+
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
+ 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
+
457
+ return null;
458
+ }
459
+
460
+ /**
461
+ * Generate cryptographically secure random bytes.
462
+ *
463
+ * Uses Web Crypto API (getRandomValues) in browsers and crypto.randomBytes in Node.js.
464
+ *
465
+ * @param {number} size - Number of random bytes to generate
466
+ * @returns {Uint8Array} Random bytes
467
+ * @throws {RangeError} If size is invalid or too large
468
+ * @throws {Error} If no secure random source is available
469
+ */
470
+ function randomBytes(size) {
471
+ if (!Number.isSafeInteger(size) || size < 0) {
472
+ throw new RangeError('size must be a non-negative integer');
473
+ }
474
+
475
+ const cryptoObj = getWebCrypto();
476
+ if (cryptoObj && typeof cryptoObj.getRandomValues === 'function') {
477
+ const out = new Uint8Array(size);
478
+ for (let i = 0; i < size; i += MAX_BYTES) {
479
+ cryptoObj.getRandomValues(out.subarray(i, Math.min(size, i + MAX_BYTES)));
480
+ }
481
+ return out;
482
+ }
483
+
484
+ const nodeRandomBytes = getNodeRandomBytes();
485
+ if (nodeRandomBytes) {
486
+ return nodeRandomBytes(size);
487
+ }
488
+
489
+ throw new Error('Secure random number generation is not supported by this environment');
490
+ }
491
+
406
492
  /**
407
493
  * Mnemonic word list used by encoding/decoding utilities.
408
494
  * @module qrl/wordlist
@@ -4551,8 +4637,12 @@ function binToMnemonic(input) {
4551
4637
  * Decode spaced hex mnemonic to bytes.
4552
4638
  * @param {string} mnemonic
4553
4639
  * @returns {Uint8Array}
4640
+ *
4641
+ * Note: Mnemonic words are normalized to lowercase for user convenience.
4642
+ * This is by design to reduce errors from capitalization differences.
4554
4643
  */
4555
4644
  function mnemonicToBin(mnemonic) {
4645
+ // Normalize to lowercase for user-friendly input (case-insensitive matching)
4556
4646
  const mnemonicWords = mnemonic.trim().toLowerCase().split(/\s+/);
4557
4647
  if (mnemonicWords.length % 2 !== 0) throw new Error('word count must be even');
4558
4648
 
@@ -4592,11 +4682,18 @@ function mnemonicToBin(mnemonic) {
4592
4682
 
4593
4683
  /**
4594
4684
  * Generate a keypair.
4685
+ *
4686
+ * Note: ML-DSA-87 (FIPS 204) requires a 32-byte seed for key generation.
4687
+ * QRL uses a 48-byte seed for mnemonic compatibility across wallet types.
4688
+ * SHA-256 hashing reduces the 48-byte seed to the required 32 bytes per spec.
4689
+ * This matches go-qrllib behavior for cross-implementation compatibility.
4690
+ *
4595
4691
  * @returns {{ pk: Uint8Array, sk: Uint8Array }}
4596
4692
  */
4597
4693
  function keygen(seed) {
4598
4694
  const pk = new Uint8Array(CryptoPublicKeyBytes);
4599
4695
  const sk = new Uint8Array(CryptoSecretKeyBytes);
4696
+ // FIPS 204 requires 32-byte seed; hash 48-byte QRL seed to derive it
4600
4697
  const seedBytes = new Uint8Array(seed.hashSHA256());
4601
4698
  cryptoSignKeypair(seedBytes, pk, sk);
4602
4699
  return { pk, sk };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theqrl/wallet.js",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
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",
@@ -17,6 +17,7 @@
17
17
  "build": "rollup -c && ./fixup",
18
18
  "prepublishOnly": "npm run build",
19
19
  "test": "mocha",
20
+ "test:browser": "playwright test",
20
21
  "lint-check": "eslint 'src/**/*.js' 'test/**/*.js'",
21
22
  "lint": "eslint --fix 'src/**/*.js' 'test/**/*.js'",
22
23
  "report-coverage": "c8 --reporter=text-lcov npm run test > coverage.lcov",
@@ -30,8 +31,7 @@
30
31
  },
31
32
  "dependencies": {
32
33
  "@noble/hashes": "^1.8.0",
33
- "@theqrl/mldsa87": "^1.0.5",
34
- "randombytes": "^2.1.0"
34
+ "@theqrl/mldsa87": "^1.0.6"
35
35
  },
36
36
  "directories": {
37
37
  "lib": "src",
@@ -43,6 +43,7 @@
43
43
  "types"
44
44
  ],
45
45
  "devDependencies": {
46
+ "@playwright/test": "^1.49.0",
46
47
  "@semantic-release/changelog": "^6.0.3",
47
48
  "@semantic-release/exec": "^7.1.0",
48
49
  "@semantic-release/git": "^10.0.1",
@@ -57,6 +58,7 @@
57
58
  "eslint-plugin-prettier": "^4.2.1",
58
59
  "fast-check": "^4.5.3",
59
60
  "mocha": "^10.2.0",
61
+ "playwright": "^1.57.0",
60
62
  "prettier": "^2.8.7",
61
63
  "rollup": "^4.55.1",
62
64
  "typescript": "^5.5.4"
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Secure random number generation for browser and Node.js environments.
3
+ * @module utils/random
4
+ */
5
+
6
+ const MAX_BYTES = 65536;
7
+ const MAX_UINT32 = 0xffffffff;
8
+
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
+ 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
+
47
+ return null;
48
+ }
49
+
50
+ /**
51
+ * Generate cryptographically secure random bytes.
52
+ *
53
+ * Uses Web Crypto API (getRandomValues) in browsers and crypto.randomBytes in Node.js.
54
+ *
55
+ * @param {number} size - Number of random bytes to generate
56
+ * @returns {Uint8Array} Random bytes
57
+ * @throws {RangeError} If size is invalid or too large
58
+ * @throws {Error} If no secure random source is available
59
+ */
60
+ export function randomBytes(size) {
61
+ if (!Number.isSafeInteger(size) || size < 0) {
62
+ throw new RangeError('size must be a non-negative integer');
63
+ }
64
+ if (size > MAX_UINT32) {
65
+ throw new RangeError('requested too many random bytes');
66
+ }
67
+ if (size === 0) return new Uint8Array(0);
68
+
69
+ const cryptoObj = getWebCrypto();
70
+ if (cryptoObj && typeof cryptoObj.getRandomValues === 'function') {
71
+ const out = new Uint8Array(size);
72
+ for (let i = 0; i < size; i += MAX_BYTES) {
73
+ cryptoObj.getRandomValues(out.subarray(i, Math.min(size, i + MAX_BYTES)));
74
+ }
75
+ return out;
76
+ }
77
+
78
+ const nodeRandomBytes = getNodeRandomBytes();
79
+ if (nodeRandomBytes) {
80
+ return nodeRandomBytes(size);
81
+ }
82
+
83
+ throw new Error('Secure random number generation is not supported by this environment');
84
+ }
@@ -1,6 +1,13 @@
1
1
  /**
2
2
  * Address helpers.
3
3
  * @module wallet/common/address
4
+ *
5
+ * Address Format:
6
+ * - String form: "Q" prefix followed by 40 lowercase hex characters (41 chars total)
7
+ * - Byte form: 20-byte SHAKE-256 hash of (descriptor || public key)
8
+ * - Output is always lowercase hex; input parsing is case-insensitive for both
9
+ * the "Q"/"q" prefix and hex characters
10
+ * - Unlike EIP-55, no checksum encoding is used in the address itself
4
11
  */
5
12
 
6
13
  /** @typedef {import('./descriptor.js').Descriptor} Descriptor */
@@ -38,8 +38,12 @@ function binToMnemonic(input) {
38
38
  * Decode spaced hex mnemonic to bytes.
39
39
  * @param {string} mnemonic
40
40
  * @returns {Uint8Array}
41
+ *
42
+ * Note: Mnemonic words are normalized to lowercase for user convenience.
43
+ * This is by design to reduce errors from capitalization differences.
41
44
  */
42
45
  function mnemonicToBin(mnemonic) {
46
+ // Normalize to lowercase for user-friendly input (case-insensitive matching)
43
47
  const mnemonicWords = mnemonic.trim().toLowerCase().split(/\s+/);
44
48
  if (mnemonicWords.length % 2 !== 0) throw new Error('word count must be even');
45
49
 
@@ -13,11 +13,18 @@ import {
13
13
 
14
14
  /**
15
15
  * Generate a keypair.
16
+ *
17
+ * Note: ML-DSA-87 (FIPS 204) requires a 32-byte seed for key generation.
18
+ * QRL uses a 48-byte seed for mnemonic compatibility across wallet types.
19
+ * SHA-256 hashing reduces the 48-byte seed to the required 32 bytes per spec.
20
+ * This matches go-qrllib behavior for cross-implementation compatibility.
21
+ *
16
22
  * @returns {{ pk: Uint8Array, sk: Uint8Array }}
17
23
  */
18
24
  function keygen(seed) {
19
25
  const pk = new Uint8Array(CryptoPublicKeyBytes);
20
26
  const sk = new Uint8Array(CryptoSecretKeyBytes);
27
+ // FIPS 204 requires 32-byte seed; hash 48-byte QRL seed to derive it
21
28
  const seedBytes = new Uint8Array(seed.hashSHA256());
22
29
  cryptoSignKeypair(seedBytes, pk, sk);
23
30
  return { pk, sk };
@@ -4,8 +4,8 @@
4
4
  */
5
5
 
6
6
  /** @typedef {import('../common/descriptor.js').Descriptor} Descriptor */
7
- import randomBytes from 'randombytes';
8
7
  import { bytesToHex } from '@noble/hashes/utils.js';
8
+ import { randomBytes } from '../../utils/random.js';
9
9
  import { mnemonicToBin, binToMnemonic } from '../misc/mnemonic.js';
10
10
  import { getAddressFromPKAndDescriptor, addressToString } from '../common/address.js';
11
11
  import { Descriptor } from '../common/descriptor.js';