Skip to main content

commonware_cryptography/
chacha20_poly1305.rs

1//! ChaCha20-Poly1305 [Cipher] with an implicit counter nonce.
2//!
3//! [ChaCha20Poly1305] holds one key and a 96-bit counter nonce that starts at zero, is encoded
4//! little-endian, and advances once per message. The nonce is never transmitted, so a message
5//! opens only in the order it was sealed. Each message carries a 16-byte tag.
6
7use crate::{Cipher, Secret};
8use commonware_math::algebra::Random;
9use commonware_utils::sequence::FixedBytes;
10use rand_core::CryptoRng;
11use zeroize::Zeroizing;
12
13/// Size of the ChaCha20-Poly1305 authentication tag.
14const TAG_SIZE: usize = 16;
15
16/// How many bytes are in a nonce.
17/// ChaCha20-Poly1305 uses a 96-bit (12 byte) nonce.
18const NONCE_SIZE_BYTES: usize = 12;
19
20/// How many bytes are in a key.
21/// ChaCha20-Poly1305 uses a 256-bit (32 byte) key.
22const KEY_SIZE_BYTES: usize = 32;
23
24cfg_if::cfg_if! {
25    if #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] {
26        use aws_lc_rs::aead::{self, CHACHA20_POLY1305, LessSafeKey, UnboundKey};
27
28        struct Key(LessSafeKey);
29
30        impl Key {
31            fn from_key(key: &[u8; KEY_SIZE_BYTES]) -> Self {
32                let unbound_key = UnboundKey::new(&CHACHA20_POLY1305, key)
33                    .expect("key size should match algorithm");
34                Self(LessSafeKey::new(unbound_key))
35            }
36
37            fn encrypt(
38                &self,
39                nonce: &[u8; NONCE_SIZE_BYTES],
40                aad: &[u8],
41                data: &mut [u8],
42            ) -> Option<[u8; TAG_SIZE]> {
43                let nonce = aead::Nonce::assume_unique_for_key(*nonce);
44                let tag = self
45                    .0
46                    .seal_in_place_separate_tag(nonce, aead::Aad::from(aad), data)
47                    .ok()?;
48                tag.as_ref().try_into().ok()
49            }
50
51            fn decrypt(
52                &self,
53                nonce: &[u8; NONCE_SIZE_BYTES],
54                aad: &[u8],
55                data: &mut [u8],
56                tag: &[u8; TAG_SIZE],
57            ) -> Option<()> {
58                let nonce = aead::Nonce::assume_unique_for_key(*nonce);
59                self.0
60                    .open_in_place_separate_tag(nonce, aead::Aad::from(aad), tag, data)
61                    .ok()
62                    .map(|_| ())
63            }
64        }
65    } else {
66        use chacha20poly1305::{ChaCha20Poly1305 as Aead, KeyInit as _, aead::AeadInOut};
67
68        struct Key(Aead);
69
70        impl Key {
71            fn from_key(key: &[u8; KEY_SIZE_BYTES]) -> Self {
72                Self(Aead::new(key.into()))
73            }
74
75            fn encrypt(
76                &self,
77                nonce: &[u8; NONCE_SIZE_BYTES],
78                aad: &[u8],
79                data: &mut [u8],
80            ) -> Option<[u8; TAG_SIZE]> {
81                self.0
82                    .encrypt_inout_detached(nonce.into(), aad, data.into())
83                    .ok()
84                    .map(Into::into)
85            }
86
87            fn decrypt(
88                &self,
89                nonce: &[u8; NONCE_SIZE_BYTES],
90                aad: &[u8],
91                data: &mut [u8],
92                tag: &[u8; TAG_SIZE],
93            ) -> Option<()> {
94                self.0
95                    .decrypt_inout_detached(nonce.into(), aad, data.into(), tag.into())
96                    .ok()
97            }
98        }
99    }
100}
101
102/// ChaCha20-Poly1305 [Cipher] with an implicit counter nonce.
103pub struct ChaCha20Poly1305 {
104    nonce: u128,
105    key: Secret<Key>,
106}
107
108impl ChaCha20Poly1305 {
109    /// Returns the current nonce and advances the counter, or `None` if no nonce remains.
110    fn next_nonce(&mut self) -> Option<[u8; NONCE_SIZE_BYTES]> {
111        // Refuse at 2^96. Truncating a larger counter to 96 bits would repeat a nonce under this
112        // key.
113        if self.nonce >= 1 << (8 * NONCE_SIZE_BYTES) {
114            return None;
115        }
116        let out = self.nonce.to_le_bytes();
117        self.nonce += 1;
118
119        // Keep the low 96 bits. The check above ensures the high bits are zero.
120        let mut nonce = [0u8; NONCE_SIZE_BYTES];
121        nonce.copy_from_slice(&out[..NONCE_SIZE_BYTES]);
122        Some(nonce)
123    }
124}
125
126impl Random for ChaCha20Poly1305 {
127    fn random(mut rng: impl CryptoRng) -> Self {
128        let mut key_bytes = Zeroizing::new([0u8; KEY_SIZE_BYTES]);
129        rng.fill_bytes(key_bytes.as_mut());
130        Self {
131            nonce: 0,
132            key: Secret::new(Key::from_key(&key_bytes)),
133        }
134    }
135}
136
137impl Cipher for ChaCha20Poly1305 {
138    type Tag = FixedBytes<TAG_SIZE>;
139
140    #[inline]
141    fn seal(mut self, aad: &[u8], data: &mut [u8]) -> Option<(Self, Self::Tag)> {
142        let nonce = self.next_nonce()?;
143        let tag = self.key.expose(|key| key.encrypt(&nonce, aad, data))?;
144        Some((self, FixedBytes::new(tag)))
145    }
146
147    #[inline]
148    fn open(mut self, aad: &[u8], data: &mut [u8], tag: &Self::Tag) -> Option<Self> {
149        let nonce = self.next_nonce()?;
150        let tag: &[u8; TAG_SIZE] = tag.as_ref().try_into().expect("tag size is fixed");
151        self.key.expose(|key| key.decrypt(&nonce, aad, data, tag))?;
152        Some(self)
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use commonware_utils::{TestRng, test_rng};
160
161    /// Seals a copy of `msg`, returning the advanced cipher, the ciphertext, and the tag.
162    fn seal(
163        cipher: ChaCha20Poly1305,
164        aad: &[u8],
165        msg: &[u8],
166    ) -> (ChaCha20Poly1305, Vec<u8>, FixedBytes<TAG_SIZE>) {
167        let mut data = msg.to_vec();
168        let (cipher, tag) = cipher.seal(aad, &mut data).unwrap();
169        (cipher, data, tag)
170    }
171
172    #[test]
173    fn test_seal_open_roundtrip() {
174        // test_rng() has a fixed seed, so both ciphers get the same key.
175        let mut send = ChaCha20Poly1305::random(test_rng());
176        let mut recv = ChaCha20Poly1305::random(test_rng());
177        for msg in [&b""[..], b"hello", b"world"] {
178            let (next, mut data, tag) = seal(send, b"aad", msg);
179            send = next;
180            if !msg.is_empty() {
181                assert_ne!(data, msg);
182            }
183            recv = recv.open(b"aad", &mut data, &tag).unwrap();
184            assert_eq!(data, msg);
185        }
186    }
187
188    #[test]
189    fn test_open_wrong_key_fails() {
190        let (_, mut data, tag) = seal(ChaCha20Poly1305::random(test_rng()), b"", b"hello");
191        let recv = ChaCha20Poly1305::random(TestRng::new(1));
192        assert!(recv.open(b"", &mut data, &tag).is_none());
193    }
194
195    #[test]
196    fn test_open_wrong_aad_fails() {
197        let (_, mut data, tag) = seal(ChaCha20Poly1305::random(test_rng()), b"aad", b"hello");
198        let recv = ChaCha20Poly1305::random(test_rng());
199        assert!(recv.open(b"other", &mut data, &tag).is_none());
200    }
201
202    #[test]
203    fn test_open_out_of_order_fails() {
204        let (send, _, _) = seal(ChaCha20Poly1305::random(test_rng()), b"", b"first");
205        let (_, mut second, tag) = seal(send, b"", b"second");
206
207        // The receiver expects the first message, so the second fails to open.
208        let recv = ChaCha20Poly1305::random(test_rng());
209        assert!(recv.open(b"", &mut second, &tag).is_none());
210    }
211
212    #[test]
213    fn test_open_corrupted_fails() {
214        let (_, mut data, tag) = seal(ChaCha20Poly1305::random(test_rng()), b"", b"hello");
215        data[0] ^= 0xFF;
216        let recv = ChaCha20Poly1305::random(test_rng());
217        assert!(recv.open(b"", &mut data, &tag).is_none());
218    }
219
220    #[test]
221    fn test_open_corrupted_tag_fails() {
222        let (_, mut data, tag) = seal(ChaCha20Poly1305::random(test_rng()), b"", b"hello");
223        let mut tag: [u8; TAG_SIZE] = tag.as_ref().try_into().unwrap();
224        tag[0] ^= 0xFF;
225        let recv = ChaCha20Poly1305::random(test_rng());
226        assert!(recv.open(b"", &mut data, &FixedBytes::new(tag)).is_none());
227    }
228
229    #[test]
230    fn test_exhausted_counter_fails() {
231        // Set the counter to its limit so no nonce remains.
232        let exhausted = |mut cipher: ChaCha20Poly1305| {
233            cipher.nonce = 1 << (8 * NONCE_SIZE_BYTES);
234            cipher
235        };
236        let send = exhausted(ChaCha20Poly1305::random(test_rng()));
237        assert!(send.seal(b"", &mut []).is_none());
238        let recv = exhausted(ChaCha20Poly1305::random(test_rng()));
239        let tag = FixedBytes::new([0u8; TAG_SIZE]);
240        assert!(recv.open(b"", &mut [], &tag).is_none());
241    }
242
243    #[cfg(feature = "arbitrary")]
244    mod conformance {
245        use super::*;
246        use commonware_codec::Encode;
247        use commonware_conformance::{Conformance, conformance_tests};
248        use rand::RngExt as _;
249
250        /// Seals several messages with random associated data under a seeded key, logging each
251        /// ciphertext and tag.
252        struct Seal;
253
254        impl Conformance for Seal {
255            async fn commit(seed: u64) -> Vec<u8> {
256                let mut rng = TestRng::new(seed);
257                let mut log = Vec::new();
258                let mut cipher = ChaCha20Poly1305::random(&mut rng);
259
260                // Seal enough messages that successive nonces are covered.
261                for _ in 0..4 {
262                    let mut aad = vec![0u8; rng.random_range(0..32)];
263                    rng.fill(&mut aad[..]);
264                    let mut data = vec![0u8; rng.random_range(0..256)];
265                    rng.fill(&mut data[..]);
266                    let (next, tag) = cipher.seal(&aad, &mut data).unwrap();
267                    cipher = next;
268                    log.extend(aad.encode());
269                    log.extend(data.encode());
270                    log.extend_from_slice(&tag);
271                }
272                log
273            }
274        }
275
276        conformance_tests! {
277            Seal => 4096,
278        }
279    }
280}