//! Pure recovery-code logic: generation, Argon2id hashing, verification. //! No I/O — that lives in `storage::recovery::RecoveryStore`. #![allow(dead_code)] // wired through RecoveryStore (Task 4) and AppState (Task 5). use argon2::{ password_hash::{ rand_core::OsRng as PhcOsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString, }, Argon2, }; use rand::Rng; /// Default validity window for a freshly generated recovery code. 7 days: /// long enough for an admin to mail the code to a remote user, short enough /// to limit blast radius if the file leaks. pub(crate) const DEFAULT_TTL_SECS: u64 = 7 * 24 * 3600; #[derive(Debug, thiserror::Error)] pub(crate) enum RecoveryError { #[error("argon2: {0}")] Argon2(String), } /// Generate a fresh 8-digit recovery code. pub(crate) fn generate_code() -> String { let n: u32 = rand::rng().random_range(0..100_000_000); format!("{n:08}") } /// Hash a code with Argon2id, producing a PHC-format string. pub(crate) fn hash_code(code: &str) -> Result { let salt = SaltString::generate(&mut PhcOsRng); let argon = Argon2::default(); let phc = argon .hash_password(code.as_bytes(), &salt) .map_err(|e| RecoveryError::Argon2(e.to_string()))? .to_string(); Ok(phc) } /// Constant-time verify a candidate code against a stored PHC hash. pub(crate) fn verify_code(candidate: &str, phc: &str) -> Result { let parsed = PasswordHash::new(phc).map_err(|e| RecoveryError::Argon2(e.to_string()))?; Ok(Argon2::default() .verify_password(candidate.as_bytes(), &parsed) .is_ok()) } #[cfg(test)] mod tests { use super::*; #[test] fn generated_code_is_eight_digits() { for _ in 0..50 { let c = generate_code(); assert_eq!(c.len(), 8); assert!(c.chars().all(|ch| ch.is_ascii_digit())); } } #[test] fn hash_roundtrips_correct_code() { let phc = hash_code("12345678").unwrap(); assert!(phc.starts_with("$argon2id$"), "phc was {phc}"); assert!(verify_code("12345678", &phc).unwrap()); } #[test] fn verify_rejects_wrong_code() { let phc = hash_code("12345678").unwrap(); assert!(!verify_code("87654321", &phc).unwrap()); } #[test] fn verify_errors_on_malformed_phc() { assert!(verify_code("12345678", "not a phc string").is_err()); } }