Skip to main content

fr_rust/otp/
otp.rs

1use crate::prelude::*;
2use rand::Rng;
3use deadpool_redis::redis::AsyncCommands;
4use crate::crypto::crypto::CryptoError;
5
6// --- ERROR HANDLING ---
7#[derive(thiserror::Error, Debug)]
8pub enum OtpError {
9    #[error("Redis manager error: {0}")]
10    RedisManager(#[from] RedisManagerError),
11
12    #[error("Redis error: {0}")]
13    Redis(#[from] deadpool_redis::redis::RedisError),
14
15    #[error("Redis pool error: {0}")]
16    RedisPool(#[from] deadpool_redis::PoolError),
17
18    #[error("Crypto error: {0}")]
19    Crypto(#[from] CryptoError),
20}
21
22
23pub type Result<T> = std::result::Result<T, OtpError>;
24
25// --- SERVICE IMPLEMENTATION ---
26
27#[derive(Clone)]
28pub struct OtpConfig {
29    pub secret: String,
30    pub crypto: CryptoService,
31    pub redis: RedisManager,
32    pub ttl_secs: u64,
33}
34
35#[derive(Clone)]
36pub struct OtpService {
37    config: OtpConfig,
38}
39
40impl OtpService {
41    pub fn new(config: OtpConfig) -> Self {
42        Self { config }
43    }
44
45    pub async fn generate_otp(&self, user_id: &str, digits: u32) -> Result<String> {
46        let otp = Self::random_digits(digits);
47        let content_to_hash = format!("{}:{}", self.config.secret, otp);
48        let hash = self.config.crypto.sha256_hash(&content_to_hash)?.hash;
49        let redis_key = format!("otp:{}", user_id);
50        
51        let mut con = self.config.redis.get_connection().await?;
52        
53        let _res: () = con.set_ex(&redis_key, &hash, self.config.ttl_secs).await?;
54        
55        Ok(otp)
56    }
57
58    pub async fn verify_otp(&self, user_id: &str, otp: &str) -> Result<bool> {
59        let redis_key = format!("otp:{}", user_id);
60        
61        let mut con = self.config.redis.get_connection().await?;
62        let stored_hash: Option<String> = con.get(&redis_key).await?;
63        
64        let hash_to_check = match stored_hash {
65            Some(h) => h,
66            None => return Ok(false),
67        };
68        
69        let content_to_hash = format!("{}:{}", self.config.secret, otp);
70        let calculated_hash = self.config.crypto.sha256_hash(&content_to_hash)?.hash;
71        let ok = calculated_hash == hash_to_check;
72        
73        if ok {
74            let _res: () = con.del(&redis_key).await?;
75        }
76        
77        Ok(ok)
78    }
79
80    fn random_digits(digits: u32) -> String {
81        let mut bytes = [0u8; 8];
82        
83        // rand::rng() gets the new, optimized cryptographically secure thread-local generator
84        rand::rng().fill_bytes(&mut bytes);
85        
86        let num = u64::from_le_bytes(bytes);
87        let otp = num % 10u64.pow(digits);
88        format!("{:0width$}", otp, width = digits as usize)
89    }
90}