@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 +6 -1
- package/dist/cjs/wallet.js +99 -1
- package/dist/mjs/wallet.js +98 -1
- package/package.json +5 -3
- package/src/utils/random.js +84 -0
- package/src/wallet/common/address.js +7 -0
- package/src/wallet/misc/mnemonic.js +4 -0
- package/src/wallet/ml_dsa_87/crypto.js +7 -0
- package/src/wallet/ml_dsa_87/wallet.js +1 -1
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)) {
|
package/dist/cjs/wallet.js
CHANGED
|
@@ -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 };
|
package/dist/mjs/wallet.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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';
|