elif_http/websocket/channel/
password.rs1use argon2::{
7 password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
8 Argon2,
9};
10use std::fmt;
11
12#[derive(Debug, Clone, PartialEq)]
14pub struct SecurePasswordHash(String);
15
16#[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 pub fn hash_password(password: &str) -> Result<Self, PasswordError> {
33 let salt = SaltString::generate(&mut OsRng);
34
35 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 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 pub fn as_str(&self) -> &str {
61 &self.0
62 }
63
64 pub fn from_hash_string(hash: String) -> Result<Self, PasswordError> {
66 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 let hash = SecurePasswordHash::hash_password(password).unwrap();
104
105 assert!(hash.verify_password(password).unwrap());
107
108 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 assert_ne!(hash1.as_str(), hash2.as_str());
121
122 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}