Skip to main content

voided_core/encryption/
mod.rs

1//! Encryption module providing AEAD encryption primitives.
2//!
3//! Supports XChaCha20-Poly1305 (the high-level default) and explicit AES-256-GCM.
4
5mod aes_gcm;
6mod key;
7mod xchacha20;
8
9pub use aes_gcm::{decrypt_aes_gcm, encrypt_aes_gcm};
10pub use key::{
11    derive_key_from_shared_secret, derive_key_hkdf, derive_key_hkdf_raw, derive_key_pbkdf2,
12    generate_key, generate_x25519_key_pair, validate_pbkdf2_parameters, x25519_shared_secret, Key,
13    X25519KeyPair, HKDF_SHA256_MAX_OUTPUT, PBKDF2_MAX_ITERATIONS, PBKDF2_MAX_SALT_SIZE,
14    PBKDF2_MIN_ITERATIONS, PBKDF2_MIN_SALT_SIZE, X25519_KEY_SIZE,
15};
16pub use xchacha20::{decrypt_xchacha20, encrypt_xchacha20};
17
18use crate::{Error, Result, FORMAT_VERSION, MAGIC_ENCRYPTED};
19use alloc::vec::Vec;
20use serde::{Deserialize, Serialize};
21use zeroize::Zeroize;
22
23/// Supported encryption algorithms
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[repr(u8)]
26pub enum Algorithm {
27    /// AES-256-GCM with 12-byte nonce
28    Aes256Gcm = 0x01,
29    /// XChaCha20-Poly1305 with 24-byte nonce
30    XChaCha20Poly1305 = 0x02,
31}
32
33impl Algorithm {
34    /// Get algorithm from byte identifier
35    pub fn from_byte(byte: u8) -> Result<Self> {
36        match byte {
37            0x01 => Ok(Algorithm::Aes256Gcm),
38            0x02 => Ok(Algorithm::XChaCha20Poly1305),
39            _ => Err(Error::UnsupportedAlgorithm(byte)),
40        }
41    }
42
43    /// Get nonce length for this algorithm
44    pub fn nonce_len(&self) -> usize {
45        match self {
46            Algorithm::Aes256Gcm => 12,
47            Algorithm::XChaCha20Poly1305 => 24,
48        }
49    }
50
51    /// Get algorithm name as string
52    pub fn name(&self) -> &'static str {
53        match self {
54            Algorithm::Aes256Gcm => "aes-256-gcm",
55            Algorithm::XChaCha20Poly1305 => "xchacha20-poly1305",
56        }
57    }
58}
59
60/// Result of an encryption operation
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct EncryptionResult {
63    /// Encrypted ciphertext
64    pub ciphertext: Vec<u8>,
65    /// Algorithm used
66    pub algorithm: Algorithm,
67    /// Nonce/IV used
68    pub nonce: Vec<u8>,
69    /// Authentication tag
70    pub tag: Vec<u8>,
71}
72
73impl EncryptionResult {
74    /// Serialize to binary format
75    pub fn to_bytes(&self) -> Vec<u8> {
76        let nonce_len = self.nonce.len();
77        let total_len = 8 + nonce_len + self.ciphertext.len() + self.tag.len();
78        let mut buf = Vec::with_capacity(total_len);
79
80        // Magic bytes
81        buf.extend_from_slice(MAGIC_ENCRYPTED);
82        // Version
83        buf.push(FORMAT_VERSION);
84        // Algorithm
85        buf.push(self.algorithm as u8);
86        // Nonce length
87        buf.push(nonce_len as u8);
88        // Reserved
89        buf.push(0x00);
90        // Nonce
91        buf.extend_from_slice(&self.nonce);
92        // Ciphertext
93        buf.extend_from_slice(&self.ciphertext);
94        // Tag
95        buf.extend_from_slice(&self.tag);
96
97        buf
98    }
99
100    /// Deserialize from binary format
101    pub fn from_bytes(data: &[u8]) -> Result<Self> {
102        if data.len() < 8 {
103            return Err(Error::TruncatedPayload {
104                expected: 8,
105                actual: data.len(),
106            });
107        }
108
109        // Check magic
110        if &data[0..4] != MAGIC_ENCRYPTED {
111            return Err(Error::InvalidFormat);
112        }
113
114        // Check version
115        let version = data[4];
116        if version != FORMAT_VERSION {
117            return Err(Error::UnsupportedVersion(version));
118        }
119
120        // Parse algorithm
121        let algorithm = Algorithm::from_byte(data[5])?;
122
123        // Parse nonce length
124        let nonce_len = data[6] as usize;
125        if nonce_len != algorithm.nonce_len() {
126            return Err(Error::InvalidNonceLength {
127                expected: algorithm.nonce_len(),
128                actual: nonce_len,
129            });
130        }
131
132        // Canonical v1 encoding requires the reserved byte to be zero. Accepting
133        // alternate encodings makes byte-level signatures and cache keys malleable.
134        if data[7] != 0 {
135            return Err(Error::InvalidFormat);
136        }
137
138        // Minimum size check
139        let min_size = 8 + nonce_len + 16; // header + nonce + tag
140        if data.len() < min_size {
141            return Err(Error::TruncatedPayload {
142                expected: min_size,
143                actual: data.len(),
144            });
145        }
146
147        // Extract nonce
148        let nonce = data[8..8 + nonce_len].to_vec();
149
150        // Extract tag (last 16 bytes)
151        let tag = data[data.len() - 16..].to_vec();
152
153        // Extract ciphertext (between nonce and tag)
154        let ciphertext = data[8 + nonce_len..data.len() - 16].to_vec();
155
156        Ok(EncryptionResult {
157            ciphertext,
158            algorithm,
159            nonce,
160            tag,
161        })
162    }
163
164    /// Serialize to JSON format
165    pub fn to_json(&self) -> Result<String> {
166        #[derive(Serialize)]
167        struct JsonFormat<'a> {
168            v: &'static str,
169            alg: &'a str,
170            nonce: String,
171            ct: String,
172            tag: String,
173        }
174
175        use base64::{engine::general_purpose::STANDARD, Engine};
176
177        let json = JsonFormat {
178            v: "1.0",
179            alg: self.algorithm.name(),
180            nonce: STANDARD.encode(&self.nonce),
181            ct: STANDARD.encode(&self.ciphertext),
182            tag: STANDARD.encode(&self.tag),
183        };
184
185        serde_json::to_string(&json).map_err(|e| Error::SerializationError(e.to_string()))
186    }
187
188    /// Deserialize from JSON format
189    pub fn from_json(json: &str) -> Result<Self> {
190        #[derive(Deserialize)]
191        struct JsonFormat {
192            v: String,
193            alg: String,
194            nonce: String,
195            ct: String,
196            tag: String,
197        }
198
199        let parsed: JsonFormat = serde_json::from_str(json)?;
200
201        if parsed.v != "1.0" {
202            return Err(Error::UnsupportedVersion(0));
203        }
204
205        let algorithm = match parsed.alg.as_str() {
206            "aes-256-gcm" => Algorithm::Aes256Gcm,
207            "xchacha20-poly1305" => Algorithm::XChaCha20Poly1305,
208            _ => return Err(Error::UnsupportedAlgorithm(0)),
209        };
210
211        use base64::{engine::general_purpose::STANDARD, Engine};
212
213        Ok(EncryptionResult {
214            ciphertext: STANDARD.decode(&parsed.ct)?,
215            algorithm,
216            nonce: STANDARD.decode(&parsed.nonce)?,
217            tag: STANDARD.decode(&parsed.tag)?,
218        })
219    }
220}
221
222impl Drop for EncryptionResult {
223    fn drop(&mut self) {
224        self.ciphertext.zeroize();
225        self.nonce.zeroize();
226        self.tag.zeroize();
227    }
228}
229
230/// Encryption options
231#[derive(Debug, Clone, Default)]
232pub struct EncryptOptions {
233    /// Preferred algorithm (defaults to XChaCha20-Poly1305)
234    pub algorithm: Option<Algorithm>,
235    /// Additional authenticated data
236    pub aad: Option<Vec<u8>>,
237}
238
239/// Encrypt data using AEAD encryption.
240///
241/// Uses XChaCha20-Poly1305 by default. AES-256-GCM is available only when
242/// explicitly selected; callers choosing it must prevent nonce reuse per key.
243///
244/// # Arguments
245///
246/// * `plaintext` - Data to encrypt
247/// * `key` - 256-bit encryption key
248/// * `options` - Optional encryption options
249///
250/// # Returns
251///
252/// Encrypted data with algorithm, nonce, and authentication tag.
253pub fn encrypt(
254    plaintext: &[u8],
255    key: &Key,
256    options: Option<EncryptOptions>,
257) -> Result<EncryptionResult> {
258    let opts = options.unwrap_or_default();
259    let algorithm = opts.algorithm.unwrap_or(Algorithm::XChaCha20Poly1305);
260    let aad = opts.aad.as_deref().unwrap_or(&[]);
261
262    match algorithm {
263        Algorithm::Aes256Gcm => encrypt_aes_gcm(plaintext, key, aad),
264        Algorithm::XChaCha20Poly1305 => encrypt_xchacha20(plaintext, key, aad),
265    }
266}
267
268/// Decrypt data using AEAD decryption.
269///
270/// Automatically detects the algorithm from the encrypted data.
271///
272/// # Arguments
273///
274/// * `encrypted` - Encrypted data (binary format or EncryptionResult)
275/// * `key` - 256-bit decryption key
276///
277/// # Returns
278///
279/// Decrypted plaintext.
280pub fn decrypt(encrypted: &EncryptionResult, key: &Key) -> Result<Vec<u8>> {
281    decrypt_with_aad(encrypted, key, &[])
282}
283
284/// Decrypt data using AEAD decryption with Additional Authenticated Data.
285///
286/// # Arguments
287///
288/// * `encrypted` - Encrypted data (binary format or EncryptionResult)
289/// * `key` - 256-bit decryption key
290/// * `aad` - Additional authenticated data (must match what was used during encryption)
291///
292/// # Returns
293///
294/// Decrypted plaintext.
295pub fn decrypt_with_aad(encrypted: &EncryptionResult, key: &Key, aad: &[u8]) -> Result<Vec<u8>> {
296    match encrypted.algorithm {
297        Algorithm::Aes256Gcm => decrypt_aes_gcm(encrypted, key, aad),
298        Algorithm::XChaCha20Poly1305 => decrypt_xchacha20(encrypted, key, aad),
299    }
300}
301
302/// Decrypt data from binary format.
303pub fn decrypt_bytes(data: &[u8], key: &Key) -> Result<Vec<u8>> {
304    let encrypted = EncryptionResult::from_bytes(data)?;
305    decrypt(&encrypted, key)
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    #[test]
313    fn test_encrypt_decrypt_roundtrip() {
314        let key = generate_key();
315        let plaintext = b"Hello, World!";
316
317        let encrypted = encrypt(plaintext, &key, None).unwrap();
318        let decrypted = decrypt(&encrypted, &key).unwrap();
319
320        assert_eq!(plaintext, &decrypted[..]);
321    }
322
323    #[test]
324    fn test_binary_serialization() {
325        let key = generate_key();
326        let plaintext = b"Test data for serialization";
327
328        let encrypted = encrypt(plaintext, &key, None).unwrap();
329        let bytes = encrypted.to_bytes();
330        let restored = EncryptionResult::from_bytes(&bytes).unwrap();
331
332        assert_eq!(encrypted.algorithm, restored.algorithm);
333        assert_eq!(encrypted.nonce, restored.nonce);
334        assert_eq!(encrypted.ciphertext, restored.ciphertext);
335        assert_eq!(encrypted.tag, restored.tag);
336    }
337
338    #[test]
339    fn test_binary_serialization_rejects_noncanonical_reserved_byte() {
340        let key = generate_key();
341        let encrypted = encrypt(b"canonical", &key, None).unwrap();
342        let mut bytes = encrypted.to_bytes();
343        bytes[7] = 1;
344        assert!(matches!(
345            EncryptionResult::from_bytes(&bytes),
346            Err(Error::InvalidFormat)
347        ));
348    }
349
350    #[test]
351    fn test_json_serialization() {
352        let key = generate_key();
353        let plaintext = b"Test data for JSON";
354
355        let encrypted = encrypt(plaintext, &key, None).unwrap();
356        let json = encrypted.to_json().unwrap();
357        let restored = EncryptionResult::from_json(&json).unwrap();
358
359        assert_eq!(encrypted.algorithm, restored.algorithm);
360        assert_eq!(encrypted.nonce, restored.nonce);
361        assert_eq!(encrypted.ciphertext, restored.ciphertext);
362        assert_eq!(encrypted.tag, restored.tag);
363    }
364}