Skip to main content

arcanum_hash/
argon2_impl.rs

1//! Argon2 password hashing.
2//!
3//! Argon2 is the winner of the Password Hashing Competition (PHC) and the
4//! recommended algorithm for password hashing. It provides:
5//!
6//! - **Memory-hardness**: Resistant to GPU/ASIC attacks
7//! - **Time-hardness**: Configurable computation time
8//! - **Parallelism**: Can utilize multiple cores
9//!
10//! We use **Argon2id** which combines Argon2i and Argon2d for best security.
11
12use crate::traits::PasswordHash;
13use arcanum_core::error::{Error, Result};
14use argon2::{
15    Algorithm, Argon2 as Argon2Inner, Params, Version,
16    password_hash::{PasswordHasher, PasswordVerifier, SaltString},
17};
18use rand::rngs::OsRng;
19use serde::{Deserialize, Serialize};
20
21/// Argon2id parameters.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct Argon2Params {
24    /// Memory cost in KiB (default: 64 MiB)
25    pub memory_cost: u32,
26    /// Time cost (iterations) (default: 3)
27    pub time_cost: u32,
28    /// Parallelism (lanes) (default: 4)
29    pub parallelism: u32,
30    /// Output length in bytes (default: 32)
31    pub output_len: usize,
32}
33
34impl Default for Argon2Params {
35    fn default() -> Self {
36        Self::moderate()
37    }
38}
39
40impl Argon2Params {
41    /// Create custom parameters.
42    pub fn new(memory_cost: u32, time_cost: u32, parallelism: u32) -> Self {
43        Self {
44            memory_cost,
45            time_cost,
46            parallelism,
47            output_len: 32,
48        }
49    }
50
51    /// Low security parameters (for testing or low-security contexts).
52    ///
53    /// Memory: 16 MiB, Time: 2, Parallelism: 1
54    pub fn low() -> Self {
55        Self {
56            memory_cost: 16 * 1024,
57            time_cost: 2,
58            parallelism: 1,
59            output_len: 32,
60        }
61    }
62
63    /// Moderate security parameters (good balance).
64    ///
65    /// Memory: 64 MiB, Time: 3, Parallelism: 4
66    pub fn moderate() -> Self {
67        Self {
68            memory_cost: 64 * 1024,
69            time_cost: 3,
70            parallelism: 4,
71            output_len: 32,
72        }
73    }
74
75    /// High security parameters (for sensitive data).
76    ///
77    /// Memory: 256 MiB, Time: 4, Parallelism: 4
78    pub fn high() -> Self {
79        Self {
80            memory_cost: 256 * 1024,
81            time_cost: 4,
82            parallelism: 4,
83            output_len: 32,
84        }
85    }
86
87    /// Maximum security parameters (for extremely sensitive data).
88    ///
89    /// Memory: 1 GiB, Time: 6, Parallelism: 4
90    pub fn maximum() -> Self {
91        Self {
92            memory_cost: 1024 * 1024,
93            time_cost: 6,
94            parallelism: 4,
95            output_len: 32,
96        }
97    }
98
99    /// OWASP recommended parameters (2024).
100    ///
101    /// Memory: 19 MiB, Time: 2, Parallelism: 1
102    pub fn owasp() -> Self {
103        Self {
104            memory_cost: 19 * 1024,
105            time_cost: 2,
106            parallelism: 1,
107            output_len: 32,
108        }
109    }
110}
111
112/// Argon2id password hashing.
113pub struct Argon2;
114
115impl PasswordHash for Argon2 {
116    type Params = Argon2Params;
117    const ALGORITHM: &'static str = "Argon2id";
118
119    fn hash_password(password: &[u8], params: &Self::Params) -> Result<String> {
120        let salt = SaltString::generate(&mut OsRng);
121
122        let argon2_params = Params::new(
123            params.memory_cost,
124            params.time_cost,
125            params.parallelism,
126            Some(params.output_len),
127        )
128        .map_err(|e| Error::InternalError(e.to_string()))?;
129
130        let argon2 = Argon2Inner::new(Algorithm::Argon2id, Version::V0x13, argon2_params);
131
132        let hash = argon2
133            .hash_password(password, &salt)
134            .map_err(|e| Error::InternalError(e.to_string()))?;
135
136        Ok(hash.to_string())
137    }
138
139    fn verify_password(password: &[u8], hash: &str) -> Result<bool> {
140        let parsed_hash =
141            argon2::PasswordHash::new(hash).map_err(|e| Error::ParseError(e.to_string()))?;
142
143        let argon2 = Argon2Inner::default();
144
145        Ok(argon2.verify_password(password, &parsed_hash).is_ok())
146    }
147
148    fn derive_key(
149        password: &[u8],
150        salt: &[u8],
151        params: &Self::Params,
152        output_len: usize,
153    ) -> Result<Vec<u8>> {
154        let argon2_params = Params::new(
155            params.memory_cost,
156            params.time_cost,
157            params.parallelism,
158            Some(output_len),
159        )
160        .map_err(|e| Error::InternalError(e.to_string()))?;
161
162        let argon2 = Argon2Inner::new(Algorithm::Argon2id, Version::V0x13, argon2_params);
163
164        let mut output = vec![0u8; output_len];
165        argon2
166            .hash_password_into(password, salt, &mut output)
167            .map_err(|e| Error::KeyDerivationFailed)?;
168
169        Ok(output)
170    }
171}
172
173impl Argon2 {
174    /// Hash a password with default (moderate) parameters.
175    pub fn hash(password: &[u8]) -> Result<String> {
176        Self::hash_password(password, &Argon2Params::default())
177    }
178
179    /// Verify a password against a hash.
180    pub fn verify(password: &[u8], hash: &str) -> Result<bool> {
181        Self::verify_password(password, hash)
182    }
183
184    /// Derive a 256-bit key from a password.
185    pub fn derive_key_256(password: &[u8], salt: &[u8]) -> Result<[u8; 32]> {
186        let key = Self::derive_key(password, salt, &Argon2Params::default(), 32)?;
187        let mut arr = [0u8; 32];
188        arr.copy_from_slice(&key);
189        Ok(arr)
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn test_hash_verify() {
199        let password = b"correct horse battery staple";
200        let hash = Argon2::hash_password(password, &Argon2Params::low()).unwrap();
201
202        // Correct password should verify
203        assert!(Argon2::verify_password(password, &hash).unwrap());
204
205        // Wrong password should fail
206        assert!(!Argon2::verify_password(b"wrong password", &hash).unwrap());
207    }
208
209    #[test]
210    fn test_hash_format() {
211        let password = b"test";
212        let hash = Argon2::hash_password(password, &Argon2Params::low()).unwrap();
213
214        // Should be in PHC string format
215        assert!(hash.starts_with("$argon2id$"));
216    }
217
218    #[test]
219    fn test_derive_key() {
220        let password = b"password";
221        let salt = b"somesalt12345678"; // 16 bytes minimum
222
223        let key1 = Argon2::derive_key(password, salt, &Argon2Params::low(), 32).unwrap();
224        let key2 = Argon2::derive_key(password, salt, &Argon2Params::low(), 32).unwrap();
225
226        // Same inputs should produce same output
227        assert_eq!(key1, key2);
228        assert_eq!(key1.len(), 32);
229
230        // Different password should produce different key
231        let key3 = Argon2::derive_key(b"different", salt, &Argon2Params::low(), 32).unwrap();
232        assert_ne!(key1, key3);
233    }
234
235    #[test]
236    fn test_different_salts() {
237        let password = b"password";
238        let hash1 = Argon2::hash_password(password, &Argon2Params::low()).unwrap();
239        let hash2 = Argon2::hash_password(password, &Argon2Params::low()).unwrap();
240
241        // Different salts should produce different hashes
242        assert_ne!(hash1, hash2);
243
244        // Both should still verify
245        assert!(Argon2::verify_password(password, &hash1).unwrap());
246        assert!(Argon2::verify_password(password, &hash2).unwrap());
247    }
248}