Skip to content
Clone
d5939db
73 lines 2.7 KB Rust
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<PasswordHashString, AuthError> {
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<bool, AuthError> {
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(())
}