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
22#[derive(Debug)]
24pub struct ChaCha20Poly1305IetfEncryptor {
25 key: Key<32>,
26}
27
28impl ChaCha20Poly1305IetfEncryptor {
29 pub const IDENTIFIER: &'static str = "chacha20-poly1305-ietf";
30
31 pub fn new(key: [u8; 32]) -> Self {
32 Self { key: Key::new(key) }
33 }
34
35 pub fn from_slice(key: &[u8]) -> Result<Self> {
36 Ok(Self {
37 key: Key::from_slice(key, LABEL)?,
38 })
39 }
40
41 pub(crate) fn encrypt_with_nonce(
47 &self,
48 nonce: &[u8; NONCE_LEN],
49 aad: &[u8],
50 plaintext: &[u8],
51 ) -> Result<Vec<u8>> {
52 let cipher = ChaCha20Poly1305::new_from_slice(self.key.as_bytes())
54 .expect("key length is fixed at 32");
55
56 let mut buffer = plaintext.to_vec();
57 let tag = cipher
58 .encrypt_in_place_detached(Nonce::from_slice(nonce), aad, &mut buffer)
59 .map_err(|_| Error::EncryptionFailed { label: LABEL })?;
60
61 let mut blob = Vec::with_capacity(PREFIX.len() + NONCE_LEN + TAG_LEN + buffer.len());
62 blob.extend_from_slice(PREFIX);
63 blob.extend_from_slice(nonce);
64 blob.extend_from_slice(&tag);
65 blob.extend_from_slice(&buffer);
66 Ok(blob)
67 }
68
69 pub(crate) fn decrypt_with_aad(&self, ciphertext: &[u8], aad: &[u8]) -> Result<Vec<u8>> {
71 let body = ciphertext
72 .strip_prefix(PREFIX)
73 .ok_or(Error::InvalidPrefix { label: LABEL })?;
74 if body.len() < NONCE_LEN + TAG_LEN {
75 return Err(Error::TooShort { label: LABEL });
76 }
77
78 let (nonce, rest) = body.split_at(NONCE_LEN);
79 let (tag, ct) = rest.split_at(TAG_LEN);
80
81 let cipher = ChaCha20Poly1305::new_from_slice(self.key.as_bytes())
82 .expect("key length is fixed at 32");
83 let mut buffer = ct.to_vec();
84 cipher
85 .decrypt_in_place_detached(
86 Nonce::from_slice(nonce),
87 aad,
88 &mut buffer,
89 Tag::from_slice(tag),
90 )
91 .map_err(|_| Error::DecryptionFailed { label: LABEL })?;
92 Ok(buffer)
93 }
94}
95
96impl Identified for ChaCha20Poly1305IetfEncryptor {
97 fn identifier(&self) -> &str {
98 Self::IDENTIFIER
99 }
100}
101
102impl SymmetricCipher for ChaCha20Poly1305IetfEncryptor {
103 fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
104 let mut nonce = [0u8; NONCE_LEN];
105 OsRng.fill_bytes(&mut nonce);
106 self.encrypt_with_nonce(&nonce, b"", plaintext)
107 }
108
109 fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
110 self.decrypt_with_aad(ciphertext, b"")
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use crate::internal::hex_decode;
118
119 const KEY: &str = "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f";
130 const NONCE: &str = "070000004041424344454647";
131 const AAD: &str = "50515253c0c1c2c3c4c5c6c7";
132 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.";
133 const CIPHERTEXT: &str = "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6\
134 3dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b\
135 3692ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7\
136 bc3ff4def08e4b7a9de576d26586cec64b6116";
137 const TAG: &str = "1ae10b594f09e26a7e902ecbd0600691";
138
139 #[test]
140 fn kat_rfc_8439_2_8_2_is_byte_for_byte() {
141 let nonce: [u8; NONCE_LEN] = hex_decode(NONCE).unwrap().try_into().unwrap();
142 let aad = hex_decode(AAD).unwrap();
143 let ct = hex_decode(CIPHERTEXT).unwrap();
144 let tag = hex_decode(TAG).unwrap();
145 assert_eq!(ct.len(), PLAINTEXT.len());
147
148 let encryptor =
149 ChaCha20Poly1305IetfEncryptor::new(hex_decode(KEY).unwrap().try_into().unwrap());
150
151 let mut expected = PREFIX.to_vec();
152 expected.extend_from_slice(&nonce);
153 expected.extend_from_slice(&tag);
154 expected.extend_from_slice(&ct);
155 assert_eq!(
156 encryptor.encrypt_with_nonce(&nonce, &aad, PLAINTEXT.as_bytes()),
157 Ok(expected)
158 );
159
160 let blob = encryptor
162 .encrypt_with_nonce(&nonce, &aad, PLAINTEXT.as_bytes())
163 .unwrap();
164 assert_eq!(
165 encryptor.decrypt_with_aad(&blob, &aad).unwrap(),
166 PLAINTEXT.as_bytes()
167 );
168 }
169
170 #[test]
171 fn roundtrip_and_tamper_detection() {
172 let encryptor = ChaCha20Poly1305IetfEncryptor::new([7u8; 32]);
173 let blob = encryptor.encrypt(b"data").unwrap();
174 assert_eq!(blob.len(), 2 + NONCE_LEN + TAG_LEN + 4);
175 assert_eq!(encryptor.decrypt(&blob).unwrap(), b"data");
176
177 let mut tampered = blob.clone();
178 let last = tampered.len() - 1;
179 tampered[last] ^= 0x01;
180 assert_eq!(
181 encryptor.decrypt(&tampered).unwrap_err(),
182 Error::DecryptionFailed { label: LABEL }
183 );
184 }
185
186 #[test]
187 fn every_encrypt_uses_a_fresh_nonce() {
188 let encryptor = ChaCha20Poly1305IetfEncryptor::new([7u8; 32]);
189 let first = encryptor.encrypt(b"same").unwrap();
190 let second = encryptor.encrypt(b"same").unwrap();
191 assert_ne!(first, second);
192 assert_ne!(first[2..2 + NONCE_LEN], second[2..2 + NONCE_LEN]);
193 }
194
195 #[test]
196 fn wrong_key_and_bad_prefix_fail() {
197 let blob = ChaCha20Poly1305IetfEncryptor::new([7u8; 32])
198 .encrypt(b"x")
199 .unwrap();
200 let other = ChaCha20Poly1305IetfEncryptor::new([8u8; 32]);
201 assert_eq!(
202 other.decrypt(&blob).unwrap_err(),
203 Error::DecryptionFailed { label: LABEL }
204 );
205 assert_eq!(
206 other.decrypt(b"v9....").unwrap_err(),
207 Error::InvalidPrefix { label: LABEL }
208 );
209 assert_eq!(
210 other.decrypt(b"v1short").unwrap_err(),
211 Error::TooShort { label: LABEL }
212 );
213 }
214
215 #[test]
216 fn key_length_is_checked() {
217 assert_eq!(
218 ChaCha20Poly1305IetfEncryptor::from_slice(&[0u8; 16]).unwrap_err(),
219 Error::InvalidKeyLength {
220 label: LABEL,
221 expected: 32,
222 got: 16
223 }
224 );
225 }
226}