#captcha #random #web #no-js

geronimo-captcha

Secure, AI-resistant, JavaScript-free CAPTCHA built in Rust. Confuses bots, but delights humans.

2 releases (1 stable)

Uses new Rust 2024

1.0.0 Nov 4, 2025
0.2.0 Nov 2, 2025

#1557 in Web programming

Apache-2.0

475KB
999 lines

geronimo-captcha

CI Crates.io Docs.rs License: Apache 2.0

Secure, AI-resistant, JavaScript-free CAPTCHA built in Rust. Confuses bots, but delights humans.

geronimo-captcha logo

What it does

  • Renders a 3×3 sprite with one correctly oriented tile
  • Random jitter, label offset, colored noise, JPEG artifacts
  • Stateless HMAC-signed challenge id with TTL

Challenge examples

Challenge examples

Roadmap

  • Captcha core, image and sprite generation helpers
  • In-memory challenge registry impl
  • Sprite as binary (in addition to base64)
  • WebP format (in addition to JPEG)
  • Code examples, demo webpage
  • Custom fonts and sample sets
  • Redis challenge registry impl

Generate and verify

use geronimo_captcha::{
    CaptchaManager, ChallengeInMemoryRegistry,
    GenerationOptions, NoiseOptions,
    SpriteFormat, SpriteUri, SpriteBinary
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let secret = "your-secret-key".to_string();
    let ttl_secs = 60;
    let noise = NoiseOptions::default();
    let gen = GenerationOptions {
        cell_size: 150,
        sprite_format: SpriteFormat::Jpeg {
            quality: 20,
        },
        limits: None,
    };
    let registry = std::sync::Arc::new(ChallengeInMemoryRegistry::new(ttl_secs, 3));

    let mgr = CaptchaManager::new(secret, ttl_secs, noise, Some(registry), gen);
    let challenge = mgr.generate_challenge::<SpriteUri>()?;

    // Generate sprite (as binary) if needed
    // let challenge = mgr.generate_challenge_with::<SpriteBinary>()?;
    // let img_binary = challenge.sprite.bytes;

    // Render to client
    let img_src = challenge.sprite.0;           // data:image/*;base64,...
    let challenge_id = challenge.challenge_id;  // send/store with form

    println!("img_src prefix: {}", &img_src[..32.min(img_src.len())]);
    println!("challenge_id: {}", challenge_id);

    // Normally you get these from the client in your API handlers/routes
    let client_challenge_id = "nonce:1730534400:BASE64_HMAC".to_string();
    let client_choice_idx: u8 = 7;

    let ok = mgr.verify_challenge(&client_challenge_id, client_choice_idx)?;
    println!("verified: {ok}");

    Ok(())
}

Benchmarks

  • JPEG generate: ~6.7 ms / ~11.1 ms / ~17.5 ms
  • WebP generate: ~11.5 ms / ~21.0 ms / ~33.1 ms
  • Verify: ~2.5 µs

With feature parallel enabled: ~5.0 ms / ~9.6 ms / ~15.6 ms (JPEG) and ~10.7 ms / ~20.0 ms / ~32.6 ms (WebP).

Apple M3 Max

How to run:

cargo bench --bench captcha -- --noplot
cargo bench --features parallel --bench captcha -- --noplot

License

This project is licensed under the Apache 2.0 License. See LICENSE for details.

Dependencies

~30MB
~488K SLoC