dcrypt_algorithms/aead/chacha20poly1305/
mod.rs

1//! ChaCha20-Poly1305 authenticated encryption
2//!
3//! This module implements the ChaCha20-Poly1305 AEAD algorithm as specified in
4//! RFC 8439.
5//!
6//! ## Constant-Time Guarantees
7//!
8//! * No variable-length early-returns after authentication is checked.  
9//! * Heap allocations and frees are balanced in both success and failure paths.
10//! * Authentication is decided with a branch-free constant-time mask; the same
11//!   byte-wise loop executes whatever the tag's validity.
12
13use crate::error::{validate, Error, Result};
14use crate::mac::poly1305::{Poly1305, POLY1305_KEY_SIZE, POLY1305_TAG_SIZE};
15use crate::stream::chacha::chacha20::{ChaCha20, CHACHA20_KEY_SIZE, CHACHA20_NONCE_SIZE};
16use crate::types::nonce::ChaCha20Compatible;
17use crate::types::Nonce;
18use crate::types::SecretBytes;
19use crate::types::Tag;
20use dcrypt_api::error::Error as CoreError;
21use dcrypt_api::traits::symmetric::{DecryptOperation, EncryptOperation, Operation};
22use dcrypt_api::traits::{AuthenticatedCipher, SymmetricCipher};
23use dcrypt_api::types::Ciphertext;
24// Import SecretBuffer for secure key storage
25use dcrypt_common::security::SecretBuffer;
26use subtle::ConstantTimeEq;
27use zeroize::{Zeroize, ZeroizeOnDrop};
28
29/// Size constants
30pub const CHACHA20POLY1305_KEY_SIZE: usize = CHACHA20_KEY_SIZE;
31/// Size of the nonce used by ChaCha20Poly1305 in bytes
32pub const CHACHA20POLY1305_NONCE_SIZE: usize = CHACHA20_NONCE_SIZE;
33/// Size of the authentication tag produced by ChaCha20Poly1305 in bytes
34pub const CHACHA20POLY1305_TAG_SIZE: usize = POLY1305_TAG_SIZE;
35
36/// ChaCha20-Poly1305 AEAD
37#[derive(Clone, Zeroize, ZeroizeOnDrop)]
38pub struct ChaCha20Poly1305 {
39    key: SecretBuffer<CHACHA20POLY1305_KEY_SIZE>,
40}
41
42/// Operation for ChaCha20Poly1305 encryption operations
43pub struct ChaCha20Poly1305EncryptOperation<'a> {
44    cipher: &'a ChaCha20Poly1305,
45    nonce: Option<&'a Nonce<CHACHA20POLY1305_NONCE_SIZE>>,
46    aad: Option<&'a [u8]>,
47}
48
49/// Operation for ChaCha20Poly1305 decryption operations
50pub struct ChaCha20Poly1305DecryptOperation<'a> {
51    cipher: &'a ChaCha20Poly1305,
52    nonce: Option<&'a Nonce<CHACHA20POLY1305_NONCE_SIZE>>,
53    aad: Option<&'a [u8]>,
54}
55
56impl ChaCha20Poly1305 {
57    /// Create a new instance from a 256-bit key.
58    pub fn new(key: &[u8; CHACHA20POLY1305_KEY_SIZE]) -> Self {
59        Self {
60            key: SecretBuffer::new(*key),
61        }
62    }
63
64    /// Derive the one-time Poly1305 key (RFC 8439 §2.8).
65    fn poly1305_key(&self, nonce: &[u8; CHACHA20POLY1305_NONCE_SIZE]) -> [u8; POLY1305_KEY_SIZE] {
66        // Create a Nonce object from the raw nonce bytes
67        let nonce_obj = Nonce::<CHACHA20_NONCE_SIZE>::from_slice(nonce).expect("Valid nonce"); // This should never fail in internal code
68
69        // Convert SecretBuffer reference to array reference
70        let key_array: &[u8; CHACHA20_KEY_SIZE] = self
71            .key
72            .as_ref()
73            .try_into()
74            .expect("SecretBuffer has correct size");
75
76        let mut chacha = ChaCha20::new(key_array, &nonce_obj);
77        let mut poly_key = [0u8; POLY1305_KEY_SIZE];
78        chacha.keystream(&mut poly_key);
79        poly_key
80    }
81
82    /* --------------------------------------------------------------------- */
83    /*                               ENCRYPT                                 */
84    /* --------------------------------------------------------------------- */
85
86    /// Encrypt plaintext with a raw nonce array
87    ///
88    /// This method performs ChaCha20-Poly1305 encryption using a raw nonce array
89    /// instead of a type-safe Nonce object. It is primarily used internally.
90    ///
91    /// # Arguments
92    /// * `nonce` - A 12-byte array to use as the nonce
93    /// * `plaintext` - The data to encrypt
94    /// * `aad` - Optional associated data to authenticate but not encrypt
95    ///
96    /// # Returns
97    /// A vector containing the ciphertext followed by the 16-byte Poly1305 authentication tag
98    pub fn encrypt_with_nonce(
99        &self,
100        nonce: &[u8; CHACHA20POLY1305_NONCE_SIZE],
101        plaintext: &[u8],
102        aad: Option<&[u8]>,
103    ) -> Result<Vec<u8>> {
104        let poly_key = self.poly1305_key(nonce);
105
106        // ciphertext || tag
107        let mut ct_buf = Vec::with_capacity(plaintext.len() + POLY1305_TAG_SIZE);
108
109        // --- encryption ----------------------------------------------------
110        ct_buf.extend_from_slice(plaintext);
111
112        // Create a Nonce object from the raw nonce bytes for ChaCha20
113        let nonce_obj = Nonce::<CHACHA20_NONCE_SIZE>::from_slice(nonce)
114            .map_err(|_| Error::param("nonce", "Failed to create nonce from slice"))?;
115
116        // Convert SecretBuffer reference to array reference
117        let key_array: &[u8; CHACHA20_KEY_SIZE] = self
118            .key
119            .as_ref()
120            .try_into()
121            .expect("SecretBuffer has correct size");
122
123        ChaCha20::with_counter(key_array, &nonce_obj, 1).encrypt(&mut ct_buf);
124
125        // --- tag -----------------------------------------------------------
126        let tag = self.calculate_tag_ct(&poly_key, aad, &ct_buf)?;
127        ct_buf.extend_from_slice(tag.as_ref());
128        Ok(ct_buf)
129    }
130
131    /* --------------------------------------------------------------------- */
132    /*                               DECRYPT                                 */
133    /* --------------------------------------------------------------------- */
134
135    /// Decrypt ciphertext with a raw nonce array
136    ///
137    /// This method performs ChaCha20-Poly1305 decryption using a raw nonce array
138    /// instead of a type-safe Nonce object. It is primarily used internally.
139    ///
140    /// # Arguments
141    /// * `nonce` - A 12-byte array to use as the nonce
142    /// * `ciphertext` - The ciphertext with appended authentication tag
143    /// * `aad` - Optional associated data that was authenticated
144    ///
145    /// # Returns
146    /// The decrypted plaintext if authentication succeeds
147    ///
148    /// # Errors
149    /// Returns an authentication error if the tag verification fails
150    pub fn decrypt_with_nonce(
151        &self,
152        nonce: &[u8; CHACHA20POLY1305_NONCE_SIZE],
153        ciphertext: &[u8],
154        aad: Option<&[u8]>,
155    ) -> Result<Vec<u8>> {
156        // Length validation using utility
157        validate::min_length(
158            "ChaCha20Poly1305 ciphertext",
159            ciphertext.len(),
160            POLY1305_TAG_SIZE,
161        )?;
162
163        let ct_len = ciphertext.len() - POLY1305_TAG_SIZE;
164        let (encrypted, tag) = ciphertext.split_at(ct_len);
165
166        // -------- one-time key & expected tag ------------------------------
167        let poly_key = self.poly1305_key(nonce);
168        let expected = self.calculate_tag_ct(&poly_key, aad, encrypted)?;
169        let tag_ok = expected.as_ref().ct_eq(tag); // subtle::Choice
170
171        // -------- decrypt ---------------------------------------------------
172        let mut m = Vec::with_capacity(encrypted.len());
173        m.extend_from_slice(encrypted);
174
175        // Create a Nonce object from the raw nonce bytes for ChaCha20
176        let nonce_obj = Nonce::<CHACHA20_NONCE_SIZE>::from_slice(nonce)
177            .map_err(|_| Error::param("nonce", "Failed to create nonce from slice"))?;
178
179        // Convert SecretBuffer reference to array reference
180        let key_array: &[u8; CHACHA20_KEY_SIZE] = self
181            .key
182            .as_ref()
183            .try_into()
184            .expect("SecretBuffer has correct size");
185
186        ChaCha20::with_counter(key_array, &nonce_obj, 1).decrypt(&mut m);
187
188        // -------- constant-time post-processing ----------------------------
189        // mask = 0xFF when tag_ok == 1, else 0x00
190        let mask = 0u8.wrapping_sub(tag_ok.unwrap_u8());
191
192        // Apply mask to all bytes
193        for byte in &mut m {
194            *byte &= mask;
195        }
196
197        // Create a burn buffer on success path to match the deallocation in failure path
198        // This ensures both paths perform identical memory operations
199        let mut burn = m.clone();
200        burn.fill(0); // wipe
201        drop(burn);
202
203        if bool::from(tag_ok) {
204            Ok(m) // m lives on success
205        } else {
206            Err(Error::Authentication {
207                algorithm: "ChaCha20Poly1305",
208            }) // drops m on failure
209        }
210    }
211
212    /* --------------------------------------------------------------------- */
213    /*                               TAG CT                                  */
214    /* --------------------------------------------------------------------- */
215
216    /// RFC 8439 §2.8: constant-time Poly1305 tag computation.
217    fn calculate_tag_ct(
218        &self,
219        poly_key: &[u8; POLY1305_KEY_SIZE],
220        aad: Option<&[u8]>,
221        ciphertext: &[u8],
222    ) -> Result<Tag<POLY1305_TAG_SIZE>> {
223        let mut poly = Poly1305::new(poly_key)?;
224        let aad_slice = aad.unwrap_or(&[]);
225
226        const ZERO16: [u8; 16] = [0u8; 16];
227
228        // AAD
229        poly.update(aad_slice)?;
230        poly.update(&ZERO16[..(16 - aad_slice.len() % 16) % 16])?;
231
232        // ciphertext
233        poly.update(ciphertext)?;
234        poly.update(&ZERO16[..(16 - ciphertext.len() % 16) % 16])?;
235
236        // length block
237        let mut len_block = [0u8; 16];
238        len_block[..8].copy_from_slice(&(aad_slice.len() as u64).to_le_bytes());
239        len_block[8..].copy_from_slice(&(ciphertext.len() as u64).to_le_bytes());
240        poly.update(&len_block)?;
241
242        // Get the finalized tag - it's already a Tag<16> so return directly
243        let tag = poly.finalize();
244        Ok(tag)
245    }
246
247    /// Encrypt data
248    pub fn encrypt<const N: usize>(
249        &self,
250        nonce: &Nonce<N>,
251        plaintext: &[u8],
252        aad: Option<&[u8]>,
253    ) -> Result<Vec<u8>>
254    where
255        Nonce<N>: ChaCha20Compatible,
256    {
257        let mut nonce_array = [0u8; CHACHA20POLY1305_NONCE_SIZE];
258        nonce_array.copy_from_slice(nonce.as_ref());
259        self.encrypt_with_nonce(&nonce_array, plaintext, aad)
260    }
261
262    /// Decrypt data
263    pub fn decrypt<const N: usize>(
264        &self,
265        nonce: &Nonce<N>,
266        ciphertext: &[u8],
267        aad: Option<&[u8]>,
268    ) -> Result<Vec<u8>>
269    where
270        Nonce<N>: ChaCha20Compatible,
271    {
272        let mut nonce_array = [0u8; CHACHA20POLY1305_NONCE_SIZE];
273        nonce_array.copy_from_slice(nonce.as_ref());
274        self.decrypt_with_nonce(&nonce_array, ciphertext, aad)
275    }
276}
277
278// Implement the marker trait AuthenticatedCipher
279impl AuthenticatedCipher for ChaCha20Poly1305 {
280    const TAG_SIZE: usize = POLY1305_TAG_SIZE;
281    const ALGORITHM_ID: &'static str = "ChaCha20Poly1305";
282}
283
284// Implement SymmetricCipher trait
285impl SymmetricCipher for ChaCha20Poly1305 {
286    type Key = SecretBytes<CHACHA20POLY1305_KEY_SIZE>;
287    type Nonce = Nonce<CHACHA20POLY1305_NONCE_SIZE>;
288    type Ciphertext = Ciphertext;
289    type EncryptOperation<'a>
290        = ChaCha20Poly1305EncryptOperation<'a>
291    where
292        Self: 'a;
293    type DecryptOperation<'a>
294        = ChaCha20Poly1305DecryptOperation<'a>
295    where
296        Self: 'a;
297
298    fn name() -> &'static str {
299        "ChaCha20Poly1305"
300    }
301
302    fn encrypt(&self) -> Self::EncryptOperation<'_> {
303        ChaCha20Poly1305EncryptOperation {
304            cipher: self,
305            nonce: None,
306            aad: None,
307        }
308    }
309
310    fn decrypt(&self) -> Self::DecryptOperation<'_> {
311        ChaCha20Poly1305DecryptOperation {
312            cipher: self,
313            nonce: None,
314            aad: None,
315        }
316    }
317
318    fn generate_key<R: rand::RngCore + rand::CryptoRng>(
319        rng: &mut R,
320    ) -> std::result::Result<Self::Key, CoreError> {
321        let mut key_data = [0u8; CHACHA20POLY1305_KEY_SIZE];
322        rng.fill_bytes(&mut key_data);
323        Ok(SecretBytes::new(key_data))
324    }
325
326    fn generate_nonce<R: rand::RngCore + rand::CryptoRng>(
327        rng: &mut R,
328    ) -> std::result::Result<Self::Nonce, CoreError> {
329        let mut nonce_data = [0u8; CHACHA20POLY1305_NONCE_SIZE];
330        rng.fill_bytes(&mut nonce_data);
331        Ok(Nonce::new(nonce_data))
332    }
333
334    fn derive_key_from_bytes(bytes: &[u8]) -> std::result::Result<Self::Key, CoreError> {
335        if bytes.len() < CHACHA20POLY1305_KEY_SIZE {
336            return Err(CoreError::InvalidLength {
337                context: "ChaCha20Poly1305 key derivation",
338                expected: CHACHA20POLY1305_KEY_SIZE,
339                actual: bytes.len(),
340            });
341        }
342
343        let mut key_data = [0u8; CHACHA20POLY1305_KEY_SIZE];
344        key_data.copy_from_slice(&bytes[..CHACHA20POLY1305_KEY_SIZE]);
345        Ok(SecretBytes::new(key_data))
346    }
347}
348
349// Implement Operation for ChaCha20Poly1305EncryptOperation
350impl Operation<Ciphertext> for ChaCha20Poly1305EncryptOperation<'_> {
351    fn execute(self) -> std::result::Result<Ciphertext, CoreError> {
352        let nonce = self.nonce.ok_or_else(|| CoreError::InvalidParameter {
353            context: "ChaCha20Poly1305 encryption",
354            #[cfg(feature = "std")]
355            message: "Nonce is required for ChaCha20Poly1305 encryption".to_string(),
356        })?;
357
358        let plaintext = b""; // Default empty plaintext
359
360        let mut nonce_array = [0u8; CHACHA20POLY1305_NONCE_SIZE];
361        nonce_array.copy_from_slice(nonce.as_ref());
362
363        let ciphertext = self
364            .cipher
365            .encrypt_with_nonce(&nonce_array, plaintext, self.aad)
366            .map_err(CoreError::from)?;
367
368        Ok(Ciphertext::new(&ciphertext))
369    }
370}
371
372impl<'a> EncryptOperation<'a, ChaCha20Poly1305> for ChaCha20Poly1305EncryptOperation<'a> {
373    fn with_nonce(mut self, nonce: &'a <ChaCha20Poly1305 as SymmetricCipher>::Nonce) -> Self {
374        self.nonce = Some(nonce);
375        self
376    }
377
378    fn with_aad(mut self, aad: &'a [u8]) -> Self {
379        self.aad = Some(aad);
380        self
381    }
382
383    fn encrypt(self, plaintext: &'a [u8]) -> std::result::Result<Ciphertext, CoreError> {
384        let nonce = self.nonce.ok_or_else(|| CoreError::InvalidParameter {
385            context: "ChaCha20Poly1305 encryption",
386            #[cfg(feature = "std")]
387            message: "Nonce is required for ChaCha20Poly1305 encryption".to_string(),
388        })?;
389
390        let mut nonce_array = [0u8; CHACHA20POLY1305_NONCE_SIZE];
391        nonce_array.copy_from_slice(nonce.as_ref());
392
393        let ciphertext = self
394            .cipher
395            .encrypt_with_nonce(&nonce_array, plaintext, self.aad)
396            .map_err(CoreError::from)?;
397
398        Ok(Ciphertext::new(&ciphertext))
399    }
400}
401
402// Implement Operation for ChaCha20Poly1305DecryptOperation
403impl Operation<Vec<u8>> for ChaCha20Poly1305DecryptOperation<'_> {
404    fn execute(self) -> std::result::Result<Vec<u8>, CoreError> {
405        Err(CoreError::InvalidParameter {
406            context: "ChaCha20Poly1305 decryption",
407            #[cfg(feature = "std")]
408            message: "Use decrypt method instead".to_string(),
409        })
410    }
411}
412
413impl<'a> DecryptOperation<'a, ChaCha20Poly1305> for ChaCha20Poly1305DecryptOperation<'a> {
414    fn with_nonce(mut self, nonce: &'a <ChaCha20Poly1305 as SymmetricCipher>::Nonce) -> Self {
415        self.nonce = Some(nonce);
416        self
417    }
418
419    fn with_aad(mut self, aad: &'a [u8]) -> Self {
420        self.aad = Some(aad);
421        self
422    }
423
424    fn decrypt(
425        self,
426        ciphertext: &'a <ChaCha20Poly1305 as SymmetricCipher>::Ciphertext,
427    ) -> std::result::Result<Vec<u8>, CoreError> {
428        let nonce = self.nonce.ok_or_else(|| CoreError::InvalidParameter {
429            context: "ChaCha20Poly1305 decryption",
430            #[cfg(feature = "std")]
431            message: "Nonce is required for ChaCha20Poly1305 decryption".to_string(),
432        })?;
433
434        let mut nonce_array = [0u8; CHACHA20POLY1305_NONCE_SIZE];
435        nonce_array.copy_from_slice(nonce.as_ref());
436
437        self.cipher
438            .decrypt_with_nonce(&nonce_array, ciphertext.as_ref(), self.aad)
439            .map_err(CoreError::from)
440    }
441}
442
443#[cfg(test)]
444mod tests;