@theqrl/dilithium5 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
@@ -64,7 +64,7 @@ Generate a keypair from a seed.
64
64
 
65
65
  Sign a message (combined mode: returns signature || message).
66
66
 
67
- - `message`: `Uint8Array` - message to sign
67
+ - `message`: `Uint8Array` or `string` - message bytes; if `string`, it must be hex only (optional `0x`, even length). Plain-text strings are not accepted.
68
68
  - `sk`: `Uint8Array(4896)` - secret key
69
69
  - `randomized`: `boolean` - `true` for hedged signing, `false` for deterministic
70
70
  - Returns: `Uint8Array` containing signature + message
@@ -82,7 +82,7 @@ Verify and extract message from signed message.
82
82
  Create a detached signature.
83
83
 
84
84
  - `sig`: `Uint8Array(4595)` - output buffer for signature
85
- - `message`: `Uint8Array` - message to sign
85
+ - `message`: `Uint8Array` or `string` - message bytes; if `string`, it must be hex only (optional `0x`, even length). Plain-text strings are not accepted.
86
86
  - `sk`: `Uint8Array(4896)` - secret key
87
87
  - `randomized`: `boolean` - `true` for hedged, `false` for deterministic
88
88
  - Returns: `0` on success
@@ -92,10 +92,12 @@ Create a detached signature.
92
92
  Verify a detached signature.
93
93
 
94
94
  - `sig`: `Uint8Array(4595)` - signature to verify
95
- - `message`: `Uint8Array` - original message
95
+ - `message`: `Uint8Array` or `string` - original message bytes; if `string`, it must be hex only (optional `0x`, even length). Plain-text strings are not accepted.
96
96
  - `pk`: `Uint8Array(2592)` - public key
97
97
  - Returns: `true` if valid, `false` otherwise
98
98
 
99
+ **Note:** To sign or verify plain text, convert it to bytes (e.g., `new TextEncoder().encode('Hello')`). String inputs are interpreted as hex only.
100
+
99
101
  #### `zeroize(buffer)`
100
102
 
101
103
  Zero out sensitive data (best-effort, see security notes).
@@ -139,7 +141,7 @@ See [SECURITY.md](../../SECURITY.md) for important information about:
139
141
 
140
142
  ## Requirements
141
143
 
142
- - Node.js 18+ or modern browsers with ES2020 support
144
+ - Node.js 18.20+ or modern browsers with ES2020 support
143
145
  - Full TypeScript definitions included
144
146
 
145
147
  ## License
@@ -1,8 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  var sha3_js = require('@noble/hashes/sha3.js');
4
- var pkg = require('randombytes');
4
+ var utils_js = require('@noble/hashes/utils.js');
5
5
 
6
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
6
7
  const Shake128Rate = 168;
7
8
  const Shake256Rate = 136;
8
9
  const Stream128BlockBytes = Shake128Rate;
