1use chacha20::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek};
2use chacha20::ChaCha20Legacy;
3use poly1305::universal_hash::KeyInit;
4use poly1305::Poly1305;
5use subtle::ConstantTimeEq;
6
7pub const CHACHA20_POLY1305_KEY_LEN: usize = 32;
8pub const CHACHA20_POLY1305_NONCE_LEN: usize = 8;
9pub const CHACHA20_POLY1305_TAG_LEN: usize = 16;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum CryptoError {
13 InvalidKey,
14 InvalidNonce,
15 CiphertextTooShort,
16 AuthenticationFailed,
17}
18
19impl std::fmt::Display for CryptoError {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 match self {
22 Self::InvalidKey => write!(f, "invalid key"),
23 Self::InvalidNonce => write!(f, "invalid nonce"),
24 Self::CiphertextTooShort => write!(f, "ciphertext is shorter than authentication tag"),
25 Self::AuthenticationFailed => write!(f, "authentication failed"),
26 }
27 }
28}
29
30impl std::error::Error for CryptoError {}
31
32pub fn decrypt_chacha20poly1305_legacy(
33 key: &[u8],
34 nonce: &[u8],
35 aad: &[u8],
36 ciphertext_and_tag: &[u8],
37) -> Result<Vec<u8>, CryptoError> {
38 if ciphertext_and_tag.len() < CHACHA20_POLY1305_TAG_LEN {
39 return Err(CryptoError::CiphertextTooShort);
40 }
41 let ciphertext_len = ciphertext_and_tag.len() - CHACHA20_POLY1305_TAG_LEN;
42 let ciphertext = &ciphertext_and_tag[..ciphertext_len];
43 let expected_tag = &ciphertext_and_tag[ciphertext_len..];
44 let tag = chacha20poly1305_legacy_tag(key, nonce, aad, ciphertext)?;
45 if tag.ct_eq(expected_tag).unwrap_u8() != 1 {
46 return Err(CryptoError::AuthenticationFailed);
47 }
48
49 let mut plaintext = ciphertext.to_vec();
50 apply_chacha20_legacy_keystream(key, nonce, 64, &mut plaintext)?;
51 Ok(plaintext)
52}
53
54pub fn encrypt_chacha20poly1305_legacy(
55 key: &[u8],
56 nonce: &[u8],
57 aad: &[u8],
58 plaintext: &[u8],
59) -> Result<Vec<u8>, CryptoError> {
60 let mut ciphertext = plaintext.to_vec();
61 apply_chacha20_legacy_keystream(key, nonce, 64, &mut ciphertext)?;
62 let tag = chacha20poly1305_legacy_tag(key, nonce, aad, &ciphertext)?;
63 ciphertext.extend_from_slice(&tag);
64 Ok(ciphertext)
65}
66
67fn chacha20poly1305_legacy_tag(
68 key: &[u8],
69 nonce: &[u8],
70 aad: &[u8],
71 ciphertext: &[u8],
72) -> Result<[u8; CHACHA20_POLY1305_TAG_LEN], CryptoError> {
73 let mut block0 = [0; 64];
74 apply_chacha20_legacy_keystream(key, nonce, 0, &mut block0)?;
75 let poly_key: [u8; 32] = block0[..32]
76 .try_into()
77 .map_err(|_| CryptoError::InvalidKey)?;
78
79 let mut mac_data = Vec::with_capacity(
80 aad.len() + pad16_len(aad.len()) + ciphertext.len() + pad16_len(ciphertext.len()) + 16,
81 );
82 mac_data.extend_from_slice(aad);
83 mac_data.extend(std::iter::repeat_n(0, pad16_len(aad.len())));
84 mac_data.extend_from_slice(ciphertext);
85 mac_data.extend(std::iter::repeat_n(0, pad16_len(ciphertext.len())));
86 mac_data.extend_from_slice(&(aad.len() as u64).to_le_bytes());
87 mac_data.extend_from_slice(&(ciphertext.len() as u64).to_le_bytes());
88
89 let tag = Poly1305::new((&poly_key).into()).compute_unpadded(&mac_data);
90 let mut out = [0; CHACHA20_POLY1305_TAG_LEN];
91 out.copy_from_slice(&tag);
92 Ok(out)
93}
94
95fn apply_chacha20_legacy_keystream(
96 key: &[u8],
97 nonce: &[u8],
98 offset: u32,
99 data: &mut [u8],
100) -> Result<(), CryptoError> {
101 let key: [u8; CHACHA20_POLY1305_KEY_LEN] =
102 key.try_into().map_err(|_| CryptoError::InvalidKey)?;
103 let nonce: [u8; CHACHA20_POLY1305_NONCE_LEN] =
104 nonce.try_into().map_err(|_| CryptoError::InvalidNonce)?;
105 let mut cipher = ChaCha20Legacy::new(&key.into(), &nonce.into());
106 cipher.seek(offset);
107 cipher.apply_keystream(data);
108 Ok(())
109}
110
111const fn pad16_len(len: usize) -> usize {
112 (16 - (len % 16)) % 16
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn legacy_aead_roundtrips_with_aad() {
121 let key = [7; 32];
122 let nonce = [9; 8];
123 let aad = b"wfb block header";
124 let plaintext = b"rtp payload bytes";
125
126 let encrypted = encrypt_chacha20poly1305_legacy(&key, &nonce, aad, plaintext).unwrap();
127 assert_ne!(&encrypted[..plaintext.len()], plaintext);
128 let decrypted = decrypt_chacha20poly1305_legacy(&key, &nonce, aad, &encrypted).unwrap();
129 assert_eq!(decrypted, plaintext);
130 }
131
132 #[test]
133 fn legacy_aead_rejects_modified_tag() {
134 let key = [7; 32];
135 let nonce = [9; 8];
136 let mut encrypted =
137 encrypt_chacha20poly1305_legacy(&key, &nonce, b"aad", b"payload").unwrap();
138 let last = encrypted.len() - 1;
139 encrypted[last] ^= 0x80;
140
141 assert_eq!(
142 decrypt_chacha20poly1305_legacy(&key, &nonce, b"aad", &encrypted).unwrap_err(),
143 CryptoError::AuthenticationFailed
144 );
145 }
146}