223 lines
6.7 KiB
Rust
Raw Normal View History

//! Password Management Service
//!
//! Secure password hashing and verification using Argon2id algorithm.
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
};
use crate::error::{auth, ControlCenterError, Result};
/// Password strength levels
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum PasswordStrength {
/// Weak password (< 8 chars, no complexity)
Weak,
/// Fair password (8+ chars, limited complexity)
Fair,
/// Good password (10+ chars, good complexity)
Good,
/// Strong password (12+ chars, high complexity)
Strong,
/// Very strong password (16+ chars, full complexity)
VeryStrong,
}
/// Password management service
pub struct PasswordService {
argon2: Argon2<'static>,
}
impl PasswordService {
/// Create new password service with default Argon2 configuration
pub fn new() -> Self {
Self {
argon2: Argon2::default(),
}
}
/// Hash password using Argon2id
///
/// Uses cryptographically secure random salt
pub fn hash_password(&self, password: &str) -> Result<String> {
let salt = SaltString::generate(&mut OsRng);
self.argon2
.hash_password(password.as_bytes(), &salt)
.map(|hash| hash.to_string())
.map_err(|e| {
ControlCenterError::Auth(auth::AuthError::Authentication(format!(
"Password hashing failed: {}",
e
)))
})
}
/// Verify password against hash
pub fn verify_password(&self, password: &str, hash: &str) -> Result<bool> {
let parsed_hash = PasswordHash::new(hash).map_err(|e| {
ControlCenterError::Auth(auth::AuthError::Authentication(format!(
"Invalid password hash: {}",
e
)))
})?;
Ok(self
.argon2
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok())
}
/// Evaluate password strength
pub fn evaluate_strength(&self, password: &str) -> PasswordStrength {
let len = password.len();
let has_lowercase = password.chars().any(|c| c.is_lowercase());
let has_uppercase = password.chars().any(|c| c.is_uppercase());
let has_digit = password.chars().any(|c| c.is_ascii_digit());
let has_special = password.chars().any(|c| !c.is_alphanumeric());
let complexity_score = [has_lowercase, has_uppercase, has_digit, has_special]
.iter()
.filter(|&&x| x)
.count();
match (len, complexity_score) {
(0..=7, _) => PasswordStrength::Weak,
(8..=9, 0..=2) => PasswordStrength::Fair,
(8..=9, 3..=4) => PasswordStrength::Good,
(10..=11, 0..=2) => PasswordStrength::Fair,
(10..=11, 3..=4) => PasswordStrength::Good,
(12..=15, 0..=2) => PasswordStrength::Good,
(12..=15, 3..=4) => PasswordStrength::Strong,
(16.., 0..=2) => PasswordStrength::Good,
(16.., 3) => PasswordStrength::Strong,
(16.., 4..) => PasswordStrength::VeryStrong,
_ => PasswordStrength::Weak,
}
}
/// Check if password meets minimum requirements
///
/// Requirements:
/// - At least 8 characters
/// - At least 2 character types (lowercase, uppercase, digit, special)
pub fn meets_requirements(&self, password: &str) -> bool {
matches!(
self.evaluate_strength(password),
PasswordStrength::Fair
| PasswordStrength::Good
| PasswordStrength::Strong
| PasswordStrength::VeryStrong
)
}
}
impl Default for PasswordService {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_password_hashing() {
let service = PasswordService::new();
let password = "test_password_123";
let hash = service.hash_password(password).unwrap();
assert!(!hash.is_empty());
assert!(hash.starts_with("$argon2"));
}
#[test]
fn test_password_verification() {
let service = PasswordService::new();
let password = "test_password_123";
let hash = service.hash_password(password).unwrap();
// Correct password should verify
assert!(service.verify_password(password, &hash).unwrap());
// Wrong password should fail
assert!(!service.verify_password("wrong_password", &hash).unwrap());
}
#[test]
fn test_password_strength_weak() {
let service = PasswordService::new();
assert_eq!(service.evaluate_strength("abc"), PasswordStrength::Weak);
assert_eq!(service.evaluate_strength("1234567"), PasswordStrength::Weak);
}
#[test]
fn test_password_strength_fair() {
let service = PasswordService::new();
assert_eq!(
service.evaluate_strength("Password1"),
PasswordStrength::Fair
);
}
#[test]
fn test_password_strength_good() {
let service = PasswordService::new();
assert_eq!(
service.evaluate_strength("Password123"),
PasswordStrength::Good
);
}
#[test]
fn test_password_strength_strong() {
let service = PasswordService::new();
assert_eq!(
service.evaluate_strength("Password123!"),
PasswordStrength::Strong
);
}
#[test]
fn test_password_strength_very_strong() {
let service = PasswordService::new();
assert_eq!(
service.evaluate_strength("MyP@ssw0rd!2024Secure"),
PasswordStrength::VeryStrong
);
}
#[test]
fn test_password_requirements() {
let service = PasswordService::new();
// Too weak
assert!(!service.meets_requirements("abc"));
assert!(!service.meets_requirements("1234567"));
// Meets requirements
assert!(service.meets_requirements("Password1"));
assert!(service.meets_requirements("MyPassword123"));
assert!(service.meets_requirements("Secure!Pass2024"));
}
#[test]
fn test_different_salts() {
let service = PasswordService::new();
let password = "test_password";
let hash1 = service.hash_password(password).unwrap();
let hash2 = service.hash_password(password).unwrap();
// Different salts should produce different hashes
assert_ne!(hash1, hash2);
// Both should verify correctly
assert!(service.verify_password(password, &hash1).unwrap());
assert!(service.verify_password(password, &hash2).unwrap());
}
}