use aes_gcm::aead::Aead; use aes_gcm::{Aes256Gcm, KeyInit, Nonce}; use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; use hmac::{Hmac, Mac}; use sha2::Sha256; use erp_core::error::{AppError, AppResult}; type HmacSha256 = Hmac; #[derive(Clone)] pub struct HealthCrypto { aes_key: [u8; 32], hmac_key: [u8; 32], } impl HealthCrypto { pub fn from_keys(aes_key_hex: &str, hmac_key_hex: &str) -> AppResult { let aes_key = hex::decode(aes_key_hex) .map_err(|e| AppError::Internal(format!("AES key hex decode failed: {}", e)))?; let hmac_key = hex::decode(hmac_key_hex) .map_err(|e| AppError::Internal(format!("HMAC key hex decode failed: {}", e)))?; if aes_key.len() != 32 || hmac_key.len() != 32 { return Err(AppError::Internal( "Encryption keys must be 32 bytes each".into(), )); } let mut aes = [0u8; 32]; let mut hmac = [0u8; 32]; aes.copy_from_slice(&aes_key); hmac.copy_from_slice(&hmac_key); Ok(Self { aes_key: aes, hmac_key: hmac, }) } /// Dev fallback: derive deterministic keys from a single dev string. /// DO NOT use in production. pub fn dev_default() -> Self { use sha2::Digest; let aes_key = ::digest(b"erp-health-aes-dev-key-DO-NOT-USE-IN-PROD"); let hmac_key = ::digest(b"erp-health-hmac-dev-key-DO-NOT-USE-IN-PROD"); let mut aes = [0u8; 32]; let mut hmac = [0u8; 32]; aes.copy_from_slice(&aes_key); hmac.copy_from_slice(&hmac_key); Self { aes_key: aes, hmac_key: hmac, } } pub fn encrypt(&self, plaintext: &str) -> AppResult { let cipher = Aes256Gcm::new_from_slice(&self.aes_key) .map_err(|e| AppError::Internal(format!("AES init failed: {}", e)))?; let nonce_bytes = uuid::Uuid::now_v7(); let nonce = Nonce::from_slice(&nonce_bytes.as_bytes()[..12]); let ciphertext = cipher .encrypt(nonce, plaintext.as_bytes()) .map_err(|e| AppError::Internal(format!("Encryption failed: {}", e)))?; let mut combined = nonce_bytes.as_bytes()[..12].to_vec(); combined.extend_from_slice(&ciphertext); Ok(BASE64.encode(&combined)) } pub fn decrypt(&self, encoded: &str) -> AppResult { let combined = BASE64 .decode(encoded) .map_err(|e| AppError::Internal(format!("Base64 decode failed: {}", e)))?; if combined.len() < 12 { return Err(AppError::Internal("Ciphertext too short".into())); } let (nonce_bytes, ciphertext) = combined.split_at(12); let cipher = Aes256Gcm::new_from_slice(&self.aes_key) .map_err(|e| AppError::Internal(format!("AES init failed: {}", e)))?; let plaintext = cipher .decrypt(Nonce::from_slice(nonce_bytes), ciphertext) .map_err(|e| AppError::Internal(format!("Decryption failed: {}", e)))?; String::from_utf8(plaintext) .map_err(|e| AppError::Internal(format!("UTF-8 decode failed: {}", e))) } pub fn hmac_hash(&self, value: &str) -> String { let mut mac = ::new_from_slice(&self.hmac_key) .expect("HMAC key length is valid"); mac.update(value.as_bytes()); hex::encode(mac.finalize().into_bytes()) } }