auth-yes/tasks/new/2026-0827.03.gem.feat.security.client-proof-of-work-shield-1041.md

5.4 KiB
Raw Blame History

TASK METADATA

  • Target Files:
    • server/pow.ts
    • server/routes/events.ts
    • server/valkey.ts
    • ui/components/events/JoinPage.tsx
    • ui/components/events/JoinScript.tsx
    • ui/components/events/EventLandingPage.tsx
    • server/tests/pow.test.ts
  • Core Objective: Implement a transparent, sub-50ms client-side WebCrypto (crypto.subtle) SHA-256 Proof-of-Work (PoW) ingress shield to protect 6-digit workshop PIN codes and event slugs against distributed botnet brute-forcing and denial-of-service.
  • Dependencies:
    • Valkey 8 for atomic replay cache (SET ... NX EX 60).
    • Standard WebCrypto APIs supported across modern browsers and Deno.
  • Additional Important Notes:
    • PoW computation must be entirely transparent to human users (solved in background Web Workers or async microtasks during page load/typing).
    • Headless CLI clients (curl ... | source) must be provided with a streamlined solver or signed temporary ingress token.

2. Architectural Considerations & Risks

Risks

  1. Client-Side CPU Exhaustion & Mobile Battery Drain: If the cryptographic difficulty target is set too high, mobile devices or older laptops might freeze the main thread or experience noticeable latency before submitting a PIN.
    • Mitigation: Calibrate the default baseline difficulty to ~1618 leading zero bits (~2040ms CPU time on standard mobile silicon). Run computation inside a Web Worker or async requestAnimationFrame chunks to guarantee zero UI frame drops.
  2. Replay Attacks on Solved Nonces: An attacker could precompute a single valid PoW solution and reuse it across thousands of simultaneous requests to brute-force the 6-digit PIN.
    • Mitigation: Bind challenges cryptographically to a short-lived timestamped seed (HMAC-SHA256(secret, client_ip + timestamp_minute + event_id)). Record solved nonces in Valkey with a strict 60-second TTL via SET key nonce NX EX 60 to enforce single-use execution.
  3. Dynamic Threat Scaling: Static difficulty is either too easy for large botnets or too heavy for low-powered mobile devices.
    • Mitigation: Implement adaptive difficulty scaling in server/pow.ts: Normal traffic requires 16 bits; if an IP or specific event encounters >3 failed attempts within 60s, automatically scale difficulty to 22 bits for that subnet/event.

Alternatives

  • Cloudflare Turnstile / Google reCAPTCHA: Heavy external dependency requiring network egress, proprietary script tags, and tracking cookies. Rejected. A self-contained, zero-dependency SHA-256 PoW preserves Auth-Yes's 100% sovereign, zero-external-dependency IAM architecture.
  • Strict IP Rate Limiting Only: In distributed botnet attacks, attackers rotate thousands of residential proxies, making single-IP rate limiting ineffective. PoW shifts the economic and compute cost onto the attacker.

3. Proposed Implementation

Phase 1: PoW Challenge Engine (server/pow.ts)

  1. Implement challenge minting:
    • generateChallenge(clientIp: string, eventId?: string, difficultyBits = 16):
      • Mint challenge payload: { salt: string, timestamp: number, difficulty: number, signature: string }.
      • Sign payload using internal HMAC key.
  2. Implement validation:
    • verifyProofOfWork(challenge: PoWChallenge, nonce: number, clientIp: string):
      • Verify HMAC signature on salt/timestamp (ensure timestamp within 120s window).
      • Compute SHA-256(salt + nonce).
      • Check that the leading N bits of the digest are zero.
      • Atomically store nonce in Valkey (SET pow:${salt}:${nonce} 1 NX EX 120); if key already exists, reject as replayed.

Phase 2: Ingress Route Protection (server/routes/events.ts)

  1. Add challenge endpoint:
    • GET /api/join/challenge: Returns { salt, timestamp, difficulty, signature }.
  2. Update POST /api/join:
    • Require pow_salt, pow_nonce, pow_timestamp, pow_signature in JSON body.
    • Validate PoW before checking the PIN code or touching PostgreSQL.
    • If invalid or missing, immediately return 400 (Bad Request: Invalid Proof-of-Work Challenge) without consuming DB connections.
  3. Adaptive Difficulty Escalation:
    • Track failed attempts per IP and per event in Valkey. Increase difficulty dynamically if failure rate spikes.

Phase 3: Client-Side WebCrypto Solver (ui/components/events/JoinScript.tsx)

  1. Background Web Worker Solver:
    • When the user lands on /join or /e/:slug, automatically fetch /api/join/challenge in the background.
    • Spawn a lightweight Web Worker executing WebCrypto crypto.subtle.digest('SHA-256', ...) in a tight loop to find the nonce while the user types their 6-digit PIN.
    • Attach solved nonce payload seamlessly to the form submit handler.
  2. Headless CLI Compatibility:
    • For CLI joins (/join/:slug?format=env), embed a micro-solver script in the piped shell payload or provide a signed 1-click token for terminal workflows.

Phase 4: Quality Gates & Benchmarks

  1. Comprehensive test suite in server/tests/pow.test.ts:
    • Unit tests for challenge generation, difficulty checks, expiration, and replay prevention.
    • Performance benchmark asserting solver verification on server takes <0.5\text{ms}.
  2. Run deno fmt, deno task lint, deno task check, and deno test -A --no-check.