use argon2::Argon2; use base64::Engine; use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; use rand::Rng; use crate::config::PasswordPolicy; use crate::error::AuthError; use super::{PasswordHashString, PasswordInput, ValidatePasswordRequest}; /// Hash a password using Argon2id with a fresh random salt. The result is a /// PHC-formatted string containing the algorithm parameters and the salt. pub fn hash_password(input: PasswordInput<'_>) -> Result { let mut bytes = [0u8; 16]; rand::rng().fill_bytes(&mut bytes); let b64 = base64::engine::general_purpose::STANDARD.encode(bytes); let salt = SaltString::from_b64(&b64) .map_err(|e| AuthError::Internal(format!("salt encoding: {e}")))?; let argon2 = Argon2::default(); let hash = argon2 .hash_password(input.password.as_bytes(), &salt) .map_err(|e| AuthError::Internal(format!("password hashing failed: {e}")))?; Ok(PasswordHashString(hash.to_string())) } /// Verify a password against a stored PHC string. pub fn verify_password( input: PasswordInput<'_>, hash: &PasswordHashString, ) -> Result { let parsed = PasswordHash::new(hash.as_ref()) .map_err(|e| AuthError::Internal(format!("malformed password hash: {e}")))?; let argon2 = Argon2::default(); Ok(argon2 .verify_password(input.password.as_bytes(), &parsed) .is_ok()) } /// Validate password strength against the configured policy. Returns an /// [`AuthError::BadRequest`] with a human-readable message on failure. pub fn validate_password( req: ValidatePasswordRequest<'_>, policy: &PasswordPolicy, ) -> Result<(), AuthError> { let p = req.password; if p.chars().count() < policy.min_length { return Err(AuthError::BadRequest(format!( "password must be at least {} characters long", policy.min_length ))); } if policy.require_upper && !p.chars().any(|c| c.is_ascii_uppercase()) { return Err(AuthError::BadRequest( "password must contain an uppercase letter".into(), )); } if policy.require_lower && !p.chars().any(|c| c.is_ascii_lowercase()) { return Err(AuthError::BadRequest( "password must contain a lowercase letter".into(), )); } if policy.require_digit && !p.chars().any(|c| c.is_ascii_digit()) { return Err(AuthError::BadRequest( "password must contain a digit".into(), )); } if policy.require_symbol && !p.chars().any(|c| !(c.is_ascii_alphanumeric())) { return Err(AuthError::BadRequest( "password must contain a symbol".into(), )); } Ok(()) }