//! AES-256-GCM encryption service for user API keys. //! //! Provides authenticated encryption with unique random nonces. //! The master key is loaded from the `MASTER_ENCRYPTION_KEY` env var //! (64 hex characters = 32 bytes). use aes_gcm::aead::{Aead, OsRng}; use aes_gcm::{AeadCore, Aes256Gcm, Key, KeyInit, Nonce}; use zeroize::Zeroize; use crate::errors::AppError; /// Master encryption key for AES-256-GCM. /// /// Holds the raw 32-byte key in memory. The key bytes are zeroized on drop. /// Intentionally does NOT derive `Clone` — clones would not be zeroized, /// leaving key material in memory. Use references (`&MasterKey`) instead. pub struct MasterKey { key_bytes: Vec, } impl MasterKey { /// Create a `MasterKey` from a 64-character hex string (32 bytes). /// /// Returns an error if the hex string is malformed or the wrong length. pub fn from_hex(hex_str: &str) -> Result { let key_bytes = hex::decode(hex_str).map_err(|e| { AppError::Internal(anyhow::anyhow!( "Failed to decode master encryption key: {}", e )) })?; if key_bytes.len() != 32 { return Err(AppError::Internal(anyhow::anyhow!( "Master encryption key must be exactly 32 bytes (64 hex chars), got {} bytes", key_bytes.len() ))); } Ok(Self { key_bytes }) } /// Returns the raw key bytes (for use with AES-256-GCM). fn as_bytes(&self) -> &[u8] { &self.key_bytes } } impl Drop for MasterKey { fn drop(&mut self) { self.key_bytes.zeroize(); } } /// Encrypt plaintext using AES-256-GCM with a random 12-byte nonce. /// /// Returns `(ciphertext, nonce)`. The nonce must be stored alongside /// the ciphertext for decryption. pub fn encrypt(master_key: &MasterKey, plaintext: &str) -> Result<(Vec, Vec), AppError> { let key = Key::::from_slice(master_key.as_bytes()); let cipher = Aes256Gcm::new(key); // Generate a unique 12-byte nonce using OS randomness let nonce = Aes256Gcm::generate_nonce(&mut OsRng); let ciphertext = cipher .encrypt(&nonce, plaintext.as_bytes()) .map_err(|e| AppError::Internal(anyhow::anyhow!("Encryption failed: {}", e)))?; Ok((ciphertext, nonce.to_vec())) } /// Decrypt ciphertext using AES-256-GCM with the provided nonce. /// /// Returns the decrypted plaintext string. Returns an error if the key /// is wrong, the data is corrupted, or the nonce doesn't match. pub fn decrypt( master_key: &MasterKey, ciphertext: &[u8], nonce_bytes: &[u8], ) -> Result { let key = Key::::from_slice(master_key.as_bytes()); let cipher = Aes256Gcm::new(key); let nonce = Nonce::from_slice(nonce_bytes); let plaintext = cipher .decrypt(nonce, ciphertext) .map_err(|_| AppError::Internal(anyhow::anyhow!("Decryption failed: invalid key or corrupted data")))?; String::from_utf8(plaintext) .map_err(|_| AppError::Internal(anyhow::anyhow!("Decrypted data is not valid UTF-8"))) } #[cfg(test)] mod tests { use super::*; /// Helper: create a valid test master key (64 hex chars = 32 bytes). fn test_master_key() -> MasterKey { MasterKey::from_hex("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") .unwrap() } #[test] fn encrypt_decrypt_roundtrip() { let key = test_master_key(); let plaintext = "AIzaSyB-test-key-12345"; let (ciphertext, nonce) = encrypt(&key, plaintext).unwrap(); let decrypted = decrypt(&key, &ciphertext, &nonce).unwrap(); assert_eq!(decrypted, plaintext); } #[test] fn different_nonces_each_time() { let key = test_master_key(); let plaintext = "same-key-value"; let (_, nonce1) = encrypt(&key, plaintext).unwrap(); let (_, nonce2) = encrypt(&key, plaintext).unwrap(); assert_ne!(nonce1, nonce2, "Each encryption must produce a unique nonce"); } #[test] fn wrong_key_fails() { let key1 = test_master_key(); let key2 = MasterKey::from_hex( "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", ) .unwrap(); let plaintext = "secret-api-key"; let (ciphertext, nonce) = encrypt(&key1, plaintext).unwrap(); let result = decrypt(&key2, &ciphertext, &nonce); assert!(result.is_err(), "Decryption with wrong key must fail"); } #[test] fn corrupted_data_fails() { let key = test_master_key(); let plaintext = "secret-api-key"; let (mut ciphertext, nonce) = encrypt(&key, plaintext).unwrap(); // Flip a byte in the ciphertext if let Some(byte) = ciphertext.first_mut() { *byte ^= 0xFF; } let result = decrypt(&key, &ciphertext, &nonce); assert!(result.is_err(), "Decryption of corrupted data must fail"); } #[test] fn invalid_hex_key_rejected() { let result = MasterKey::from_hex("not-valid-hex"); assert!(result.is_err()); } #[test] fn wrong_length_key_rejected() { // 32 hex chars = 16 bytes, too short let result = MasterKey::from_hex("0123456789abcdef0123456789abcdef"); assert!(result.is_err()); } #[test] fn nonce_is_12_bytes() { let key = test_master_key(); let (_, nonce) = encrypt(&key, "test").unwrap(); assert_eq!(nonce.len(), 12); } #[test] fn empty_plaintext_roundtrip() { let key = test_master_key(); let (ciphertext, nonce) = encrypt(&key, "").unwrap(); let decrypted = decrypt(&key, &ciphertext, &nonce).unwrap(); assert_eq!(decrypted, ""); } }