1use aes_gcm::{
4 Aes256Gcm, Nonce as AesNonce,
5 aead::{Aead, KeyInit},
6};
7use argon2::{Algorithm as Argon2Algorithm, Argon2, Params, Version};
8use chacha20poly1305::{ChaCha20Poly1305, Nonce as ChaChaNonce};
9use hkdf::Hkdf;
10use sha2::Sha256;
11
12use crate::crypto::EncryptionAlgorithm;
13use crate::error::AgentError;
14
15pub const TAG_LEN: usize = 1;
17pub const NONCE_LEN: usize = 12; pub const SALT_LEN: usize = 16;
21pub const SYMMETRIC_KEY_LEN: usize = 32;
23
24pub const ARGON2_TAG: u8 = 3;
26const ARGON2_PARAMS_LEN: usize = 12;
28
29pub fn get_kdf_params() -> Result<Params, AgentError> {
46 #[cfg(not(any(test, feature = "test-utils")))]
47 let params = Params::new(65536, 3, 1, Some(SYMMETRIC_KEY_LEN));
48 #[cfg(any(test, feature = "test-utils"))]
49 let params = Params::new(8, 1, 1, Some(SYMMETRIC_KEY_LEN));
50 params.map_err(|e| AgentError::CryptoError(format!("Invalid Argon2 params: {}", e)))
51}
52
53pub fn encrypt_bytes(
55 data: &[u8],
56 passphrase: &str,
57 algo: EncryptionAlgorithm,
58) -> Result<Vec<u8>, AgentError> {
59 let salt: [u8; SALT_LEN] = rand::random();
60 let hk = Hkdf::<Sha256>::new(Some(&salt), passphrase.as_bytes());
61 let mut key = [0u8; SYMMETRIC_KEY_LEN];
62 hk.expand(&[], &mut key)
63 .map_err(|_| AgentError::CryptoError("HKDF expand failed".into()))?;
64
65 let nonce: [u8; NONCE_LEN] = rand::random();
66
67 match algo {
68 EncryptionAlgorithm::AesGcm256 => {
69 let cipher = Aes256Gcm::new_from_slice(&key)
70 .map_err(|_| AgentError::CryptoError("Invalid AES key".into()))?;
71
72 let ciphertext = cipher
73 .encrypt(AesNonce::from_slice(&nonce), data)
74 .map_err(|_| AgentError::CryptoError("AES encryption failed".into()))?;
75
76 let mut out = vec![algo.tag()];
77 out.extend_from_slice(&salt);
78 out.extend_from_slice(&nonce);
79 out.extend_from_slice(&ciphertext);
80 Ok(out)
81 }
82
83 EncryptionAlgorithm::ChaCha20Poly1305 => {
84 let cipher = ChaCha20Poly1305::new_from_slice(&key)
85 .map_err(|_| AgentError::CryptoError("Invalid ChaCha key".into()))?;
86
87 let ciphertext = cipher
88 .encrypt(ChaChaNonce::from_slice(&nonce), data)
89 .map_err(|_| AgentError::CryptoError("ChaCha encryption failed".into()))?;
90
91 let mut out = vec![algo.tag()];
92 out.extend_from_slice(&salt);
93 out.extend_from_slice(&nonce);
94 out.extend_from_slice(&ciphertext);
95 Ok(out)
96 }
97 }
98}
99
100pub fn validate_passphrase(passphrase: &str) -> Result<(), AgentError> {
105 if passphrase.len() < 12 {
106 return Err(AgentError::WeakPassphrase(format!(
107 "Passphrase must be at least 12 characters (got {})",
108 passphrase.len()
109 )));
110 }
111
112 let has_lower = passphrase.chars().any(|c| c.is_ascii_lowercase());
113 let has_upper = passphrase.chars().any(|c| c.is_ascii_uppercase());
114 let has_digit = passphrase.chars().any(|c| c.is_ascii_digit());
115 let has_symbol = passphrase.chars().any(|c| {
116 c.is_ascii_punctuation() || (c.is_ascii() && !c.is_ascii_alphanumeric() && c != ' ')
117 });
118
119 let class_count = has_lower as u8 + has_upper as u8 + has_digit as u8 + has_symbol as u8;
120
121 if class_count < 3 {
122 return Err(AgentError::WeakPassphrase(format!(
123 "Passphrase must contain at least 3 of 4 character classes \
124 (lowercase, uppercase, digit, symbol); found {}",
125 class_count
126 )));
127 }
128
129 Ok(())
130}
131
132pub fn encrypt_bytes_argon2(
136 data: &[u8],
137 passphrase: &str,
138 algo: EncryptionAlgorithm,
139) -> Result<Vec<u8>, AgentError> {
140 validate_passphrase(passphrase)?;
141
142 let salt: [u8; SALT_LEN] = rand::random();
143
144 let params = get_kdf_params()?;
146 let m_cost = params.m_cost();
147 let t_cost = params.t_cost();
148 let p_cost = params.p_cost();
149 let argon2 = Argon2::new(Argon2Algorithm::Argon2id, Version::V0x13, params);
150 let mut key = [0u8; SYMMETRIC_KEY_LEN];
151 argon2
152 .hash_password_into(passphrase.as_bytes(), &salt, &mut key)
153 .map_err(|e| AgentError::CryptoError(format!("Argon2 key derivation failed: {}", e)))?;
154
155 let nonce: [u8; NONCE_LEN] = rand::random();
156
157 let ciphertext = match algo {
158 EncryptionAlgorithm::AesGcm256 => {
159 let cipher = Aes256Gcm::new_from_slice(&key)
160 .map_err(|_| AgentError::CryptoError("Invalid AES key".into()))?;
161 cipher
162 .encrypt(AesNonce::from_slice(&nonce), data)
163 .map_err(|_| AgentError::CryptoError("AES encryption failed".into()))?
164 }
165 EncryptionAlgorithm::ChaCha20Poly1305 => {
166 let cipher = ChaCha20Poly1305::new_from_slice(&key)
167 .map_err(|_| AgentError::CryptoError("Invalid ChaCha key".into()))?;
168 cipher
169 .encrypt(ChaChaNonce::from_slice(&nonce), data)
170 .map_err(|_| AgentError::CryptoError("ChaCha encryption failed".into()))?
171 }
172 };
173
174 let mut out = Vec::with_capacity(
176 TAG_LEN + SALT_LEN + ARGON2_PARAMS_LEN + TAG_LEN + NONCE_LEN + ciphertext.len(),
177 );
178 out.push(ARGON2_TAG);
179 out.extend_from_slice(&salt);
180 out.extend_from_slice(&m_cost.to_le_bytes());
181 out.extend_from_slice(&t_cost.to_le_bytes());
182 out.extend_from_slice(&p_cost.to_le_bytes());
183 out.push(algo.tag());
184 out.extend_from_slice(&nonce);
185 out.extend_from_slice(&ciphertext);
186 Ok(out)
187}
188
189pub fn decrypt_bytes(encrypted: &[u8], passphrase: &str) -> Result<Vec<u8>, AgentError> {
198 if encrypted.is_empty() {
199 return Err(AgentError::CryptoError("Encrypted data too short".into()));
200 }
201
202 let tag = encrypted[0];
203
204 if tag == ARGON2_TAG {
205 return decrypt_bytes_argon2(encrypted, passphrase);
206 }
207
208 if encrypted.len() < TAG_LEN + SALT_LEN + NONCE_LEN {
210 return Err(AgentError::CryptoError("Encrypted data too short".into()));
211 }
212
213 let algo = EncryptionAlgorithm::from_tag(tag)
214 .ok_or_else(|| AgentError::CryptoError(format!("Unknown encryption tag: {}", tag)))?;
215
216 let rest = &encrypted[TAG_LEN..];
217 let (salt, remaining) = rest.split_at(SALT_LEN);
218 let (nonce, ciphertext) = remaining.split_at(NONCE_LEN);
219
220 let hkdf = Hkdf::<Sha256>::new(Some(salt), passphrase.as_bytes());
221 let mut key = [0u8; SYMMETRIC_KEY_LEN];
222 hkdf.expand(&[], &mut key)
223 .map_err(|_| AgentError::CryptoError("HKDF expand failed".into()))?;
224
225 let result = match algo {
226 EncryptionAlgorithm::AesGcm256 => Aes256Gcm::new_from_slice(&key)
227 .map_err(|_| AgentError::CryptoError("Invalid AES key".into()))?
228 .decrypt(AesNonce::from_slice(nonce), ciphertext),
229
230 EncryptionAlgorithm::ChaCha20Poly1305 => ChaCha20Poly1305::new_from_slice(&key)
231 .map_err(|_| AgentError::CryptoError("Invalid ChaCha key".into()))?
232 .decrypt(ChaChaNonce::from_slice(nonce), ciphertext),
233 };
234
235 match result {
236 Ok(plaintext) => Ok(plaintext),
237 Err(_) => Err(AgentError::IncorrectPassphrase),
238 }
239}
240
241fn decrypt_bytes_argon2(encrypted: &[u8], passphrase: &str) -> Result<Vec<u8>, AgentError> {
245 const MIN_LEN: usize = TAG_LEN + SALT_LEN + ARGON2_PARAMS_LEN + TAG_LEN + NONCE_LEN;
246 if encrypted.len() < MIN_LEN {
247 return Err(AgentError::CryptoError(
248 "Argon2id encrypted data too short".into(),
249 ));
250 }
251
252 let mut offset = TAG_LEN; let salt = &encrypted[offset..offset + SALT_LEN];
255 offset += SALT_LEN;
256
257 let m_cost = u32::from_le_bytes(
258 encrypted[offset..offset + 4]
259 .try_into()
260 .map_err(|_| AgentError::CryptoError("invalid m_cost bytes".into()))?,
261 );
262 offset += 4;
263 let t_cost = u32::from_le_bytes(
264 encrypted[offset..offset + 4]
265 .try_into()
266 .map_err(|_| AgentError::CryptoError("invalid t_cost bytes".into()))?,
267 );
268 offset += 4;
269 let p_cost = u32::from_le_bytes(
270 encrypted[offset..offset + 4]
271 .try_into()
272 .map_err(|_| AgentError::CryptoError("invalid p_cost bytes".into()))?,
273 );
274 offset += 4;
275
276 let algo_tag = encrypted[offset];
277 offset += 1;
278 let algo = EncryptionAlgorithm::from_tag(algo_tag)
279 .ok_or_else(|| AgentError::CryptoError(format!("Unknown encryption tag: {}", algo_tag)))?;
280
281 let nonce = &encrypted[offset..offset + NONCE_LEN];
282 offset += NONCE_LEN;
283
284 let ciphertext = &encrypted[offset..];
285
286 let params = Params::new(m_cost, t_cost, p_cost, Some(SYMMETRIC_KEY_LEN))
288 .map_err(|e| AgentError::CryptoError(format!("Invalid Argon2 params: {}", e)))?;
289 let argon2 = Argon2::new(Argon2Algorithm::Argon2id, Version::V0x13, params);
290 let mut key = [0u8; SYMMETRIC_KEY_LEN];
291 argon2
292 .hash_password_into(passphrase.as_bytes(), salt, &mut key)
293 .map_err(|e| AgentError::CryptoError(format!("Argon2 key derivation failed: {}", e)))?;
294
295 let result = match algo {
296 EncryptionAlgorithm::AesGcm256 => Aes256Gcm::new_from_slice(&key)
297 .map_err(|_| AgentError::CryptoError("Invalid AES key".into()))?
298 .decrypt(AesNonce::from_slice(nonce), ciphertext),
299
300 EncryptionAlgorithm::ChaCha20Poly1305 => ChaCha20Poly1305::new_from_slice(&key)
301 .map_err(|_| AgentError::CryptoError("Invalid ChaCha key".into()))?
302 .decrypt(ChaChaNonce::from_slice(nonce), ciphertext),
303 };
304
305 match result {
306 Ok(plaintext) => Ok(plaintext),
307 Err(_) => Err(AgentError::IncorrectPassphrase),
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314 use crate::crypto::EncryptionAlgorithm;
315
316 const STRONG_PASS: &str = "MyStr0ng!Pass";
317
318 #[test]
319 fn test_argon2_roundtrip_aes() {
320 let data = b"hello argon2 aes";
321 let encrypted =
322 encrypt_bytes_argon2(data, STRONG_PASS, EncryptionAlgorithm::AesGcm256).unwrap();
323 let decrypted = decrypt_bytes(&encrypted, STRONG_PASS).unwrap();
324 assert_eq!(data.as_slice(), decrypted.as_slice());
325 }
326
327 #[test]
328 fn test_argon2_roundtrip_chacha() {
329 let data = b"hello argon2 chacha";
330 let encrypted =
331 encrypt_bytes_argon2(data, STRONG_PASS, EncryptionAlgorithm::ChaCha20Poly1305).unwrap();
332 let decrypted = decrypt_bytes(&encrypted, STRONG_PASS).unwrap();
333 assert_eq!(data.as_slice(), decrypted.as_slice());
334 }
335
336 #[test]
337 fn test_legacy_hkdf_decrypt_still_works() {
338 let data = b"legacy data";
339 let encrypted =
340 encrypt_bytes(data, "any-passphrase", EncryptionAlgorithm::AesGcm256).unwrap();
341 let decrypted = decrypt_bytes(&encrypted, "any-passphrase").unwrap();
342 assert_eq!(data.as_slice(), decrypted.as_slice());
343 }
344
345 #[test]
346 fn test_argon2_wrong_passphrase() {
347 let data = b"secret";
348 let encrypted =
349 encrypt_bytes_argon2(data, STRONG_PASS, EncryptionAlgorithm::AesGcm256).unwrap();
350 let result = decrypt_bytes(&encrypted, "Wr0ng!Passphrase");
351 assert!(matches!(result, Err(AgentError::IncorrectPassphrase)));
352 }
353
354 #[test]
355 fn test_argon2_blob_starts_with_tag_3() {
356 let data = b"tag check";
357 let encrypted =
358 encrypt_bytes_argon2(data, STRONG_PASS, EncryptionAlgorithm::AesGcm256).unwrap();
359 assert_eq!(encrypted[0], ARGON2_TAG);
360 }
361
362 #[test]
363 fn test_unknown_tag_returns_error() {
364 let blob = vec![0xFF; 64];
365 let result = decrypt_bytes(&blob, "irrelevant");
366 assert!(matches!(result, Err(AgentError::CryptoError(_))));
367 }
368
369 #[test]
370 fn test_validate_passphrase_too_short() {
371 let result = validate_passphrase("Short1!");
372 assert!(matches!(result, Err(AgentError::WeakPassphrase(_))));
373 }
374
375 #[test]
376 fn test_validate_passphrase_insufficient_classes() {
377 let result = validate_passphrase("abcdefABCDEFgh");
379 assert!(matches!(result, Err(AgentError::WeakPassphrase(_))));
380 }
381
382 #[test]
383 fn test_validate_passphrase_strong() {
384 assert!(validate_passphrase(STRONG_PASS).is_ok());
385 }
386
387 #[test]
388 fn test_argon2_encrypt_rejects_weak() {
389 let result = encrypt_bytes_argon2(b"data", "weak", EncryptionAlgorithm::AesGcm256);
390 assert!(matches!(result, Err(AgentError::WeakPassphrase(_))));
391 }
392}