1use argon2::{Algorithm, Argon2, Params, Version};
16use password_hash::{Salt, PasswordHash, PasswordHasher as PhcHasher, PasswordVerifier, SaltString};
17use rand::RngCore;
18
19use crate::error::{Error, Result};
20
21const DEFAULT_MEMORY_KIB: u32 = 65_536;
23const DEFAULT_TIME_COST: u32 = 3;
26const DEFAULT_PARALLELISM: u32 = 4;
30
31#[derive(Clone)]
35pub struct PasswordHasher {
36 argon: Argon2<'static>,
37 params: Params,
38}
39
40impl Default for PasswordHasher {
41 fn default() -> Self {
42 let params = Params::new(
43 DEFAULT_MEMORY_KIB,
44 DEFAULT_TIME_COST,
45 DEFAULT_PARALLELISM,
46 None,
47 )
48 .expect("argon2 default params are within library limits");
49 Self {
50 argon: Argon2::new(Algorithm::Argon2id, Version::V0x13, params.clone()),
51 params,
52 }
53 }
54}
55
56impl PasswordHasher {
57 pub fn with_params(params: Params) -> Self {
61 Self {
62 argon: Argon2::new(Algorithm::Argon2id, Version::V0x13, params.clone()),
63 params,
64 }
65 }
66
67 pub fn hash(&self, plaintext: &str) -> Result<String> {
75 let mut salt_bytes = [0u8; Salt::RECOMMENDED_LENGTH];
76 rand::rng().fill_bytes(&mut salt_bytes);
77 let salt = SaltString::encode_b64(&salt_bytes).map_err(map_phc_err)?;
78 let phc = self
79 .argon
80 .hash_password(plaintext.as_bytes(), &salt)
81 .map_err(map_phc_err)?;
82 Ok(phc.to_string())
83 }
84
85 pub fn verify(&self, plaintext: &str, phc: &str) -> Result<bool> {
93 let parsed = PasswordHash::new(phc).map_err(map_phc_err)?;
94 match self.argon.verify_password(plaintext.as_bytes(), &parsed) {
95 Ok(()) => Ok(true),
96 Err(password_hash::Error::Password) => Ok(false),
100 Err(other) => Err(map_phc_err(other)),
101 }
102 }
103
104 pub fn needs_rehash(&self, phc: &str) -> Result<bool> {
109 let parsed = PasswordHash::new(phc).map_err(map_phc_err)?;
110 if parsed.algorithm.as_str() != Algorithm::Argon2id.ident().as_str() {
112 return Ok(true);
113 }
114 let stored = Params::try_from(&parsed).map_err(map_phc_err)?;
115 Ok(stored.m_cost() != self.params.m_cost()
116 || stored.t_cost() != self.params.t_cost()
117 || stored.p_cost() != self.params.p_cost())
118 }
119}
120
121fn map_phc_err(e: password_hash::Error) -> Error {
122 Error::Backend(anyhow::anyhow!("argon2: {e}"))
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn hash_and_verify_round_trip() {
131 let hasher = PasswordHasher::default();
132 let phc = hasher.hash("hunter2").unwrap();
133 assert!(phc.starts_with("$argon2id$"));
134 assert!(hasher.verify("hunter2", &phc).unwrap());
135 }
136
137 #[test]
138 fn wrong_password_returns_ok_false() {
139 let hasher = PasswordHasher::default();
140 let phc = hasher.hash("correct").unwrap();
141 assert!(!hasher.verify("incorrect", &phc).unwrap());
142 }
143
144 #[test]
145 fn malformed_hash_returns_err() {
146 let hasher = PasswordHasher::default();
147 let result = hasher.verify("anything", "not-a-phc-string");
148 assert!(matches!(result, Err(Error::Backend(_))));
149 }
150
151 #[test]
152 fn salt_is_per_call_so_two_hashes_of_same_input_differ() {
153 let hasher = PasswordHasher::default();
154 let a = hasher.hash("same").unwrap();
155 let b = hasher.hash("same").unwrap();
156 assert_ne!(a, b);
157 }
158
159 #[test]
160 fn needs_rehash_false_for_same_params() {
161 let hasher = PasswordHasher::default();
162 let phc = hasher.hash("pw").unwrap();
163 assert!(!hasher.needs_rehash(&phc).unwrap());
164 }
165
166 #[test]
167 fn needs_rehash_true_when_params_drift() {
168 let weak = PasswordHasher::with_params(Params::new(8, 1, 1, None).unwrap());
169 let phc = weak.hash("pw").unwrap();
170 let strong = PasswordHasher::default();
171 assert!(strong.needs_rehash(&phc).unwrap());
172 }
173}