1use crate::{Cipher, Secret};
8use commonware_math::algebra::Random;
9use commonware_utils::sequence::FixedBytes;
10use rand_core::CryptoRng;
11use zeroize::Zeroizing;
12
13const TAG_SIZE: usize = 16;
15
16const NONCE_SIZE_BYTES: usize = 12;
19
20const 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
102pub struct ChaCha20Poly1305 {
104 nonce: u128,
105 key: Secret<Key>,
106}
107
108impl ChaCha20Poly1305 {
109 fn next_nonce(&mut self) -> Option<[u8; NONCE_SIZE_BYTES]> {
111 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 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 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 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 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 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 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 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}