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