elif_http/websocket/channel/
password.rs

1//! Secure password hashing for channel authentication
2//! 
3//! This module provides production-ready password hashing using Argon2id,
4//! the winner of the Password Hashing Competition and recommended by OWASP.
5
6use argon2::{
7    password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
8    Argon2,
9};
10use std::fmt;
11
12/// Secure password hash using Argon2id
13#[derive(Debug, Clone, PartialEq)]
14pub struct SecurePasswordHash(String);
15
16/// Errors that can occur during password hashing operations
17#[derive(Debug, thiserror::Error)]
18pub enum PasswordError {
19    #[error("Failed to hash password: {0}")]
20    HashError(String),
21    #[error("Failed to verify password: {0}")]
22    VerifyError(String),
23    #[error("Invalid password hash format")]
24    InvalidHash,
25}
26
27impl SecurePasswordHash {
28    /// Hash a plaintext password using Argon2id with secure defaults
29    /// 
30    /// Uses Argon2id variant which provides resistance against both 
31    /// side-channel and GPU-based attacks.
32    pub fn hash_password(password: &str) -> Result<Self, PasswordError> {
33        let salt = SaltString::generate(&mut OsRng);
34        
35        // Use Argon2id with OWASP recommended parameters
36        let argon2 = Argon2::default();
37        
38        let password_hash = argon2
39            .hash_password(password.as_bytes(), &salt)
40            .map_err(|e| PasswordError::HashError(e.to_string()))?;
41        
42        Ok(Self(password_hash.to_string()))
43    }
44    
45    /// Verify a plaintext password against this hash
46    pub fn verify_password(&self, password: &str) -> Result<bool, PasswordError> {
47        let parsed_hash = PasswordHash::new(&self.0)
48            .map_err(|_| PasswordError::InvalidHash)?;
49        
50        let argon2 = Argon2::default();
51        
52        match argon2.verify_password(password.as_bytes(), &parsed_hash) {
53            Ok(()) => Ok(true),
54            Err(argon2::password_hash::Error::Password) => Ok(false),
55            Err(e) => Err(PasswordError::VerifyError(e.to_string())),
56        }
57    }
58    
59    /// Get the hash string (for storage)
60    pub fn as_str(&self) -> &str {
61        &self.0
62    }
63    
64    /// Create from a stored hash string
65    pub fn from_hash_string(hash: String) -> Result<Self, PasswordError> {
66        // Validate the hash format
67        PasswordHash::new(&hash)
68            .map_err(|_| PasswordError::InvalidHash)?;
69        
70        Ok(Self(hash))
71    }
72}
73
74impl fmt::Display for SecurePasswordHash {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        write!(f, "{}", self.0)
77    }
78}
79
80impl From<SecurePasswordHash> for String {
81    fn from(hash: SecurePasswordHash) -> String {
82        hash.0
83    }
84}
85
86impl TryFrom<String> for SecurePasswordHash {
87    type Error = PasswordError;
88    
89    fn try_from(hash: String) -> Result<Self, Self::Error> {
90        Self::from_hash_string(hash)
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_password_hashing_and_verification() {
100        let password = "secure_password_123!";
101        
102        // Hash the password
103        let hash = SecurePasswordHash::hash_password(password).unwrap();
104        
105        // Verify correct password
106        assert!(hash.verify_password(password).unwrap());
107        
108        // Verify incorrect password
109        assert!(!hash.verify_password("wrong_password").unwrap());
110    }
111    
112    #[test]
113    fn test_hash_uniqueness() {
114        let password = "same_password";
115        
116        let hash1 = SecurePasswordHash::hash_password(password).unwrap();
117        let hash2 = SecurePasswordHash::hash_password(password).unwrap();
118        
119        // Hashes should be different due to random salts
120        assert_ne!(hash1.as_str(), hash2.as_str());
121        
122        // But both should verify the same password
123        assert!(hash1.verify_password(password).unwrap());
124        assert!(hash2.verify_password(password).unwrap());
125    }
126    
127    #[test]
128    fn test_hash_string_conversion() {
129        let password = "test_password";
130        let hash = SecurePasswordHash::hash_password(password).unwrap();
131        
132        let hash_string = hash.to_string();
133        let reconstructed = SecurePasswordHash::from_hash_string(hash_string).unwrap();
134        
135        assert!(reconstructed.verify_password(password).unwrap());
136    }
137    
138    #[test]
139    fn test_invalid_hash_format() {
140        let result = SecurePasswordHash::from_hash_string("invalid_hash".to_string());
141        assert!(matches!(result, Err(PasswordError::InvalidHash)));
142    }
143}