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 if ciphertext_and_tag.len() < CHACHA20_POLY1305_TAG_LEN {
48 return Err(CryptoError::CiphertextTooShort);
49 }
50 let ciphertext_len = ciphertext_and_tag.len() - CHACHA20_POLY1305_TAG_LEN;
51 let ciphertext = &ciphertext_and_tag[..ciphertext_len];
52 let expected_tag = &ciphertext_and_tag[ciphertext_len..];
53 let tag = chacha20poly1305_legacy_tag(key, nonce, aad, ciphertext)?;
54 if tag.ct_eq(expected_tag).unwrap_u8() != 1 {
55 return Err(CryptoError::AuthenticationFailed);
56 }
57
58 let mut plaintext = ciphertext.to_vec();
59 apply_chacha20_legacy_keystream(key, nonce, 64, &mut plaintext)?;
60 Ok(plaintext)
61}
62
63pub fn encrypt_chacha20poly1305_legacy(
65 key: &[u8],
66 nonce: &[u8],
67 aad: &[u8],
68 plaintext: &[u8],
69) -> Result<Vec<u8>, CryptoError> {
70 let mut ciphertext = plaintext.to_vec();
71 apply_chacha20_legacy_keystream(key, nonce, 64, &mut ciphertext)?;
72 let tag = chacha20poly1305_legacy_tag(key, nonce, aad, &ciphertext)?;
73 ciphertext.extend_from_slice(&tag);
74 Ok(ciphertext)
75}
76
77fn chacha20poly1305_legacy_tag(
78 key: &[u8],
79 nonce: &[u8],
80 aad: &[u8],
81 ciphertext: &[u8],
82) -> Result<[u8; CHACHA20_POLY1305_TAG_LEN], CryptoError> {
83 let mut block0 = [0; 64];
84 apply_chacha20_legacy_keystream(key, nonce, 0, &mut block0)?;
85 let poly_key: [u8; 32] = block0[..32]
86 .try_into()
87 .map_err(|_| CryptoError::InvalidKey)?;
88
89 let mut mac_data = Vec::with_capacity(
90 aad.len() + pad16_len(aad.len()) + ciphertext.len() + pad16_len(ciphertext.len()) + 16,
91 );
92 mac_data.extend_from_slice(aad);
93 mac_data.extend(std::iter::repeat_n(0, pad16_len(aad.len())));
94 mac_data.extend_from_slice(ciphertext);
95 mac_data.extend(std::iter::repeat_n(0, pad16_len(ciphertext.len())));
96 mac_data.extend_from_slice(&(aad.len() as u64).to_le_bytes());
97 mac_data.extend_from_slice(&(ciphertext.len() as u64).to_le_bytes());
98
99 let tag = Poly1305::new((&poly_key).into()).compute_unpadded(&mac_data);
100 let mut out = [0; CHACHA20_POLY1305_TAG_LEN];
101 out.copy_from_slice(&tag);
102 Ok(out)
103}
104
105fn apply_chacha20_legacy_keystream(
106 key: &[u8],
107 nonce: &[u8],
108 offset: u32,
109 data: &mut [u8],
110) -> Result<(), CryptoError> {
111 let key: [u8; CHACHA20_POLY1305_KEY_LEN] =
112 key.try_into().map_err(|_| CryptoError::InvalidKey)?;
113 let nonce: [u8; CHACHA20_POLY1305_NONCE_LEN] =
114 nonce.try_into().map_err(|_| CryptoError::InvalidNonce)?;
115 let mut cipher = ChaCha20Legacy::new(&key.into(), &nonce.into());
116 cipher.seek(offset);
117 cipher.apply_keystream(data);
118 Ok(())
119}
120
121const fn pad16_len(len: usize) -> usize {
122 (16 - (len % 16)) % 16
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn legacy_aead_roundtrips_with_aad() {
131 let key = [7; 32];
132 let nonce = [9; 8];
133 let aad = b"wfb block header";
134 let plaintext = b"rtp payload bytes";
135
136 let encrypted = encrypt_chacha20poly1305_legacy(&key, &nonce, aad, plaintext).unwrap();
137 assert_ne!(&encrypted[..plaintext.len()], plaintext);
138 let decrypted = decrypt_chacha20poly1305_legacy(&key, &nonce, aad, &encrypted).unwrap();
139 assert_eq!(decrypted, plaintext);
140 }
141
142 #[test]
143 fn legacy_aead_rejects_modified_tag() {
144 let key = [7; 32];
145 let nonce = [9; 8];
146 let mut encrypted =
147 encrypt_chacha20poly1305_legacy(&key, &nonce, b"aad", b"payload").unwrap();
148 let last = encrypted.len() - 1;
149 encrypted[last] ^= 0x80;
150
151 assert_eq!(
152 decrypt_chacha20poly1305_legacy(&key, &nonce, b"aad", &encrypted).unwrap_err(),
153 CryptoError::AuthenticationFailed
154 );
155 }
156}