use aes_gcm::{ Aes256Gcm, Key, Nonce, aead::{Aead, AeadCore, KeyInit, OsRng}, }; use anyhow::{Context, Result, bail}; use serde_json::Value; const NONCE_LEN: usize = 12; // ─── AES-256-GCM encrypt / decrypt ─────────────────────────────────────────── /// Encrypt plaintext bytes with AES-256-GCM. /// Returns `nonce (12 B) || ciphertext+tag`. pub fn encrypt(master_key: &[u8; 32], plaintext: &[u8]) -> Result> { let key = Key::::from_slice(master_key); let cipher = Aes256Gcm::new(key); let nonce = Aes256Gcm::generate_nonce(&mut OsRng); let ciphertext = cipher .encrypt(&nonce, plaintext) .map_err(|e| anyhow::anyhow!("AES-256-GCM encryption failed: {}", e))?; let mut out = Vec::with_capacity(NONCE_LEN + ciphertext.len()); out.extend_from_slice(&nonce); out.extend_from_slice(&ciphertext); Ok(out) } /// Decrypt `nonce (12 B) || ciphertext+tag` with AES-256-GCM. pub fn decrypt(master_key: &[u8; 32], data: &[u8]) -> Result> { if data.len() < NONCE_LEN { bail!( "encrypted data too short ({}B); possibly corrupted", data.len() ); } let (nonce_bytes, ciphertext) = data.split_at(NONCE_LEN); let key = Key::::from_slice(master_key); let cipher = Aes256Gcm::new(key); let nonce = Nonce::from_slice(nonce_bytes); cipher .decrypt(nonce, ciphertext) .map_err(|_| anyhow::anyhow!("decryption failed — wrong master key or corrupted data")) } // ─── JSON helpers ───────────────────────────────────────────────────────────── /// Serialize a JSON Value and encrypt it. Returns the encrypted blob. pub fn encrypt_json(master_key: &[u8; 32], value: &Value) -> Result> { let bytes = serde_json::to_vec(value).context("serialize JSON for encryption")?; encrypt(master_key, &bytes) } /// Decrypt an encrypted blob and deserialize it as a JSON Value. pub fn decrypt_json(master_key: &[u8; 32], data: &[u8]) -> Result { let bytes = decrypt(master_key, data)?; serde_json::from_slice(&bytes).context("deserialize decrypted JSON") } // ─── Client-supplied key extraction ────────────────────────────────────────── /// Parse a 64-char hex string (from X-Encryption-Key header) into a 32-byte key. pub fn extract_key_from_hex(hex_str: &str) -> Result<[u8; 32]> { let bytes = hex::decode_hex(hex_str.trim())?; if bytes.len() != 32 { bail!( "X-Encryption-Key must be 64 hex chars (32 bytes), got {} bytes", bytes.len() ); } let mut key = [0u8; 32]; key.copy_from_slice(&bytes); Ok(key) } // ─── Public hex helpers ─────────────────────────────────────────────────────── pub mod hex { use anyhow::{Result, bail}; pub fn encode_hex(bytes: &[u8]) -> String { bytes.iter().map(|b| format!("{:02x}", b)).collect() } pub fn decode_hex(s: &str) -> Result> { let s = s.trim(); if !s.len().is_multiple_of(2) { bail!("hex string has odd length"); } (0..s.len()) .step_by(2) .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| anyhow::anyhow!("{}", e))) .collect() } } #[cfg(test)] mod tests { use super::*; #[test] fn roundtrip_encrypt_decrypt() { let key = [0x42u8; 32]; let plaintext = b"hello world"; let enc = encrypt(&key, plaintext).unwrap(); let dec = decrypt(&key, &enc).unwrap(); assert_eq!(dec, plaintext); } #[test] fn encrypt_produces_different_ciphertexts() { let key = [0x42u8; 32]; let plaintext = b"hello world"; let enc1 = encrypt(&key, plaintext).unwrap(); let enc2 = encrypt(&key, plaintext).unwrap(); assert_ne!(enc1, enc2); } #[test] fn wrong_key_fails_decryption() { let key1 = [0x42u8; 32]; let key2 = [0x43u8; 32]; let enc = encrypt(&key1, b"secret").unwrap(); assert!(decrypt(&key2, &enc).is_err()); } #[test] fn json_roundtrip() { let key = [0x42u8; 32]; let value = serde_json::json!({"token": "abc123", "password": "hunter2"}); let enc = encrypt_json(&key, &value).unwrap(); let dec = decrypt_json(&key, &enc).unwrap(); assert_eq!(dec, value); } }