@@ -367,6 +368,8 @@ function polyUniform(a, seed, nonce) {
367
368
 
368
369
  let ctr = rejUniform(a.coeffs, 0, N, buf, bufLen);
369
370
 
371
+ // Note: With current parameters, needing extra blocks is vanishingly unlikely.
372
+ /* c8 ignore start */
370
373
  while (ctr < N) {
371
374
  off = bufLen % 3;
372
375
  for (let i = 0; i < off; ++i) buf[i] = buf[bufLen - off + i];
@@ -375,6 +378,7 @@ function polyUniform(a, seed, nonce) {
375
378
  bufLen = Stream128BlockBytes + off;
376
379
  ctr += rejUniform(a.coeffs, ctr, N - ctr, buf, bufLen);
377
380
  }
381
+ /* c8 ignore stop */
378
382
  }
379
383
 
380
384
  function rejEta(aP, aOffset, len, buf, bufLen) {
@@ -468,10 +472,13 @@ function polyChallenge(cP, seed) {
468
472
  }
469
473
  for (let i = N - TAU; i < N; ++i) {
470
474
  do {
475
+ // Note: Re-squeezing here is extremely unlikely with TAU=60.
476
+ /* c8 ignore start */
471
477
  if (pos >= Shake256Rate) {
472
478
  shake256SqueezeBlocks(buf, 0, 1, state);
473
479
  pos = 0;
474
480
  }
481
+ /* c8 ignore stop */
475
482
 
476
483
  b = buf[pos++];
477
484
  } while (b > i);
@@ -1016,21 +1023,118 @@ function unpackSig(cP, z, hP, sig) {
1016
1023
  return 0;
1017
1024
  }
1018
1025
 
1019
- const randomBytes = pkg;
1026
+ const MAX_BYTES = 65536;
1027
+ const MAX_UINT32 = 0xffffffff;
1028
+
1029
+ function getGlobalScope() {
1030
+ if (typeof globalThis === 'object') return globalThis;
1031
+ if (typeof self === 'object') return self;
1032
+ if (typeof window === 'object') return window;
1033
+ if (typeof global === 'object') return global;
1034
+ return {};
1035
+ }
1036
+
1037
+ function getWebCrypto() {
1038
+ const scope = getGlobalScope();
1039
+ return scope.crypto || scope.msCrypto || null;
1040
+ }
1041
+
1042
+ function getNodeRandomBytes() {
1043
+ /* c8 ignore next */
1044
+ const isNode = typeof process === 'object' && process !== null && process.versions && process.versions.node;
1045
+ if (!isNode) return null;
1046
+
1047
+ const req =
1048
+ typeof module !== 'undefined' && module && typeof module.require === 'function'
1049
+ ? module.require.bind(module)
1050
+ : typeof module !== 'undefined' && module && typeof module.createRequire === 'function'
1051
+ ? module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('dilithium5.js', document.baseURI).href)))
1052
+ : typeof require === 'function'
1053
+ ? require
1054
+ : null;
1055
+ if (!req) return null;
1056
+
1057
+ try {
1058
+ const nodeCrypto = req('crypto');
1059
+ if (nodeCrypto && typeof nodeCrypto.randomBytes === 'function') {
1060
+ return nodeCrypto.randomBytes;
1061
+ }
1062
+ } catch {
1063
+ return null;
1064
+ }
1065
+
1066
+ return null;
1067
+ }
1068
+
1069
+ function randomBytes(size) {
1070
+ if (!Number.isSafeInteger(size) || size < 0) {
1071
+ throw new RangeError('size must be a non-negative integer');
1072
+ }
1073
+ if (size > MAX_UINT32) {
1074
+ throw new RangeError('requested too many random bytes');
1075
+ }
1076
+ if (size === 0) return new Uint8Array(0);
1077
+
1078
+ const cryptoObj = getWebCrypto();
1079
+ if (cryptoObj && typeof cryptoObj.getRandomValues === 'function') {
1080
+ const out = new Uint8Array(size);
1081
+ for (let i = 0; i < size; i += MAX_BYTES) {
1082
+ cryptoObj.getRandomValues(out.subarray(i, Math.min(size, i + MAX_BYTES)));
1083
+ }
1084
+ return out;
1085
+ }
1086
+
1087
+ const nodeRandomBytes = getNodeRandomBytes();
1088
+ if (nodeRandomBytes) {
1089
+ return nodeRandomBytes(size);
1090
+ }
1091
+
1092
+ throw new Error('Secure random number generation is not supported by this environment');
1093
+ }
1020
1094
 
1021
1095
  /**
1022
- * Convert hex string to Uint8Array
1023
- * @param {string} hex - Hex-encoded string
1024
- * @returns {Uint8Array} Decoded bytes
1096
+ * Convert hex string to Uint8Array with strict validation.
1097
+ *
1098
+ * NOTE: This function accepts multiple hex formats (with/without 0x prefix,
1099
+ * leading/trailing whitespace). While user-friendly, this flexibility could
1100
+ * mask input errors. Applications requiring strict format validation should
1101
+ * validate hex format before calling cryptographic functions, e.g.:
1102
+ * - Reject strings with 0x prefix if raw hex is expected
1103
+ * - Reject strings with whitespace
1104
+ * - Enforce consistent casing (lowercase/uppercase)
1105
+ *
1106
+ * @param {string} hex - Hex string (optional 0x prefix, even length).
1107
+ * @returns {Uint8Array} Decoded bytes.
1025
1108
  * @private
1026
1109
  */
