use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; /// Argon2id parameters for memory-hard PoW. Matches the upstream /// `argon2::Params` shape; values are stamped on each challenge so the /// client knows exactly which hash to compute and the server can tune /// without redeploying clients. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct Argon2idParams { /// Memory cost in KiB. Default 8192 (8 MiB) — small enough to fit on /// mobile, large enough to defeat trivial GPU parallelisation. pub m_cost: u32, /// Iteration count. Default 2. pub t_cost: u32, /// Parallelism / lanes. Default 1 (single-thread worker). pub p_cost: u32, } impl Default for Argon2idParams { fn default() -> Self { Self { m_cost: 8_192, t_cost: 2, p_cost: 1, } } } /// PoW algorithm. Chosen per server (via `PUZZLE_ALGORITHM`) and stamped on /// each challenge so the client picks the right hash function. /// /// Wire format is backward-compatible: `Sha256` serialises as the bare /// string `"sha256"` (matching the original API), while /// `Argon2id(params)` serialises as `{"argon2id": {...}}`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Algorithm { Sha256, Argon2id(Argon2idParams), } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Challenge { pub id: Uuid, pub site_key: Uuid, pub algorithm: Algorithm, pub prefix: String, pub difficulty: u32, pub created_at: DateTime, /// Anchor for the verify-time dwell calculation. Equals `created_at` for a /// first issuance, and is *inherited* when this challenge was minted to /// replace one about to expire (see `refresh_of` on `GET /v1/puzzle`). /// /// Separate from `created_at` because the two answer different questions /// and only one of them may move: `created_at` anchors `expires_at` and /// the decision log, while this anchors "how long has the visitor had this /// form open". Without it, the widget's pre-expiry refresh silently reset /// dwell to zero, so a visitor who spent five minutes on a form and /// submitted just after a background refresh was scored — and logged — as /// having submitted in 400ms. pub dwell_since: DateTime, pub expires_at: DateTime, /// Failed PoW verify attempts against this challenge. A wrong nonce /// deliberately leaves the challenge live for retry, so without a cap one /// challenge could absorb unlimited (potentially memory-hard) verify /// attempts. The store evicts the challenge once this reaches /// `VERIFY_MAX_ATTEMPTS`. #[serde(default)] pub attempts: u32, /// The client address this challenge was issued to (reverse-proxy /// resolved, full address), so `/v1/verify` can check an /// integrator-forwarded `remote_ip` against it. Held in memory for the /// challenge's TTL only and never logged. `None` means unknown (a /// challenge minted before this field existed), which disables the check /// rather than failing it. #[serde(default)] pub issued_to: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Solution { pub challenge_id: Uuid, pub nonce: u64, } #[derive(Debug, Clone)] pub struct PuzzleConfig { pub algorithm: Algorithm, pub default_difficulty: u32, pub ttl_secs: u64, } impl Default for PuzzleConfig { fn default() -> Self { Self { algorithm: Algorithm::Sha256, default_difficulty: 20, ttl_secs: 300, } } }