saorsa_pqc/
symmetric.rs

1//! Quantum-resistant symmetric encryption using ChaCha20-Poly1305
2//!
3//! This module provides symmetric encryption capabilities using ChaCha20-Poly1305,
4//! which is considered quantum-resistant due to its reliance on symmetric cryptography.
5//! ChaCha20-Poly1305 is an AEAD (Authenticated Encryption with Associated Data) cipher
6//! that provides both confidentiality and authenticity.
7//!
8//! # Features
9//! - Quantum-resistant symmetric encryption
10//! - Authenticated encryption with associated data (AEAD)
11//! - 256-bit key support
12//! - Secure random nonce generation
13//! - Memory-safe operations with zeroization
14//! - High performance `ChaCha20` stream cipher with Poly1305 MAC
15//!
16//! # Example
17//! ```ignore
18//! use saorsa_pqc::symmetric::{ChaCha20Poly1305Cipher, SymmetricKey};
19//!
20//! // Generate a new key
21//! let key = SymmetricKey::generate();
22//! let cipher = ChaCha20Poly1305Cipher::new(&key);
23//!
24//! // Encrypt data
25//! let plaintext = b"Hello, quantum-resistant world!";
26//! let (ciphertext, nonce) = cipher.encrypt(plaintext, None)?;
27//!
28//! // Decrypt data
29//! let decrypted = cipher.decrypt(&ciphertext, &nonce, None)?;
30//! assert_eq!(plaintext, &decrypted[..]);
31//! # Ok::<(), Box<dyn std::error::Error>>(())
32//! ```
33
34use chacha20poly1305::{
35    aead::{Aead, AeadCore, KeyInit, OsRng, Payload},
36    ChaCha20Poly1305, Key, Nonce,
37};
38use serde::{Deserialize, Serialize};
39use thiserror::Error;
40use zeroize::{Zeroize, ZeroizeOnDrop};
41
42/// Errors that can occur during symmetric encryption operations
43#[derive(Debug, Error)]
44pub enum SymmetricError {
45    /// Encryption operation failed
46    #[error("Encryption failed")]
47    EncryptionFailed,
48
49    /// Decryption operation failed (may indicate tampering or wrong key)
50    #[error("Decryption failed")]
51    DecryptionFailed,
52
53    /// Key has invalid length (must be 32 bytes for `ChaCha20`)
54    #[error("Invalid key length: expected 32 bytes, got {0}")]
55    InvalidKeyLength(usize),
56
57    /// Nonce has invalid length (must be 12 bytes for ChaCha20-Poly1305)
58    #[error("Invalid nonce length: expected 12 bytes, got {0}")]
59    InvalidNonceLength(usize),
60
61    /// Failed to generate cryptographic key
62    #[error("Key generation failed")]
63    KeyGenerationFailed,
64
65    /// Ciphertext is malformed or corrupted
66    #[error("Invalid ciphertext format")]
67    InvalidCiphertextFormat,
68}
69
70/// A 256-bit symmetric encryption key for ChaCha20-Poly1305
71#[derive(Clone, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
72pub struct SymmetricKey {
73    /// The raw key bytes (32 bytes for `ChaCha20`)
74    key: [u8; 32],
75}
76
77impl SymmetricKey {
78    /// Generate a new random 256-bit symmetric key
79    ///
80    /// Uses the OS random number generator to create a cryptographically secure key.
81    ///
82    /// # Example
83    /// ```rust
84    /// use saorsa_pqc::symmetric::SymmetricKey;
85    ///
86    /// let key = SymmetricKey::generate();
87    /// ```
88    pub fn generate() -> Self {
89        let cipher = ChaCha20Poly1305::generate_key(&mut OsRng);
90        Self {
91            key: *cipher.as_ref(),
92        }
93    }
94
95    /// Create a symmetric key from raw bytes
96    ///
97    /// # Arguments
98    /// * `key_bytes` - A 32-byte array containing the key material
99    ///
100    /// # Example
101    /// ```rust
102    /// use saorsa_pqc::symmetric::SymmetricKey;
103    ///
104    /// let key_bytes = [0u8; 32]; // Not recommended for production!
105    /// let key = SymmetricKey::from_bytes(key_bytes);
106    /// ```
107    #[must_use]
108    pub const fn from_bytes(key_bytes: [u8; 32]) -> Self {
109        Self { key: key_bytes }
110    }
111
112    /// Create a symmetric key from a byte slice
113    ///
114    /// # Arguments
115    /// * `key_bytes` - A byte slice containing exactly 32 bytes
116    ///
117    /// # Errors
118    /// Returns `SymmetricError::InvalidKeyLength` if the slice is not exactly 32 bytes.
119    ///
120    /// # Example
121    /// ```rust
122    /// use saorsa_pqc::symmetric::SymmetricKey;
123    ///
124    /// let key_vec = vec![0u8; 32];
125    /// let key = SymmetricKey::from_slice(&key_vec)?;
126    /// # Ok::<(), saorsa_pqc::symmetric::SymmetricError>(())
127    /// ```
128    pub fn from_slice(key_bytes: &[u8]) -> Result<Self, SymmetricError> {
129        if key_bytes.len() != 32 {
130            return Err(SymmetricError::InvalidKeyLength(key_bytes.len()));
131        }
132
133        let mut key = [0u8; 32];
134        key.copy_from_slice(key_bytes);
135        Ok(Self { key })
136    }
137
138    /// Get the raw key bytes
139    ///
140    /// # Security Note
141    /// Be careful when using this method as it exposes the raw key material.
142    /// Ensure the returned bytes are properly zeroized after use.
143    #[must_use]
144    pub const fn as_bytes(&self) -> &[u8; 32] {
145        &self.key
146    }
147
148    /// Export the key as a byte vector
149    ///
150    /// This creates a copy of the key bytes. The caller is responsible for
151    /// securely handling and zeroizing the returned vector.
152    #[must_use]
153    pub fn to_bytes(&self) -> Vec<u8> {
154        self.key.to_vec()
155    }
156}
157
158impl std::fmt::Debug for SymmetricKey {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        f.debug_struct("SymmetricKey")
161            .field("key", &"[REDACTED]")
162            .finish()
163    }
164}
165
166/// ChaCha20-Poly1305 AEAD cipher for quantum-resistant symmetric encryption
167///
168/// This struct provides authenticated encryption with associated data using
169/// the `ChaCha20` stream cipher for encryption and Poly1305 for authentication.
170///
171/// ChaCha20-Poly1305 is considered quantum-resistant because:
172/// - It relies on symmetric cryptography rather than mathematical problems
173/// - `ChaCha20` uses a 256-bit key with a large keyspace (2^256)
174/// - Quantum attacks on symmetric ciphers require approximately 2^(n/2) operations
175/// - This means ~2^128 operations for `ChaCha20`, which is still computationally infeasible
176pub struct ChaCha20Poly1305Cipher {
177    /// The ChaCha20-Poly1305 cipher instance
178    cipher: ChaCha20Poly1305,
179}
180
181impl ChaCha20Poly1305Cipher {
182    /// Create a new ChaCha20-Poly1305 cipher with the given key
183    ///
184    /// # Arguments
185    /// * `key` - The symmetric encryption key
186    ///
187    /// # Example
188    /// ```rust
189    /// use saorsa_pqc::symmetric::{ChaCha20Poly1305Cipher, SymmetricKey};
190    ///
191    /// let key = SymmetricKey::generate();
192    /// let cipher = ChaCha20Poly1305Cipher::new(&key);
193    /// ```
194    #[must_use]
195    pub fn new(key: &SymmetricKey) -> Self {
196        let cipher_key = Key::from_slice(key.as_bytes());
197        let cipher = ChaCha20Poly1305::new(cipher_key);
198
199        Self { cipher }
200    }
201
202    /// Encrypt plaintext data
203    ///
204    /// This method encrypts the provided plaintext and returns the ciphertext
205    /// along with the nonce used for encryption. The nonce is randomly generated
206    /// for each encryption operation.
207    ///
208    /// # Arguments
209    /// * `plaintext` - The data to encrypt
210    /// * `associated_data` - Optional associated data that is authenticated but not encrypted
211    ///
212    /// # Returns
213    /// A tuple containing the ciphertext and the nonce used for encryption
214    ///
215    /// # Errors
216    /// Returns `SymmetricError::EncryptionFailed` if encryption fails
217    ///
218    /// # Example
219    /// ```rust
220    /// use saorsa_pqc::symmetric::{ChaCha20Poly1305Cipher, SymmetricKey};
221    ///
222    /// let key = SymmetricKey::generate();
223    /// let cipher = ChaCha20Poly1305Cipher::new(&key);
224    ///
225    /// let plaintext = b"Secret message";
226    /// let (ciphertext, nonce) = cipher.encrypt(plaintext, None)?;
227    /// # Ok::<(), saorsa_pqc::symmetric::SymmetricError>(())
228    /// ```
229    pub fn encrypt(
230        &self,
231        plaintext: &[u8],
232        associated_data: Option<&[u8]>,
233    ) -> Result<(Vec<u8>, [u8; 12]), SymmetricError> {
234        // Generate a random nonce
235        let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
236
237        // Encrypt the plaintext with optional associated data
238        let payload = match associated_data {
239            Some(aad) => Payload {
240                msg: plaintext,
241                aad,
242            },
243            None => Payload {
244                msg: plaintext,
245                aad: b"",
246            },
247        };
248
249        let ciphertext = self
250            .cipher
251            .encrypt(&nonce, payload)
252            .map_err(|_| SymmetricError::EncryptionFailed)?;
253
254        Ok((ciphertext, *nonce.as_ref()))
255    }
256
257    /// Decrypt ciphertext data
258    ///
259    /// This method decrypts the provided ciphertext using the given nonce.
260    ///
261    /// # Arguments
262    /// * `ciphertext` - The encrypted data to decrypt
263    /// * `nonce` - The nonce that was used during encryption
264    /// * `associated_data` - Optional associated data that was authenticated during encryption
265    ///
266    /// # Returns
267    /// The decrypted plaintext
268    ///
269    /// # Errors
270    /// Returns `SymmetricError::DecryptionFailed` if decryption or authentication fails
271    ///
272    /// # Example
273    /// ```rust
274    /// use saorsa_pqc::symmetric::{ChaCha20Poly1305Cipher, SymmetricKey};
275    ///
276    /// let key = SymmetricKey::generate();
277    /// let cipher = ChaCha20Poly1305Cipher::new(&key);
278    ///
279    /// let plaintext = b"Secret message";
280    /// let (ciphertext, nonce) = cipher.encrypt(plaintext, None)?;
281    /// let decrypted = cipher.decrypt(&ciphertext, &nonce, None)?;
282    /// assert_eq!(plaintext, &decrypted[..]);
283    /// # Ok::<(), saorsa_pqc::symmetric::SymmetricError>(())
284    /// ```
285    pub fn decrypt(
286        &self,
287        ciphertext: &[u8],
288        nonce: &[u8; 12],
289        associated_data: Option<&[u8]>,
290    ) -> Result<Vec<u8>, SymmetricError> {
291        let nonce = Nonce::from_slice(nonce);
292
293        // Decrypt the ciphertext with optional associated data
294        let payload = match associated_data {
295            Some(aad) => Payload {
296                msg: ciphertext,
297                aad,
298            },
299            None => Payload {
300                msg: ciphertext,
301                aad: b"",
302            },
303        };
304
305        let plaintext = self
306            .cipher
307            .decrypt(nonce, payload)
308            .map_err(|_| SymmetricError::DecryptionFailed)?;
309
310        Ok(plaintext)
311    }
312
313    /// Encrypt with a provided nonce (for testing purposes)
314    ///
315    /// # Security Warning
316    /// Never reuse nonces with the same key! This method is provided primarily
317    /// for testing and should be used with extreme caution in production.
318    ///
319    /// # Arguments
320    /// * `plaintext` - The data to encrypt
321    /// * `nonce` - The nonce to use for encryption
322    /// * `associated_data` - Optional associated data that is authenticated but not encrypted
323    ///
324    /// # Errors
325    /// Returns `SymmetricError::EncryptionFailed` if encryption fails
326    #[cfg(test)]
327    pub fn encrypt_with_nonce(
328        &self,
329        plaintext: &[u8],
330        nonce: &[u8; 12],
331        associated_data: Option<&[u8]>,
332    ) -> Result<Vec<u8>, SymmetricError> {
333        let nonce = Nonce::from_slice(nonce);
334
335        // Encrypt the plaintext with optional associated data
336        let payload = match associated_data {
337            Some(aad) => Payload {
338                msg: plaintext,
339                aad,
340            },
341            None => Payload {
342                msg: plaintext,
343                aad: b"",
344            },
345        };
346
347        let ciphertext = self
348            .cipher
349            .encrypt(nonce, payload)
350            .map_err(|_| SymmetricError::EncryptionFailed)?;
351
352        Ok(ciphertext)
353    }
354}
355
356impl std::fmt::Debug for ChaCha20Poly1305Cipher {
357    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358        f.debug_struct("ChaCha20Poly1305Cipher")
359            .field("cipher", &"[REDACTED]")
360            .finish()
361    }
362}
363
364/// A complete encrypted message containing ciphertext and nonce
365///
366/// This structure packages the encrypted data with its nonce for easy
367/// serialization and transmission.
368#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct EncryptedMessage {
370    /// The encrypted data
371    pub ciphertext: Vec<u8>,
372    /// The nonce used for encryption
373    pub nonce: [u8; 12],
374    /// Optional associated data that was authenticated
375    pub associated_data: Option<Vec<u8>>,
376}
377
378impl EncryptedMessage {
379    /// Create a new encrypted message
380    ///
381    /// # Arguments
382    /// * `ciphertext` - The encrypted data
383    /// * `nonce` - The nonce used for encryption
384    /// * `associated_data` - Optional associated data
385    #[must_use]
386    pub const fn new(
387        ciphertext: Vec<u8>,
388        nonce: [u8; 12],
389        associated_data: Option<Vec<u8>>,
390    ) -> Self {
391        Self {
392            ciphertext,
393            nonce,
394            associated_data,
395        }
396    }
397
398    /// Decrypt this message using the provided cipher
399    ///
400    /// # Arguments
401    /// * `cipher` - The cipher to use for decryption
402    ///
403    /// # Returns
404    /// The decrypted plaintext
405    ///
406    /// # Errors
407    /// Returns `SymmetricError::DecryptionFailed` if decryption fails
408    pub fn decrypt(&self, cipher: &ChaCha20Poly1305Cipher) -> Result<Vec<u8>, SymmetricError> {
409        cipher.decrypt(
410            &self.ciphertext,
411            &self.nonce,
412            self.associated_data.as_deref(),
413        )
414    }
415
416    /// Get the size of the encrypted message in bytes
417    #[must_use]
418    pub fn size(&self) -> usize {
419        self.ciphertext.len() + 12 + self.associated_data.as_ref().map_or(0, std::vec::Vec::len)
420    }
421}
422
423/// Utility functions for symmetric encryption
424pub mod utils {
425    use super::{ChaCha20Poly1305Cipher, EncryptedMessage, SymmetricError, SymmetricKey};
426
427    /// Encrypt data and return a complete `EncryptedMessage`
428    ///
429    /// # Arguments
430    /// * `key` - The symmetric encryption key
431    /// * `plaintext` - The data to encrypt
432    /// * `associated_data` - Optional associated data
433    ///
434    /// # Returns
435    /// An `EncryptedMessage` containing the ciphertext, nonce, and associated data
436    ///
437    /// # Example
438    /// ```rust
439    /// use saorsa_pqc::symmetric::{SymmetricKey, utils};
440    ///
441    /// let key = SymmetricKey::generate();
442    /// let message = utils::encrypt_message(&key, b"Hello, world!", None)?;
443    /// # Ok::<(), saorsa_pqc::symmetric::SymmetricError>(())
444    /// ```
445    ///
446    /// # Errors
447    ///
448    /// Returns an error if the underlying cipher fails to encrypt the message
449    pub fn encrypt_message(
450        key: &SymmetricKey,
451        plaintext: &[u8],
452        associated_data: Option<&[u8]>,
453    ) -> Result<EncryptedMessage, SymmetricError> {
454        let cipher = ChaCha20Poly1305Cipher::new(key);
455        let (ciphertext, nonce) = cipher.encrypt(plaintext, associated_data)?;
456
457        Ok(EncryptedMessage::new(
458            ciphertext,
459            nonce,
460            associated_data.map(<[u8]>::to_vec),
461        ))
462    }
463
464    /// Decrypt an `EncryptedMessage`
465    ///
466    /// This is a convenience function that creates a cipher and decrypts
467    /// the message in one step.
468    ///
469    /// # Arguments
470    /// * `key` - The symmetric key to use for decryption
471    /// * `message` - The encrypted message to decrypt
472    ///
473    /// # Returns
474    /// The decrypted plaintext as a `Vec<u8>`
475    ///
476    /// # Examples
477    /// ```no_run
478    /// use saorsa_pqc::symmetric::{SymmetricKey, utils};
479    ///
480    /// let key = SymmetricKey::generate();
481    /// let message = utils::encrypt_message(&key, b"Hello, world!", None)?;
482    /// let plaintext = utils::decrypt_message(&key, &message)?;
483    /// assert_eq!(b"Hello, world!", &plaintext[..]);
484    /// # Ok::<(), saorsa_pqc::symmetric::SymmetricError>(())
485    /// ```
486    ///
487    /// # Errors
488    ///
489    /// Returns an error if:
490    /// - The message is corrupted or tampered with
491    /// - The key used for encryption doesn't match the key used for decryption
492    /// - The underlying cipher fails to decrypt the message
493    pub fn decrypt_message(
494        key: &SymmetricKey,
495        message: &EncryptedMessage,
496    ) -> Result<Vec<u8>, SymmetricError> {
497        let cipher = ChaCha20Poly1305Cipher::new(key);
498        message.decrypt(&cipher)
499    }
500
501    /// Generate a key from a password using PBKDF2
502    ///
503    /// # Arguments
504    /// * `password` - The password to derive the key from
505    /// * `salt` - Random salt bytes (should be at least 16 bytes)
506    /// * `iterations` - Number of PBKDF2 iterations (recommended: 100,000+)
507    ///
508    /// # Security Notes
509    /// - Use a cryptographically secure random salt
510    /// - Use at least 100,000 iterations for good security
511    /// - Consider using higher iteration counts on faster hardware
512    ///
513    /// # Examples
514    /// ```no_run
515    /// use saorsa_pqc::symmetric::utils;
516    ///
517    /// let password = b"my_secure_password";
518    /// let salt = b"random_salt_16bytes"; // In practice, use random bytes
519    /// let key = utils::derive_key_from_password(password, salt, 100_000)?;
520    /// # Ok::<(), saorsa_pqc::symmetric::SymmetricError>(())
521    /// ```
522    /// # Errors
523    ///
524    /// Returns an error if the key derivation process fails (though this is rare with valid inputs)
525    pub fn derive_key_from_password(
526        password: &[u8],
527        salt: &[u8],
528        iterations: u32,
529    ) -> Result<SymmetricKey, SymmetricError> {
530        use pbkdf2::pbkdf2_hmac_array;
531        use sha2::Sha256;
532
533        // Use PBKDF2 to derive a key
534        let key = pbkdf2_hmac_array::<Sha256, 32>(password, salt, iterations);
535
536        Ok(SymmetricKey::from_bytes(key))
537    }
538}
539
540#[cfg(test)]
541#[allow(clippy::unwrap_used, clippy::expect_used)]
542mod tests {
543    use super::*;
544
545    #[test]
546    fn test_key_generation() {
547        let key1 = SymmetricKey::generate();
548        let key2 = SymmetricKey::generate();
549
550        // Keys should be different
551        assert_ne!(key1.as_bytes(), key2.as_bytes());
552        assert_eq!(key1.as_bytes().len(), 32);
553    }
554
555    #[test]
556    fn test_key_from_bytes() {
557        let key_bytes = [42u8; 32];
558        let key = SymmetricKey::from_bytes(key_bytes);
559        assert_eq!(key.as_bytes(), &key_bytes);
560    }
561
562    #[test]
563    fn test_key_from_slice() {
564        let key_vec = vec![42u8; 32];
565        let key = SymmetricKey::from_slice(&key_vec).unwrap();
566        assert_eq!(key.as_bytes(), &[42u8; 32]);
567
568        // Test invalid length
569        let invalid_key = vec![42u8; 31];
570        assert!(SymmetricKey::from_slice(&invalid_key).is_err());
571    }
572
573    #[test]
574    fn test_basic_encryption_decryption() -> Result<(), SymmetricError> {
575        let key = SymmetricKey::generate();
576        let cipher = ChaCha20Poly1305Cipher::new(&key);
577
578        let plaintext = b"Hello, quantum-resistant world!";
579        let (ciphertext, nonce) = cipher.encrypt(plaintext, None)?;
580
581        // Ciphertext should be different from plaintext
582        assert_ne!(ciphertext, plaintext);
583        assert_eq!(nonce.len(), 12);
584
585        // Decrypt and verify
586        let decrypted = cipher.decrypt(&ciphertext, &nonce, None)?;
587        assert_eq!(plaintext, &decrypted[..]);
588
589        Ok(())
590    }
591
592    #[test]
593    fn test_encryption_with_associated_data() -> Result<(), SymmetricError> {
594        let key = SymmetricKey::generate();
595        let cipher = ChaCha20Poly1305Cipher::new(&key);
596
597        let plaintext = b"Secret message";
598        let associated_data = b"public metadata";
599        let (ciphertext, nonce) = cipher.encrypt(plaintext, Some(associated_data))?;
600
601        // Decrypt with correct associated data
602        let decrypted = cipher.decrypt(&ciphertext, &nonce, Some(associated_data))?;
603        assert_eq!(plaintext, &decrypted[..]);
604
605        // Decrypt with wrong associated data should fail
606        let wrong_associated_data = b"wrong metadata";
607        assert!(cipher
608            .decrypt(&ciphertext, &nonce, Some(wrong_associated_data))
609            .is_err());
610
611        Ok(())
612    }
613
614    #[test]
615    fn test_different_nonces_produce_different_ciphertext() -> Result<(), SymmetricError> {
616        let key = SymmetricKey::generate();
617        let cipher = ChaCha20Poly1305Cipher::new(&key);
618
619        let plaintext = b"Same message";
620        let (ciphertext1, nonce1) = cipher.encrypt(plaintext, None)?;
621        let (ciphertext2, nonce2) = cipher.encrypt(plaintext, None)?;
622
623        // Different encryptions should produce different ciphertext and nonces
624        assert_ne!(ciphertext1, ciphertext2);
625        assert_ne!(nonce1, nonce2);
626
627        // Both should decrypt to the same plaintext
628        let decrypted1 = cipher.decrypt(&ciphertext1, &nonce1, None)?;
629        let decrypted2 = cipher.decrypt(&ciphertext2, &nonce2, None)?;
630        assert_eq!(decrypted1, decrypted2);
631        assert_eq!(plaintext, &decrypted1[..]);
632
633        Ok(())
634    }
635
636    #[test]
637    fn test_tampering_detection() -> Result<(), SymmetricError> {
638        let key = SymmetricKey::generate();
639        let cipher = ChaCha20Poly1305Cipher::new(&key);
640
641        let plaintext = b"Important message";
642        let (mut ciphertext, nonce) = cipher.encrypt(plaintext, None)?;
643
644        // Tamper with the ciphertext
645        if let Some(byte) = ciphertext.get_mut(0) {
646            *byte = byte.wrapping_add(1);
647        }
648
649        // Decryption should fail due to authentication failure
650        assert!(cipher.decrypt(&ciphertext, &nonce, None).is_err());
651
652        Ok(())
653    }
654
655    #[test]
656    fn test_encrypted_message() -> Result<(), SymmetricError> {
657        let key = SymmetricKey::generate();
658        let plaintext = b"Test message";
659        let associated_data = Some(b"metadata".as_slice());
660
661        let message = utils::encrypt_message(&key, plaintext, associated_data)?;
662        assert!(message.size() > 0);
663
664        let decrypted = utils::decrypt_message(&key, &message)?;
665        assert_eq!(plaintext, &decrypted[..]);
666
667        Ok(())
668    }
669
670    #[test]
671    fn test_key_derivation_from_password() -> Result<(), SymmetricError> {
672        let password = b"my_secure_password";
673        let salt = b"random_salt_1234";
674        let iterations = 1000; // Low for testing
675
676        let key1 = utils::derive_key_from_password(password, salt, iterations)?;
677        let key2 = utils::derive_key_from_password(password, salt, iterations)?;
678
679        // Same inputs should produce same key
680        assert_eq!(key1.as_bytes(), key2.as_bytes());
681
682        // Different salt should produce different key
683        let different_salt = b"different_salt12";
684        let key3 = utils::derive_key_from_password(password, different_salt, iterations)?;
685        assert_ne!(key1.as_bytes(), key3.as_bytes());
686
687        Ok(())
688    }
689
690    #[test]
691    fn test_empty_plaintext() -> Result<(), SymmetricError> {
692        let key = SymmetricKey::generate();
693        let cipher = ChaCha20Poly1305Cipher::new(&key);
694
695        let plaintext = b"";
696        let (ciphertext, nonce) = cipher.encrypt(plaintext, None)?;
697
698        let decrypted = cipher.decrypt(&ciphertext, &nonce, None)?;
699        assert_eq!(plaintext, &decrypted[..]);
700
701        Ok(())
702    }
703
704    #[test]
705    fn test_large_plaintext() -> Result<(), SymmetricError> {
706        let key = SymmetricKey::generate();
707        let cipher = ChaCha20Poly1305Cipher::new(&key);
708
709        // Test with 1MB of data
710        let plaintext = vec![42u8; 1024 * 1024];
711        let (ciphertext, nonce) = cipher.encrypt(&plaintext, None)?;
712
713        let decrypted = cipher.decrypt(&ciphertext, &nonce, None)?;
714        assert_eq!(plaintext, decrypted);
715
716        Ok(())
717    }
718
719    #[test]
720    fn test_key_zeroization() {
721        let mut key = SymmetricKey::generate();
722        let original_bytes = *key.as_bytes();
723
724        // Explicitly zeroize the key
725        key.zeroize();
726
727        // The key should now be all zeros
728        assert_eq!(key.as_bytes(), &[0u8; 32]);
729        assert_ne!(&original_bytes, &[0u8; 32]); // Original wasn't all zeros
730    }
731
732    #[test]
733    fn test_deterministic_encryption_for_testing() -> Result<(), SymmetricError> {
734        let key = SymmetricKey::from_bytes([1u8; 32]);
735        let cipher = ChaCha20Poly1305Cipher::new(&key);
736
737        let plaintext = b"Test message";
738        let nonce = [2u8; 12];
739
740        let ciphertext1 = cipher.encrypt_with_nonce(plaintext, &nonce, None)?;
741        let ciphertext2 = cipher.encrypt_with_nonce(plaintext, &nonce, None)?;
742
743        // With same key, plaintext, and nonce, ciphertext should be identical
744        assert_eq!(ciphertext1, ciphertext2);
745
746        // Should decrypt correctly
747        let decrypted = cipher.decrypt(&ciphertext1, &nonce, None)?;
748        assert_eq!(plaintext, &decrypted[..]);
749
750        Ok(())
751    }
752}