jmix_rs/
encryption.rs

1//! AES-256-GCM encryption with ECDH key agreement for JMIX envelopes.
2//!
3//! This module implements the JMIX encryption specification using:
4//! - ECDH key agreement over Curve25519
5//! - HKDF key derivation with SHA-256
6//! - AES-256-GCM for authenticated encryption
7
8use aes_gcm::{
9    aead::{Aead, KeyInit},
10    Aes256Gcm, Nonce, Key
11};
12use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
13use hkdf::Hkdf;
14use rand_core::{OsRng, RngCore};
15use sha2::Sha256;
16use std::{fs, io::Write, path::Path};
17use thiserror::Error;
18use x25519_dalek::{EphemeralSecret, PublicKey, x25519};
19use zeroize::Zeroizing;
20use crate::types::EncryptionInfo;
21
22/// Errors that can occur during encryption/decryption operations
23#[derive(Debug, Error)]
24pub enum EncryptionError {
25    #[error("Failed to generate random bytes: {0}")]
26    RandomGeneration(String),
27    
28    #[error("Failed to encrypt data: {0}")]
29    EncryptionFailed(String),
30    
31    #[error("Failed to decrypt data: {0}")]
32    DecryptionFailed(String),
33    
34    #[error("Invalid key format: {0}")]
35    InvalidKey(String),
36    
37    #[error("IO error: {0}")]
38    Io(#[from] std::io::Error),
39    
40    #[error("Base64 decode error: {0}")]
41    Base64Decode(#[from] base64::DecodeError),
42    
43    #[error("Invalid nonce size: expected 12 bytes, got {0}")]
44    InvalidNonceSize(usize),
45    
46    #[error("Invalid auth tag size: expected 16 bytes, got {0}")]
47    InvalidAuthTagSize(usize),
48}
49
50
51/// Manager for JMIX envelope encryption using AES-256-GCM with ECDH
52pub struct EncryptionManager {
53    /// The recipient's long-term public key
54    recipient_public_key: PublicKey,
55}
56
57/// Result of encryption operation
58#[derive(Debug)]
59pub struct EncryptionResult {
60    /// Encrypted data
61    pub ciphertext: Vec<u8>,
62    /// Encryption metadata for the manifest
63    pub info: EncryptionInfo,
64}
65
66/// Key pair for JMIX encryption (Curve25519)
67/// The secret key is automatically zeroed when dropped for security
68pub struct KeyPair {
69    /// Secret key (32 bytes) - automatically zeroed on drop
70    pub secret: Zeroizing<[u8; 32]>,
71    /// Public key (32 bytes) 
72    pub public: PublicKey,
73}
74
75impl EncryptionManager {
76    /// Create a new encryption manager with the recipient's public key
77    pub fn new(recipient_public_key: PublicKey) -> Self {
78        Self {
79            recipient_public_key,
80        }
81    }
82    
83    /// Create an encryption manager from a base64-encoded recipient public key
84    pub fn from_base64_public_key(public_key_b64: &str) -> Result<Self, EncryptionError> {
85        let key_bytes = BASE64.decode(public_key_b64)?;
86        if key_bytes.len() != 32 {
87            return Err(EncryptionError::InvalidKey(
88                format!("Expected 32 bytes, got {}", key_bytes.len())
89            ));
90        }
91        
92        let mut key_array = [0u8; 32];
93        key_array.copy_from_slice(&key_bytes);
94        let public_key = PublicKey::from(key_array);
95        
96        Ok(Self::new(public_key))
97    }
98    
99    /// Create an encryption manager by loading a public key from file
100    pub fn from_public_key_file<P: AsRef<Path>>(path: P) -> Result<Self, EncryptionError> {
101        let key_bytes = fs::read(path)?;
102        if key_bytes.len() != 32 {
103            return Err(EncryptionError::InvalidKey(
104                format!("Expected 32 bytes in key file, got {}", key_bytes.len())
105            ));
106        }
107        
108        let mut key_array = [0u8; 32];
109        key_array.copy_from_slice(&key_bytes);
110        let public_key = PublicKey::from(key_array);
111        
112        Ok(Self::new(public_key))
113    }
114    
115    /// Encrypt data using AES-256-GCM with ephemeral ECDH key agreement
116    pub fn encrypt(&self, plaintext: &[u8]) -> Result<EncryptionResult, EncryptionError> {
117        // Generate ephemeral keypair for this encryption
118        let ephemeral_secret = EphemeralSecret::random_from_rng(OsRng);
119        let ephemeral_public = PublicKey::from(&ephemeral_secret);
120        
121        // Perform ECDH key agreement
122        let shared_secret = ephemeral_secret.diffie_hellman(&self.recipient_public_key);
123        
124        // Derive symmetric key using HKDF-SHA256
125        let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
126        let mut symmetric_key = [0u8; 32]; // AES-256 key
127        hkdf.expand(b"JMIX-AES256-GCM", &mut symmetric_key)
128            .map_err(|e| EncryptionError::EncryptionFailed(format!("HKDF expansion failed: {}", e)))?;
129        
130        // Generate random IV (12 bytes for GCM)
131        let mut iv = [0u8; 12];
132        OsRng.fill_bytes(&mut iv);
133        
134        // Encrypt with AES-256-GCM
135        let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&symmetric_key));
136        let nonce = Nonce::from_slice(&iv);
137        
138        let ciphertext = cipher.encrypt(nonce, plaintext)
139            .map_err(|e| EncryptionError::EncryptionFailed(format!("AES-GCM encryption failed: {}", e)))?;
140        
141        // Split ciphertext and auth tag (last 16 bytes)
142        if ciphertext.len() < 16 {
143            return Err(EncryptionError::EncryptionFailed("Ciphertext too short".to_string()));
144        }
145        
146        let (data, auth_tag) = ciphertext.split_at(ciphertext.len() - 16);
147        
148        // Create encryption info
149        let info = EncryptionInfo {
150            algorithm: "AES-256-GCM".to_string(),
151            ephemeral_public_key: BASE64.encode(ephemeral_public.as_bytes()),
152            iv: BASE64.encode(&iv),
153            auth_tag: BASE64.encode(auth_tag),
154        };
155        
156        Ok(EncryptionResult {
157            ciphertext: data.to_vec(),
158            info,
159        })
160    }
161}
162
163/// Decryption manager for JMIX envelopes
164/// The secret key is automatically zeroed when dropped for security
165pub struct DecryptionManager {
166    /// The recipient's long-term secret key - automatically zeroed on drop
167    secret_key: Zeroizing<[u8; 32]>,
168}
169
170impl DecryptionManager {
171    /// Create a new decryption manager with the recipient's secret key
172    pub fn new(secret_key: [u8; 32]) -> Self {
173        Self { secret_key: Zeroizing::new(secret_key) }
174    }
175    
176    /// Create a decryption manager from raw secret key bytes
177    pub fn from_bytes(key_bytes: [u8; 32]) -> Self {
178        Self::new(key_bytes)
179    }
180    
181    /// Create a decryption manager by loading a secret key from file
182    pub fn from_secret_key_file<P: AsRef<Path>>(path: P) -> Result<Self, EncryptionError> {
183        let key_bytes = fs::read(path)?;
184        if key_bytes.len() != 32 {
185            return Err(EncryptionError::InvalidKey(
186                format!("Expected 32 bytes in key file, got {}", key_bytes.len())
187            ));
188        }
189        
190        let mut key_array = [0u8; 32];
191        key_array.copy_from_slice(&key_bytes);
192        
193        Ok(Self::from_bytes(key_array))
194    }
195    
196    /// Decrypt data using the encryption info from the manifest
197    pub fn decrypt(&self, ciphertext: &[u8], info: &EncryptionInfo) -> Result<Vec<u8>, EncryptionError> {
198        // Validate algorithm
199        if info.algorithm != "AES-256-GCM" {
200            return Err(EncryptionError::DecryptionFailed(
201                format!("Unsupported algorithm: {}", info.algorithm)
202            ));
203        }
204        
205        // Decode the ephemeral public key
206        let ephemeral_public_bytes = BASE64.decode(&info.ephemeral_public_key)?;
207        if ephemeral_public_bytes.len() != 32 {
208            return Err(EncryptionError::InvalidKey(
209                format!("Invalid ephemeral public key length: {}", ephemeral_public_bytes.len())
210            ));
211        }
212        
213        let mut key_array = [0u8; 32];
214        key_array.copy_from_slice(&ephemeral_public_bytes);
215        let ephemeral_public = PublicKey::from(key_array);
216        
217        // Decode IV and auth tag
218        let iv_bytes = BASE64.decode(&info.iv)?;
219        let auth_tag_bytes = BASE64.decode(&info.auth_tag)?;
220        
221        if iv_bytes.len() != 12 {
222            return Err(EncryptionError::InvalidNonceSize(iv_bytes.len()));
223        }
224        
225        if auth_tag_bytes.len() != 16 {
226            return Err(EncryptionError::InvalidAuthTagSize(auth_tag_bytes.len()));
227        }
228        
229        // Perform ECDH key agreement
230        // Use raw x25519 function: shared_secret = our_secret * their_public
231        let shared_secret_bytes = x25519(*self.secret_key, ephemeral_public.to_bytes());
232        
233        // Derive symmetric key using HKDF-SHA256
234        let hkdf = Hkdf::<Sha256>::new(None, &shared_secret_bytes);
235        let mut symmetric_key = [0u8; 32];
236        hkdf.expand(b"JMIX-AES256-GCM", &mut symmetric_key)
237            .map_err(|e| EncryptionError::DecryptionFailed(format!("HKDF expansion failed: {}", e)))?;
238        
239        // Reconstruct the full ciphertext with auth tag
240        let mut full_ciphertext = ciphertext.to_vec();
241        full_ciphertext.extend_from_slice(&auth_tag_bytes);
242        
243        // Decrypt with AES-256-GCM
244        let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&symmetric_key));
245        let nonce = Nonce::from_slice(&iv_bytes);
246        
247        let plaintext = cipher.decrypt(nonce, full_ciphertext.as_slice())
248            .map_err(|e| EncryptionError::DecryptionFailed(format!("AES-GCM decryption failed: {}", e)))?;
249        
250        Ok(plaintext)
251    }
252}
253
254impl KeyPair {
255    /// Generate a new random keypair for encryption
256    pub fn generate() -> Self {
257        let mut secret = [0u8; 32];
258        OsRng.fill_bytes(&mut secret);
259        
260        // Note: We use manual base point multiplication here because x25519-dalek 2.0
261        // doesn't expose secret key bytes from EphemeralSecret. In production code,
262        // consider using a StaticSecret equivalent or a different key management approach.
263        // The base point for X25519 is 9 (little-endian encoding)
264        let mut base_point = [0u8; 32];
265        base_point[0] = 9;
266        let public_bytes = x25519(secret, base_point);
267        let public = PublicKey::from(public_bytes);
268        
269        Self { 
270            secret: Zeroizing::new(secret), 
271            public 
272        }
273    }
274    
275    /// Create a keypair from raw secret key bytes
276    pub fn from_secret_bytes(secret_bytes: [u8; 32]) -> Self {
277        // Derive public key from secret key using x25519 base point multiplication
278        // Note: This performs the same operation as the well-tested x25519-dalek library
279        // The base point for X25519 is 9 (little-endian encoding)
280        let mut base_point = [0u8; 32];
281        base_point[0] = 9;
282        let public_bytes = x25519(secret_bytes, base_point);
283        let public = PublicKey::from(public_bytes);
284        Self { 
285            secret: Zeroizing::new(secret_bytes), 
286            public 
287        }
288    }
289    
290    /// Get the secret key as bytes
291    pub fn secret_bytes(&self) -> [u8; 32] {
292        *self.secret
293    }
294    
295    /// Get the public key as bytes
296    pub fn public_bytes(&self) -> [u8; 32] {
297        self.public.to_bytes()
298    }
299    
300    /// Get the public key as base64 string
301    pub fn public_key_base64(&self) -> String {
302        BASE64.encode(self.public.as_bytes())
303    }
304    
305    /// Save the keypair to files (secret key and public key)
306    pub fn save_to_files<P: AsRef<Path>>(&self, secret_path: P, public_path: P) -> Result<(), EncryptionError> {
307        // Save secret key
308        let mut secret_file = fs::File::create(secret_path)?;
309        secret_file.write_all(&self.secret_bytes())?;
310        
311        // Save public key
312        let mut public_file = fs::File::create(public_path)?;
313        public_file.write_all(&self.public_bytes())?;
314        
315        Ok(())
316    }
317    
318    /// Load a keypair from a secret key file (derives public key)
319    pub fn load_from_secret_file<P: AsRef<Path>>(secret_path: P) -> Result<Self, EncryptionError> {
320        let secret_bytes = fs::read(secret_path)?;
321        if secret_bytes.len() != 32 {
322            return Err(EncryptionError::InvalidKey(
323                format!("Expected 32 bytes in secret key file, got {}", secret_bytes.len())
324            ));
325        }
326        
327        let mut key_array = [0u8; 32];
328        key_array.copy_from_slice(&secret_bytes);
329        
330        Ok(Self::from_secret_bytes(key_array))
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337    use tempfile::TempDir;
338    
339    #[test]
340    fn test_keypair_generation() {
341        let keypair = KeyPair::generate();
342        
343        // Keys should be 32 bytes each
344        assert_eq!(keypair.secret_bytes().len(), 32);
345        assert_eq!(keypair.public_bytes().len(), 32);
346        
347        // Base64 encoding should work
348        let public_b64 = keypair.public_key_base64();
349        assert!(!public_b64.is_empty());
350    }
351    
352    #[test]
353    fn test_keypair_save_load() -> Result<(), Box<dyn std::error::Error>> {
354        let temp_dir = TempDir::new()?;
355        let secret_path = temp_dir.path().join("secret.key");
356        let public_path = temp_dir.path().join("public.key");
357        
358        // Generate and save keypair
359        let original_keypair = KeyPair::generate();
360        original_keypair.save_to_files(&secret_path, &public_path)?;
361        
362        // Load keypair back
363        let loaded_keypair = KeyPair::load_from_secret_file(&secret_path)?;
364        
365        // Should be identical
366        assert_eq!(original_keypair.secret_bytes(), loaded_keypair.secret_bytes());
367        assert_eq!(original_keypair.public_bytes(), loaded_keypair.public_bytes());
368        
369        Ok(())
370    }
371    
372    #[test]
373    fn test_encryption_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
374        // Generate recipient keypair
375        let recipient_keypair = KeyPair::generate();
376        
377        // Create encryption and decryption managers
378        let encryption_manager = EncryptionManager::new(recipient_keypair.public);
379        let decryption_manager = DecryptionManager::new(*recipient_keypair.secret);
380        
381        // Test data
382        let plaintext = b"Hello, JMIX encryption!";
383        
384        // Encrypt
385        let result = encryption_manager.encrypt(plaintext)?;
386        assert!(result.ciphertext.len() > 0);
387        assert_eq!(result.info.algorithm, "AES-256-GCM");
388        
389        // Decrypt
390        let decrypted = decryption_manager.decrypt(&result.ciphertext, &result.info)?;
391        assert_eq!(decrypted, plaintext);
392        
393        Ok(())
394    }
395    
396    #[test]
397    fn test_encryption_manager_from_base64() -> Result<(), Box<dyn std::error::Error>> {
398        let keypair = KeyPair::generate();
399        let public_b64 = keypair.public_key_base64();
400        
401        let manager = EncryptionManager::from_base64_public_key(&public_b64)?;
402        
403        // Should be able to encrypt
404        let plaintext = b"Test message";
405        let result = manager.encrypt(plaintext)?;
406        assert!(result.ciphertext.len() > 0);
407        
408        Ok(())
409    }
410    
411    #[test]
412    fn test_encryption_different_ephemeral_keys() -> Result<(), Box<dyn std::error::Error>> {
413        let recipient_keypair = KeyPair::generate();
414        let encryption_manager = EncryptionManager::new(recipient_keypair.public);
415        
416        let plaintext = b"Same message";
417        
418        // Encrypt twice
419        let result1 = encryption_manager.encrypt(plaintext)?;
420        let result2 = encryption_manager.encrypt(plaintext)?;
421        
422        // Should have different ephemeral keys and IVs
423        assert_ne!(result1.info.ephemeral_public_key, result2.info.ephemeral_public_key);
424        assert_ne!(result1.info.iv, result2.info.iv);
425        assert_ne!(result1.ciphertext, result2.ciphertext);
426        
427        Ok(())
428    }
429    
430    #[test]
431    fn test_invalid_decryption() -> Result<(), Box<dyn std::error::Error>> {
432        let recipient_keypair = KeyPair::generate();
433        let wrong_keypair = KeyPair::generate(); // Different keypair
434        
435        let encryption_manager = EncryptionManager::new(recipient_keypair.public);
436        let wrong_decryption_manager = DecryptionManager::new(*wrong_keypair.secret);
437        
438        let plaintext = b"Secret message";
439        let result = encryption_manager.encrypt(plaintext)?;
440        
441        // Should fail to decrypt with wrong key
442        let decrypt_result = wrong_decryption_manager.decrypt(&result.ciphertext, &result.info);
443        assert!(decrypt_result.is_err());
444        
445        Ok(())
446    }
447}