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}