webgates_secrets/hashing/
argon2.rs1use super::HashedValue;
29use crate::Result;
30use crate::hashing::errors::{HashingError, HashingOperation};
31use crate::hashing::hashing_service::HashingService;
32use argon2::password_hash::{PasswordHasher, SaltString, rand_core::OsRng};
33use argon2::{Algorithm, Argon2, Params, PasswordHash, PasswordVerifier, Version};
34use webgates_core::verification_result::VerificationResult;
35
36#[derive(Debug, Clone, Copy)]
38pub struct Argon2Config {
39 pub memory_kib: u32,
41 pub time_cost: u32,
43 pub parallelism: u32,
45}
46
47impl Argon2Config {
48 pub fn high_security() -> Self {
52 Self {
53 memory_kib: 64 * 1024, time_cost: 3,
55 parallelism: 1,
56 }
57 }
58 pub fn interactive() -> Self {
62 Self {
63 memory_kib: 32 * 1024,
64 time_cost: 2,
65 parallelism: 1,
66 }
67 }
68 pub fn with_memory_kib(mut self, v: u32) -> Self {
70 self.memory_kib = v;
71 self
72 }
73 pub fn with_time_cost(mut self, v: u32) -> Self {
75 self.time_cost = v;
76 self
77 }
78 pub fn with_parallelism(mut self, v: u32) -> Self {
80 self.parallelism = v;
81 self
82 }
83}
84
85impl Default for Argon2Config {
86 fn default() -> Self {
87 Argon2Config::high_security()
88 }
89}
90
91#[derive(Debug, Clone, Copy)]
93pub enum Argon2Preset {
94 HighSecurity,
96 Interactive,
98}
99
100impl Argon2Preset {
101 pub fn to_config(self) -> Argon2Config {
103 match self {
104 Self::HighSecurity => Argon2Config::high_security(),
105 Self::Interactive => Argon2Config::interactive(),
106 }
107 }
108}
109
110#[derive(Clone)]
114pub struct Argon2Hasher {
115 config: Argon2Config,
116 engine: Argon2<'static>,
117}
118
119impl Argon2Hasher {
120 pub fn new_recommended() -> Result<Self> {
122 Self::high_security()
123 }
124 pub fn from_config(config: Argon2Config) -> Result<Self> {
126 let params = Params::new(
127 config.memory_kib,
128 config.time_cost,
129 config.parallelism,
130 None,
131 )?;
132 let engine = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
133 Ok(Self { config, engine })
134 }
135
136 pub fn from_preset(preset: Argon2Preset) -> Result<Self> {
138 Self::from_config(preset.to_config())
139 }
140
141 pub fn config(&self) -> &Argon2Config {
143 &self.config
144 }
145
146 pub fn high_security() -> Result<Self> {
154 Self::from_preset(Argon2Preset::HighSecurity)
155 }
156
157 pub fn interactive() -> Result<Self> {
165 Self::from_preset(Argon2Preset::Interactive)
166 }
167}
168
169impl HashingService for Argon2Hasher {
170 fn hash_value(&self, plain_value: &str) -> Result<HashedValue, HashingError> {
171 let salt = SaltString::generate(&mut OsRng);
172 Ok(self
173 .engine
174 .hash_password(plain_value.as_bytes(), &salt)
175 .map_err(|e| {
176 HashingError::with_context(
177 HashingOperation::Hash,
178 format!("Could not hash secret: {e}"),
179 Some("Argon2id".to_string()),
180 Some("PHC".to_string()),
181 )
182 })?
183 .to_string())
184 }
185
186 fn verify_value(
187 &self,
188 plain_value: &str,
189 hashed_value: &str,
190 ) -> Result<VerificationResult, HashingError> {
191 let hash = PasswordHash::new(hashed_value).map_err(|e| {
192 HashingError::with_context(
193 HashingOperation::Verify,
194 format!("Could not parse stored hash: {e}"),
195 Some("Argon2id".to_string()),
196 Some("PHC".to_string()),
197 )
198 })?;
199 Ok(VerificationResult::from(
200 self.engine
201 .verify_password(plain_value.as_bytes(), &hash)
202 .is_ok(),
203 ))
204 }
205}
206
207#[cfg(test)]
208#[allow(clippy::unwrap_used)]
209mod tests {
210 use super::*;
211 use crate::hashing::hashing_service::HashingService;
212
213 #[test]
214 fn recommended_hasher_verifies_matching_secret() {
215 let hasher = Argon2Hasher::new_recommended().unwrap();
216 let hash = hasher.hash_value("pw").unwrap();
217 assert!(matches!(
218 hasher.verify_value("pw", &hash),
219 Ok(VerificationResult::Ok)
220 ));
221 }
222
223 #[test]
224 fn presets_verify_matching_and_non_matching_secrets() {
225 for preset in [Argon2Preset::HighSecurity, Argon2Preset::Interactive] {
226 let hasher = Argon2Hasher::from_preset(preset).unwrap();
227 let h = hasher.hash_value("secret").unwrap();
228 assert_eq!(
229 VerificationResult::Ok,
230 hasher.verify_value("secret", &h).unwrap()
231 );
232 assert_eq!(
233 VerificationResult::Unauthorized,
234 hasher.verify_value("other", &h).unwrap()
235 );
236 }
237 }
238
239 #[test]
240 fn custom_config_verifies_matching_secret() {
241 let cfg = Argon2Config::default()
242 .with_memory_kib(48 * 1024)
243 .with_time_cost(2)
244 .with_parallelism(1);
245 let hasher = Argon2Hasher::from_config(cfg).unwrap();
246 let h = hasher.hash_value("abc").unwrap();
247 assert!(matches!(
248 hasher.verify_value("abc", &h),
249 Ok(VerificationResult::Ok)
250 ));
251 }
252}