Skip to main content

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