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).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 pub fn as_str(&self) -> &str {
60 &self.0
61 }
62
63 pub fn from_hash_string(hash: String) -> Result<Self, PasswordError> {
65 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 let hash = SecurePasswordHash::hash_password(password).unwrap();
102
103 assert!(hash.verify_password(password).unwrap());
105
106 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 assert_ne!(hash1.as_str(), hash2.as_str());
119
120 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}