use chrono::{DateTime, Duration, Utc}; use rand::{Rng, rng}; use sha2::{Digest, Sha256}; #[derive(Debug, Clone)] pub struct OtpData { pub code: u32, pub hash: String, pub expires_at: DateTime, } pub struct OtpManager; impl OtpManager { pub fn generate_otp() -> OtpData { let code = rng().random_range(100_000..1_000_000); let otp_str = code.to_string(); let mut hasher = Sha256::new(); hasher.update(otp_str.as_bytes()); let hash = format!("{:x}", hasher.finalize()); let expires_at = Utc::now() + Duration::minutes(5); OtpData { code, hash, expires_at, } } pub fn validate_otp(stored: &OtpData, user_otp: u32) -> bool { Self::validate_otp_hash(&stored.hash, &stored.expires_at, user_otp) } /// Validate a user-supplied OTP against a stored hash + expiry (e.g. from a /// cache table where the plaintext code is not persisted). pub fn validate_otp_hash( stored_hash: &str, expires_at: &DateTime, user_otp: u32, ) -> bool { if Utc::now() > *expires_at { return false; } let user_otp_str = user_otp.to_string(); let mut hasher = Sha256::new(); hasher.update(user_otp_str.as_bytes()); let user_hash = format!("{:x}", hasher.finalize()); user_hash == stored_hash } } #[cfg(test)] mod tests { use super::*; #[test] fn test_generate_otp() { let otp = OtpManager::generate_otp(); assert!(otp.code >= 100_000 && otp.code < 1_000_000); assert!(!otp.hash.is_empty()); assert!(otp.expires_at > Utc::now()); assert!(otp.expires_at <= Utc::now() + chrono::Duration::minutes(5)); } #[test] fn test_validate_otp_valid() { let otp = OtpManager::generate_otp(); assert!(OtpManager::validate_otp(&otp, otp.code)); } #[test] fn test_validate_otp_invalid_code() { let otp = OtpManager::generate_otp(); assert!(!OtpManager::validate_otp(&otp, 123456)); } #[test] fn test_validate_otp_hash_valid() { let otp = OtpManager::generate_otp(); assert!(OtpManager::validate_otp_hash( &otp.hash, &otp.expires_at, otp.code )); } #[test] fn test_validate_otp_hash_invalid() { let otp = OtpManager::generate_otp(); assert!(!OtpManager::validate_otp_hash( &otp.hash, &otp.expires_at, 123456 )); } #[test] fn test_validate_otp_hash_expired() { let mut otp = OtpManager::generate_otp(); otp.expires_at = Utc::now() - chrono::Duration::seconds(1); assert!(!OtpManager::validate_otp_hash( &otp.hash, &otp.expires_at, otp.code )); } #[test] fn test_validate_otp_expired() { let mut otp = OtpManager::generate_otp(); otp.expires_at = Utc::now() - chrono::Duration::seconds(1); assert!(!OtpManager::validate_otp(&otp, otp.code)); } #[test] fn test_otp_uniqueness() { let otp1 = OtpManager::generate_otp(); let otp2 = OtpManager::generate_otp(); assert_ne!(otp1.code, otp2.code); assert_ne!(otp1.hash, otp2.hash); } }