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