223 lines
6.7 KiB
Rust
223 lines
6.7 KiB
Rust
|
|
//! 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());
|
||
|
|
}
|
||
|
|
}
|