Skip to main content

dcrypt_algorithms/aead/xchacha20poly1305/
mod.rs

1//! Standard XChaCha20-Poly1305 authenticated encryption.
2//!
3//! This module implements the construction from the XChaCha draft: HChaCha20
4//! derives a subkey from the key and the first 16 nonce bytes, then the owned
5//! RFC 8439 ChaCha20-Poly1305 implementation uses the nonce
6//! `00000000 || nonce[16..24]`.
7//!
8//! Every published `dcrypt-algorithms` version from `0.9.0-beta.1` through
9//! `1.2.3` used a nonstandard construction under this name. This standard
10//! implementation intentionally does not decrypt those bytes. Migrate legacy
11//! data only through the separately isolated decrypt-only tool; never relabel
12//! it as XChaCha20-Poly1305.
13
14use crate::aead::chacha20poly1305::{
15    ChaCha20Poly1305, CHACHA20POLY1305_KEY_SIZE, CHACHA20POLY1305_TAG_SIZE,
16};
17use crate::error::{validate, Error, Result};
18use crate::stream::chacha::chacha20::{hchacha20, CHACHA20_NONCE_SIZE};
19use crate::types::nonce::XChaCha20Compatible;
20use crate::types::Nonce;
21#[cfg(not(feature = "std"))]
22use alloc::vec::Vec;
23use dcrypt_api::traits::AuthenticatedCipher;
24use dcrypt_common::security::{SecretBuffer, SecureZeroingType};
25use dcrypt_internal::zeroing::{Zeroize, ZeroizeOnDrop, Zeroizing};
26
27/// Size of the XChaCha20Poly1305 nonce in bytes
28pub const XCHACHA20POLY1305_NONCE_SIZE: usize = 24;
29
30/// Standard XChaCha20-Poly1305 with an extended 24-byte nonce.
31///
32/// This type does not accept the nonstandard ciphertext format emitted by
33/// the affected legacy format published from `0.9.0-beta.1` through `1.2.3`.
34#[derive(Clone)]
35pub struct XChaCha20Poly1305 {
36    key: SecretBuffer<CHACHA20POLY1305_KEY_SIZE>,
37}
38
39impl Zeroize for XChaCha20Poly1305 {
40    fn zeroize(&mut self) {
41        self.key.zeroize();
42    }
43}
44
45impl Drop for XChaCha20Poly1305 {
46    fn drop(&mut self) {
47        self.zeroize();
48    }
49}
50
51impl ZeroizeOnDrop for XChaCha20Poly1305 {}
52
53impl XChaCha20Poly1305 {
54    /// Create a new XChaCha20Poly1305 instance
55    pub fn new(key: &[u8; CHACHA20POLY1305_KEY_SIZE]) -> Self {
56        Self {
57            key: SecretBuffer::new(*key),
58        }
59    }
60
61    /// Creates an instance from raw key bytes
62    pub fn from_key(key: &[u8]) -> Result<Self> {
63        validate::length(
64            "XChaCha20Poly1305 key",
65            key.len(),
66            CHACHA20POLY1305_KEY_SIZE,
67        )?;
68
69        let mut key_bytes = Zeroizing::new([0u8; CHACHA20POLY1305_KEY_SIZE]);
70        key_bytes.copy_from_slice(&key[..CHACHA20POLY1305_KEY_SIZE]);
71        Ok(Self {
72            key: SecretBuffer::new(*key_bytes),
73        })
74    }
75
76    /// Derive the HChaCha20 subkey and RFC 8439 nonce used by the inner AEAD.
77    fn derive_subkey_and_nonce(
78        &self,
79        nonce: &[u8],
80    ) -> (
81        Zeroizing<[u8; CHACHA20POLY1305_KEY_SIZE]>,
82        [u8; CHACHA20_NONCE_SIZE],
83    ) {
84        let mut hchacha_nonce = [0u8; 16];
85        hchacha_nonce.copy_from_slice(&nonce[..16]);
86
87        let key: &[u8; CHACHA20POLY1305_KEY_SIZE] = self
88            .key
89            .as_ref()
90            .try_into()
91            .expect("SecretBuffer has the declared XChaCha20 key size");
92        let subkey = hchacha20(key, &hchacha_nonce);
93
94        // XChaCha20's IETF ChaCha20 nonce is 32 zero bits followed by the
95        // final 64 bits of the extended nonce.
96        let mut chacha_nonce = [0u8; CHACHA20_NONCE_SIZE];
97        chacha_nonce[4..].copy_from_slice(&nonce[16..24]);
98
99        hchacha_nonce.zeroize();
100        (subkey, chacha_nonce)
101    }
102
103    /// Encrypt plaintext using XChaCha20Poly1305
104    pub fn encrypt<const N: usize>(
105        &self,
106        nonce: &Nonce<N>,
107        plaintext: &[u8],
108        aad: Option<&[u8]>,
109    ) -> Result<Vec<u8>>
110    where
111        Nonce<N>: XChaCha20Compatible,
112    {
113        validate::length(
114            "XChaCha20Poly1305 nonce",
115            nonce.as_ref().len(),
116            XCHACHA20POLY1305_NONCE_SIZE,
117        )?;
118        let (subkey, chacha_nonce) = self.derive_subkey_and_nonce(nonce.as_ref());
119        ChaCha20Poly1305::new(&subkey).encrypt_with_nonce(&chacha_nonce, plaintext, aad)
120    }
121
122    /// Decrypt ciphertext using XChaCha20Poly1305
123    pub fn decrypt<const N: usize>(
124        &self,
125        nonce: &Nonce<N>,
126        ciphertext: &[u8],
127        aad: Option<&[u8]>,
128    ) -> Result<Vec<u8>>
129    where
130        Nonce<N>: XChaCha20Compatible,
131    {
132        validate::length(
133            "XChaCha20Poly1305 nonce",
134            nonce.as_ref().len(),
135            XCHACHA20POLY1305_NONCE_SIZE,
136        )?;
137        let (subkey, chacha_nonce) = self.derive_subkey_and_nonce(nonce.as_ref());
138        ChaCha20Poly1305::new(&subkey)
139            .decrypt_with_nonce(&chacha_nonce, ciphertext, aad)
140            .map_err(|error| match error {
141                Error::Authentication { .. } => Error::Authentication {
142                    algorithm: "XChaCha20Poly1305",
143                },
144                other => other,
145            })
146    }
147}
148
149// Implement SecureZeroingType for XChaCha20Poly1305
150impl SecureZeroingType for XChaCha20Poly1305 {
151    fn zeroed() -> Self {
152        Self {
153            key: SecretBuffer::zeroed(),
154        }
155    }
156
157    fn secure_clone(&self) -> Self {
158        Self {
159            key: self.key.secure_clone(),
160        }
161    }
162}
163
164// Implement the marker trait AuthenticatedCipher correctly
165impl AuthenticatedCipher for XChaCha20Poly1305 {
166    const TAG_SIZE: usize = CHACHA20POLY1305_TAG_SIZE;
167    const ALGORITHM_ID: &'static str = "XChaCha20Poly1305";
168}
169
170#[cfg(test)]
171mod tests;