Skip to main content

miden_crypto/aead/xchacha/
mod.rs

1//! Cryptographic utilities for encrypting and decrypting data using XChaCha20-Poly1305 AEAD.
2//!
3//! This module provides secure encryption and decryption functionality. It uses
4//! the XChaCha20-Poly1305 authenticated encryption with associated data (AEAD) algorithm,
5//! which provides both confidentiality and integrity.
6//!
7//! # Key Components
8//!
9//! - [`SecretKey`]: A 256-bit secret key for encryption and decryption operations
10//! - [`Nonce`]: A 192-bit nonce that should be sampled randomly per encryption operation
11//! - [`EncryptedData`]: Encrypted data
12
13use alloc::{string::ToString, vec::Vec};
14
15use chacha20poly1305::{
16    XChaCha20Poly1305,
17    aead::{Aead, AeadCore, KeyInit},
18};
19use rand::{CryptoRng, RngCore};
20#[cfg(any(test, feature = "testing"))]
21use subtle::ConstantTimeEq;
22
23use crate::{
24    Felt,
25    aead::{AeadScheme, DataType, EncryptionError},
26    utils::{
27        ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
28        bytes_to_elements_exact, elements_to_bytes,
29        zeroize::{Zeroize, ZeroizeOnDrop},
30    },
31};
32
33#[cfg(test)]
34mod test;
35
36// CONSTANTS
37// ================================================================================================
38
39/// Size of nonce in bytes
40const NONCE_SIZE_BYTES: usize = 24;
41/// Size of secret key in bytes
42const SK_SIZE_BYTES: usize = 32;
43
44// STRUCTS AND IMPLEMENTATIONS
45// ================================================================================================
46
47/// Encrypted data
48#[derive(Debug, PartialEq, Eq)]
49pub struct EncryptedData {
50    /// Indicates the original format of the data before encryption
51    data_type: DataType,
52    /// The encrypted ciphertext, including the authentication tag
53    ciphertext: Vec<u8>,
54    /// The nonce used during encryption
55    nonce: Nonce,
56}
57
58/// A 192-bit nonce
59///
60/// Note: This should be drawn randomly from a CSPRNG.
61#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct Nonce {
63    inner: chacha20poly1305::XNonce,
64}
65
66impl Nonce {
67    /// Creates a new random nonce using the provided random number generator
68    pub fn with_rng<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
69        // we use a seedable CSPRNG and seed it with `rng`
70        // this is a work around the fact that the version of the `rand` dependency in our crate
71        // is different than the one used in the `chacha20poly1305`. This solution will
72        // no longer be needed once `chacha20poly1305` gets a new release with a version of
73        // the `rand` dependency matching ours
74        use chacha20poly1305::aead::rand_core::SeedableRng;
75        let mut seed = [0_u8; 32];
76        rand::RngCore::fill_bytes(rng, &mut seed);
77        let rng = rand_hc::Hc128Rng::from_seed(seed);
78
79        Nonce {
80            inner: XChaCha20Poly1305::generate_nonce(rng),
81        }
82    }
83
84    /// Creates a new nonce from the provided array of bytes
85    pub fn from_slice(bytes: &[u8; NONCE_SIZE_BYTES]) -> Self {
86        Nonce { inner: (*bytes).into() }
87    }
88}
89
90/// A 256-bit secret key
91#[derive(Debug)]
92pub struct SecretKey([u8; SK_SIZE_BYTES]);
93
94#[cfg(any(test, feature = "testing"))]
95impl PartialEq for SecretKey {
96    fn eq(&self, other: &Self) -> bool {
97        // Constant-time comparison to avoid timing side channels in test builds.
98        bool::from(self.0.ct_eq(&other.0))
99    }
100}
101
102#[cfg(any(test, feature = "testing"))]
103impl Eq for SecretKey {}
104
105impl SecretKey {
106    // CONSTRUCTORS
107    // --------------------------------------------------------------------------------------------
108
109    /// Creates a new random secret key using the default random number generator
110    #[cfg(feature = "std")]
111    #[allow(clippy::new_without_default)]
112    pub fn new() -> Self {
113        let mut rng = rand::rng();
114        Self::with_rng(&mut rng)
115    }
116
117    /// Creates a new random secret key using the provided random number generator
118    pub fn with_rng<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
119        // we use a seedable CSPRNG and seed it with `rng`
120        // this is a work around the fact that the version of the `rand` dependency in our crate
121        // is different than the one used in the `chacha20poly1305`. This solution will
122        // no longer be needed once `chacha20poly1305` gets a new release with a version of
123        // the `rand` dependency matching ours
124        use chacha20poly1305::aead::rand_core::SeedableRng;
125        let mut seed = [0_u8; 32];
126        rand::RngCore::fill_bytes(rng, &mut seed);
127        let rng = rand_hc::Hc128Rng::from_seed(seed);
128
129        let key = XChaCha20Poly1305::generate_key(rng);
130        Self(key.into())
131    }
132
133    // BYTE ENCRYPTION
134    // --------------------------------------------------------------------------------------------
135
136    /// Encrypts and authenticates the provided data using this secret key and a random
137    /// nonce
138    #[cfg(feature = "std")]
139    pub fn encrypt_bytes(&self, data: &[u8]) -> Result<EncryptedData, EncryptionError> {
140        self.encrypt_bytes_with_associated_data(data, &[])
141    }
142
143    /// Encrypts the provided data and authenticates both the ciphertext as well as
144    /// the provided associated data using this secret key and a random nonce
145    #[cfg(feature = "std")]
146    pub fn encrypt_bytes_with_associated_data(
147        &self,
148        data: &[u8],
149        associated_data: &[u8],
150    ) -> Result<EncryptedData, EncryptionError> {
151        let mut rng = rand::rng();
152        let nonce = Nonce::with_rng(&mut rng);
153
154        self.encrypt_bytes_with_nonce(data, associated_data, nonce)
155    }
156
157    /// Encrypts the provided data using this secret key and a specified nonce
158    pub fn encrypt_bytes_with_nonce(
159        &self,
160        data: &[u8],
161        associated_data: &[u8],
162        nonce: Nonce,
163    ) -> Result<EncryptedData, EncryptionError> {
164        let payload = chacha20poly1305::aead::Payload { msg: data, aad: associated_data };
165
166        let cipher = XChaCha20Poly1305::new(&self.0.into());
167
168        let ciphertext = cipher
169            .encrypt(&nonce.inner, payload)
170            .map_err(|_| EncryptionError::FailedOperation)?;
171
172        Ok(EncryptedData {
173            data_type: DataType::Bytes,
174            ciphertext,
175            nonce,
176        })
177    }
178
179    // ELEMENT ENCRYPTION
180    // --------------------------------------------------------------------------------------------
181
182    /// Encrypts and authenticates the provided sequence of field elements using this secret key
183    /// and a random nonce.
184    #[cfg(feature = "std")]
185    pub fn encrypt_elements(&self, data: &[Felt]) -> Result<EncryptedData, EncryptionError> {
186        self.encrypt_elements_with_associated_data(data, &[])
187    }
188
189    /// Encrypts the provided sequence of field elements and authenticates both the ciphertext as
190    /// well as the provided associated data using this secret key and a random nonce.
191    #[cfg(feature = "std")]
192    pub fn encrypt_elements_with_associated_data(
193        &self,
194        data: &[Felt],
195        associated_data: &[Felt],
196    ) -> Result<EncryptedData, EncryptionError> {
197        let mut rng = rand::rng();
198        let nonce = Nonce::with_rng(&mut rng);
199
200        self.encrypt_elements_with_nonce(data, associated_data, nonce)
201    }
202
203    /// Encrypts the provided sequence of field elements and authenticates both the ciphertext as
204    /// well as the provided associated data using this secret key and the specified nonce.
205    pub fn encrypt_elements_with_nonce(
206        &self,
207        data: &[Felt],
208        associated_data: &[Felt],
209        nonce: Nonce,
210    ) -> Result<EncryptedData, EncryptionError> {
211        let data_bytes = elements_to_bytes(data);
212        let ad_bytes = elements_to_bytes(associated_data);
213
214        let mut encrypted_data = self.encrypt_bytes_with_nonce(&data_bytes, &ad_bytes, nonce)?;
215        encrypted_data.data_type = DataType::Elements;
216        Ok(encrypted_data)
217    }
218
219    // BYTE DECRYPTION
220    // --------------------------------------------------------------------------------------------
221
222    /// Decrypts the provided encrypted data using this secret key.
223    ///
224    /// # Errors
225    /// Returns an error if decryption fails or if the underlying data was encrypted as elements
226    /// rather than as bytes.
227    pub fn decrypt_bytes(
228        &self,
229        encrypted_data: &EncryptedData,
230    ) -> Result<Vec<u8>, EncryptionError> {
231        self.decrypt_bytes_with_associated_data(encrypted_data, &[])
232    }
233
234    /// Decrypts the provided encrypted data given some associated data using this secret key.
235    ///
236    /// # Errors
237    /// Returns an error if decryption fails or if the underlying data was encrypted as elements
238    /// rather than as bytes.
239    pub fn decrypt_bytes_with_associated_data(
240        &self,
241        encrypted_data: &EncryptedData,
242        associated_data: &[u8],
243    ) -> Result<Vec<u8>, EncryptionError> {
244        if encrypted_data.data_type != DataType::Bytes {
245            return Err(EncryptionError::InvalidDataType {
246                expected: DataType::Bytes,
247                found: encrypted_data.data_type,
248            });
249        }
250        self.decrypt_bytes_with_associated_data_unchecked(encrypted_data, associated_data)
251    }
252
253    /// Decrypts the provided encrypted data given some associated data using this secret key.
254    fn decrypt_bytes_with_associated_data_unchecked(
255        &self,
256        encrypted_data: &EncryptedData,
257        associated_data: &[u8],
258    ) -> Result<Vec<u8>, EncryptionError> {
259        let EncryptedData { ciphertext, nonce, data_type: _ } = encrypted_data;
260        let payload = chacha20poly1305::aead::Payload { msg: ciphertext, aad: associated_data };
261
262        let cipher = XChaCha20Poly1305::new(&self.0.into());
263
264        cipher
265            .decrypt(&nonce.inner, payload)
266            .map_err(|_| EncryptionError::FailedOperation)
267    }
268
269    // ELEMENT DECRYPTION
270    // --------------------------------------------------------------------------------------------
271
272    /// Decrypts the provided encrypted data using this secret key.
273    ///
274    /// # Errors
275    /// Returns an error if decryption fails or if the underlying data was encrypted as bytes
276    /// rather than as field elements.
277    pub fn decrypt_elements(
278        &self,
279        encrypted_data: &EncryptedData,
280    ) -> Result<Vec<Felt>, EncryptionError> {
281        self.decrypt_elements_with_associated_data(encrypted_data, &[])
282    }
283
284    /// Decrypts the provided encrypted data, given some associated data, using this secret key.
285    ///
286    /// # Errors
287    /// Returns an error if decryption fails or if the underlying data was encrypted as bytes
288    /// rather than as field elements.
289    pub fn decrypt_elements_with_associated_data(
290        &self,
291        encrypted_data: &EncryptedData,
292        associated_data: &[Felt],
293    ) -> Result<Vec<Felt>, EncryptionError> {
294        if encrypted_data.data_type != DataType::Elements {
295            return Err(EncryptionError::InvalidDataType {
296                expected: DataType::Elements,
297                found: encrypted_data.data_type,
298            });
299        }
300
301        let ad_bytes = elements_to_bytes(associated_data);
302
303        let plaintext_bytes =
304            self.decrypt_bytes_with_associated_data_unchecked(encrypted_data, &ad_bytes)?;
305        match bytes_to_elements_exact(&plaintext_bytes) {
306            Some(elements) => Ok(elements),
307            None => Err(EncryptionError::FailedBytesToElementsConversion),
308        }
309    }
310}
311
312impl AsRef<[u8]> for SecretKey {
313    fn as_ref(&self) -> &[u8] {
314        &self.0
315    }
316}
317
318impl Drop for SecretKey {
319    fn drop(&mut self) {
320        self.zeroize();
321    }
322}
323
324impl Zeroize for SecretKey {
325    fn zeroize(&mut self) {
326        self.0.zeroize();
327    }
328}
329
330impl ZeroizeOnDrop for SecretKey {}
331
332// IES IMPLEMENTATION
333// ================================================================================================
334
335pub struct XChaCha;
336
337impl AeadScheme for XChaCha {
338    const KEY_SIZE: usize = SK_SIZE_BYTES;
339
340    type Key = SecretKey;
341
342    fn key_from_bytes(bytes: &[u8]) -> Result<Self::Key, EncryptionError> {
343        if bytes.len() != SK_SIZE_BYTES {
344            return Err(EncryptionError::FailedOperation);
345        }
346
347        SecretKey::read_from_bytes_with_budget(bytes, SK_SIZE_BYTES)
348            .map_err(|_| EncryptionError::FailedOperation)
349    }
350
351    fn encrypt_bytes<R: rand::CryptoRng + rand::RngCore>(
352        key: &Self::Key,
353        rng: &mut R,
354        plaintext: &[u8],
355        associated_data: &[u8],
356    ) -> Result<Vec<u8>, EncryptionError> {
357        let nonce = Nonce::with_rng(rng);
358        let encrypted_data = key
359            .encrypt_bytes_with_nonce(plaintext, associated_data, nonce)
360            .map_err(|_| EncryptionError::FailedOperation)?;
361        Ok(encrypted_data.to_bytes())
362    }
363
364    fn decrypt_bytes_with_associated_data(
365        key: &Self::Key,
366        ciphertext: &[u8],
367        associated_data: &[u8],
368    ) -> Result<Vec<u8>, EncryptionError> {
369        let encrypted_data =
370            EncryptedData::read_from_bytes_with_budget(ciphertext, ciphertext.len())
371                .map_err(|_| EncryptionError::FailedOperation)?;
372
373        key.decrypt_bytes_with_associated_data(&encrypted_data, associated_data)
374            .map_err(|_| EncryptionError::FailedOperation)
375    }
376}
377
378// SERIALIZATION / DESERIALIZATION
379// ================================================================================================
380
381impl Serializable for SecretKey {
382    fn write_into<W: ByteWriter>(&self, target: &mut W) {
383        target.write_bytes(&self.0);
384    }
385}
386
387impl Deserializable for SecretKey {
388    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
389        let inner: [u8; SK_SIZE_BYTES] = source.read_array()?;
390
391        Ok(SecretKey(inner))
392    }
393}
394
395impl Serializable for Nonce {
396    fn write_into<W: ByteWriter>(&self, target: &mut W) {
397        target.write_bytes(&self.inner);
398    }
399}
400
401impl Deserializable for Nonce {
402    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
403        let inner: [u8; NONCE_SIZE_BYTES] = source.read_array()?;
404
405        Ok(Nonce { inner: inner.into() })
406    }
407}
408
409impl Serializable for EncryptedData {
410    fn write_into<W: ByteWriter>(&self, target: &mut W) {
411        target.write_u8(self.data_type as u8);
412        target.write_usize(self.ciphertext.len());
413        target.write_bytes(&self.ciphertext);
414        target.write_bytes(&self.nonce.inner);
415    }
416}
417
418impl Deserializable for EncryptedData {
419    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
420        let data_type_value: u8 = source.read_u8()?;
421        let data_type = data_type_value.try_into().map_err(|_| {
422            DeserializationError::InvalidValue("invalid data type value".to_string())
423        })?;
424
425        let ciphertext = Vec::<u8>::read_from(source)?;
426
427        let inner: [u8; NONCE_SIZE_BYTES] = source.read_array()?;
428
429        Ok(Self {
430            ciphertext,
431            nonce: Nonce { inner: inner.into() },
432            data_type,
433        })
434    }
435}