encryption/encryptor/
chacha20poly1305_ietf.rs1use chacha20poly1305::aead::AeadInPlace;
10use chacha20poly1305::{ChaCha20Poly1305, KeyInit, Nonce, Tag};
11use rand_core::{OsRng, RngCore};
12
13use crate::contract::{Identified, SymmetricCipher};
14use crate::error::{Error, Result};
15use crate::key::Key;
16
17const LABEL: &str = "ChaCha20-Poly1305-IETF";
18const PREFIX: &[u8] = b"v1";
19const NONCE_LEN: usize = 12;
20const TAG_LEN: usize = 16;
21
22pub struct ChaCha20Poly1305IetfEncryptor {
27 cipher: ChaCha20Poly1305,
28}
29
30impl ChaCha20Poly1305IetfEncryptor {
31 pub const IDENTIFIER: &'static str = "chacha20-poly1305-ietf";
32
33 pub fn new(key: [u8; 32]) -> Self {
34 Self {
35 cipher: ChaCha20Poly1305::new_from_slice(&key).expect("key length is fixed at 32"),
36 }
37 }
38
39 pub fn from_slice(key: &[u8]) -> Result<Self> {
40 let key = Key::<32>::from_slice(key, LABEL)?;
42 Ok(Self {
43 cipher: ChaCha20Poly1305::new_from_slice(key.as_bytes())
44 .expect("key length is fixed at 32"),
45 })
46 }
47
48 pub(crate) fn encrypt_with_nonce(
54 &self,
55 nonce: &[u8; NONCE_LEN],
56 aad: &[u8],
57 plaintext: &[u8],
58 ) -> Result<Vec<u8>> {
59 let mut buffer = plaintext.to_vec();
60 let tag = self
61 .cipher
62 .encrypt_in_place_detached(Nonce::from_slice(nonce), aad, &mut buffer)
63 .map_err(|_| Error::EncryptionFailed { label: LABEL })?;
64
65 let mut blob = Vec::with_capacity(PREFIX.len() + NONCE_LEN + TAG_LEN + buffer.len());
66 blob.extend_from_slice(PREFIX);
67 blob.extend_from_slice(nonce);
68 blob.extend_from_slice(&tag);
69 blob.extend_from_slice(&buffer);
70 Ok(blob)
71 }
72
73 pub(crate) fn decrypt_with_aad(&self, ciphertext: &[u8], aad: &[u8]) -> Result<Vec<u8>> {
75 let body = ciphertext
76 .strip_prefix(PREFIX)
77 .ok_or(Error::InvalidPrefix { label: LABEL })?;
78 if body.len() < NONCE_LEN + TAG_LEN {
79 return Err(Error::TooShort { label: LABEL });
80 }
81
82 let (nonce, rest) = body.split_at(NONCE_LEN);
83 let (tag, ct) = rest.split_at(TAG_LEN);
84
85 let mut buffer = ct.to_vec();
86 self.cipher
87 .decrypt_in_place_detached(
88 Nonce::from_slice(nonce),
89 aad,
90 &mut buffer,
91 Tag::from_slice(tag),
92 )
93 .map_err(|_| Error::DecryptionFailed { label: LABEL })?;
94 Ok(buffer)
95 }
96}
97
98impl std::fmt::Debug for ChaCha20Poly1305IetfEncryptor {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 f.debug_struct("ChaCha20Poly1305IetfEncryptor")
103 .field("identifier", &Self::IDENTIFIER)
104 .finish_non_exhaustive()
105 }
106}
107
108impl Identified for ChaCha20Poly1305IetfEncryptor {
109 fn identifier(&self) -> &str {
110 Self::IDENTIFIER
111 }
112}
113
114impl SymmetricCipher for ChaCha20Poly1305IetfEncryptor {
115 fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
116 let mut nonce = [0u8; NONCE_LEN];
117 OsRng.fill_bytes(&mut nonce);
118 self.encrypt_with_nonce(&nonce, b"", plaintext)
119 }
120
121 fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
122 self.decrypt_with_aad(ciphertext, b"")
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::internal::hex_decode;
130
131 const KEY: &str = "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f";
142 const NONCE: &str = "070000004041424344454647";
143 const AAD: &str = "50515253c0c1c2c3c4c5c6c7";
144 const PLAINTEXT: &str = "Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it.";
145 const CIPHERTEXT: &str = "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6\
146 3dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b\
147 3692ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7\
148 bc3ff4def08e4b7a9de576d26586cec64b6116";
149 const TAG: &str = "1ae10b594f09e26a7e902ecbd0600691";
150
151 #[test]
152 fn kat_rfc_8439_2_8_2_is_byte_for_byte() {
153 let nonce: [u8; NONCE_LEN] = hex_decode(NONCE).unwrap().try_into().unwrap();
154 let aad = hex_decode(AAD).unwrap();
155 let ct = hex_decode(CIPHERTEXT).unwrap();
156 let tag = hex_decode(TAG).unwrap();
157 assert_eq!(ct.len(), PLAINTEXT.len());
159
160 let encryptor =
161 ChaCha20Poly1305IetfEncryptor::new(hex_decode(KEY).unwrap().try_into().unwrap());
162
163 let mut expected = PREFIX.to_vec();
164 expected.extend_from_slice(&nonce);
165 expected.extend_from_slice(&tag);
166 expected.extend_from_slice(&ct);
167 assert_eq!(
168 encryptor.encrypt_with_nonce(&nonce, &aad, PLAINTEXT.as_bytes()),
169 Ok(expected)
170 );
171
172 let blob = encryptor
174 .encrypt_with_nonce(&nonce, &aad, PLAINTEXT.as_bytes())
175 .unwrap();
176 assert_eq!(
177 encryptor.decrypt_with_aad(&blob, &aad).unwrap(),
178 PLAINTEXT.as_bytes()
179 );
180 }
181
182 #[test]
183 fn roundtrip_and_tamper_detection() {
184 let encryptor = ChaCha20Poly1305IetfEncryptor::new([7u8; 32]);
185 let blob = encryptor.encrypt(b"data").unwrap();
186 assert_eq!(blob.len(), 2 + NONCE_LEN + TAG_LEN + 4);
187 assert_eq!(encryptor.decrypt(&blob).unwrap(), b"data");
188
189 let mut tampered = blob.clone();
190 let last = tampered.len() - 1;
191 tampered[last] ^= 0x01;
192 assert_eq!(
193 encryptor.decrypt(&tampered).unwrap_err(),
194 Error::DecryptionFailed { label: LABEL }
195 );
196 }
197
198 #[test]
199 fn every_encrypt_uses_a_fresh_nonce() {
200 let encryptor = ChaCha20Poly1305IetfEncryptor::new([7u8; 32]);
201 let first = encryptor.encrypt(b"same").unwrap();
202 let second = encryptor.encrypt(b"same").unwrap();
203 assert_ne!(first, second);
204 assert_ne!(first[2..2 + NONCE_LEN], second[2..2 + NONCE_LEN]);
205 }
206
207 #[test]
208 fn wrong_key_and_bad_prefix_fail() {
209 let blob = ChaCha20Poly1305IetfEncryptor::new([7u8; 32])
210 .encrypt(b"x")
211 .unwrap();
212 let other = ChaCha20Poly1305IetfEncryptor::new([8u8; 32]);
213 assert_eq!(
214 other.decrypt(&blob).unwrap_err(),
215 Error::DecryptionFailed { label: LABEL }
216 );
217 assert_eq!(
218 other.decrypt(b"v9....").unwrap_err(),
219 Error::InvalidPrefix { label: LABEL }
220 );
221 assert_eq!(
222 other.decrypt(b"v1short").unwrap_err(),
223 Error::TooShort { label: LABEL }
224 );
225 }
226
227 #[test]
228 fn key_length_is_checked() {
229 assert_eq!(
230 ChaCha20Poly1305IetfEncryptor::from_slice(&[0u8; 16]).unwrap_err(),
231 Error::InvalidKeyLength {
232 label: LABEL,
233 expected: 32,
234 got: 16
235 }
236 );
237 }
238}