pulseengine_mcp_auth/crypto/
encryption.rs1use aes_gcm::{
7 Aes256Gcm, Key, Nonce,
8 aead::{Aead, AeadCore, KeyInit, OsRng},
9};
10use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct EncryptedData {
16 pub ciphertext: String,
18 pub nonce: String,
20 pub algorithm: String,
22}
23
24#[derive(Debug, thiserror::Error)]
26pub enum EncryptionError {
27 #[error("Encryption failed: {0}")]
28 EncryptionFailed(String),
29
30 #[error("Decryption failed: {0}")]
31 DecryptionFailed(String),
32
33 #[error("Invalid key: {0}")]
34 InvalidKey(String),
35
36 #[error("Invalid data format: {0}")]
37 InvalidFormat(String),
38}
39
40pub fn encrypt_data(data: &[u8], key: &[u8; 32]) -> Result<EncryptedData, EncryptionError> {
42 let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key));
43 let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
44
45 let ciphertext = cipher
46 .encrypt(&nonce, data)
47 .map_err(|e| EncryptionError::EncryptionFailed(e.to_string()))?;
48
49 Ok(EncryptedData {
50 ciphertext: BASE64.encode(&ciphertext),
51 nonce: BASE64.encode(nonce),
52 algorithm: "AES-256-GCM".to_string(),
53 })
54}
55
56pub fn decrypt_data(encrypted: &EncryptedData, key: &[u8; 32]) -> Result<Vec<u8>, EncryptionError> {
58 if encrypted.algorithm != "AES-256-GCM" {
59 return Err(EncryptionError::InvalidFormat(format!(
60 "Unsupported algorithm: {}",
61 encrypted.algorithm
62 )));
63 }
64
65 let ciphertext = BASE64
66 .decode(&encrypted.ciphertext)
67 .map_err(|e| EncryptionError::InvalidFormat(format!("Invalid ciphertext base64: {e}")))?;
68
69 let nonce_bytes = BASE64
70 .decode(&encrypted.nonce)
71 .map_err(|e| EncryptionError::InvalidFormat(format!("Invalid nonce base64: {e}")))?;
72
73 let nonce = Nonce::from_slice(&nonce_bytes);
74 let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key));
75
76 cipher
77 .decrypt(nonce, ciphertext.as_ref())
78 .map_err(|e| EncryptionError::DecryptionFailed(e.to_string()))
79}
80
81pub fn derive_encryption_key(master_key: &[u8], context: &str) -> [u8; 32] {
86 use hkdf::Hkdf;
87 use sha2::Sha256;
88
89 let hkdf = Hkdf::<Sha256>::new(None, master_key);
90 let mut okm = [0u8; 32];
91 let info = format!("pulseengine-mcp-auth-{context}");
92 hkdf.expand(info.as_bytes(), &mut okm)
93 .expect("32 bytes is a valid length for HKDF-SHA256");
94
95 okm
96}
97
98pub fn generate_encryption_key() -> [u8; 32] {
100 let mut key = [0u8; 32];
101 use rand::RngCore;
102 rand::thread_rng().fill_bytes(&mut key);
103 key
104}
105
106pub fn secure_zero(data: &mut [u8]) {
108 use zeroize::Zeroize;
109 data.zeroize();
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn test_encryption_decryption() {
118 let key = generate_encryption_key();
119 let plaintext = b"sensitive-api-key-data";
120
121 let encrypted = encrypt_data(plaintext, &key).unwrap();
123 assert!(!encrypted.ciphertext.is_empty());
124 assert!(!encrypted.nonce.is_empty());
125 assert_eq!(encrypted.algorithm, "AES-256-GCM");
126
127 let decrypted = decrypt_data(&encrypted, &key).unwrap();
129 assert_eq!(decrypted, plaintext);
130 }
131
132 #[test]
133 fn test_encryption_with_wrong_key() {
134 let key1 = generate_encryption_key();
135 let key2 = generate_encryption_key();
136 let plaintext = b"sensitive-api-key-data";
137
138 let encrypted = encrypt_data(plaintext, &key1).unwrap();
140
141 let result = decrypt_data(&encrypted, &key2);
143 assert!(result.is_err());
144 }
145
146 #[test]
147 fn test_key_derivation() {
148 let master_key = b"master-key-material";
149
150 let key1 = derive_encryption_key(master_key, "api-keys");
151 let key2 = derive_encryption_key(master_key, "api-keys");
152 let key3 = derive_encryption_key(master_key, "audit-logs");
153
154 assert_eq!(key1, key2);
156
157 assert_ne!(key1, key3);
159 }
160
161 #[test]
162 fn test_secure_zero() {
163 let mut sensitive_data = b"sensitive-key".to_vec();
164 let original = sensitive_data.clone();
165
166 secure_zero(&mut sensitive_data);
167
168 assert_ne!(sensitive_data, original);
170 assert!(sensitive_data.iter().all(|&b| b == 0));
171 }
172}