Skip to main content

modern_crypto_toolkit/
encrypt.rs

1//! Symmetric AEAD encryption with safe, modern defaults.
2
3use 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
65/// Encrypt plaintext using AES-256-GCM.
66pub fn encrypt(plaintext: &[u8], key: &[u8], aad: Option<&[u8]>) -> Result<Vec<u8>> {
67    encrypt_with(plaintext, key, aad, "aes-256-gcm")
68}
69
70/// Encrypt a UTF-8 string using AES-256-GCM.
71pub 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
75/// Encrypt plaintext with the chosen algorithm.
76pub 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
144/// Decrypt and authenticate a token.
145pub 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
163/// Decrypt and authenticate a token, returning a string.
164pub 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
278/// Deprecated alias for `encrypt`.
279pub fn symmetric(plaintext: &str, key: &[u8]) -> Result<Vec<u8>> {
280    encrypt_string(plaintext, key, None)
281}