1027
1110
  function hexToBytes(hex) {
1028
- const len = hex.length / 2;
1029
- const result = new Uint8Array(len);
1030
- for (let i = 0; i < len; i++) {
1031
- result[i] = parseInt(hex.substr(i * 2, 2), 16);
1111
+ /* c8 ignore start */
1112
+ if (typeof hex !== 'string') {
1113
+ throw new Error('message must be a hex string');
1114
+ }
1115
+ /* c8 ignore stop */
1116
+ let clean = hex.trim();
1117
+ // Accepts both "0x..." and raw hex formats for convenience
1118
+ if (clean.startsWith('0x') || clean.startsWith('0X')) {
1119
+ clean = clean.slice(2);
1120
+ }
1121
+ if (clean.length % 2 !== 0) {
1122
+ throw new Error('hex string must have an even length');
1123
+ }
1124
+ if (!/^[0-9a-fA-F]*$/.test(clean)) {
1125
+ throw new Error('hex string contains non-hex characters');
1126
+ }
1127
+ return utils_js.hexToBytes(clean);
1128
+ }
1129
+
1130
+ function messageToBytes(message) {
1131
+ if (typeof message === 'string') {
1132
+ return hexToBytes(message);
1133
+ }
1134
+ if (message instanceof Uint8Array) {
1135
+ return message;
1032
1136
  }
1033
- return result;
1137
+ throw new Error('message must be Uint8Array or hex string');
1034
1138
  }
1035
1139
 
1036
1140
  /**
@@ -1122,7 +1226,7 @@ function cryptoSignKeypair(passedSeed, pk, sk) {
1122
1226
  * Uses the Dilithium-5 (Round 3) signing algorithm with rejection sampling.
1123
1227
  *
1124
1228
  * @param {Uint8Array} sig - Output buffer for signature (must be at least CryptoBytes = 4595 bytes)
1125
- * @param {string|Uint8Array} m - Message to sign (hex string or Uint8Array)
1229
+ * @param {string|Uint8Array} m - Message to sign (hex string, optional 0x prefix, or Uint8Array)
1126
1230
  * @param {Uint8Array} sk - Secret key (must be CryptoSecretKeyBytes = 4896 bytes)
1127
1231
  * @param {boolean} randomizedSigning - If true, use random nonce for hedged signing.
1128
1232
  * If false, use deterministic nonce derived from message and key.
@@ -1134,10 +1238,15 @@ function cryptoSignKeypair(passedSeed, pk, sk) {
1134
1238
  * cryptoSignSignature(sig, message, sk, false);
1135
1239
  */
1136
1240
  function cryptoSignSignature(sig, m, sk, randomizedSigning) {
1241
+ if (!sig || sig.length < CryptoBytes) {
1242
+ throw new Error(`sig must be at least ${CryptoBytes} bytes`);
1243
+ }
1137
1244
  if (sk.length !== CryptoSecretKeyBytes) {
1138
1245
  throw new Error(`invalid sk length ${sk.length} | Expected length ${CryptoSecretKeyBytes}`);
1139
1246
  }
1140
1247
 
1248
+ const mBytes = messageToBytes(m);
1249
+
1141
1250
  const rho = new Uint8Array(SeedBytes);
1142
1251
  const tr = new Uint8Array(TRBytes);
1143
1252
  const key = new Uint8Array(SeedBytes);
@@ -1158,8 +1267,6 @@ function cryptoSignSignature(sig, m, sk, randomizedSigning) {
1158
1267
 
1159
1268
  unpackSk(rho, tr, key, t0, s1, s2, sk);
1160
1269
 
1161
- // Convert hex message to bytes
1162
- const mBytes = typeof m === 'string' ? hexToBytes(m) : m;
1163
1270
  const mu = sha3_js.shake256.create({}).update(tr).update(mBytes).xof(CRHBytes);
1164
1271
 
1165
1272
  if (randomizedSigning) {
@@ -1217,15 +1324,19 @@ function cryptoSignSignature(sig, m, sk, randomizedSigning) {
1217
1324
  polyVecKPointWisePolyMontgomery(h, cp, t0);
1218
1325
  polyVecKInvNTTToMont(h);
1219
1326
  polyVecKReduce(h);
1327
+ /* c8 ignore start */
1220
1328
  if (polyVecKChkNorm(h, GAMMA2) !== 0) {
1221
1329
  continue;
1222
1330
  }
1331
+ /* c8 ignore stop */
1223
1332
 
1224
1333
  polyVecKAdd(w0, w0, h);
1225
1334
  const n = polyVecKMakeHint(h, w0, w1);
1335
+ /* c8 ignore start */
1226
1336
  if (n > OMEGA) {
1227
1337
  continue;
1228
1338
  }
1339
+ /* c8 ignore stop */
1229
1340
 
1230
1341
  packSig(sig, sig, z, h);
1231
1342
  return 0;
@@ -1238,7 +1349,7 @@ function cryptoSignSignature(sig, m, sk, randomizedSigning) {
1238
1349
  * This is the combined sign operation that produces a "signed message" containing
1239
1350
  * both the signature and the original message (signature || message).
1240
1351
  *
1241
- * @param {Uint8Array} msg - Message to sign
1352
+ * @param {string|Uint8Array} msg - Message to sign (hex string, optional 0x prefix, or Uint8Array)
1242
1353
  * @param {Uint8Array} sk - Secret key (must be CryptoSecretKeyBytes = 4896 bytes)
1243
1354
  * @param {boolean} randomizedSigning - If true, use random nonce; if false, deterministic
1244
1355
  * @returns {Uint8Array} Signed message (CryptoBytes + msg.length bytes)
@@ -1249,16 +1360,20 @@ function cryptoSignSignature(sig, m, sk, randomizedSigning) {
1249
1360
  * // signedMsg contains: signature (4595 bytes) || message
1250
1361
  */
1251
1362
  function cryptoSign(msg, sk, randomizedSigning) {
1252
- const sm = new Uint8Array(CryptoBytes + msg.length);
1253
- const mLen = msg.length;
1363
+ const msgBytes = messageToBytes(msg);
1364
+
1365
+ const sm = new Uint8Array(CryptoBytes + msgBytes.length);
1366
+ const mLen = msgBytes.length;
1254
1367
  for (let i = 0; i < mLen; ++i) {
1255
- sm[CryptoBytes + mLen - 1 - i] = msg[mLen - 1 - i];
1368
+ sm[CryptoBytes + mLen - 1 - i] = msgBytes[mLen - 1 - i];
1256
1369
  }
1257
- const result = cryptoSignSignature(sm, msg, sk, randomizedSigning);
1370
+ const result = cryptoSignSignature(sm, msgBytes, sk, randomizedSigning);
1258
1371
 
1372
+ /* c8 ignore start */
1259
1373
  if (result !== 0) {
1260
1374
  throw new Error('failed to sign');
1261
1375
  }
1376
+ /* c8 ignore stop */
1262
1377
  return sm;
1263
1378
  }
1264
1379
 
@@ -1268,7 +1383,7 @@ function cryptoSign(msg, sk, randomizedSigning) {
1268
1383
  * Performs constant-time verification to prevent timing side-channel attacks.
1269
1384
  *
1270
1385
  * @param {Uint8Array} sig - Signature to verify (must be CryptoBytes = 4595 bytes)
1271
- * @param {string|Uint8Array} m - Message that was signed (hex string or Uint8Array)
1386
+ * @param {string|Uint8Array} m - Message that was signed (hex string, optional 0x prefix, or Uint8Array)
1272
1387
  * @param {Uint8Array} pk - Public key (must be CryptoPublicKeyBytes = 2592 bytes)
1273
1388
  * @returns {boolean} true if signature is valid, false otherwise
1274
1389
  *
@@ -1311,8 +1426,12 @@ function cryptoSignVerify(sig, m, pk) {
1311
1426
  const tr = sha3_js.shake256.create({}).update(pk).xof(TRBytes);
1312
1427
  mu.set(tr);
1313
1428
 
1314
- // Convert hex message to bytes
1315
- const mBytes = typeof m === 'string' ? hexToBytes(m) : m;
1429
+ let mBytes;
1430
+ try {
1431
+ mBytes = messageToBytes(m);
1432
+ } catch {
1433
+ return false;
1434
+ }
1316
1435
  const muFull = sha3_js.shake256.create({}).update(mu.slice(0, TRBytes)).update(mBytes).xof(CRHBytes);
1317
1436
  mu.set(muFull);
1318
1437
 
@@ -1,5 +1,5 @@
1
1
  import { shake128, shake256 } from '@noble/hashes/sha3.js';
2
- import pkg from 'randombytes';
2
+ import { hexToBytes as hexToBytes$1 } from '@noble/hashes/utils.js';
3
3
 
4
4
  const Shake128Rate = 168;
5
5
  const Shake256Rate = 136;
@@ -365,6 +365,8 @@ function polyUniform(a, seed, nonce) {
365
365
 
366
366
  let ctr = rejUniform(a.coeffs, 0, N, buf, bufLen);
367
367
 
368
+ // Note: With current parameters, needing extra blocks is vanishingly unlikely.
369
+ /* c8 ignore start */
368
370
  while (ctr < N) {
369
371
  off = bufLen % 3;
370
372
  for (let i = 0; i < off; ++i) buf[i] = buf[bufLen - off + i];
@@ -373,6 +375,7 @@ function polyUniform(a, seed, nonce) {
373
375
  bufLen = Stream128BlockBytes + off;
374
376
  ctr += rejUniform(a.coeffs, ctr, N - ctr, buf, bufLen);
375
377
  }
378
+ /* c8 ignore stop */
376
379
  }
377
380
 
378
381
  function rejEta(aP, aOffset, len, buf, bufLen) {
@@ -466,10 +469,13 @@ function polyChallenge(cP, seed) {
466
469
  }
467
470
  for (let i = N - TAU; i < N; ++i) {
468
471
  do {
472
+ // Note: Re-squeezing here is extremely unlikely with TAU=60.
473
+ /* c8 ignore start */
469
474
  if (pos >= Shake256Rate) {
470
475
  shake256SqueezeBlocks(buf, 0, 1, state);
471
476
  pos = 0;
472
477
  }
478
+ /* c8 ignore stop */
473
479
 
474
480
  b = buf[pos++];
475
481
  } while (b > i);
@@ -1014,21 +1020,118 @@ function unpackSig(cP, z, hP, sig) {
1014
1020
  return 0;
1015
1021
  }
1016
1022
 
1017
- const randomBytes = pkg;
1023
+ const MAX_BYTES = 65536;
1024
+ const MAX_UINT32 = 0xffffffff;
1025
+
1026
+ function getGlobalScope() {
1027
+ if (typeof globalThis === 'object') return globalThis;
1028
+ if (typeof self === 'object') return self;
1029
+ if (typeof window === 'object') return window;
1030
+ if (typeof global === 'object') return global;
1031
+ return {};
1032
+ }
1033
+
1034
+ function getWebCrypto() {
1035
+ const scope = getGlobalScope();
1036
+ return scope.crypto || scope.msCrypto || null;
1037
+ }
1038
+
1039
+ function getNodeRandomBytes() {
1040
+ /* c8 ignore next */
1041
+ const isNode = typeof process === 'object' && process !== null && process.versions && process.versions.node;
1042
+ if (!isNode) return null;
1043
+
1044
+ const req =
1045
+ typeof module !== 'undefined' && module && typeof module.require === 'function'
1046
+ ? module.require.bind(module)
1047
+ : typeof module !== 'undefined' && module && typeof module.createRequire === 'function'
1048
+ ? module.createRequire(import.meta.url)
1049
+ : typeof require === 'function'
1050
+ ? require
1051
+ : null;
1052
+ if (!req) return null;
1053
+
1054
+ try {
1055
+ const nodeCrypto = req('crypto');
1056
+ if (nodeCrypto && typeof nodeCrypto.randomBytes === 'function') {
1057
+ return nodeCrypto.randomBytes;
1058
+ }
1059
+ } catch {
1060
+ return null;
1061
+ }
1062
+
1063
+ return null;
1064
+ }
1065
+
1066
+ function randomBytes(size) {
1067
+ if (!Number.isSafeInteger(size) || size < 0) {
1068
+ throw new RangeError('size must be a non-negative integer');
1069
+ }
1070
+ if (size > MAX_UINT32) {
1071
+ throw new RangeError('requested too many random bytes');
1072
+ }
1073
+ if (size === 0) return new Uint8Array(0);
1074
+
1075
+ const cryptoObj = getWebCrypto();
1076
+ if (cryptoObj && typeof cryptoObj.getRandomValues === 'function') {
1077
+ const out = new Uint8Array(size);
1078
+ for (let i = 0; i < size; i += MAX_BYTES) {
1079
+ cryptoObj.getRandomValues(out.subarray(i, Math.min(size, i + MAX_BYTES)));
1080
+ }
1081
+ return out;
1082
+ }
1083
+
1084
+ const nodeRandomBytes = getNodeRandomBytes();
1085
+ if (nodeRandomBytes) {
1086
+ return nodeRandomBytes(size);
1087
+ }
1088
+
1089
+ throw new Error('Secure random number generation is not supported by this environment');
1090
+ }
1018
1091
 
1019
1092
  /**
1020
- * Convert hex string to Uint8Array
1021
- * @param {string} hex - Hex-encoded string
1022
- * @returns {Uint8Array} Decoded bytes
1093
+ * Convert hex string to Uint8Array with strict validation.
1094
+ *
1095
+ * NOTE: This function accepts multiple hex formats (with/without 0x prefix,
1096
+ * leading/trailing whitespace). While user-friendly, this flexibility could
1097
+ * mask input errors. Applications requiring strict format validation should
1098
+ * validate hex format before calling cryptographic functions, e.g.:
1099
+ * - Reject strings with 0x prefix if raw hex is expected
1100
+ * - Reject strings with whitespace
1101
+ * - Enforce consistent casing (lowercase/uppercase)
1102
+ *
1103
+ * @param {string} hex - Hex string (optional 0x prefix, even length).
1104
+ * @returns {Uint8Array} Decoded bytes.
1023
1105
  * @private
1024
1106
  */
1025
1107
  function hexToBytes(hex) {
1026
- const len = hex.length / 2;
1027
- const result = new Uint8Array(len);
1028
- for (let i = 0; i < len; i++) {
1029
- result[i] = parseInt(hex.substr(i * 2, 2), 16);
1108
+ /* c8 ignore start */
1109
+ if (typeof hex !== 'string') {
1110
+ throw new Error('message must be a hex string');
1111
+ }
1112
+ /* c8 ignore stop */
1113
+ let clean = hex.trim();
1114
+ // Accepts both "0x..." and raw hex formats for convenience
1115
+ if (clean.startsWith('0x') || clean.startsWith('0X')) {
1116
+ clean = clean.slice(2);
1117
+ }
1118
+ if (clean.length % 2 !== 0) {
1119
+ throw new Error('hex string must have an even length');
1120
+ }
1121
+ if (!/^[0-9a-fA-F]*$/.test(clean)) {
1122
+ throw new Error('hex string contains non-hex characters');
1123
+ }
1124
+ return hexToBytes$1(clean);
1125
+ }
1126
+
1127
+ function messageToBytes(message) {
1128
+ if (typeof message === 'string') {
1129
+ return hexToBytes(message);
1130
+ }
1131
+ if (message instanceof Uint8Array) {
1132
+ return message;
1030
1133
  }
1031
- return result;
1134
+ throw new Error('message must be Uint8Array or hex string');
1032
1135
  }
1033
1136
 
1034
1137
  /**
@@ -1120,7 +1223,7 @@ function cryptoSignKeypair(passedSeed, pk, sk) {
1120
1223
  * Uses the Dilithium-5 (Round 3) signing algorithm with rejection sampling.
1121
1224
  *
1122
1225
  * @param {Uint8Array} sig - Output buffer for signature (must be at least CryptoBytes = 4595 bytes)
1123
- * @param {string|Uint8Array} m - Message to sign (hex string or Uint8Array)
1226
+ * @param {string|Uint8Array} m - Message to sign (hex string, optional 0x prefix, or Uint8Array)
1124
1227
  * @param {Uint8Array} sk - Secret key (must be CryptoSecretKeyBytes = 4896 bytes)
1125
1228
  * @param {boolean} randomizedSigning - If true, use random nonce for hedged signing.
1126
1229
  * If false, use deterministic nonce derived from message and key.
@@ -1132,10 +1235,15 @@ function cryptoSignKeypair(passedSeed, pk, sk) {
1132
1235
  * cryptoSignSignature(sig, message, sk, false);
1133
1236
  */
1134
1237
  function cryptoSignSignature(sig, m, sk, randomizedSigning) {
1238
+ if (!sig || sig.length < CryptoBytes) {
1239
+ throw new Error(`sig must be at least ${CryptoBytes} bytes`);
1240
+ }
1135
1241
  if (sk.length !== CryptoSecretKeyBytes) {
1136
1242
  throw new Error(`invalid sk length ${sk.length} | Expected length ${CryptoSecretKeyBytes}`);
1137
1243
  }
1138
1244
 
1245
+ const mBytes = messageToBytes(m);
1246
+
1139
1247
  const rho = new Uint8Array(SeedBytes);
1140
1248
  const tr = new Uint8Array(TRBytes);
1141
1249
  const key = new Uint8Array(SeedBytes);
@@ -1156,8 +1264,6 @@ function cryptoSignSignature(sig, m, sk, randomizedSigning) {
1156
1264
 
1157
1265
  unpackSk(rho, tr, key, t0, s1, s2, sk);
1158
1266
 
1159
- // Convert hex message to bytes
1160
- const mBytes = typeof m === 'string' ? hexToBytes(m) : m;
1161
1267
  const mu = shake256.create({}).update(tr).update(mBytes).xof(CRHBytes);
1162
1268
 
1163
1269
  if (randomizedSigning) {
@@ -1215,15 +1321,19 @@ function cryptoSignSignature(sig, m, sk, randomizedSigning) {
1215
1321
  polyVecKPointWisePolyMontgomery(h, cp, t0);
1216
1322
  polyVecKInvNTTToMont(h);
1217
1323
  polyVecKReduce(h);
1324
+ /* c8 ignore start */
1218
1325
  if (polyVecKChkNorm(h, GAMMA2) !== 0) {
1219
1326
  continue;
1220
1327
  }
1328
+ /* c8 ignore stop */
1221
1329
 
1222
1330
  polyVecKAdd(w0, w0, h);
1223
1331
  const n = polyVecKMakeHint(h, w0, w1);
1332
+ /* c8 ignore start */
1224
1333
  if (n > OMEGA) {
1225
1334
  continue;
1226
1335
  }
1336
+ /* c8 ignore stop */
1227
1337
 
1228
1338
  packSig(sig, sig, z, h);
1229
1339
  return 0;
@@ -1236,7 +1346,7 @@ function cryptoSignSignature(sig, m, sk, randomizedSigning) {
1236
1346
  * This is the combined sign operation that produces a "signed message" containing
1237
1347
  * both the signature and the original message (signature || message).
1238
1348
  *
1239
- * @param {Uint8Array} msg - Message to sign
1349
+ * @param {string|Uint8Array} msg - Message to sign (hex string, optional 0x prefix, or Uint8Array)
1240
1350
  * @param {Uint8Array} sk - Secret key (must be CryptoSecretKeyBytes = 4896 bytes)
1241
1351
  * @param {boolean} randomizedSigning - If true, use random nonce; if false, deterministic
1242
1352
  * @returns {Uint8Array} Signed message (CryptoBytes + msg.length bytes)
@@ -1247,16 +1357,20 @@ function cryptoSignSignature(sig, m, sk, randomizedSigning) {
1247
1357
  * // signedMsg contains: signature (4595 bytes) || message
1248
1358
  */
1249
1359
  function cryptoSign(msg, sk, randomizedSigning) {
1250
- const sm = new Uint8Array(CryptoBytes + msg.length);
1251
- const mLen = msg.length;
1360
+ const msgBytes = messageToBytes(msg);
1361
+
1362
+ const sm = new Uint8Array(CryptoBytes + msgBytes.length);
1363
+ const mLen = msgBytes.length;
1252
1364
  for (let i = 0; i < mLen; ++i) {
1253
- sm[CryptoBytes + mLen - 1 - i] = msg[mLen - 1 - i];
1365
+ sm[CryptoBytes + mLen - 1 - i] = msgBytes[mLen - 1 - i];
1254
1366
  }
1255
- const result = cryptoSignSignature(sm, msg, sk, randomizedSigning);
1367
+ const result = cryptoSignSignature(sm, msgBytes, sk, randomizedSigning);
1256
1368
 
1369
+ /* c8 ignore start */
1257
1370
  if (result !== 0) {
1258
1371
  throw new Error('failed to sign');
1259
1372
  }
1373
+ /* c8 ignore stop */
1260
1374
  return sm;
1261
1375
  }
1262
1376
 
@@ -1266,7 +1380,7 @@ function cryptoSign(msg, sk, randomizedSigning) {
1266
1380
  * Performs constant-time verification to prevent timing side-channel attacks.
1267
1381
  *
1268
1382
  * @param {Uint8Array} sig - Signature to verify (must be CryptoBytes = 4595 bytes)
1269
- * @param {string|Uint8Array} m - Message that was signed (hex string or Uint8Array)
1383
+ * @param {string|Uint8Array} m - Message that was signed (hex string, optional 0x prefix, or Uint8Array)
1270
1384
  * @param {Uint8Array} pk - Public key (must be CryptoPublicKeyBytes = 2592 bytes)
1271
1385
  * @returns {boolean} true if signature is valid, false otherwise
1272
1386
  *
@@ -1309,8 +1423,12 @@ function cryptoSignVerify(sig, m, pk) {
1309
1423
  const tr = shake256.create({}).update(pk).xof(TRBytes);
1310
1424
  mu.set(tr);
1311
1425
 
1312
- // Convert hex message to bytes
1313
- const mBytes = typeof m === 'string' ? hexToBytes(m) : m;
1426
+ let mBytes;
1427
+ try {
1428
+ mBytes = messageToBytes(m);
1429
+ } catch {
1430
+ return false;
1431
+ }
1314
1432
  const muFull = shake256.create({}).update(mu.slice(0, TRBytes)).update(mBytes).xof(CRHBytes);
1315
1433
  mu.set(muFull);
1316
1434
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theqrl/dilithium5",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Dilithium-5 cryptography",
5
5
  "keywords": [
6
6
  "dilithium",
@@ -31,10 +31,12 @@
31
31
  "url": "git+https://github.com/theQRL/qrypto.js.git"
32
32
  },
33
33
  "scripts": {
34
- "test": "../../node_modules/mocha/bin/mocha.js --timeout 10000",
34
+ "test": "../../node_modules/mocha/bin/mocha.js --require ../../scripts/node-test-setup.cjs --timeout 10000",
35
+ "test:browser": "playwright test",
35
36
  "build": "rollup src/index.js --file ./dist/cjs/dilithium5.js --format cjs && rollup src/index.js --file ./dist/mjs/dilithium5.js --format esm && ./fixup",
36
37
  "lint-check": "eslint 'src/**/*.js' 'test/**/*.js'",
37
38
  "lint": "eslint --fix 'src/**/*.js' 'test/**/*.js'",
39
+ "coverage": "c8 npm run test",
38
40
  "report-coverage": "c8 --reporter=text-lcov npm run test > coverage.lcov"
39
41
  },
40
42
  "bugs": {
@@ -47,6 +49,9 @@
47
49
  }
48
50
  },
49
51
  "type": "module",
52
+ "engines": {
53
+ "node": ">=18.20.0"
54
+ },
50
55
  "devDependencies": {
51
56
  "@eslint/js": "^9.39.0",
52
57
  "c8": "^10.1.3",
@@ -55,13 +60,12 @@
55
60
  "eslint-config-prettier": "^10.1.8",
56
61
  "eslint-plugin-import-x": "^4.15.0",
57
62
  "eslint-plugin-prettier": "^5.5.4",
58
- "globals": "^16.2.0",
63
+ "globals": "^17.0.0",
59
64
  "mocha": "^11.7.5",
60
65
  "prettier": "^3.7.4",
61
66
  "rollup": "^4.55.1"
62
67
  },
63
68
  "dependencies": {
64
- "@noble/hashes": "^2.0.1",
65
- "randombytes": "^2.1.0"
69
+ "@noble/hashes": "^2.0.1"
66
70
  }
67
71
  }
package/src/index.d.ts CHANGED
@@ -56,7 +56,7 @@ export function cryptoSignKeypair(
56
56
  /**
57
57
  * Create a signature for a message
58
58
  * @param sig - Output buffer for signature (must be CryptoBytes length minimum)
59
- * @param m - Message to sign (hex-encoded string)
59
+ * @param m - Message to sign (hex string or Uint8Array; strings are parsed as hex only)
60
60
  * @param sk - Secret key
61
61
  * @param randomizedSigning - If true, use random nonce; if false, deterministic
62
62
  * @returns 0 on success
@@ -64,7 +64,7 @@ export function cryptoSignKeypair(
64
64
  */
65
65
  export function cryptoSignSignature(
66
66
  sig: Uint8Array,
67
- m: string,
67
+ m: Uint8Array | string,
68
68
  sk: Uint8Array,
69
69
  randomizedSigning: boolean
70
70
  ): number;
@@ -78,7 +78,7 @@ export function cryptoSignSignature(
78
78
  * @throws Error if signing fails
79
79
  */
80
80
  export function cryptoSign(
81
- msg: Uint8Array,
81
+ msg: Uint8Array | string,
82
82
  sk: Uint8Array,
83
83
  randomizedSigning: boolean
84
84
  ): Uint8Array;
@@ -86,13 +86,13 @@ export function cryptoSign(
86
86
  /**
87
87
  * Verify a signature
88
88
  * @param sig - Signature to verify
89
- * @param m - Message that was signed (hex-encoded string)
89
+ * @param m - Message that was signed (hex string or Uint8Array; strings are parsed as hex only)
90
90
  * @param pk - Public key
91
91
  * @returns true if signature is valid, false otherwise
92
92
  */
93
93
  export function cryptoSignVerify(
94
94
  sig: Uint8Array,
95
- m: string,
95
+ m: Uint8Array | string,
96
96
  pk: Uint8Array
97
97
  ): boolean;
98
98