pulseengine_mcp_auth/crypto/
encryption.rs

1//! Encryption for API keys at rest
2//!
3//! This module provides AES-256-GCM encryption for storing API keys
4//! securely, inspired by Loxone's RSA/AES encryption approach.
5
6use 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/// Encrypted data with nonce
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct EncryptedData {
16    /// Base64-encoded encrypted data
17    pub ciphertext: String,
18    /// Base64-encoded nonce (96 bits for AES-GCM)
19    pub nonce: String,
20    /// Encryption algorithm identifier
21    pub algorithm: String,
22}
23
24/// Encryption errors
25#[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
40/// Encrypt data using AES-256-GCM
41pub 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
56/// Decrypt data using AES-256-GCM
57pub 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
81/// Derive an encryption key from a master key and context
82///
83/// This uses HKDF (HMAC-based Key Derivation Function) to derive
84/// context-specific keys from a master key.
85pub 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
98/// Generate a random encryption key
99pub 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
106/// Zero out sensitive data in memory
107pub 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        // Encrypt
122        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        // Decrypt
128        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        // Encrypt with key1
139        let encrypted = encrypt_data(plaintext, &key1).unwrap();
140
141        // Try to decrypt with key2 - should fail
142        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        // Same context should produce same key
155        assert_eq!(key1, key2);
156
157        // Different context should produce different key
158        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        // Data should be zeroed
169        assert_ne!(sensitive_data, original);
170        assert!(sensitive_data.iter().all(|&b| b == 0));
171    }
172}