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

123 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 ~16–18 leading
zero bits (~20–40ms 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`.