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).map_err(|_| PasswordError::InvalidHash)?;
48
49        let argon2 = Argon2::default();
50
51        match argon2.verify_password(password.as_bytes(), &parsed_hash) {
52            Ok(()) => Ok(true),
53            Err(argon2::password_hash::Error::Password) => Ok(false),
54            Err(e) => Err(PasswordError::VerifyError(e.to_string())),
55        }
56    }
57
58    /// Get the hash string (for storage)
59    pub fn as_str(&self) -> &str {
60        &self.0
61    }
62
63    /// Create from a stored hash string
64    pub fn from_hash_string(hash: String) -> Result<Self, PasswordError> {
65        // Validate the hash format
66        PasswordHash::new(&hash).map_err(|_| PasswordError::InvalidHash)?;
67
68        Ok(Self(hash))
69    }
70}
71
72impl fmt::Display for SecurePasswordHash {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        write!(f, "{}", self.0)
75    }
76}
77
78impl From<SecurePasswordHash> for String {
79    fn from(hash: SecurePasswordHash) -> String {
80        hash.0
81    }
82}
83
84impl TryFrom<String> for SecurePasswordHash {
85    type Error = PasswordError;
86
87    fn try_from(hash: String) -> Result<Self, Self::Error> {
88        Self::from_hash_string(hash)
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_password_hashing_and_verification() {
98        let password = "secure_password_123!";
99
100        // Hash the password
101        let hash = SecurePasswordHash::hash_password(password).unwrap();
102
103        // Verify correct password
104        assert!(hash.verify_password(password).unwrap());
105
106        // Verify incorrect password
107        assert!(!hash.verify_password("wrong_password").unwrap());
108    }
109
110    #[test]
111    fn test_hash_uniqueness() {
112        let password = "same_password";
113
114        let hash1 = SecurePasswordHash::hash_password(password).unwrap();
115        let hash2 = SecurePasswordHash::hash_password(password).unwrap();
116
117        // Hashes should be different due to random salts
118        assert_ne!(hash1.as_str(), hash2.as_str());
119
120        // But both should verify the same password
121        assert!(hash1.verify_password(password).unwrap());
122        assert!(hash2.verify_password(password).unwrap());
123    }
124
125    #[test]
126    fn test_hash_string_conversion() {
127        let password = "test_password";
128        let hash = SecurePasswordHash::hash_password(password).unwrap();
129
130        let hash_string = hash.to_string();
131        let reconstructed = SecurePasswordHash::from_hash_string(hash_string).unwrap();
132
133        assert!(reconstructed.verify_password(password).unwrap());
134    }
135
136    #[test]
137    fn test_invalid_hash_format() {
138        let result = SecurePasswordHash::from_hash_string("invalid_hash".to_string());
139        assert!(matches!(result, Err(PasswordError::InvalidHash)));
140    }
141}