axum_gate/hashing/argon2.rs
1//! Value hashing implementations.
2//!
3//! Provides a *single* configurable Argon2id password hashing service with security‑first defaults.
4//!
5//! Build mode defaults:
6//! - Release builds (`debug_assertions` disabled): HighSecurity preset
7//! - Debug builds (`debug_assertions` enabled): DevFast preset (faster local iteration)
8//!
9//! You can override the default explicitly with presets or a custom configuration.
10//!
11//! # Example
12//! ```rust
13//! use axum_gate::hashing::argon2::Argon2Hasher;
14//! use axum_gate::hashing::HashingService;
15//!
16//! // Default (build‑mode appropriate) hasher
17//! let hasher = Argon2Hasher::new_recommended().unwrap();
18//! let hash = hasher.hash_value("secret").unwrap();
19//! assert!(hasher.verify_value("secret", &hash).is_ok());
20//! ```
21//!
22//! ⚠ The `DevFast` preset MUST NOT be used in production; it exists only to keep debug builds
23//! responsive. When you explicitly construct a hasher, choose an appropriate security profile.
24use super::HashedValue;
25use crate::errors::{Error, Result};
26use crate::hashing::HashingService;
27use crate::hashing::{HashingError, HashingOperation};
28use crate::verification_result::VerificationResult;
29use argon2::password_hash::{PasswordHasher, SaltString, rand_core::OsRng};
30use argon2::{Algorithm, Argon2, Params, PasswordHash, PasswordVerifier, Version};
31
32/// Argon2 parameter configuration (memory in KiB).
33#[derive(Debug, Clone, Copy)]
34pub struct Argon2Config {
35 /// Memory usage in KiB for the Argon2 algorithm.
36 pub memory_kib: u32,
37 /// Number of iterations (time cost) for the Argon2 algorithm.
38 pub time_cost: u32,
39 /// Number of parallel threads to use during hashing.
40 pub parallelism: u32,
41}
42
43impl Argon2Config {
44 /// High security configuration for production environments.
45 ///
46 /// Uses 64 MiB memory, 3 iterations, and 1 thread for maximum security.
47 pub fn high_security() -> Self {
48 Self {
49 memory_kib: 64 * 1024, // 64 MiB
50 time_cost: 3,
51 parallelism: 1,
52 }
53 }
54 /// Interactive configuration balanced for user-facing applications.
55 ///
56 /// Uses 32 MiB memory, 2 iterations, and 1 thread for reasonable performance.
57 pub fn interactive() -> Self {
58 Self {
59 memory_kib: 32 * 1024,
60 time_cost: 2,
61 parallelism: 1,
62 }
63 }
64 /// Fast configuration for development and testing.
65 ///
66 /// Uses minimal resources: 1 MiB memory, 1 iteration, and 1 thread.
67 #[cfg(any(feature = "insecure-fast-hash", debug_assertions))]
68 pub fn dev_fast() -> Self {
69 Self {
70 memory_kib: 4 * 1024,
71 time_cost: 1,
72 parallelism: 1,
73 }
74 }
75 /// Override the memory usage in KiB.
76 pub fn with_memory_kib(mut self, v: u32) -> Self {
77 self.memory_kib = v;
78 self
79 }
80 /// Override the time cost (number of iterations).
81 pub fn with_time_cost(mut self, v: u32) -> Self {
82 self.time_cost = v;
83 self
84 }
85 /// Override the number of parallel threads.
86 pub fn with_parallelism(mut self, v: u32) -> Self {
87 self.parallelism = v;
88 self
89 }
90}
91
92impl Default for Argon2Config {
93 fn default() -> Self {
94 Argon2Config::high_security()
95 }
96}
97
98/// Preset selector for convenience.
99#[derive(Debug, Clone, Copy)]
100pub enum Argon2Preset {
101 /// High security preset for production environments (64 MiB memory, 3 iterations).
102 HighSecurity,
103 /// Interactive preset balanced for user-facing applications (32 MiB memory, 2 iterations).
104 Interactive,
105 /// Fast preset for development and testing (4 MiB memory, 1 iteration).
106 #[cfg(any(feature = "insecure-fast-hash", debug_assertions))]
107 DevFast,
108}
109
110impl Argon2Preset {
111 /// Convert this preset to an `Argon2Config`.
112 pub fn to_config(self) -> Argon2Config {
113 match self {
114 Self::HighSecurity => Argon2Config::high_security(),
115 Self::Interactive => Argon2Config::interactive(),
116 #[cfg(any(feature = "insecure-fast-hash", debug_assertions))]
117 Self::DevFast => Argon2Config::dev_fast(),
118 }
119 }
120}
121
122/// Configurable Argon2id hasher.
123#[derive(Clone)]
124pub struct Argon2Hasher {
125 config: Argon2Config,
126 engine: Argon2<'static>,
127}
128
129impl Argon2Hasher {
130 /// Creates a new instance with recommended settings based on the current build (dev/release).
131 pub fn new_recommended() -> Result<Self> {
132 if cfg!(debug_assertions) {
133 #[cfg(any(feature = "insecure-fast-hash", debug_assertions))]
134 {
135 return Self::dev_fast();
136 }
137 }
138 // Fallback / release: always high security
139 Self::high_security()
140 }
141 /// Create from explicit configuration.
142 pub fn from_config(config: Argon2Config) -> Result<Self> {
143 let params = Params::new(
144 config.memory_kib,
145 config.time_cost,
146 config.parallelism,
147 None,
148 )?;
149 let engine = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
150 Ok(Self { config, engine })
151 }
152
153 /// Create from a preset.
154 pub fn from_preset(preset: Argon2Preset) -> Result<Self> {
155 Self::from_config(preset.to_config())
156 }
157
158 /// Return current configuration.
159 pub fn config(&self) -> &Argon2Config {
160 &self.config
161 }
162
163 /// Maximum security hasher for production environments.
164 ///
165 /// **Parameters:**
166 /// - Memory: 64 MiB (65,536 KiB)
167 /// - Time cost: 3 iterations
168 /// - Parallelism: 1 thread
169 ///
170 /// **Use cases:**
171 /// - Production servers with sufficient memory
172 /// - High-value accounts requiring maximum security
173 /// - Applications where authentication latency is acceptable (~100-200ms)
174 ///
175 /// **Security:** Provides excellent protection against brute-force attacks
176 /// and rainbow tables, suitable for protecting sensitive user credentials.
177 ///
178 /// **Performance:** Slowest option, designed for security over speed.
179 pub fn high_security() -> Result<Self> {
180 Self::from_preset(Argon2Preset::HighSecurity)
181 }
182
183 /// Balanced hasher for interactive applications.
184 ///
185 /// **Parameters:**
186 /// - Memory: 32 MiB (32,768 KiB)
187 /// - Time cost: 2 iterations
188 /// - Parallelism: 1 thread
189 ///
190 /// **Use cases:**
191 /// - Web applications with user-facing login forms
192 /// - Mobile applications where response time matters
193 /// - Services with moderate security requirements
194 /// - Memory-constrained production environments
195 ///
196 /// **Security:** Good security level, still resistant to most attacks
197 /// while providing reasonable authentication response times.
198 ///
199 /// **Performance:** Moderate speed (~50-100ms), good balance of security and usability.
200 pub fn interactive() -> Result<Self> {
201 Self::from_preset(Argon2Preset::Interactive)
202 }
203
204 /// Fast hasher for development and testing only.
205 ///
206 /// **⚠️ WARNING: DO NOT USE IN PRODUCTION**
207 ///
208 /// **Parameters:**
209 /// - Memory: 4 MiB (4,096 KiB)
210 /// - Time cost: 1 iteration
211 /// - Parallelism: 1 thread
212 ///
213 /// **Use cases:**
214 /// - Local development to speed up test cycles
215 /// - Unit tests that need fast password hashing
216 /// - CI/CD pipelines to reduce build times
217 /// - Debug builds (automatically used by `default()`)
218 ///
219 /// **Security:** ⚠️ Insufficient for production use - vulnerable to brute-force attacks
220 ///
221 /// **Performance:** Very fast (~5-20ms), prioritizes development speed over security.
222 ///
223 /// This preset is only available in debug builds or when the `insecure-fast-hash`
224 /// feature is explicitly enabled.
225 #[cfg(any(feature = "insecure-fast-hash", debug_assertions))]
226 pub fn dev_fast() -> Result<Self> {
227 Self::from_preset(Argon2Preset::DevFast)
228 }
229}
230
231impl HashingService for Argon2Hasher {
232 fn hash_value(&self, plain_value: &str) -> Result<HashedValue> {
233 let salt = SaltString::generate(&mut OsRng);
234 Ok(self
235 .engine
236 .hash_password(plain_value.as_bytes(), &salt)
237 .map_err(|e| {
238 Error::Hashing(HashingError::with_context(
239 HashingOperation::Hash,
240 format!("Could not hash secret: {e}"),
241 Some("Argon2id".to_string()),
242 Some("PHC".to_string()),
243 ))
244 })?
245 .to_string())
246 }
247
248 fn verify_value(&self, plain_value: &str, hashed_value: &str) -> Result<VerificationResult> {
249 let hash = PasswordHash::new(hashed_value).map_err(|e| {
250 Error::Hashing(HashingError::with_context(
251 HashingOperation::Verify,
252 format!("Could not parse stored hash: {e}"),
253 Some("Argon2id".to_string()),
254 Some("PHC".to_string()),
255 ))
256 })?;
257 Ok(VerificationResult::from(
258 self.engine
259 .verify_password(plain_value.as_bytes(), &hash)
260 .is_ok(),
261 ))
262 }
263}
264
265#[cfg(test)]
266#[allow(clippy::unwrap_used)]
267mod tests {
268 use super::*;
269 use crate::hashing::HashingService;
270
271 #[test]
272 fn default_build_mode() {
273 let hasher = Argon2Hasher::new_recommended().unwrap();
274 let hash = hasher.hash_value("pw").unwrap();
275 assert!(matches!(
276 hasher.verify_value("pw", &hash),
277 Ok(VerificationResult::Ok)
278 ));
279 }
280
281 #[test]
282 fn presets_work() {
283 for preset in [
284 Argon2Preset::HighSecurity,
285 Argon2Preset::Interactive,
286 #[cfg(any(feature = "insecure-fast-hash", debug_assertions))]
287 Argon2Preset::DevFast,
288 ] {
289 let hasher = Argon2Hasher::from_preset(preset).unwrap();
290 let h = hasher.hash_value("secret").unwrap();
291 assert_eq!(
292 VerificationResult::Ok,
293 hasher.verify_value("secret", &h).unwrap()
294 );
295 assert_eq!(
296 VerificationResult::Unauthorized,
297 hasher.verify_value("other", &h).unwrap()
298 );
299 }
300 }
301
302 #[test]
303 fn custom_config() {
304 let cfg = Argon2Config::default()
305 .with_memory_kib(48 * 1024)
306 .with_time_cost(2)
307 .with_parallelism(1);
308 let hasher = Argon2Hasher::from_config(cfg).unwrap();
309 let h = hasher.hash_value("abc").unwrap();
310 assert!(matches!(
311 hasher.verify_value("abc", &h),
312 Ok(VerificationResult::Ok)
313 ));
314 }
315}