Skip to main content

symbi_runtime/
crypto.rs

1//! Cryptographic utilities for Symbiont
2//!
3//! This module provides encryption and decryption capabilities using industry-standard
4//! algorithms like AES-256-GCM for symmetric encryption and Argon2 for key derivation.
5
6use 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,
13};
14use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
15use serde::{Deserialize, Serialize};
16use std::fmt;
17use thiserror::Error;
18
19/// Errors that can occur during cryptographic operations
20#[derive(Debug, Error)]
21pub enum CryptoError {
22    /// Invalid key format or length
23    #[error("Invalid key: {message}")]
24    InvalidKey { message: String },
25
26    /// Encryption operation failed
27    #[error("Encryption failed: {message}")]
28    EncryptionFailed { message: String },
29
30    /// Decryption operation failed
31    #[error("Decryption failed: {message}")]
32    DecryptionFailed { message: String },
33
34    /// Key derivation failed
35    #[error("Key derivation failed: {message}")]
36    KeyDerivationFailed { message: String },
37
38    /// Invalid ciphertext format
39    #[error("Invalid ciphertext format: {message}")]
40    InvalidCiphertext { message: String },
41
42    /// Base64 encoding/decoding error
43    #[error("Base64 error: {message}")]
44    Base64Error { message: String },
45}
46
47/// Encrypted data container
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct EncryptedData {
50    /// Base64-encoded ciphertext
51    pub ciphertext: String,
52    /// Base64-encoded nonce/IV
53    pub nonce: String,
54    /// Base64-encoded salt used for key derivation
55    pub salt: String,
56    /// Algorithm used for encryption
57    pub algorithm: String,
58    /// Key derivation function used
59    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
68/// AES-256-GCM encryption/decryption utilities
69pub struct Aes256GcmCrypto;
70
71impl Default for Aes256GcmCrypto {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77impl Aes256GcmCrypto {
78    /// Create a new Aes256GcmCrypto instance
79    pub fn new() -> Self {
80        Self
81    }
82
83    /// Encrypt data using AES-256-GCM with a direct key (for CLI usage)
84    pub fn encrypt(&self, plaintext: &[u8], key: &str) -> Result<Vec<u8>, CryptoError> {
85        // Decode the base64 key
86        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        // Generate random nonce
100        let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
101
102        // Encrypt
103        let ciphertext =
104            cipher
105                .encrypt(&nonce, plaintext)
106                .map_err(|e| CryptoError::EncryptionFailed {
107                    message: e.to_string(),
108                })?;
109
110        // Combine nonce + ciphertext
111        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    /// Decrypt data using AES-256-GCM with a direct key (for CLI usage)
119    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        // Decode the base64 key
127        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        // Extract nonce and ciphertext
141        let (nonce_bytes, ciphertext) = encrypted_data.split_at(12);
142        let nonce = Nonce::from_slice(nonce_bytes);
143
144        // Decrypt
145        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    /// Encrypt data using AES-256-GCM with Argon2 key derivation (original method)
156    pub fn encrypt_with_password(
157        plaintext: &[u8],
158        password: &str,
159    ) -> Result<EncryptedData, CryptoError> {
160        // Generate random salt
161        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        // Derive key using Argon2
169        let argon2 = Argon2::default();
170        let password_hash = argon2
171            .hash_password(password.as_bytes(), &salt_string)
172            .map_err(|e| CryptoError::KeyDerivationFailed {
173                message: e.to_string(),
174            })?;
175
176        // Extract the hash bytes for the encryption key
177        let hash_binding = password_hash
178            .hash
179            .ok_or_else(|| CryptoError::KeyDerivationFailed {
180                message: "Password hash generation returned None".to_string(),
181            })?;
182        let key_bytes = hash_binding.as_bytes();
183        if key_bytes.len() < 32 {
184            return Err(CryptoError::InvalidKey {
185                message: "Derived key too short".to_string(),
186            });
187        }
188
189        let key = Key::<Aes256Gcm>::from_slice(&key_bytes[..32]);
190        let cipher = Aes256Gcm::new(key);
191
192        // Generate random nonce
193        let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
194
195        // Encrypt
196        let ciphertext =
197            cipher
198                .encrypt(&nonce, plaintext)
199                .map_err(|e| CryptoError::EncryptionFailed {
200                    message: e.to_string(),
201                })?;
202
203        Ok(EncryptedData {
204            ciphertext: BASE64.encode(&ciphertext),
205            nonce: BASE64.encode(nonce),
206            salt: BASE64.encode(salt),
207            algorithm: "AES-256-GCM".to_string(),
208            kdf: "Argon2".to_string(),
209        })
210    }
211
212    /// Decrypt data using AES-256-GCM with Argon2 key derivation (static method)
213    pub fn decrypt_with_password(
214        encrypted_data: &EncryptedData,
215        password: &str,
216    ) -> Result<Vec<u8>, CryptoError> {
217        // Decode base64 components
218        let ciphertext =
219            BASE64
220                .decode(&encrypted_data.ciphertext)
221                .map_err(|e| CryptoError::Base64Error {
222                    message: e.to_string(),
223                })?;
224
225        let nonce_bytes =
226            BASE64
227                .decode(&encrypted_data.nonce)
228                .map_err(|e| CryptoError::Base64Error {
229                    message: e.to_string(),
230                })?;
231
232        let salt = BASE64
233            .decode(&encrypted_data.salt)
234            .map_err(|e| CryptoError::Base64Error {
235                message: e.to_string(),
236            })?;
237
238        // Reconstruct salt string
239        let salt_string =
240            SaltString::encode_b64(&salt).map_err(|e| CryptoError::KeyDerivationFailed {
241                message: e.to_string(),
242            })?;
243
244        // Derive key using the same parameters
245        let argon2 = Argon2::default();
246        let password_hash = argon2
247            .hash_password(password.as_bytes(), &salt_string)
248            .map_err(|e| CryptoError::KeyDerivationFailed {
249                message: e.to_string(),
250            })?;
251
252        let hash_binding = password_hash
253            .hash
254            .ok_or_else(|| CryptoError::KeyDerivationFailed {
255                message: "Password hash generation returned None".to_string(),
256            })?;
257        let key_bytes = hash_binding.as_bytes();
258        if key_bytes.len() < 32 {
259            return Err(CryptoError::InvalidKey {
260                message: "Derived key too short".to_string(),
261            });
262        }
263
264        let key = Key::<Aes256Gcm>::from_slice(&key_bytes[..32]);
265        let cipher = Aes256Gcm::new(key);
266
267        // Create nonce
268        if nonce_bytes.len() != 12 {
269            return Err(CryptoError::InvalidCiphertext {
270                message: "Invalid nonce length".to_string(),
271            });
272        }
273        let nonce = Nonce::from_slice(&nonce_bytes);
274
275        // Decrypt
276        let plaintext = cipher.decrypt(nonce, ciphertext.as_ref()).map_err(|e| {
277            CryptoError::DecryptionFailed {
278                message: e.to_string(),
279            }
280        })?;
281
282        Ok(plaintext)
283    }
284}
285
286/// Utilities for key management
287pub struct KeyUtils;
288
289impl Default for KeyUtils {
290    fn default() -> Self {
291        Self::new()
292    }
293}
294
295impl KeyUtils {
296    /// Create a new KeyUtils instance
297    pub fn new() -> Self {
298        Self
299    }
300
301    /// Get or create a key, prioritizing keychain, then environment, then generating new
302    ///
303    /// # Security Warning
304    ///
305    /// This method will generate a new encryption key if none is found. This can lead to
306    /// data loss if you have existing encrypted data that was encrypted with a different key.
307    ///
308    /// Key priority order:
309    /// 1. System keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service)
310    /// 2. Environment variable SYMBIONT_MASTER_KEY
311    /// 3. Generate new random key (⚠️ will make old encrypted data unrecoverable)
312    ///
313    /// # Recommendations
314    ///
315    /// For production deployments:
316    /// - Use a secrets management system (HashiCorp Vault, AWS Secrets Manager)
317    /// - Set SYMBIONT_MASTER_KEY environment variable with a secure key
318    /// - Ensure proper key backup and rotation procedures
319    pub fn get_or_create_key(&self) -> Result<String, CryptoError> {
320        // Try keychain first
321        if let Ok(key) = self.get_key_from_keychain("symbiont", "secrets") {
322            tracing::debug!("Using encryption key from system keychain");
323            return Ok(key);
324        }
325
326        // Try environment variable
327        if let Ok(key) = Self::get_key_from_env("SYMBIONT_MASTER_KEY") {
328            tracing::info!("Using encryption key from SYMBIONT_MASTER_KEY environment variable");
329            return Ok(key);
330        }
331
332        // ⚠️ SECURITY WARNING: No existing key found, generating new one
333        tracing::warn!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
334        tracing::warn!("⚠️  SECURITY WARNING: No encryption key found!");
335        tracing::warn!("⚠️  Generating a new random encryption key.");
336        tracing::warn!("⚠️  If you have existing encrypted data, it will be UNRECOVERABLE!");
337        tracing::warn!("⚠️  The new key will be stored in the system keychain.");
338        tracing::warn!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
339
340        eprintln!("\n⚠️  CRITICAL SECURITY WARNING:");
341        eprintln!("⚠️  No encryption key found in keychain or environment!");
342        eprintln!("⚠️  Generating new random key - existing encrypted data will be lost!");
343        eprintln!("⚠️  Set SYMBIONT_MASTER_KEY environment variable to use a specific key.\n");
344
345        // Generate a new key
346        let new_key = self.generate_key();
347
348        // Try to store in keychain with clear error handling
349        match self.store_key_in_keychain("symbiont", "secrets", &new_key) {
350            Ok(_) => {
351                tracing::info!("✓ New encryption key stored in system keychain");
352                eprintln!("✓ New encryption key stored in system keychain");
353            }
354            Err(e) => {
355                tracing::error!("✗ Failed to store key in keychain: {}", e);
356                eprintln!("✗ ERROR: Failed to store key in keychain: {}", e);
357                eprintln!("✗ You MUST set SYMBIONT_MASTER_KEY environment variable.");
358                eprintln!("✗ The generated key has been used but could not be persisted.");
359
360                // Return error in production-like scenarios
361                if std::env::var("SYMBIONT_ENV").unwrap_or_default() == "production" {
362                    return Err(CryptoError::InvalidKey {
363                        message: format!(
364                            "Failed to store encryption key in production mode: {}",
365                            e
366                        ),
367                    });
368                }
369            }
370        }
371
372        Ok(new_key)
373    }
374
375    /// Generate a new random key
376    pub fn generate_key(&self) -> String {
377        use base64::Engine;
378        let mut key_bytes = [0u8; 32];
379        OsRng.fill_bytes(&mut key_bytes);
380        BASE64.encode(key_bytes)
381    }
382
383    /// Store a key in the OS keychain
384    #[cfg(feature = "keychain")]
385    fn store_key_in_keychain(
386        &self,
387        service: &str,
388        account: &str,
389        key: &str,
390    ) -> Result<(), CryptoError> {
391        use keyring::Entry;
392
393        let entry = Entry::new(service, account).map_err(|e| CryptoError::InvalidKey {
394            message: format!("Failed to create keychain entry: {}", e),
395        })?;
396
397        entry
398            .set_password(key)
399            .map_err(|e| CryptoError::InvalidKey {
400                message: format!("Failed to store in keychain: {}", e),
401            })
402    }
403
404    #[cfg(not(feature = "keychain"))]
405    fn store_key_in_keychain(
406        &self,
407        _service: &str,
408        _account: &str,
409        _key: &str,
410    ) -> Result<(), CryptoError> {
411        Err(CryptoError::InvalidKey {
412            message: "Keychain support not enabled. Compile with 'keychain' feature.".to_string(),
413        })
414    }
415
416    /// Retrieve a key from environment variable
417    pub fn get_key_from_env(env_var: &str) -> Result<String, CryptoError> {
418        std::env::var(env_var).map_err(|_| CryptoError::InvalidKey {
419            message: format!("Environment variable {} not found", env_var),
420        })
421    }
422
423    /// Retrieve a key from OS keychain (cross-platform)
424    #[cfg(feature = "keychain")]
425    pub fn get_key_from_keychain(
426        &self,
427        service: &str,
428        account: &str,
429    ) -> Result<String, CryptoError> {
430        use keyring::Entry;
431
432        let entry = Entry::new(service, account).map_err(|e| CryptoError::InvalidKey {
433            message: format!("Failed to create keychain entry: {}", e),
434        })?;
435
436        entry.get_password().map_err(|e| CryptoError::InvalidKey {
437            message: format!("Failed to retrieve from keychain: {}", e),
438        })
439    }
440
441    #[cfg(not(feature = "keychain"))]
442    pub fn get_key_from_keychain(
443        &self,
444        _service: &str,
445        _account: &str,
446    ) -> Result<String, CryptoError> {
447        Err(CryptoError::InvalidKey {
448            message: "Keychain support not enabled. Compile with 'keychain' feature.".to_string(),
449        })
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456
457    #[test]
458    fn test_encrypt_decrypt_roundtrip() {
459        let plaintext = b"Hello, world!";
460        let password = "test1"; // Test password
461
462        let encrypted = Aes256GcmCrypto::encrypt_with_password(plaintext, password).unwrap();
463        let decrypted = Aes256GcmCrypto::decrypt_with_password(&encrypted, password).unwrap();
464
465        assert_eq!(plaintext, decrypted.as_slice());
466    }
467
468    #[test]
469    fn test_encrypt_decrypt_wrong_password() {
470        let plaintext = b"Hello, world!";
471        let password = "test1"; // Test password
472        let wrong_password = "wrong1"; // Wrong test password
473
474        let encrypted = Aes256GcmCrypto::encrypt_with_password(plaintext, password).unwrap();
475        let result = Aes256GcmCrypto::decrypt_with_password(&encrypted, wrong_password);
476
477        assert!(result.is_err());
478    }
479
480    #[test]
481    fn test_direct_encrypt_decrypt_roundtrip() {
482        let plaintext = b"Hello, world!";
483        let key_utils = KeyUtils::new();
484        let key = key_utils.generate_key();
485
486        let crypto = Aes256GcmCrypto::new();
487        let encrypted = crypto.encrypt(plaintext, &key).unwrap();
488        let decrypted = crypto.decrypt(&encrypted, &key).unwrap();
489
490        assert_eq!(plaintext, decrypted.as_slice());
491    }
492
493    #[test]
494    fn test_get_key_from_env() {
495        std::env::set_var("TEST_KEY", "test_value");
496        let result = KeyUtils::get_key_from_env("TEST_KEY").unwrap();
497        assert_eq!(result, "test_value");
498
499        let missing_result = KeyUtils::get_key_from_env("MISSING_KEY");
500        assert!(missing_result.is_err());
501    }
502}