geronimo-captcha

Crates.iogeronimo-captcha
lib.rsgeronimo-captcha
version1.0.0
created_at2025-11-02 09:31:57.418299+00
updated_at2025-11-04 12:17:13.084316+00
descriptionSecure, AI-resistant, JavaScript-free CAPTCHA built in Rust. Confuses bots, but delights humans.
homepage
repositoryhttps://github.com/yoozzeek/geronimo-captcha
max_upload_size
id1912904
size556,176
Andrew (yoozzeek)

documentation

https://docs.rs/geronimo-captcha

README

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.

Commit count: 0

cargo fmt