Skip to main content

egide_crypto/
aead.rs

1//! AES-256-GCM authenticated encryption.
2//!
3//! Provides authenticated encryption with associated data (AEAD) using AES-256-GCM.
4//! This is the primary encryption algorithm used throughout Egide for encrypting secrets.
5
6use aes_gcm::{
7    aead::{Aead, KeyInit},
8    Aes256Gcm, Nonce,
9};
10use zeroize::Zeroizing;
11
12use crate::error::CryptoError;
13use crate::random::generate_nonce;
14
15/// Size of an AES-256 key in bytes.
16pub const KEY_SIZE: usize = 32;
17
18/// Size of a GCM nonce in bytes.
19pub const NONCE_SIZE: usize = 12;
20
21/// Size of a GCM authentication tag in bytes.
22pub const TAG_SIZE: usize = 16;
23
24/// Encrypts plaintext using AES-256-GCM.
25///
26/// The nonce is automatically generated and prepended to the ciphertext.
27/// Format: `nonce (12 bytes) || ciphertext || tag (16 bytes)`
28///
29/// # Arguments
30///
31/// * `key` - 32-byte encryption key
32/// * `plaintext` - Data to encrypt
33/// * `associated_data` - Optional additional data to authenticate (not encrypted)
34///
35/// # Returns
36///
37/// Ciphertext with prepended nonce and appended authentication tag.
38pub fn encrypt(
39    key: &[u8],
40    plaintext: &[u8],
41    associated_data: Option<&[u8]>,
42) -> Result<Vec<u8>, CryptoError> {
43    if key.len() != KEY_SIZE {
44        return Err(CryptoError::InvalidKey(format!(
45            "expected {} bytes, got {}",
46            KEY_SIZE,
47            key.len()
48        )));
49    }
50
51    let cipher =
52        Aes256Gcm::new_from_slice(key).map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;
53
54    let nonce_bytes = generate_nonce()?;
55    let nonce = Nonce::from_slice(&nonce_bytes);
56
57    let ciphertext = match associated_data {
58        Some(aad) => cipher
59            .encrypt(
60                nonce,
61                aes_gcm::aead::Payload {
62                    msg: plaintext,
63                    aad,
64                },
65            )
66            .map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?,
67        None => cipher
68            .encrypt(nonce, plaintext)
69            .map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?,
70    };
71
72    let mut result = Vec::with_capacity(NONCE_SIZE + ciphertext.len());
73    result.extend_from_slice(&nonce_bytes);
74    result.extend_from_slice(&ciphertext);
75
76    Ok(result)
77}
78
79/// Decrypts ciphertext using AES-256-GCM.
80///
81/// Expects the nonce to be prepended to the ciphertext (as produced by `encrypt`).
82///
83/// # Arguments
84///
85/// * `key` - 32-byte encryption key
86/// * `ciphertext` - Data to decrypt (nonce || ciphertext || tag)
87/// * `associated_data` - Optional additional data that was authenticated
88///
89/// # Returns
90///
91/// Decrypted plaintext wrapped in `Zeroizing` for automatic memory cleanup.
92pub fn decrypt(
93    key: &[u8],
94    ciphertext: &[u8],
95    associated_data: Option<&[u8]>,
96) -> Result<Zeroizing<Vec<u8>>, CryptoError> {
97    if key.len() != KEY_SIZE {
98        return Err(CryptoError::InvalidKey(format!(
99            "expected {} bytes, got {}",
100            KEY_SIZE,
101            key.len()
102        )));
103    }
104
105    if ciphertext.len() < NONCE_SIZE + TAG_SIZE {
106        return Err(CryptoError::InvalidInput(
107            "ciphertext too short".to_string(),
108        ));
109    }
110
111    let cipher =
112        Aes256Gcm::new_from_slice(key).map_err(|e| CryptoError::DecryptionFailed(e.to_string()))?;
113
114    let nonce = Nonce::from_slice(&ciphertext[..NONCE_SIZE]);
115    let encrypted = &ciphertext[NONCE_SIZE..];
116
117    let plaintext = match associated_data {
118        Some(aad) => cipher
119            .decrypt(
120                nonce,
121                aes_gcm::aead::Payload {
122                    msg: encrypted,
123                    aad,
124                },
125            )
126            .map_err(|_| CryptoError::DecryptionFailed("authentication failed".to_string()))?,
127        None => cipher
128            .decrypt(nonce, encrypted)
129            .map_err(|_| CryptoError::DecryptionFailed("authentication failed".to_string()))?,
130    };
131
132    Ok(Zeroizing::new(plaintext))
133}
134
135#[cfg(test)]
136#[allow(clippy::disallowed_methods)]
137mod tests {
138    use super::*;
139    use crate::random::generate_key;
140
141    #[test]
142    fn test_encrypt_decrypt_roundtrip() {
143        let key = generate_key().unwrap();
144        let plaintext = b"Hello, Egide!";
145
146        let ciphertext = encrypt(&*key, plaintext, None).unwrap();
147        let decrypted = decrypt(&*key, &ciphertext, None).unwrap();
148
149        assert_eq!(&*decrypted, plaintext);
150    }
151
152    #[test]
153    fn test_encrypt_decrypt_with_aad() {
154        let key = generate_key().unwrap();
155        let plaintext = b"secret data";
156        let aad = b"additional authenticated data";
157
158        let ciphertext = encrypt(&*key, plaintext, Some(aad)).unwrap();
159        let decrypted = decrypt(&*key, &ciphertext, Some(aad)).unwrap();
160
161        assert_eq!(&*decrypted, plaintext);
162    }
163
164    #[test]
165    fn test_decrypt_wrong_aad_fails() {
166        let key = generate_key().unwrap();
167        let plaintext = b"secret data";
168        let aad = b"correct aad";
169        let wrong_aad = b"wrong aad";
170
171        let ciphertext = encrypt(&*key, plaintext, Some(aad)).unwrap();
172        let result = decrypt(&*key, &ciphertext, Some(wrong_aad));
173
174        assert!(result.is_err());
175    }
176
177    #[test]
178    fn test_decrypt_wrong_key_fails() {
179        let key1 = generate_key().unwrap();
180        let key2 = generate_key().unwrap();
181        let plaintext = b"secret data";
182
183        let ciphertext = encrypt(&*key1, plaintext, None).unwrap();
184        let result = decrypt(&*key2, &ciphertext, None);
185
186        assert!(result.is_err());
187    }
188
189    #[test]
190    fn test_invalid_key_size() {
191        let short_key = vec![0u8; 16];
192        let plaintext = b"test";
193
194        let result = encrypt(&short_key, plaintext, None);
195        assert!(matches!(result, Err(CryptoError::InvalidKey(_))));
196    }
197
198    #[test]
199    fn test_ciphertext_format() {
200        let key = generate_key().unwrap();
201        let plaintext = b"test";
202
203        let ciphertext = encrypt(&*key, plaintext, None).unwrap();
204
205        assert_eq!(ciphertext.len(), NONCE_SIZE + plaintext.len() + TAG_SIZE);
206    }
207
208    #[test]
209    fn test_tampered_ciphertext_fails() {
210        let key = generate_key().unwrap();
211        let plaintext = b"secret data";
212
213        let mut ciphertext = encrypt(&*key, plaintext, None).unwrap();
214        ciphertext[NONCE_SIZE] ^= 0xFF;
215
216        let result = decrypt(&*key, &ciphertext, None);
217        assert!(result.is_err());
218    }
219}