geronimo_captcha/
manager.rs1use crate::error::{CaptchaError, Result};
2use crate::image_ops::NoiseOptions;
3use crate::registry::ChallengeRegistry;
4use crate::{RegistryCheckResult, challenge};
5
6use rand::prelude::IndexedRandom;
7use rand::rng;
8use std::sync::Arc;
9use tracing::{info, warn};
10use zeroize::Zeroizing;
11
12pub const SAMPLE_IMAGES: &[&[u8]] = &[
13 include_bytes!("../assets/sample1.jpg"),
14 include_bytes!("../assets/sample2.jpg"),
15 include_bytes!("../assets/sample3.jpg"),
16 include_bytes!("../assets/sample4.jpg"),
17 include_bytes!("../assets/sample5.jpg"),
18 include_bytes!("../assets/sample6.jpg"),
19 include_bytes!("../assets/sample7.jpg"),
20];
21
22pub struct CaptchaManager {
23 registry: Option<Arc<dyn ChallengeRegistry>>,
24 challenge_ttl: u64,
25 noise: NoiseOptions,
26 secret: Zeroizing<Vec<u8>>,
27 gen_opts: challenge::GenerationOptions,
28}
29
30impl CaptchaManager {
31 pub fn new(
32 secret: String,
33 challenge_ttl: u64,
34 noise: NoiseOptions,
35 registry: Option<Arc<dyn ChallengeRegistry>>,
36 gen_opts: challenge::GenerationOptions,
37 ) -> Self {
38 Self {
39 registry,
40 challenge_ttl,
41 noise,
42 secret: Zeroizing::new(secret.into_bytes()),
43 gen_opts,
44 }
45 }
46
47 pub fn generate_challenge(&self) -> Result<challenge::CaptchaChallenge> {
48 let sample_image = match SAMPLE_IMAGES.choose(&mut rng()) {
49 Some(img) => *img,
50 None => return Err(CaptchaError::Internal("no sample images available".into())),
51 };
52
53 let challenge = challenge::generate(
54 sample_image,
55 self.secret.as_slice(),
56 &self.gen_opts,
57 self.noise,
58 )?;
59
60 if let Some(reg) = &self.registry {
61 reg.register(&challenge.challenge_id);
62 }
63
64 info!(
65 cell_size = self.gen_opts.cell_size,
66 jpeg_quality = self.gen_opts.jpeg_quality,
67 "captcha generated"
68 );
69
70 Ok(challenge)
71 }
72
73 pub fn verify_challenge(&self, challenge_id: &str, selected_index: u8) -> Result<bool> {
74 if challenge_id.is_empty() {
75 return Err(CaptchaError::InvalidInput("Challenge ID cannot be empty"));
76 }
77
78 if selected_index == 0 || selected_index > 9 {
79 return Err(CaptchaError::InvalidInput("Selected index out of bounds"));
80 }
81
82 if let Some(registry) = &self.registry {
83 let result = registry.check(challenge_id);
84 if result != RegistryCheckResult::Ok {
85 warn!("challenge rejected by registry: {result}");
86 return Err(CaptchaError::Registry(result));
87 }
88 }
89
90 let valid = challenge::verify(
91 self.secret.as_slice(),
92 challenge_id,
93 selected_index,
94 self.challenge_ttl,
95 );
96
97 if valid {
98 if let Some(registry) = &self.registry {
99 registry.verify(challenge_id);
100 }
101
102 info!("captcha verified successfully");
103 } else if let Some(registry) = &self.registry {
104 registry.note_attempt(challenge_id, false);
105 warn!("captcha verification failed");
106 }
107
108 Ok(valid)
109 }
110}