chacha20poly1305_nostd/
lib.rs

1//! ChaCha20-Poly1305 Authenticated Encryption with Associated Data (AEAD)
2//!
3//! Pure-Rust implementation combining:
4//! - ChaCha20 stream cipher (encryption)
5//! - Poly1305 MAC (authentication)
6//!
7//! This is a custom implementation to avoid LLVM SIMD issues on x86_64-unknown-none.
8//!
9//! Properties:
10//! - 256-bit key, 96-bit nonce
11//! - 128-bit authentication tag
12//! - Constant-time operations
13//! - ~4 cycles/byte on modern x86_64
14//!
15//! Used by:
16//! - WireGuard: Transport data encryption
17//! - SSH: chacha20-poly1305@openssh.com cipher
18
19#![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
32/// Key size for ChaCha20-Poly1305 (32 bytes)
33pub const CHACHA20_KEY_SIZE: usize = 32;
34
35/// Nonce size for ChaCha20-Poly1305 (12 bytes)
36pub const CHACHA20_NONCE_SIZE: usize = 12;
37
38/// Authentication tag size (16 bytes)
39pub const POLY1305_TAG_SIZE: usize = 16;
40
41/// Cryptographic error types
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum CryptoError {
44    /// Authentication tag verification failed
45    AuthenticationFailed,
46    /// Invalid key size
47    InvalidKeySize,
48    /// Invalid nonce size
49    InvalidNonceSize,
50    /// Invalid ciphertext length
51    InvalidLength,
52}
53
54pub type CryptoResult<T> = Result<T, CryptoError>;
55
56/// ChaCha20-Poly1305 AEAD cipher
57#[derive(Debug, PartialEq)]
58pub struct ChaCha20Poly1305 {
59    key: [u8; 32],
60}
61
62impl ChaCha20Poly1305 {
63    /// Create a new cipher from a 32-byte key
64    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    /// Encrypt plaintext with authentication
76    ///
77    /// # Arguments
78    /// * `nonce` - 12-byte nonce (MUST be unique per message)
79    /// * `plaintext` - Data to encrypt
80    /// * `aad` - Optional associated data (authenticated but not encrypted)
81    ///
82    /// # Returns
83    /// Ciphertext with 16-byte Poly1305 tag appended
84    #[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        // Create ChaCha20 cipher
91        let mut cipher = ChaCha20::new(&self.key.into(), nonce.into());
92
93        // Generate Poly1305 key using ChaCha20 block 0
94        // RFC 8439: Use first 32 bytes of block 0, but consume full 64-byte block
95        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        // Encrypt plaintext with ChaCha20 (now at block 1)
101        let mut ciphertext = plaintext.to_vec();
102        cipher.apply_keystream(&mut ciphertext);
103
104        // Compute Poly1305 MAC over AAD (if any) and ciphertext
105        let tag = self.compute_tag(&poly_key, aad.unwrap_or(&[]), &ciphertext);
106
107        // Append tag to ciphertext
108        ciphertext.extend_from_slice(&tag);
109
110        Ok(ciphertext)
111    }
112
113    /// Decrypt ciphertext and verify authentication tag
114    ///
115    /// # Arguments
116    /// * `nonce` - 12-byte nonce (same as used for encryption)
117    /// * `ciphertext` - Encrypted data with 16-byte tag appended
118    /// * `aad` - Optional associated data (must match encryption AAD)
119    ///
120    /// # Returns
121    /// Plaintext on success, or error if authentication fails
122    #[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        // Split ciphertext and tag
133        let ct_len = ciphertext.len() - POLY1305_TAG_SIZE;
134        let ct = &ciphertext[..ct_len];
135        let received_tag = &ciphertext[ct_len..];
136
137        // Create ChaCha20 cipher
138        let mut cipher = ChaCha20::new(&self.key.into(), nonce.into());
139
140        // Generate Poly1305 key using ChaCha20 block 0
141        // RFC 8439: Use first 32 bytes of block 0, but consume full 64-byte block
142        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        // Recompute and verify tag
148        let expected_tag = self.compute_tag(&poly_key, aad.unwrap_or(&[]), ct);
149
150        // Constant-time comparison
151        if !constant_time_eq(&expected_tag, received_tag) {
152            return Err(CryptoError::AuthenticationFailed);
153        }
154
155        // Decrypt ciphertext (cipher now at block 1)
156        let mut plaintext = ct.to_vec();
157        cipher.apply_keystream(&mut plaintext);
158
159        Ok(plaintext)
160    }
161
162    /// Compute Poly1305 MAC over AAD || ciphertext
163    ///
164    /// RFC 8439 construction:
165    /// - Pad AAD to 16-byte boundary
166    /// - Append ciphertext
167    /// - Pad ciphertext to 16-byte boundary
168    /// - Append AAD length (8 bytes, little-endian)
169    /// - Append ciphertext length (8 bytes, little-endian)
170    fn compute_tag(&self, poly_key: &[u8; 32], aad: &[u8], ciphertext: &[u8]) -> [u8; 16] {
171        let mut poly = Poly1305::new(poly_key);
172
173        // Add AAD
174        poly.update(aad);
175
176        // Pad AAD to 16-byte boundary
177        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        // Add ciphertext
184        poly.update(ciphertext);
185
186        // Pad ciphertext to 16-byte boundary
187        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        // Add lengths (little-endian u64)
194        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
203/// Constant-time equality comparison
204fn 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        // Wrong AAD should fail authentication
250        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        // Tamper with one byte
265        ciphertext[0] ^= 0xFF;
266
267        // Decryption should fail
268        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        // Tamper with tag
282        let len = ciphertext.len();
283        ciphertext[len - 1] ^= 0xFF;
284
285        // Decryption should fail
286        let result = cipher.decrypt(&nonce, &ciphertext, None);
287        assert_eq!(result, Err(CryptoError::AuthenticationFailed));
288    }
289
290    #[test]
291    fn test_rfc8439_vector() {
292        // RFC 8439 Appendix A.5 Test Vector
293        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        // Expected ciphertext (from RFC)
313        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        // Verify ciphertext
332        assert_eq!(&ciphertext[..plaintext.len()], &expected_ct[..]);
333
334        // Verify tag
335        let tag_offset = ciphertext.len() - 16;
336        assert_eq!(&ciphertext[tag_offset..], &expected_tag[..]);
337
338        // Verify decryption works
339        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}