anya_core/security/crypto/
symmetric.rs

1// Symmetric Encryption Module
2// [AIR-2][AIS-2][BPC-2][AIT-2][RES-2]
3//
4// This module provides symmetric encryption utilities using modern algorithms.
5use aes_gcm::{aead::Aead as AesAead, Aes256Gcm};
6/// Supports AES-256 (GCM, CBC, CTR modes) and ChaCha20-Poly1305.
7use chacha20poly1305::{
8    aead::{KeyInit, Payload},
9    ChaCha20Poly1305, Key,
10};
11use thiserror::Error;
12
13use crate::security::crypto::random;
14
15/// Symmetric encryption error type
16#[derive(Debug, Error)]
17pub enum SymmetricError {
18    #[error("Encryption error: {0}")]
19    EncryptionError(String),
20
21    #[error("Decryption error: {0}")]
22    DecryptionError(String),
23
24    #[error("Invalid key error: {0}")]
25    InvalidKeyError(String),
26
27    #[error("Invalid data error: {0}")]
28    InvalidDataError(String),
29
30    #[error("Invalid nonce error: {0}")]
31    InvalidNonceError(String),
32
33    #[error("Other error: {0}")]
34    OtherError(String),
35}
36
37/// Symmetric encryption algorithm type
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum SymmetricAlgorithm {
40    /// AES-256 in GCM mode
41    Aes256Gcm,
42    /// AES-256 in CBC mode
43    Aes256Cbc,
44    /// AES-256 in CTR mode
45    Aes256Ctr,
46    /// ChaCha20-Poly1305
47    ChaCha20Poly1305,
48}
49
50/// Symmetric encryption/decryption handler
51#[derive(Debug)]
52pub struct SymmetricCrypto {
53    /// Algorithm to use
54    algorithm: SymmetricAlgorithm,
55}
56
57impl SymmetricCrypto {
58    /// Create a new symmetric crypto handler
59    pub fn new(algorithm: SymmetricAlgorithm) -> Self {
60        Self { algorithm }
61    }
62
63    /// Generate a random key suitable for the selected algorithm
64    pub fn generate_key(&self) -> Vec<u8> {
65        match self.algorithm {
66            SymmetricAlgorithm::Aes256Gcm
67            | SymmetricAlgorithm::Aes256Cbc
68            | SymmetricAlgorithm::Aes256Ctr => {
69                // For AES-256, we need a 32-byte key
70                random::random_bytes(32)
71            }
72            SymmetricAlgorithm::ChaCha20Poly1305 => {
73                // For ChaCha20-Poly1305, we need a 32-byte key
74                random::random_bytes(32)
75            }
76        }
77    }
78
79    /// Generate a random nonce/IV suitable for the selected algorithm
80    pub fn generate_nonce(&self) -> Vec<u8> {
81        match self.algorithm {
82            SymmetricAlgorithm::Aes256Gcm => {
83                // For AES-GCM, standard nonce size is 12 bytes
84                random::random_bytes(12)
85            }
86            SymmetricAlgorithm::Aes256Cbc => {
87                // For AES-CBC, IV size is 16 bytes (block size)
88                random::random_bytes(16)
89            }
90            SymmetricAlgorithm::Aes256Ctr => {
91                // For AES-CTR, nonce size is 16 bytes (block size)
92                random::random_bytes(16)
93            }
94            SymmetricAlgorithm::ChaCha20Poly1305 => {
95                // For ChaCha20-Poly1305, nonce size is 12 bytes
96                random::random_bytes(12)
97            }
98        }
99    }
100
101    /// Encrypt data using the selected algorithm
102    pub fn encrypt(
103        &self,
104        key: &[u8],
105        nonce: &[u8],
106        plaintext: &[u8],
107        aad: Option<&[u8]>,
108    ) -> Result<Vec<u8>, SymmetricError> {
109        match self.algorithm {
110            SymmetricAlgorithm::Aes256Gcm => self.encrypt_aes_gcm(key, nonce, plaintext, aad),
111            SymmetricAlgorithm::ChaCha20Poly1305 => {
112                self.encrypt_chacha20_poly1305(key, nonce, plaintext, aad)
113            }
114            _ => Err(SymmetricError::EncryptionError(format!(
115                "Algorithm {:?} not yet implemented",
116                self.algorithm
117            ))),
118        }
119    }
120
121    /// Decrypt data using the selected algorithm
122    pub fn decrypt(
123        &self,
124        key: &[u8],
125        nonce: &[u8],
126        ciphertext: &[u8],
127        aad: Option<&[u8]>,
128    ) -> Result<Vec<u8>, SymmetricError> {
129        match self.algorithm {
130            SymmetricAlgorithm::Aes256Gcm => self.decrypt_aes_gcm(key, nonce, ciphertext, aad),
131            SymmetricAlgorithm::ChaCha20Poly1305 => {
132                self.decrypt_chacha20_poly1305(key, nonce, ciphertext, aad)
133            }
134            _ => Err(SymmetricError::DecryptionError(format!(
135                "Algorithm {:?} not yet implemented",
136                self.algorithm
137            ))),
138        }
139    }
140
141    /// Encrypt data using AES-GCM
142    fn encrypt_aes_gcm(
143        &self,
144        key: &[u8],
145        nonce: &[u8],
146        plaintext: &[u8],
147        aad: Option<&[u8]>,
148    ) -> Result<Vec<u8>, SymmetricError> {
149        // Validate key and nonce
150        if key.len() != 32 {
151            return Err(SymmetricError::InvalidKeyError(format!(
152                "AES-256-GCM requires a 32-byte key, got {}",
153                key.len()
154            )));
155        }
156
157        if nonce.len() != 12 {
158            return Err(SymmetricError::InvalidNonceError(format!(
159                "AES-256-GCM requires a 12-byte nonce, got {}",
160                nonce.len()
161            )));
162        }
163
164        // Create cipher
165        let cipher = Aes256Gcm::new_from_slice(key)
166            .map_err(|e| SymmetricError::EncryptionError(e.to_string()))?;
167
168        // Create nonce
169        let nonce = aes_gcm::Nonce::from_slice(nonce);
170
171        // Encrypt
172        let payload = if let Some(aad_data) = aad {
173            aes_gcm::aead::Payload {
174                msg: plaintext,
175                aad: aad_data,
176            }
177        } else {
178            aes_gcm::aead::Payload {
179                msg: plaintext,
180                aad: &[],
181            }
182        };
183
184        cipher
185            .encrypt(nonce, payload)
186            .map_err(|e| SymmetricError::EncryptionError(e.to_string()))
187    }
188
189    /// Decrypt data using AES-GCM
190    fn decrypt_aes_gcm(
191        &self,
192        key: &[u8],
193        nonce: &[u8],
194        ciphertext: &[u8],
195        aad: Option<&[u8]>,
196    ) -> Result<Vec<u8>, SymmetricError> {
197        // Validate key and nonce
198        if key.len() != 32 {
199            return Err(SymmetricError::InvalidKeyError(format!(
200                "AES-256-GCM requires a 32-byte key, got {}",
201                key.len()
202            )));
203        }
204
205        if nonce.len() != 12 {
206            return Err(SymmetricError::InvalidNonceError(format!(
207                "AES-256-GCM requires a 12-byte nonce, got {}",
208                nonce.len()
209            )));
210        }
211
212        // Create cipher
213        let cipher = Aes256Gcm::new_from_slice(key)
214            .map_err(|e| SymmetricError::DecryptionError(e.to_string()))?;
215
216        // Create nonce
217        let nonce = aes_gcm::Nonce::from_slice(nonce);
218
219        // Decrypt
220        let payload = if let Some(aad_data) = aad {
221            aes_gcm::aead::Payload {
222                msg: ciphertext,
223                aad: aad_data,
224            }
225        } else {
226            aes_gcm::aead::Payload {
227                msg: ciphertext,
228                aad: &[],
229            }
230        };
231
232        cipher
233            .decrypt(nonce, payload)
234            .map_err(|e| SymmetricError::DecryptionError(e.to_string()))
235    }
236
237    /// Encrypt data using ChaCha20-Poly1305
238    fn encrypt_chacha20_poly1305(
239        &self,
240        key: &[u8],
241        nonce: &[u8],
242        plaintext: &[u8],
243        aad: Option<&[u8]>,
244    ) -> Result<Vec<u8>, SymmetricError> {
245        // Validate key and nonce
246        if key.len() != 32 {
247            return Err(SymmetricError::InvalidKeyError(format!(
248                "ChaCha20-Poly1305 requires a 32-byte key, got {}",
249                key.len()
250            )));
251        }
252
253        if nonce.len() != 12 {
254            return Err(SymmetricError::InvalidNonceError(format!(
255                "ChaCha20-Poly1305 requires a 12-byte nonce, got {}",
256                nonce.len()
257            )));
258        }
259
260        // Create cipher
261        let key = Key::from_slice(key);
262        let cipher = ChaCha20Poly1305::new(key);
263
264        // Create nonce
265        let nonce = chacha20poly1305::Nonce::from_slice(nonce);
266
267        // Encrypt with payload
268        let payload = if let Some(aad_data) = aad {
269            Payload {
270                msg: plaintext,
271                aad: aad_data,
272            }
273        } else {
274            Payload {
275                msg: plaintext,
276                aad: &[],
277            }
278        };
279
280        cipher
281            .encrypt(nonce, payload)
282            .map_err(|e| SymmetricError::EncryptionError(e.to_string()))
283    }
284
285    /// Decrypt data using ChaCha20-Poly1305
286    fn decrypt_chacha20_poly1305(
287        &self,
288        key: &[u8],
289        nonce: &[u8],
290        ciphertext: &[u8],
291        aad: Option<&[u8]>,
292    ) -> Result<Vec<u8>, SymmetricError> {
293        // Validate key and nonce
294        if key.len() != 32 {
295            return Err(SymmetricError::InvalidKeyError(format!(
296                "ChaCha20-Poly1305 requires a 32-byte key, got {}",
297                key.len()
298            )));
299        }
300
301        if nonce.len() != 12 {
302            return Err(SymmetricError::InvalidNonceError(format!(
303                "ChaCha20-Poly1305 requires a 12-byte nonce, got {}",
304                nonce.len()
305            )));
306        }
307
308        // Create cipher
309        let key = Key::from_slice(key);
310        let cipher = ChaCha20Poly1305::new(key);
311
312        // Create nonce
313        let nonce = chacha20poly1305::Nonce::from_slice(nonce);
314
315        // Decrypt with payload
316        let payload = if let Some(aad_data) = aad {
317            Payload {
318                msg: ciphertext,
319                aad: aad_data,
320            }
321        } else {
322            Payload {
323                msg: ciphertext,
324                aad: &[],
325            }
326        };
327
328        cipher
329            .decrypt(nonce, payload)
330            .map_err(|e| SymmetricError::DecryptionError(e.to_string()))
331    }
332}
333
334/// Helper function to create an AES-256-GCM cipher
335pub fn create_aes_256_gcm() -> SymmetricCrypto {
336    SymmetricCrypto::new(SymmetricAlgorithm::Aes256Gcm)
337}
338
339/// Helper function to create a ChaCha20-Poly1305 cipher
340pub fn create_chacha20_poly1305() -> SymmetricCrypto {
341    SymmetricCrypto::new(SymmetricAlgorithm::ChaCha20Poly1305)
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn test_aes_gcm() -> Result<(), Box<dyn std::error::Error>> {
350        let crypto = SymmetricCrypto::new(SymmetricAlgorithm::Aes256Gcm);
351
352        // Generate key and nonce
353        let key = crypto.generate_key();
354        let nonce = crypto.generate_nonce();
355
356        // Test data
357        let plaintext = b"This is a test message";
358        let aad = b"Additional authenticated data";
359
360        // Encrypt
361        let ciphertext = crypto.encrypt(&key, &nonce, plaintext, Some(aad))?;
362
363        // Verify not the same as plaintext
364        assert_ne!(&ciphertext, plaintext);
365
366        // Decrypt
367        let decrypted = crypto.decrypt(&key, &nonce, &ciphertext, Some(aad))?;
368
369        // Verify decrypted matches original
370        assert_eq!(&decrypted, plaintext);
371
372        // Verify decryption fails with wrong AAD
373        let wrong_aad = b"Wrong additional data";
374        let result = crypto.decrypt(&key, &nonce, &ciphertext, Some(wrong_aad));
375        assert!(result.is_err());
376
377        Ok(())
378    }
379
380    #[test]
381    fn test_chacha20_poly1305() -> Result<(), Box<dyn std::error::Error>> {
382        let crypto = SymmetricCrypto::new(SymmetricAlgorithm::ChaCha20Poly1305);
383
384        // Generate key and nonce
385        let key = crypto.generate_key();
386        let nonce = crypto.generate_nonce();
387
388        // Test data
389        let plaintext = b"This is a test message for ChaCha20-Poly1305";
390
391        // Encrypt
392        let ciphertext = crypto.encrypt(&key, &nonce, plaintext, None)?;
393
394        // Verify not the same as plaintext
395        assert_ne!(&ciphertext, plaintext);
396
397        // Decrypt
398        let decrypted = crypto.decrypt(&key, &nonce, &ciphertext, None)?;
399
400        // Verify decrypted matches original
401        assert_eq!(&decrypted, plaintext);
402
403        Ok(())
404    }
405
406    #[test]
407    fn test_helper_functions() {
408        let aes_crypto = create_aes_256_gcm();
409        let chacha_crypto = create_chacha20_poly1305();
410
411        assert_eq!(aes_crypto.algorithm, SymmetricAlgorithm::Aes256Gcm);
412        assert_eq!(
413            chacha_crypto.algorithm,
414            SymmetricAlgorithm::ChaCha20Poly1305
415        );
416    }
417}