Skip to main content

voided_core/encryption/
mod.rs

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