Technical explainer

How CWaptcha works

CWaptcha is invisible to users because the protection happens entirely at the cryptographic layer — not the UI layer. Here's what happens on every form submission.

1

Server issues a nonce

When your page loads, cwaptcha.js automatically calls GET /cwaptcha/issue. Your server responds with a JSON payload containing four values:

{
  "captchaId":  "a1b2c3...",   // unique ID for this session
  "nonce":      "d4e5f6...",   // 32-byte random hex
  "token":      "g7h8i9...",   // HMAC of (captchaId|nonce|expiry|userAgent)
  "fieldSalt":  "j0k1l2..."    // per-session key for client-side HMAC
}

The server stores captchaId → { nonce, fieldSalt, expiry, used: false } with a configurable TTL (default 5 minutes). The fieldSalt is derived from the master SecretKey and is never the key itself.

2

Browser canonicalises and signs

When the user submits the form, cwaptcha.js intercepts the submit event and:

  1. Snapshots all form field values (excluding the honeypot field)
  2. Sorts them alphabetically by field name (ordinal comparison)
  3. Produces a canonical string: name=value\n per field, then appends the nonce and fieldSalt
  4. Computes HMAC-SHA256(fieldSalt, canonicalString) using the browser's SubtleCrypto API
  5. Injects captchaId, token, and userCalculatedCWaptchaHash as hidden fields
⚡ The master SecretKey never reaches the browser. The client only sees the per-session fieldSalt.
3

Server validates — six checks

The middleware (or [CWaptchaValidation] attribute) runs these checks in order before your handler is called:

1.
Honeypot empty
A hidden field named cw_hp_email is injected by the JS. Bots that fill all fields fail immediately.
2.
Nonce exists and is unused
The server looks up captchaId in the nonce store. If it's missing, expired, or already redeemed, the request is rejected.
3.
Expiry not passed
The stored expiry is checked against the current UTC time. Stale tokens from cached pages are rejected.
4.
Token signature valid
The server recomputes HMAC(secret, captchaId|nonce|expiry|userAgent) and compares with constant-time equality. Tampered tokens fail.
5.
Field hash matches
The server recomputes the canonical field HMAC and compares with userCalculatedCWaptchaHash using constant-time equality. Altered field values fail.
6.
Nonce marked used
On success the nonce is atomically marked used (or deleted). Replay attacks with the same token always fail.

Failure reasons

Reason Meaning
expiredNonce not found, TTL passed, or already redeemed
replayedToken submitted a second time
hash_mismatchField HMAC doesn't match — form was tampered
honeypotHidden field was filled — likely a bot
tamperedToken signature invalid — token was modified
https_requiredRequest is not HTTPS and RequireHttps: true