Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
90 changes: 63 additions & 27 deletions dist/cjs/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)) {
Expand All @@ -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) {
Expand All @@ -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');
}
Expand All @@ -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);
Expand All @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -1665,18 +1688,28 @@ 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);
* const ctx = new Uint8Array([0x01, 0x02]);
* 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');
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1837,16 +1872,17 @@ 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.
*
* @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);
Expand All @@ -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;
}

Expand Down
Loading
Loading