Skip to main content

webgates_secrets/hashing/
argon2.rs

1//! Argon2id hashing implementation.
2//!
3//! This module provides the default [`HashingService`] implementation for
4//! `webgates-secrets`.
5//!
6//! [`Argon2Hasher`] uses explicit, reviewable presets and produces PHC-formatted
7//! hash strings that can be stored directly.
8//!
9//! # Examples
10//!
11//! ```rust
12//! use webgates_core::verification_result::VerificationResult;
13//! use webgates_secrets::hashing::argon2::Argon2Hasher;
14//! use webgates_secrets::hashing::hashing_service::HashingService;
15//!
16//! let hasher = Argon2Hasher::new_recommended().unwrap();
17//! let hash = hasher.hash_value("secret").unwrap();
18//!
19//! assert_eq!(
20//!     hasher.verify_value("secret", &hash).unwrap(),
21//!     VerificationResult::Ok
22//! );
23//! assert_eq!(
24//!     hasher.verify_value("other", &hash).unwrap(),
25//!     VerificationResult::Unauthorized
26//! );
27//! ```
28use 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/// Configures the Argon2id memory, time, and parallelism parameters.
37#[derive(Debug, Clone, Copy)]
38pub struct Argon2Config {
39    /// Memory usage in KiB for the Argon2 algorithm.
40    pub memory_kib: u32,
41    /// Number of iterations (time cost) for the Argon2 algorithm.
42    pub time_cost: u32,
43    /// Number of parallel threads to use during hashing.
44    pub parallelism: u32,
45}
46
47impl Argon2Config {
48    /// Returns a high-security configuration for production environments.
49    ///
50    /// This preset uses 64 MiB of memory, 3 iterations, and 1 thread.
51    pub fn high_security() -> Self {
52        Self {
53            memory_kib: 64 * 1024, // 64 MiB
54            time_cost: 3,
55            parallelism: 1,
56        }
57    }
58    /// Returns an interactive configuration for user-facing applications.
59    ///
60    /// This preset uses 32 MiB of memory, 2 iterations, and 1 thread.
61    pub fn interactive() -> Self {
62        Self {
63            memory_kib: 32 * 1024,
64            time_cost: 2,
65            parallelism: 1,
66        }
67    }
68    /// Override the memory usage in KiB.
69    pub fn with_memory_kib(mut self, v: u32) -> Self {
70        self.memory_kib = v;
71        self
72    }
73    /// Override the time cost (number of iterations).
74    pub fn with_time_cost(mut self, v: u32) -> Self {
75        self.time_cost = v;
76        self
77    }
78    /// Override the number of parallel threads.
79    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/// Preset selector for common Argon2id configurations.
92#[derive(Debug, Clone, Copy)]
93pub enum Argon2Preset {
94    /// High security preset for production environments (64 MiB memory, 3 iterations).
95    HighSecurity,
96    /// Interactive preset balanced for user-facing applications (32 MiB memory, 2 iterations).
97    Interactive,
98}
99
100impl Argon2Preset {
101    /// Convert this preset to an `Argon2Config`.
102    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/// Argon2id hasher with explicit, reviewable configuration.
111///
112/// This is the default hashing implementation provided by the crate.
113#[derive(Clone)]
114pub struct Argon2Hasher {
115    config: Argon2Config,
116    engine: Argon2<'static>,
117}
118
119impl Argon2Hasher {
120    /// Creates a new hasher using the crate's recommended preset.
121    pub fn new_recommended() -> Result<Self> {
122        Self::high_security()
123    }
124    /// Creates a hasher from explicit configuration.
125    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    /// Creates a hasher from a preset.
137    pub fn from_preset(preset: Argon2Preset) -> Result<Self> {
138        Self::from_config(preset.to_config())
139    }
140
141    /// Returns the current configuration.
142    pub fn config(&self) -> &Argon2Config {
143        &self.config
144    }
145
146    /// Creates a high-security hasher for production environments.
147    ///
148    /// # Defaults
149    ///
150    /// - memory: 64 MiB
151    /// - time cost: 3 iterations
152    /// - parallelism: 1 thread
153    pub fn high_security() -> Result<Self> {
154        Self::from_preset(Argon2Preset::HighSecurity)
155    }
156
157    /// Creates an interactive hasher for user-facing applications.
158    ///
159    /// # Defaults
160    ///
161    /// - memory: 32 MiB
162    /// - time cost: 2 iterations
163    /// - parallelism: 1 thread
164    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}