use std::time::{SystemTime, UNIX_EPOCH}; use aes_gcm::Aes256Gcm; use aes_gcm::aead::{Aead, AeadCore, KeyInit, OsRng}; use argon2::{Argon2, PasswordHasher, PasswordVerifier}; use base64::Engine; use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}; use config::auth::PasswordPolicy; use rand::Rng; use sha2::{Digest, Sha256}; use crate::errors::AuthError; pub fn hash_password(password: &str) -> Result { Argon2::default() .hash_password(password.as_bytes()) .map(|hash| hash.to_string()) .map_err(|error| AuthError::Internal(format!("password hashing failed: {error}"))) } pub fn verify_password(password: &str, hash: &str) -> bool { Argon2::default() .verify_password(password.as_bytes(), hash) .is_ok() } pub fn validate_password(password: &str, policy: &PasswordPolicy) -> Result<(), AuthError> { if password.len() < policy.min_length { return Err(AuthError::BadRequest(format!( "password must be at least {} characters", policy.min_length ))); } if policy.require_upper && !password.chars().any(|c| c.is_ascii_uppercase()) { return Err(AuthError::BadRequest( "password must contain an uppercase letter".to_string(), )); } if policy.require_lower && !password.chars().any(|c| c.is_ascii_lowercase()) { return Err(AuthError::BadRequest( "password must contain a lowercase letter".to_string(), )); } if policy.require_digit && !password.chars().any(|c| c.is_ascii_digit()) { return Err(AuthError::BadRequest( "password must contain a digit".to_string(), )); } if policy.require_symbol && !password.chars().any(|c| !c.is_ascii_alphanumeric()) { return Err(AuthError::BadRequest( "password must contain a symbol".to_string(), )); } Ok(()) } pub fn generate_numeric_code(len: usize) -> String { let mut rng = rand::thread_rng(); (0..len) .map(|_| rng.gen_range(0..10u32).to_string()) .collect() } pub fn random_bytes(len: usize) -> Vec { let mut bytes = vec![0u8; len]; rand::thread_rng().fill(&mut bytes[..]); bytes } pub fn random_token(len: usize) -> String { URL_SAFE_NO_PAD.encode(random_bytes(len)) } pub fn sha256_b64url(data: &[u8]) -> String { URL_SAFE_NO_PAD.encode(Sha256::digest(data)) } pub fn sha256_hex(data: &[u8]) -> String { Sha256::digest(data) .iter() .map(|byte| format!("{byte:02x}")) .collect() } pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { if a.len() != b.len() { return false; } let mut diff = 0u8; for (x, y) in a.iter().zip(b.iter()) { diff |= x ^ y; } diff == 0 } pub fn aes_encrypt(key_material: &str, plaintext: &str) -> Result { let key = aes_key(key_material); let cipher = Aes256Gcm::new((&key).into()); let nonce = Aes256Gcm::generate_nonce(&mut OsRng); let ciphertext = cipher .encrypt(&nonce, plaintext.as_bytes()) .map_err(|error| { tracing::error!(%error, "aes encryption failed"); AuthError::Internal("aes encryption failed".to_string()) })?; let mut output = Vec::with_capacity(nonce.len() + ciphertext.len()); output.extend_from_slice(&nonce); output.extend_from_slice(&ciphertext); Ok(STANDARD.encode(output)) } pub fn aes_decrypt(key_material: &str, stored: &str) -> Result { let bytes = STANDARD.decode(stored).map_err(|error| { tracing::error!(%error, "malformed encrypted value"); AuthError::Internal("malformed encrypted value".to_string()) })?; if bytes.len() < 12 { return Err(AuthError::Internal("malformed encrypted value".to_string())); } let (nonce_bytes, ciphertext) = bytes.split_at(12); let Ok(nonce_arr): Result<&[u8; 12], _> = nonce_bytes.try_into() else { return Err(AuthError::Internal("malformed encrypted value".to_string())); }; let key = aes_key(key_material); let cipher = Aes256Gcm::new((&key).into()); let plaintext = cipher .decrypt(nonce_arr.into(), ciphertext) .map_err(|error| { tracing::error!(%error, "aes decryption failed"); AuthError::Internal("aes decryption failed".to_string()) })?; String::from_utf8(plaintext).map_err(|error| { tracing::error!(%error, "decrypted value is not utf8"); AuthError::Internal("decrypted value is not utf8".to_string()) }) } fn aes_key(material: &str) -> [u8; 32] { Sha256::digest(material.as_bytes()).into() } pub fn now_secs() -> i64 { SystemTime::now() .duration_since(UNIX_EPOCH) .map(|duration| duration.as_secs() as i64) .unwrap_or(0) }