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}