sentinel_crypto/encrypt/
xchacha20_poly1305.rs

1use chacha20poly1305::{
2    aead::{Aead, KeyInit},
3    Key,
4    XChaCha20Poly1305,
5    XNonce,
6};
7use rand::RngCore;
8use tracing::{debug, trace};
9
10use crate::{encrypt_trait::EncryptionAlgorithm, error::CryptoError};
11
12/// XChaCha20Poly1305 encryption implementation.
13/// Uses the extended ChaCha20 nonce (XChaCha20) with Poly1305 for authenticated encryption.
14/// Provides excellent performance and security, especially on systems without AES hardware
15/// acceleration.
16///
17/// Design choice: XChaCha20Poly1305 is preferred over standard ChaCha20Poly1305 for its
18/// larger nonce size (192 bits vs 96 bits), providing better nonce collision resistance.
19/// It's a rustcrypto crate, preferred for consistency.
20pub struct XChaCha20Poly1305Encryptor;
21
22impl EncryptionAlgorithm for XChaCha20Poly1305Encryptor {
23    fn encrypt_data(data: &[u8], key: &[u8; 32]) -> Result<String, CryptoError> {
24        trace!(
25            "Encrypting data with XChaCha20Poly1305, data length: {}",
26            data.len()
27        );
28        let cipher = XChaCha20Poly1305::new(Key::from_slice(key));
29        let mut nonce_bytes = [0u8; 24]; // XChaCha20 uses 24-byte nonce
30        rand::rng().fill_bytes(&mut nonce_bytes);
31        let nonce = XNonce::from_slice(&nonce_bytes);
32
33        let ciphertext = cipher
34            .encrypt(nonce, data)
35            .map_err(|_| CryptoError::Encryption)?;
36        let mut result = nonce_bytes.to_vec();
37        result.extend_from_slice(&ciphertext);
38        let encrypted_hex = hex::encode(result);
39        debug!(
40            "XChaCha20Poly1305 encryption completed, encrypted length: {}",
41            encrypted_hex.len()
42        );
43        Ok(encrypted_hex)
44    }
45
46    fn decrypt_data(encrypted_data: &str, key: &[u8; 32]) -> Result<Vec<u8>, CryptoError> {
47        trace!(
48            "Decrypting data with XChaCha20Poly1305, encrypted length: {}",
49            encrypted_data.len()
50        );
51        let data = hex::decode(encrypted_data).map_err(|_| CryptoError::Decryption)?;
52        if data.len() < 40 {
53            // 24-byte nonce + 16-byte Poly1305 tag
54            debug!("Encrypted data too short: {} bytes", data.len());
55            return Err(CryptoError::Decryption);
56        }
57        let (nonce_bytes, ciphertext) = data.split_at(24);
58        let cipher = XChaCha20Poly1305::new(Key::from_slice(key));
59        let nonce = XNonce::from_slice(nonce_bytes);
60        let plaintext = cipher
61            .decrypt(nonce, ciphertext)
62            .map_err(|_| CryptoError::Decryption)?;
63        debug!(
64            "XChaCha20Poly1305 decryption completed, plaintext length: {}",
65            plaintext.len()
66        );
67        Ok(plaintext)
68    }
69}
70
71impl crate::encrypt_trait::private::Sealed for XChaCha20Poly1305Encryptor {}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_encrypt_decrypt() {
79        let key = [0u8; 32];
80        let data = b"Hello, world!";
81        let encrypted = XChaCha20Poly1305Encryptor::encrypt_data(data, &key).unwrap();
82        let decrypted = XChaCha20Poly1305Encryptor::decrypt_data(&encrypted, &key).unwrap();
83        assert_eq!(decrypted, data);
84    }
85
86    #[test]
87    fn test_decrypt_invalid_hex() {
88        let key = [0u8; 32];
89        let result = XChaCha20Poly1305Encryptor::decrypt_data("invalid_hex", &key);
90        assert!(result.is_err());
91    }
92}