1use aes_gcm::{
7 aead::{Aead, AeadCore, KeyInit, OsRng},
8 Aes256Gcm, Key, Nonce,
9};
10use argon2::{
11 password_hash::{rand_core::RngCore, PasswordHasher, SaltString},
12 Argon2, Params,
13};
14use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
15use serde::{Deserialize, Serialize};
16use std::fmt;
17use thiserror::Error;
18
19#[derive(Debug, Error)]
21pub enum CryptoError {
22 #[error("Invalid key: {message}")]
24 InvalidKey { message: String },
25
26 #[error("Encryption failed: {message}")]
28 EncryptionFailed { message: String },
29
30 #[error("Decryption failed: {message}")]
32 DecryptionFailed { message: String },
33
34 #[error("Key derivation failed: {message}")]
36 KeyDerivationFailed { message: String },
37
38 #[error("Invalid ciphertext format: {message}")]
40 InvalidCiphertext { message: String },
41
42 #[error("Base64 error: {message}")]
44 Base64Error { message: String },
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct EncryptedData {
50 pub ciphertext: String,
52 pub nonce: String,
54 pub salt: String,
56 pub algorithm: String,
58 pub kdf: String,
60}
61
62impl fmt::Display for EncryptedData {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 write!(f, "EncryptedData(algorithm={})", self.algorithm)
65 }
66}
67
68pub struct Aes256GcmCrypto;
70
71impl Default for Aes256GcmCrypto {
72 fn default() -> Self {
73 Self::new()
74 }
75}
76
77impl Aes256GcmCrypto {
78 pub fn new() -> Self {
80 Self
81 }
82
83 pub fn encrypt(&self, plaintext: &[u8], key: &str) -> Result<Vec<u8>, CryptoError> {
85 let key_bytes = BASE64.decode(key).map_err(|e| CryptoError::InvalidKey {
87 message: format!("Invalid base64 key: {}", e),
88 })?;
89
90 if key_bytes.len() != 32 {
91 return Err(CryptoError::InvalidKey {
92 message: "Key must be 32 bytes".to_string(),
93 });
94 }
95
96 let cipher_key = Key::<Aes256Gcm>::from_slice(&key_bytes);
97 let cipher = Aes256Gcm::new(cipher_key);
98
99 let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
101
102 let ciphertext =
104 cipher
105 .encrypt(&nonce, plaintext)
106 .map_err(|e| CryptoError::EncryptionFailed {
107 message: e.to_string(),
108 })?;
109
110 let mut result = Vec::with_capacity(12 + ciphertext.len());
112 result.extend_from_slice(&nonce);
113 result.extend_from_slice(&ciphertext);
114
115 Ok(result)
116 }
117
118 pub fn decrypt(&self, encrypted_data: &[u8], key: &str) -> Result<Vec<u8>, CryptoError> {
120 if encrypted_data.len() < 12 {
121 return Err(CryptoError::InvalidCiphertext {
122 message: "Encrypted data too short".to_string(),
123 });
124 }
125
126 let key_bytes = BASE64.decode(key).map_err(|e| CryptoError::InvalidKey {
128 message: format!("Invalid base64 key: {}", e),
129 })?;
130
131 if key_bytes.len() != 32 {
132 return Err(CryptoError::InvalidKey {
133 message: "Key must be 32 bytes".to_string(),
134 });
135 }
136
137 let cipher_key = Key::<Aes256Gcm>::from_slice(&key_bytes);
138 let cipher = Aes256Gcm::new(cipher_key);
139
140 let (nonce_bytes, ciphertext) = encrypted_data.split_at(12);
142 let nonce = Nonce::from_slice(nonce_bytes);
143
144 let plaintext =
146 cipher
147 .decrypt(nonce, ciphertext)
148 .map_err(|e| CryptoError::DecryptionFailed {
149 message: e.to_string(),
150 })?;
151
152 Ok(plaintext)
153 }
154
155 pub fn encrypt_with_password(
157 plaintext: &[u8],
158 password: &str,
159 ) -> Result<EncryptedData, CryptoError> {
160 let mut salt = [0u8; 32];
162 OsRng.fill_bytes(&mut salt);
163 let salt_string =
164 SaltString::encode_b64(&salt).map_err(|e| CryptoError::KeyDerivationFailed {
165 message: e.to_string(),
166 })?;
167
168 let params = Params::new(19 * 1024, 2, 1, Some(32)).map_err(|e| {
173 CryptoError::KeyDerivationFailed {
174 message: format!("Invalid Argon2 parameters: {}", e),
175 }
176 })?;
177 let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
178 let password_hash = argon2
179 .hash_password(password.as_bytes(), &salt_string)
180 .map_err(|e| CryptoError::KeyDerivationFailed {
181 message: e.to_string(),
182 })?;
183
184 let hash_binding = password_hash
186 .hash
187 .ok_or_else(|| CryptoError::KeyDerivationFailed {
188 message: "Password hash generation returned None".to_string(),
189 })?;
190 let key_bytes = hash_binding.as_bytes();
191 if key_bytes.len() < 32 {
192 return Err(CryptoError::InvalidKey {
193 message: "Derived key too short".to_string(),
194 });
195 }
196
197 let key = Key::<Aes256Gcm>::from_slice(&key_bytes[..32]);
198 let cipher = Aes256Gcm::new(key);
199
200 let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
202
203 let ciphertext =
205 cipher
206 .encrypt(&nonce, plaintext)
207 .map_err(|e| CryptoError::EncryptionFailed {
208 message: e.to_string(),
209 })?;
210
211 Ok(EncryptedData {
212 ciphertext: BASE64.encode(&ciphertext),
213 nonce: BASE64.encode(nonce),
214 salt: BASE64.encode(salt),
215 algorithm: "AES-256-GCM".to_string(),
216 kdf: "Argon2".to_string(),
217 })
218 }
219
220 pub fn decrypt_with_password(
222 encrypted_data: &EncryptedData,
223 password: &str,
224 ) -> Result<Vec<u8>, CryptoError> {
225 let ciphertext =
227 BASE64
228 .decode(&encrypted_data.ciphertext)
229 .map_err(|e| CryptoError::Base64Error {
230 message: e.to_string(),
231 })?;
232
233 let nonce_bytes =
234 BASE64
235 .decode(&encrypted_data.nonce)
236 .map_err(|e| CryptoError::Base64Error {
237 message: e.to_string(),
238 })?;
239
240 let salt = BASE64
241 .decode(&encrypted_data.salt)
242 .map_err(|e| CryptoError::Base64Error {
243 message: e.to_string(),
244 })?;
245
246 let salt_string =
248 SaltString::encode_b64(&salt).map_err(|e| CryptoError::KeyDerivationFailed {
249 message: e.to_string(),
250 })?;
251
252 let params = Params::new(19 * 1024, 2, 1, Some(32)).map_err(|e| {
254 CryptoError::KeyDerivationFailed {
255 message: format!("Invalid Argon2 parameters: {}", e),
256 }
257 })?;
258 let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
259 let password_hash = argon2
260 .hash_password(password.as_bytes(), &salt_string)
261 .map_err(|e| CryptoError::KeyDerivationFailed {
262 message: e.to_string(),
263 })?;
264
265 let hash_binding = password_hash
266 .hash
267 .ok_or_else(|| CryptoError::KeyDerivationFailed {
268 message: "Password hash generation returned None".to_string(),
269 })?;
270 let key_bytes = hash_binding.as_bytes();
271 if key_bytes.len() < 32 {
272 return Err(CryptoError::InvalidKey {
273 message: "Derived key too short".to_string(),
274 });
275 }
276
277 let key = Key::<Aes256Gcm>::from_slice(&key_bytes[..32]);
278 let cipher = Aes256Gcm::new(key);
279
280 if nonce_bytes.len() != 12 {
282 return Err(CryptoError::InvalidCiphertext {
283 message: "Invalid nonce length".to_string(),
284 });
285 }
286 let nonce = Nonce::from_slice(&nonce_bytes);
287
288 let plaintext = cipher.decrypt(nonce, ciphertext.as_ref()).map_err(|e| {
290 CryptoError::DecryptionFailed {
291 message: e.to_string(),
292 }
293 })?;
294
295 Ok(plaintext)
296 }
297}
298
299pub struct KeyUtils;
301
302impl Default for KeyUtils {
303 fn default() -> Self {
304 Self::new()
305 }
306}
307
308impl KeyUtils {
309 pub fn new() -> Self {
311 Self
312 }
313
314 pub fn get_or_create_key(&self) -> Result<String, CryptoError> {
333 if let Ok(key) = self.get_key_from_keychain("symbiont", "secrets") {
335 tracing::debug!("Using encryption key from system keychain");
336 return Ok(key);
337 }
338
339 if let Ok(key) = Self::get_key_from_env("SYMBIONT_MASTER_KEY") {
341 tracing::info!("Using encryption key from SYMBIONT_MASTER_KEY environment variable");
342 return Ok(key);
343 }
344
345 tracing::warn!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
347 tracing::warn!("⚠️ SECURITY WARNING: No encryption key found!");
348 tracing::warn!("⚠️ Generating a new random encryption key.");
349 tracing::warn!("⚠️ If you have existing encrypted data, it will be UNRECOVERABLE!");
350 tracing::warn!("⚠️ The new key will be stored in the system keychain.");
351 tracing::warn!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
352
353 eprintln!("\n⚠️ CRITICAL SECURITY WARNING:");
354 eprintln!("⚠️ No encryption key found in keychain or environment!");
355 eprintln!("⚠️ Generating new random key - existing encrypted data will be lost!");
356 eprintln!("⚠️ Set SYMBIONT_MASTER_KEY environment variable to use a specific key.\n");
357
358 let new_key = self.generate_key();
360
361 match self.store_key_in_keychain("symbiont", "secrets", &new_key) {
363 Ok(_) => {
364 tracing::info!("✓ New encryption key stored in system keychain");
365 eprintln!("✓ New encryption key stored in system keychain");
366 }
367 Err(e) => {
368 tracing::error!("✗ Failed to store key in keychain: {}", e);
369 eprintln!("✗ ERROR: Failed to store key in keychain: {}", e);
370 eprintln!("✗ You MUST set SYMBIONT_MASTER_KEY environment variable.");
371 eprintln!("✗ The generated key has been used but could not be persisted.");
372
373 if std::env::var("SYMBIONT_ENV").unwrap_or_default() == "production" {
375 return Err(CryptoError::InvalidKey {
376 message: format!(
377 "Failed to store encryption key in production mode: {}",
378 e
379 ),
380 });
381 }
382 }
383 }
384
385 Ok(new_key)
386 }
387
388 pub fn generate_key(&self) -> String {
390 use base64::Engine;
391 let mut key_bytes = [0u8; 32];
392 OsRng.fill_bytes(&mut key_bytes);
393 BASE64.encode(key_bytes)
394 }
395
396 #[cfg(feature = "keychain")]
398 fn store_key_in_keychain(
399 &self,
400 service: &str,
401 account: &str,
402 key: &str,
403 ) -> Result<(), CryptoError> {
404 use keyring::Entry;
405
406 let entry = Entry::new(service, account).map_err(|e| CryptoError::InvalidKey {
407 message: format!("Failed to create keychain entry: {}", e),
408 })?;
409
410 entry
411 .set_password(key)
412 .map_err(|e| CryptoError::InvalidKey {
413 message: format!("Failed to store in keychain: {}", e),
414 })
415 }
416
417 #[cfg(not(feature = "keychain"))]
418 fn store_key_in_keychain(
419 &self,
420 _service: &str,
421 _account: &str,
422 _key: &str,
423 ) -> Result<(), CryptoError> {
424 Err(CryptoError::InvalidKey {
425 message: "Keychain support not enabled. Compile with 'keychain' feature.".to_string(),
426 })
427 }
428
429 pub fn get_key_from_env(env_var: &str) -> Result<String, CryptoError> {
431 std::env::var(env_var).map_err(|_| CryptoError::InvalidKey {
432 message: format!("Environment variable {} not found", env_var),
433 })
434 }
435
436 #[cfg(feature = "keychain")]
438 pub fn get_key_from_keychain(
439 &self,
440 service: &str,
441 account: &str,
442 ) -> Result<String, CryptoError> {
443 use keyring::Entry;
444
445 let entry = Entry::new(service, account).map_err(|e| CryptoError::InvalidKey {
446 message: format!("Failed to create keychain entry: {}", e),
447 })?;
448
449 entry.get_password().map_err(|e| CryptoError::InvalidKey {
450 message: format!("Failed to retrieve from keychain: {}", e),
451 })
452 }
453
454 #[cfg(not(feature = "keychain"))]
455 pub fn get_key_from_keychain(
456 &self,
457 _service: &str,
458 _account: &str,
459 ) -> Result<String, CryptoError> {
460 Err(CryptoError::InvalidKey {
461 message: "Keychain support not enabled. Compile with 'keychain' feature.".to_string(),
462 })
463 }
464}
465
466#[cfg(test)]
467mod tests {
468 use super::*;
469
470 #[test]
471 fn test_encrypt_decrypt_roundtrip() {
472 let plaintext = b"Hello, world!";
473 let password = "test1"; let encrypted = Aes256GcmCrypto::encrypt_with_password(plaintext, password).unwrap();
476 let decrypted = Aes256GcmCrypto::decrypt_with_password(&encrypted, password).unwrap();
477
478 assert_eq!(plaintext, decrypted.as_slice());
479 }
480
481 #[test]
482 fn test_encrypt_decrypt_wrong_password() {
483 let plaintext = b"Hello, world!";
484 let password = "test1"; let wrong_password = "wrong1"; let encrypted = Aes256GcmCrypto::encrypt_with_password(plaintext, password).unwrap();
488 let result = Aes256GcmCrypto::decrypt_with_password(&encrypted, wrong_password);
489
490 assert!(result.is_err());
491 }
492
493 #[test]
494 fn test_direct_encrypt_decrypt_roundtrip() {
495 let plaintext = b"Hello, world!";
496 let key_utils = KeyUtils::new();
497 let key = key_utils.generate_key();
498
499 let crypto = Aes256GcmCrypto::new();
500 let encrypted = crypto.encrypt(plaintext, &key).unwrap();
501 let decrypted = crypto.decrypt(&encrypted, &key).unwrap();
502
503 assert_eq!(plaintext, decrypted.as_slice());
504 }
505
506 #[test]
507 fn test_get_key_from_env() {
508 std::env::set_var("TEST_KEY", "test_value");
509 let result = KeyUtils::get_key_from_env("TEST_KEY").unwrap();
510 assert_eq!(result, "test_value");
511
512 let missing_result = KeyUtils::get_key_from_env("MISSING_KEY");
513 assert!(missing_result.is_err());
514 }
515}