1#![no_std]
20#![forbid(unsafe_code)]
21
22#[cfg(feature = "alloc")]
23extern crate alloc;
24
25#[cfg(feature = "alloc")]
26use alloc::vec::Vec;
27
28use poly1305_nostd::Poly1305;
29use chacha20::cipher::{KeyIvInit, StreamCipher};
30use chacha20::ChaCha20;
31
32pub const CHACHA20_KEY_SIZE: usize = 32;
34
35pub const CHACHA20_NONCE_SIZE: usize = 12;
37
38pub const POLY1305_TAG_SIZE: usize = 16;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum CryptoError {
44 AuthenticationFailed,
46 InvalidKeySize,
48 InvalidNonceSize,
50 InvalidLength,
52}
53
54pub type CryptoResult<T> = Result<T, CryptoError>;
55
56#[derive(Debug, PartialEq)]
58pub struct ChaCha20Poly1305 {
59 key: [u8; 32],
60}
61
62impl ChaCha20Poly1305 {
63 pub fn new(key: &[u8]) -> CryptoResult<Self> {
65 if key.len() != CHACHA20_KEY_SIZE {
66 return Err(CryptoError::InvalidKeySize);
67 }
68
69 let mut key_array = [0u8; 32];
70 key_array.copy_from_slice(key);
71
72 Ok(Self { key: key_array })
73 }
74
75 #[cfg(feature = "alloc")]
85 pub fn encrypt(&self, nonce: &[u8], plaintext: &[u8], aad: Option<&[u8]>) -> CryptoResult<Vec<u8>> {
86 if nonce.len() != CHACHA20_NONCE_SIZE {
87 return Err(CryptoError::InvalidNonceSize);
88 }
89
90 let mut cipher = ChaCha20::new(&self.key.into(), nonce.into());
92
93 let mut block0 = [0u8; 64];
96 cipher.apply_keystream(&mut block0);
97 let mut poly_key = [0u8; 32];
98 poly_key.copy_from_slice(&block0[..32]);
99
100 let mut ciphertext = plaintext.to_vec();
102 cipher.apply_keystream(&mut ciphertext);
103
104 let tag = self.compute_tag(&poly_key, aad.unwrap_or(&[]), &ciphertext);
106
107 ciphertext.extend_from_slice(&tag);
109
110 Ok(ciphertext)
111 }
112
113 #[cfg(feature = "alloc")]
123 pub fn decrypt(&self, nonce: &[u8], ciphertext: &[u8], aad: Option<&[u8]>) -> CryptoResult<Vec<u8>> {
124 if nonce.len() != CHACHA20_NONCE_SIZE {
125 return Err(CryptoError::InvalidNonceSize);
126 }
127
128 if ciphertext.len() < POLY1305_TAG_SIZE {
129 return Err(CryptoError::InvalidLength);
130 }
131
132 let ct_len = ciphertext.len() - POLY1305_TAG_SIZE;
134 let ct = &ciphertext[..ct_len];
135 let received_tag = &ciphertext[ct_len..];
136
137 let mut cipher = ChaCha20::new(&self.key.into(), nonce.into());
139
140 let mut block0 = [0u8; 64];
143 cipher.apply_keystream(&mut block0);
144 let mut poly_key = [0u8; 32];
145 poly_key.copy_from_slice(&block0[..32]);
146
147 let expected_tag = self.compute_tag(&poly_key, aad.unwrap_or(&[]), ct);
149
150 if !constant_time_eq(&expected_tag, received_tag) {
152 return Err(CryptoError::AuthenticationFailed);
153 }
154
155 let mut plaintext = ct.to_vec();
157 cipher.apply_keystream(&mut plaintext);
158
159 Ok(plaintext)
160 }
161
162 fn compute_tag(&self, poly_key: &[u8; 32], aad: &[u8], ciphertext: &[u8]) -> [u8; 16] {
171 let mut poly = Poly1305::new(poly_key);
172
173 poly.update(aad);
175
176 if aad.len() % 16 != 0 {
178 let pad_len = 16 - (aad.len() % 16);
179 let padding = [0u8; 16];
180 poly.update(&padding[..pad_len]);
181 }
182
183 poly.update(ciphertext);
185
186 if ciphertext.len() % 16 != 0 {
188 let pad_len = 16 - (ciphertext.len() % 16);
189 let padding = [0u8; 16];
190 poly.update(&padding[..pad_len]);
191 }
192
193 let mut lengths = [0u8; 16];
195 lengths[0..8].copy_from_slice(&(aad.len() as u64).to_le_bytes());
196 lengths[8..16].copy_from_slice(&(ciphertext.len() as u64).to_le_bytes());
197 poly.update(&lengths);
198
199 poly.finalize()
200 }
201}
202
203fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
205 if a.len() != b.len() {
206 return false;
207 }
208
209 let mut diff = 0u8;
210 for (x, y) in a.iter().zip(b.iter()) {
211 diff |= x ^ y;
212 }
213
214 diff == 0
215}
216
217#[cfg(all(test, feature = "alloc"))]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn test_encrypt_decrypt_basic() {
223 let key = [0x42u8; 32];
224 let nonce = [0x07u8; 12];
225 let plaintext = b"Hello, Luna!";
226
227 let cipher = ChaCha20Poly1305::new(&key).unwrap();
228
229 let ciphertext = cipher.encrypt(&nonce, plaintext, None).unwrap();
230 assert_eq!(ciphertext.len(), plaintext.len() + POLY1305_TAG_SIZE);
231
232 let decrypted = cipher.decrypt(&nonce, &ciphertext, None).unwrap();
233 assert_eq!(decrypted, plaintext);
234 }
235
236 #[test]
237 fn test_encrypt_decrypt_with_aad() {
238 let key = [0x42u8; 32];
239 let nonce = [0x07u8; 12];
240 let plaintext = b"Secret message";
241 let aad = b"public header";
242
243 let cipher = ChaCha20Poly1305::new(&key).unwrap();
244
245 let ciphertext = cipher.encrypt(&nonce, plaintext, Some(aad)).unwrap();
246 let decrypted = cipher.decrypt(&nonce, &ciphertext, Some(aad)).unwrap();
247 assert_eq!(decrypted, plaintext);
248
249 let wrong_aad = b"wrong header!";
251 let result = cipher.decrypt(&nonce, &ciphertext, Some(wrong_aad));
252 assert_eq!(result, Err(CryptoError::AuthenticationFailed));
253 }
254
255 #[test]
256 fn test_tampered_ciphertext() {
257 let key = [0x42u8; 32];
258 let nonce = [0x07u8; 12];
259 let plaintext = b"Original message";
260
261 let cipher = ChaCha20Poly1305::new(&key).unwrap();
262 let mut ciphertext = cipher.encrypt(&nonce, plaintext, None).unwrap();
263
264 ciphertext[0] ^= 0xFF;
266
267 let result = cipher.decrypt(&nonce, &ciphertext, None);
269 assert_eq!(result, Err(CryptoError::AuthenticationFailed));
270 }
271
272 #[test]
273 fn test_tampered_tag() {
274 let key = [0x42u8; 32];
275 let nonce = [0x07u8; 12];
276 let plaintext = b"Original message";
277
278 let cipher = ChaCha20Poly1305::new(&key).unwrap();
279 let mut ciphertext = cipher.encrypt(&nonce, plaintext, None).unwrap();
280
281 let len = ciphertext.len();
283 ciphertext[len - 1] ^= 0xFF;
284
285 let result = cipher.decrypt(&nonce, &ciphertext, None);
287 assert_eq!(result, Err(CryptoError::AuthenticationFailed));
288 }
289
290 #[test]
291 fn test_rfc8439_vector() {
292 let key = [
294 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,
295 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f,
296 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,
297 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f,
298 ];
299
300 let nonce = [
301 0x07, 0x00, 0x00, 0x00,
302 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,
303 ];
304
305 let aad = [0x50, 0x51, 0x52, 0x53, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7];
306
307 let plaintext = b"Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it.";
308
309 let cipher = ChaCha20Poly1305::new(&key).unwrap();
310 let ciphertext = cipher.encrypt(&nonce, plaintext, Some(&aad)).unwrap();
311
312 let expected_ct = [
314 0xd3, 0x1a, 0x8d, 0x34, 0x64, 0x8e, 0x60, 0xdb, 0x7b, 0x86, 0xaf, 0xbc,
315 0x53, 0xef, 0x7e, 0xc2, 0xa4, 0xad, 0xed, 0x51, 0x29, 0x6e, 0x08, 0xfe,
316 0xa9, 0xe2, 0xb5, 0xa7, 0x36, 0xee, 0x62, 0xd6, 0x3d, 0xbe, 0xa4, 0x5e,
317 0x8c, 0xa9, 0x67, 0x12, 0x82, 0xfa, 0xfb, 0x69, 0xda, 0x92, 0x72, 0x8b,
318 0x1a, 0x71, 0xde, 0x0a, 0x9e, 0x06, 0x0b, 0x29, 0x05, 0xd6, 0xa5, 0xb6,
319 0x7e, 0xcd, 0x3b, 0x36, 0x92, 0xdd, 0xbd, 0x7f, 0x2d, 0x77, 0x8b, 0x8c,
320 0x98, 0x03, 0xae, 0xe3, 0x28, 0x09, 0x1b, 0x58, 0xfa, 0xb3, 0x24, 0xe4,
321 0xfa, 0xd6, 0x75, 0x94, 0x55, 0x85, 0x80, 0x8b, 0x48, 0x31, 0xd7, 0xbc,
322 0x3f, 0xf4, 0xde, 0xf0, 0x8e, 0x4b, 0x7a, 0x9d, 0xe5, 0x76, 0xd2, 0x65,
323 0x86, 0xce, 0xc6, 0x4b, 0x61, 0x16,
324 ];
325
326 let expected_tag = [
327 0x1a, 0xe1, 0x0b, 0x59, 0x4f, 0x09, 0xe2, 0x6a,
328 0x7e, 0x90, 0x2e, 0xcb, 0xd0, 0x60, 0x06, 0x91,
329 ];
330
331 assert_eq!(&ciphertext[..plaintext.len()], &expected_ct[..]);
333
334 let tag_offset = ciphertext.len() - 16;
336 assert_eq!(&ciphertext[tag_offset..], &expected_tag[..]);
337
338 let decrypted = cipher.decrypt(&nonce, &ciphertext, Some(&aad)).unwrap();
340 assert_eq!(decrypted, plaintext);
341 }
342
343 #[test]
344 fn test_invalid_key_size() {
345 let short_key = [0x42u8; 16];
346 let result = ChaCha20Poly1305::new(&short_key);
347 assert_eq!(result, Err(CryptoError::InvalidKeySize));
348 }
349
350 #[test]
351 fn test_invalid_nonce_size() {
352 let key = [0x42u8; 32];
353 let short_nonce = [0x07u8; 8];
354 let plaintext = b"test";
355
356 let cipher = ChaCha20Poly1305::new(&key).unwrap();
357 let result = cipher.encrypt(&short_nonce, plaintext, None);
358 assert_eq!(result, Err(CryptoError::InvalidNonceSize));
359 }
360}