1use aead::{Aead, AeadCore, KeyInit, Payload};
4use aes_gcm::Aes256Gcm;
5use chacha20poly1305::ChaCha20Poly1305;
6use rand::rngs::OsRng;
7
8use crate::error::{Error, Result};
9
10const VERSION: u8 = 2;
11
12#[derive(Clone, Copy)]
13enum Algorithm {
14 Aes256Gcm = 1,
15 ChaCha20Poly1305 = 2,
16}
17
18impl Algorithm {
19 fn from_id(id: u8) -> Option<Algorithm> {
20 match id {
21 1 => Some(Algorithm::Aes256Gcm),
22 2 => Some(Algorithm::ChaCha20Poly1305),
23 _ => None,
24 }
25 }
26
27 fn name(&self) -> &'static str {
28 match self {
29 Algorithm::Aes256Gcm => "aes-256-gcm",
30 Algorithm::ChaCha20Poly1305 => "chacha20-poly1305",
31 }
32 }
33
34 fn key_len(&self) -> usize {
35 32
36 }
37
38 fn nonce_len(&self) -> usize {
39 12
40 }
41
42 fn tag_len(&self) -> usize {
43 16
44 }
45}
46
47fn parse_algorithm(name: &str) -> Result<Algorithm> {
48 match name.to_lowercase().as_str() {
49 "aes-256-gcm" | "aes_256_gcm" => Ok(Algorithm::Aes256Gcm),
50 "chacha20-poly1305" | "chacha20_poly1305" => Ok(Algorithm::ChaCha20Poly1305),
51 _ => Err(Error::UnsupportedAlgorithm(name.to_string())),
52 }
53}
54
55fn build_aad(version: u8, algo_id: u8, aad: &[u8], nonce: &[u8]) -> Vec<u8> {
56 let mut out = Vec::with_capacity(4 + aad.len() + nonce.len());
57 out.push(version);
58 out.push(algo_id);
59 out.extend_from_slice(&(aad.len() as u16).to_be_bytes());
60 out.extend_from_slice(aad);
61 out.extend_from_slice(nonce);
62 out
63}
64
65pub fn encrypt(plaintext: &[u8], key: &[u8], aad: Option<&[u8]>) -> Result<Vec<u8>> {
67 encrypt_with(plaintext, key, aad, "aes-256-gcm")
68}
69
70pub fn encrypt_string(plaintext: &str, key: &[u8], aad: Option<&[u8]>) -> Result<Vec<u8>> {
72 encrypt_with(plaintext.as_bytes(), key, aad, "aes-256-gcm")
73}
74
75pub fn encrypt_with(
77 plaintext: &[u8],
78 key: &[u8],
79 aad: Option<&[u8]>,
80 algorithm: &str,
81) -> Result<Vec<u8>> {
82 let algorithm = parse_algorithm(algorithm)?;
83 if key.len() != algorithm.key_len() {
84 return Err(Error::InvalidKey(format!(
85 "{} requires a {}-byte key",
86 algorithm.name(),
87 algorithm.key_len()
88 )));
89 }
90
91 let user_aad = aad.unwrap_or(&[]);
92 if user_aad.len() > u16::MAX as usize {
93 return Err(Error::InvalidParameter(format!(
94 "additional authenticated data length {} exceeds maximum of {} bytes",
95 user_aad.len(),
96 u16::MAX
97 )));
98 }
99 let (nonce, sealed) = match algorithm {
100 Algorithm::Aes256Gcm => {
101 let cipher = Aes256Gcm::new_from_slice(key)
102 .map_err(|e| Error::Internal(e.to_string()))?;
103 let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
104 let aad_for_cipher = build_aad(VERSION, algorithm as u8, user_aad, nonce.as_slice());
105 let ct = cipher
106 .encrypt(
107 &nonce,
108 Payload {
109 aad: &aad_for_cipher,
110 msg: plaintext,
111 },
112 )
113 .map_err(|e| Error::Internal(e.to_string()))?;
114 (nonce.to_vec(), ct)
115 }
116 Algorithm::ChaCha20Poly1305 => {
117 let cipher = ChaCha20Poly1305::new_from_slice(key)
118 .map_err(|e| Error::Internal(e.to_string()))?;
119 let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
120 let aad_for_cipher = build_aad(VERSION, algorithm as u8, user_aad, nonce.as_slice());
121 let ct = cipher
122 .encrypt(
123 &nonce,
124 Payload {
125 aad: &aad_for_cipher,
126 msg: plaintext,
127 },
128 )
129 .map_err(|e| Error::Internal(e.to_string()))?;
130 (nonce.to_vec(), ct)
131 }
132 };
133
134 let mut token = Vec::with_capacity(4 + user_aad.len() + nonce.len() + sealed.len());
135 token.push(VERSION);
136 token.push(algorithm as u8);
137 token.extend_from_slice(&(user_aad.len() as u16).to_be_bytes());
138 token.extend_from_slice(user_aad);
139 token.extend_from_slice(&nonce);
140 token.extend_from_slice(&sealed);
141 Ok(token)
142}
143
144pub fn decrypt(ciphertext: &[u8], key: &[u8], aad: Option<&[u8]>) -> Result<Vec<u8>> {
146 if ciphertext.len() < 2 {
147 return Err(Error::Decryption("ciphertext too short".to_string()));
148 }
149 match ciphertext[0] {
150 VERSION => decrypt_v2(ciphertext, key, aad),
151 1 => {
152 if aad.is_some() && !aad.unwrap().is_empty() {
153 return Err(Error::Decryption(
154 "v1 ciphertext does not support AAD".to_string(),
155 ));
156 }
157 decrypt_v1(ciphertext, key)
158 }
159 v => Err(Error::Decryption(format!("unsupported version {}", v))),
160 }
161}
162
163pub fn decrypt_string(ciphertext: &[u8], key: &[u8], aad: Option<&[u8]>) -> Result<String> {
165 let plaintext = decrypt(ciphertext, key, aad)?;
166 String::from_utf8(plaintext).map_err(|e| Error::Decryption(e.to_string()))
167}
168
169fn decrypt_v2(ciphertext: &[u8], key: &[u8], aad: Option<&[u8]>) -> Result<Vec<u8>> {
170 if ciphertext.len() < 4 {
171 return Err(Error::Decryption("ciphertext too short".to_string()));
172 }
173 let algo_id = ciphertext[1];
174 let aad_len = u16::from_be_bytes([ciphertext[2], ciphertext[3]]) as usize;
175 let aad_start = 4usize;
176 let aad_end = aad_start + aad_len;
177 if ciphertext.len() < aad_end {
178 return Err(Error::Decryption("ciphertext too short".to_string()));
179 }
180 let stored_aad = &ciphertext[aad_start..aad_end];
181 let expected_aad = aad.unwrap_or(&[]);
182 if stored_aad != expected_aad {
183 return Err(Error::Decryption(
184 "additional authenticated data does not match".to_string(),
185 ));
186 }
187
188 let algorithm = Algorithm::from_id(algo_id)
189 .ok_or_else(|| Error::Decryption(format!("unknown algorithm id {}", algo_id)))?;
190 if key.len() != algorithm.key_len() {
191 return Err(Error::InvalidKey(format!(
192 "{} requires a {}-byte key",
193 algorithm.name(),
194 algorithm.key_len()
195 )));
196 }
197 if ciphertext.len() < aad_end + algorithm.nonce_len() + algorithm.tag_len() {
198 return Err(Error::Decryption("ciphertext too short".to_string()));
199 }
200
201 let nonce = &ciphertext[aad_end..aad_end + algorithm.nonce_len()];
202 let sealed = &ciphertext[aad_end + algorithm.nonce_len()..];
203 let aad_for_cipher = build_aad(VERSION, algo_id, stored_aad, nonce);
204
205 let plaintext = match algorithm {
206 Algorithm::Aes256Gcm => {
207 let cipher = Aes256Gcm::new_from_slice(key)
208 .map_err(|e| Error::Internal(e.to_string()))?;
209 let nonce = aes_gcm::Nonce::from_slice(nonce);
210 cipher
211 .decrypt(
212 nonce,
213 Payload {
214 aad: &aad_for_cipher,
215 msg: sealed,
216 },
217 )
218 .map_err(|e| Error::Decryption(e.to_string()))?
219 }
220 Algorithm::ChaCha20Poly1305 => {
221 let cipher = ChaCha20Poly1305::new_from_slice(key)
222 .map_err(|e| Error::Internal(e.to_string()))?;
223 let nonce = chacha20poly1305::Nonce::from_slice(nonce);
224 cipher
225 .decrypt(
226 nonce,
227 Payload {
228 aad: &aad_for_cipher,
229 msg: sealed,
230 },
231 )
232 .map_err(|e| Error::Decryption(e.to_string()))?
233 }
234 };
235
236 Ok(plaintext)
237}
238
239fn decrypt_v1(ciphertext: &[u8], key: &[u8]) -> Result<Vec<u8>> {
240 let algorithm = Algorithm::from_id(ciphertext[1])
241 .ok_or_else(|| Error::Decryption(format!("unknown algorithm id {}", ciphertext[1])))?;
242 if key.len() != algorithm.key_len() {
243 return Err(Error::InvalidKey(format!(
244 "{} requires a {}-byte key",
245 algorithm.name(),
246 algorithm.key_len()
247 )));
248 }
249 if ciphertext.len() < 2 + algorithm.nonce_len() + algorithm.tag_len() {
250 return Err(Error::Decryption("ciphertext too short".to_string()));
251 }
252
253 let nonce = &ciphertext[2..2 + algorithm.nonce_len()];
254 let sealed = &ciphertext[2 + algorithm.nonce_len()..];
255
256 let plaintext = match algorithm {
257 Algorithm::Aes256Gcm => {
258 let cipher = Aes256Gcm::new_from_slice(key)
259 .map_err(|e| Error::Internal(e.to_string()))?;
260 let nonce = aes_gcm::Nonce::from_slice(nonce);
261 cipher
262 .decrypt(nonce, sealed)
263 .map_err(|e| Error::Decryption(e.to_string()))?
264 }
265 Algorithm::ChaCha20Poly1305 => {
266 let cipher = ChaCha20Poly1305::new_from_slice(key)
267 .map_err(|e| Error::Internal(e.to_string()))?;
268 let nonce = chacha20poly1305::Nonce::from_slice(nonce);
269 cipher
270 .decrypt(nonce, sealed)
271 .map_err(|e| Error::Decryption(e.to_string()))?
272 }
273 };
274
275 Ok(plaintext)
276}
277
278pub fn symmetric(plaintext: &str, key: &[u8]) -> Result<Vec<u8>> {
280 encrypt_string(plaintext, key, None)
281}