diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ccd727e..1d92b62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,25 @@ jobs: - run: npm ci - run: npm test + dist-check: + name: Verify dist/ is up to date + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Use Node.js 22.x + uses: actions/setup-node@v6 + with: + node-version: '22.x' + - run: npm ci + - run: npm run build + - name: Check for uncommitted dist changes + run: | + if [ -n "$(git diff --name-only dist/)" ]; then + echo "::error::dist/ files are out of date. Run 'npm run build' and commit the result." + git diff --stat dist/ + exit 1 + fi + browser-tests: name: Browser Tests (Playwright) runs-on: ubuntu-latest diff --git a/dist/cjs/wallet.js b/dist/cjs/wallet.js index dcbc864..1893dc4 100644 --- a/dist/cjs/wallet.js +++ b/dist/cjs/wallet.js @@ -582,12 +582,16 @@ function montgomeryReduce(a) { return t; } +// Partial reduction modulo Q. Input must satisfy |a| < 2^31 - 2^22. +// Output is in (-Q, Q). Mirrors the reference C implementation. function reduce32(a) { let t = (a + (1 << 22)) >> 23; t = a - t * Q; return t; } +// Conditional add Q: if a is negative, add Q. Input must satisfy -Q < a < 2^31. +// Output is in [0, Q). Mirrors the reference C implementation. function cAddQ(a) { let ar = a; ar += (ar >> 31) & Q; @@ -749,10 +753,7 @@ function polyChkNorm(a, b) { } for (let i = 0; i < N; i++) { - let t = a.coeffs[i] >> 31; - t = a.coeffs[i] - (t & (2 * a.coeffs[i])); - - if (t >= b) { + if (Math.abs(a.coeffs[i]) >= b) { return 1; } } @@ -1293,6 +1294,9 @@ function packPk(pkp, rho, t1) { } function unpackPk(rhop, t1, pk) { + if (!(pk instanceof Uint8Array) || pk.length !== CryptoPublicKeyBytes) { + throw new Error(`pk must be a Uint8Array of ${CryptoPublicKeyBytes} bytes`); + } const rho = rhop; for (let i = 0; i < SeedBytes; ++i) { rho[i] = pk[i]; @@ -1337,6 +1341,9 @@ function packSk(skp, rho, tr, key, t0, s1, s2) { } function unpackSk(rhoP, trP, keyP, t0, s1, s2, sk) { + if (!(sk instanceof Uint8Array) || sk.length !== CryptoSecretKeyBytes) { + throw new Error(`sk must be a Uint8Array of ${CryptoSecretKeyBytes} bytes`); + } let skOffset = 0; const rho = rhoP; const tr = trP; @@ -1400,7 +1407,12 @@ function packSig(sigP, ctilde, z, h) { } } +// Returns 0 on success, 1 on failure. On failure, output buffers (c, z, h) +// may contain partial data and must not be used. function unpackSig(cP, z, hP, sig) { + if (!(sig instanceof Uint8Array) || sig.length !== CryptoBytes) { + throw new Error(`sig must be a Uint8Array of ${CryptoBytes} bytes`); + } let sigOffset = 0; const c = cP; // ctilde const h = hP; @@ -1495,6 +1507,8 @@ function randomBytes$1(size) { * * @param {Uint8Array} buffer - The buffer to zero * @returns {void} + * @throws {TypeError} If buffer is not a Uint8Array + * @throws {Error} If zeroization verification fails */ function zeroize(buffer) { if (!(buffer instanceof Uint8Array)) { @@ -1514,16 +1528,12 @@ function zeroize(buffer) { /** * Convert hex string to Uint8Array with strict validation. * - * NOTE: This function accepts multiple hex formats (with/without 0x prefix, - * leading/trailing whitespace). While user-friendly, this flexibility could - * mask input errors. Applications requiring strict format validation should - * validate hex format before calling cryptographic functions, e.g.: - * - Reject strings with 0x prefix if raw hex is expected - * - Reject strings with whitespace - * - Enforce consistent casing (lowercase/uppercase) + * Accepts an optional 0x/0X prefix. Leading/trailing whitespace is rejected. + * Empty strings and whitespace-only strings are rejected. * - * @param {string} hex - Hex string (optional 0x prefix, even length). + * @param {string} hex - Hex string (optional 0x prefix, even length, no whitespace). * @returns {Uint8Array} Decoded bytes. + * @throws {Error} If input is not a valid hex string * @private */ function hexToBytes(hex) { @@ -1532,11 +1542,16 @@ function hexToBytes(hex) { throw new Error('message must be a hex string'); } /* c8 ignore stop */ - let clean = hex.trim(); - // Accepts both "0x..." and raw hex formats for convenience + if (hex !== hex.trim()) { + throw new Error('hex string must not have leading or trailing whitespace'); + } + let clean = hex; if (clean.startsWith('0x') || clean.startsWith('0X')) { clean = clean.slice(2); } + if (clean.length === 0) { + throw new Error('hex string must not be empty'); + } if (clean.length % 2 !== 0) { throw new Error('hex string must have an even length'); } @@ -1546,6 +1561,14 @@ function hexToBytes(hex) { return hexToBytes$1(clean); } +/** + * Convert a message to Uint8Array. + * + * @param {string|Uint8Array} message - Message as hex string (optional 0x prefix) or Uint8Array. + * @returns {Uint8Array} Message bytes. + * @throws {Error} If message is not a Uint8Array or valid hex string + * @private + */ function messageToBytes(message) { if (typeof message === 'string') { return hexToBytes(message); @@ -1562,8 +1585,8 @@ function messageToBytes(message) { * Key generation follows FIPS 204, using domain separator [K, L] during * seed expansion to ensure algorithm binding. * - * @param {Uint8Array|null} passedSeed - Optional 32-byte seed for deterministic key generation. - * Pass null for random key generation. + * @param {Uint8Array|null} [passedSeed=null] - Optional 32-byte seed for deterministic key generation. + * Pass null or undefined for random key generation. * @param {Uint8Array} pk - Output buffer for public key (must be CryptoPublicKeyBytes = 2592 bytes) * @param {Uint8Array} sk - Output buffer for secret key (must be CryptoSecretKeyBytes = 4896 bytes) * @returns {Uint8Array} The seed used for key generation (useful when passedSeed is null) @@ -1655,7 +1678,7 @@ function cryptoSignKeypair(passedSeed, pk, sk) { } /** - * Create a detached signature for a message with optional context. + * Create a detached signature for a message with context. * * Uses the ML-DSA-87 (FIPS 204) signing algorithm with rejection sampling. * The context parameter provides domain separation as required by FIPS 204. @@ -1665,9 +1688,16 @@ function cryptoSignKeypair(passedSeed, pk, sk) { * @param {Uint8Array} sk - Secret key (must be CryptoSecretKeyBytes = 4896 bytes) * @param {boolean} randomizedSigning - If true, use random nonce for hedged signing. * If false, use deterministic nonce derived from message and key. - * @param {Uint8Array} ctx - Context string for domain separation (max 255 bytes). + * @param {Uint8Array} ctx - Context string for domain separation (required, max 255 bytes). + * Pass an empty Uint8Array for no context. * @returns {number} 0 on success - * @throws {Error} If ctx is missing, sk is wrong size, or context exceeds 255 bytes + * @throws {TypeError} If sig is not a Uint8Array or is smaller than CryptoBytes + * @throws {TypeError} If sk is not a Uint8Array + * @throws {TypeError} If ctx is not a Uint8Array + * @throws {TypeError} If randomizedSigning is not a boolean + * @throws {Error} If ctx exceeds 255 bytes + * @throws {Error} If sk length does not equal CryptoSecretKeyBytes + * @throws {Error} If message is not a Uint8Array or valid hex string * * @example * const sig = new Uint8Array(CryptoBytes); @@ -1675,8 +1705,11 @@ function cryptoSignKeypair(passedSeed, pk, sk) { * cryptoSignSignature(sig, message, sk, false, ctx); */ function cryptoSignSignature(sig, m, sk, randomizedSigning, ctx) { - if (!sig || sig.length < CryptoBytes) { - throw new Error(`sig must be at least ${CryptoBytes} bytes`); + if (!(sig instanceof Uint8Array) || sig.length < CryptoBytes) { + throw new TypeError(`sig must be at least ${CryptoBytes} bytes and a Uint8Array`); + } + if (!(sk instanceof Uint8Array)) { + throw new TypeError('sk must be a Uint8Array'); } if (!(ctx instanceof Uint8Array)) { throw new TypeError('ctx is required and must be a Uint8Array'); @@ -1807,9 +1840,11 @@ function cryptoSignSignature(sig, m, sk, randomizedSigning, ctx) { * @param {string|Uint8Array} msg - Message to sign (hex string, optional 0x prefix, or Uint8Array) * @param {Uint8Array} sk - Secret key (must be CryptoSecretKeyBytes = 4896 bytes) * @param {boolean} randomizedSigning - If true, use random nonce; if false, deterministic - * @param {Uint8Array} ctx - Context string for domain separation (max 255 bytes). + * @param {Uint8Array} ctx - Context string for domain separation (required, max 255 bytes). * @returns {Uint8Array} Signed message (CryptoBytes + msg.length bytes) - * @throws {Error} If signing fails + * @throws {TypeError} If ctx is not a Uint8Array + * @throws {TypeError} If sk or randomizedSigning fail type validation (see cryptoSignSignature) + * @throws {Error} If signing fails or message/sk/ctx are invalid * * @example * const signedMsg = cryptoSign(message, sk, false, ctx); @@ -1837,7 +1872,7 @@ function cryptoSign(msg, sk, randomizedSigning, ctx) { } /** - * Verify a detached signature with optional context. + * Verify a detached signature with context. * * Performs constant-time verification to prevent timing side-channel attacks. * The context must match the one used during signing. @@ -1845,8 +1880,9 @@ function cryptoSign(msg, sk, randomizedSigning, ctx) { * @param {Uint8Array} sig - Signature to verify (must be CryptoBytes = 4627 bytes) * @param {string|Uint8Array} m - Message that was signed (hex string, optional 0x prefix, or Uint8Array) * @param {Uint8Array} pk - Public key (must be CryptoPublicKeyBytes = 2592 bytes) - * @param {Uint8Array} ctx - Context string used during signing (max 255 bytes). + * @param {Uint8Array} ctx - Context string used during signing (required, max 255 bytes). * @returns {boolean} true if signature is valid, false otherwise + * @throws {TypeError} If ctx is not a Uint8Array * * @example * const isValid = cryptoSignVerify(signature, message, pk, ctx); @@ -1872,10 +1908,10 @@ function cryptoSignVerify(sig, m, pk, ctx) { const w1 = new PolyVecK(); const h = new PolyVecK(); - if (sig.length !== CryptoBytes) { + if (!(sig instanceof Uint8Array) || sig.length !== CryptoBytes) { return false; } - if (pk.length !== CryptoPublicKeyBytes) { + if (!(pk instanceof Uint8Array) || pk.length !== CryptoPublicKeyBytes) { return false; } diff --git a/package-lock.json b/package-lock.json index 9a460f9..1fa0a29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "@theqrl/wallet.js", - "version": "1.1.2", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@theqrl/wallet.js", - "version": "1.1.2", + "version": "2.0.0", "license": "MIT", "dependencies": { "@noble/hashes": "2.0.1", - "@theqrl/mldsa87": "2.0.0" + "@theqrl/mldsa87": "2.0.1" }, "devDependencies": { "@eslint/js": "10.0.1", @@ -138,9 +138,9 @@ } }, "node_modules/@emnapi/core": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", - "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", "dev": true, "license": "MIT", "optional": true, @@ -150,9 +150,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", - "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", "dev": true, "license": "MIT", "optional": true, @@ -1694,9 +1694,9 @@ } }, "node_modules/@theqrl/mldsa87": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@theqrl/mldsa87/-/mldsa87-2.0.0.tgz", - "integrity": "sha512-j8+w9OPB9P4QzlOfVcJRN4TXZ3t522UwwBTU0H4+6f3tmNA/o4LoPseeaO7hWGZLd4mwUBIBTcz87wgGoSuOcw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@theqrl/mldsa87/-/mldsa87-2.0.1.tgz", + "integrity": "sha512-BPEvwrrphkMjyPnDorMzQdfrtICTZsiUxiBnwFkv2SfIcQwTTPQV3kTVfidZXXE7033ZCvhu1u5Aiw6TrjxPPQ==", "license": "MIT", "dependencies": { "@noble/hashes": "2.0.1" @@ -1770,9 +1770,9 @@ "license": "MIT" }, "node_modules/@typescript-eslint/types": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", - "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", "dev": true, "license": "MIT", "engines": { @@ -3717,9 +3717,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.13.6", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", - "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4345,9 +4345,9 @@ "license": "MIT" }, "node_modules/json-with-bigint": { - "version": "3.5.7", - "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.7.tgz", - "integrity": "sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw==", + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.8.tgz", + "integrity": "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==", "dev": true, "license": "MIT", "peer": true @@ -4661,9 +4661,9 @@ } }, "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -7386,9 +7386,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -7606,9 +7606,9 @@ } }, "node_modules/pure-rand": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.2.0.tgz", - "integrity": "sha512-KHnUjm68KSO/hqpWlVwagMDPrIjnDNY9r0DbKN79xEa5RU2MLUe0lICBGpWDF8cwmhUiN8r9A8DLGPVcFB62/A==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.3.0.tgz", + "integrity": "sha512-1ws1Ab8fnsf4bvpL+SujgBnr3KFs5abgCLVzavBp+f2n8Ld5YTOZlkv/ccYPhu3X9s+MEeqPRMqKlJz/kWDK8A==", "dev": true, "funding": [ { @@ -7670,9 +7670,9 @@ } }, "node_modules/read-package-up/node_modules/type-fest": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.4.tgz", - "integrity": "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.5.0.tgz", + "integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==", "dev": true, "license": "(MIT OR CC0-1.0)", "peer": true, @@ -7708,9 +7708,9 @@ } }, "node_modules/read-pkg/node_modules/type-fest": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.4.tgz", - "integrity": "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.5.0.tgz", + "integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==", "dev": true, "license": "(MIT OR CC0-1.0)", "peer": true, @@ -8878,9 +8878,9 @@ } }, "node_modules/undici": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz", - "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.5.tgz", + "integrity": "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==", "dev": true, "license": "MIT", "peer": true, diff --git a/package.json b/package.json index e1aebaf..d93cc79 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ }, "dependencies": { "@noble/hashes": "2.0.1", - "@theqrl/mldsa87": "2.0.0" + "@theqrl/mldsa87": "2.0.1" }, "directories": { "lib": "src",