Skip to main content

alimentar/format/
encryption.rs

1//! Encryption support for .ald format (§5.1)
2//!
3//! Provides AES-256-GCM encryption with two key derivation modes:
4//! - Password mode: Argon2id KDF for human-memorable passwords
5//! - Recipient mode: X25519 key agreement for asymmetric encryption
6
7use crate::error::{Error, Result};
8
9/// Salt size for Argon2id (16 bytes)
10pub const SALT_SIZE: usize = 16;
11
12/// Nonce size for AES-GCM (12 bytes)
13pub const NONCE_SIZE: usize = 12;
14
15/// Result of password-based encryption: (mode_byte, salt, nonce, ciphertext)
16pub type PasswordEncryptResult = (u8, [u8; 16], [u8; 12], Vec<u8>);
17
18/// Result of recipient-based encryption: (mode_byte, ephemeral_pub_key, nonce,
19/// ciphertext)
20pub type RecipientEncryptResult = (u8, [u8; 32], [u8; 12], Vec<u8>);
21
22/// AES-GCM authentication tag size (16 bytes)
23pub const TAG_SIZE: usize = 16;
24
25/// X25519 public key size (32 bytes)
26pub const X25519_PUBLIC_KEY_SIZE: usize = 32;
27
28/// Encryption mode byte values
29pub mod mode {
30    /// Password-based encryption using Argon2id
31    pub const PASSWORD: u8 = 0x00;
32    /// Recipient-based encryption using X25519
33    pub const RECIPIENT: u8 = 0x01;
34}
35
36/// Encryption mode configuration
37#[derive(Debug, Clone)]
38pub enum EncryptionMode {
39    /// Password-based encryption using Argon2id KDF
40    Password(String),
41    /// Recipient-based encryption using X25519 key agreement
42    Recipient {
43        /// Recipient's X25519 public key (32 bytes)
44        recipient_public_key: [u8; 32],
45    },
46}
47
48/// Encryption parameters for saving
49#[derive(Debug, Clone)]
50pub struct EncryptionParams {
51    /// The encryption mode and key material
52    pub mode: EncryptionMode,
53}
54
55impl EncryptionParams {
56    /// Create password-based encryption parameters
57    #[must_use]
58    pub fn password(password: impl Into<String>) -> Self {
59        Self {
60            mode: EncryptionMode::Password(password.into()),
61        }
62    }
63
64    /// Create recipient-based encryption parameters
65    #[must_use]
66    pub fn recipient(public_key: [u8; 32]) -> Self {
67        Self {
68            mode: EncryptionMode::Recipient {
69                recipient_public_key: public_key,
70            },
71        }
72    }
73}
74
75/// Decryption parameters for loading
76#[derive(Debug, Clone)]
77pub enum DecryptionParams {
78    /// Password for password-based decryption
79    Password(String),
80    /// Private key for recipient-based decryption
81    PrivateKey([u8; 32]),
82}
83
84impl DecryptionParams {
85    /// Create password-based decryption parameters
86    #[must_use]
87    pub fn password(password: impl Into<String>) -> Self {
88        Self::Password(password.into())
89    }
90
91    /// Create private-key-based decryption parameters
92    #[must_use]
93    pub fn private_key(key: [u8; 32]) -> Self {
94        Self::PrivateKey(key)
95    }
96}
97
98/// Encrypt data using password-based encryption (Argon2id + AES-256-GCM)
99///
100/// Returns: (mode_byte, salt, nonce, ciphertext_with_tag)
101#[cfg(feature = "format-encryption")]
102pub fn encrypt_password(plaintext: &[u8], password: &str) -> Result<PasswordEncryptResult> {
103    use aes_gcm::{
104        aead::{Aead, KeyInit},
105        Aes256Gcm, Nonce,
106    };
107    use argon2::Argon2;
108
109    // Generate random salt and nonce
110    let mut salt = [0u8; SALT_SIZE];
111    let mut nonce = [0u8; NONCE_SIZE];
112    getrandom::getrandom(&mut salt).map_err(|e| Error::Format(format!("RNG error: {e}")))?;
113    getrandom::getrandom(&mut nonce).map_err(|e| Error::Format(format!("RNG error: {e}")))?;
114
115    // Derive key using Argon2id
116    let mut key = [0u8; 32];
117    Argon2::default()
118        .hash_password_into(password.as_bytes(), &salt, &mut key)
119        .map_err(|e| Error::Format(format!("Argon2 error: {e}")))?;
120
121    // Encrypt with AES-256-GCM
122    let cipher = Aes256Gcm::new_from_slice(&key)
123        .map_err(|e| Error::Format(format!("AES init error: {e}")))?;
124    let nonce_obj = Nonce::from_slice(&nonce);
125    let ciphertext = cipher
126        .encrypt(nonce_obj, plaintext)
127        .map_err(|e| Error::Format(format!("Encryption error: {e}")))?;
128
129    Ok((mode::PASSWORD, salt, nonce, ciphertext))
130}
131
132/// Decrypt data using password-based decryption
133#[cfg(feature = "format-encryption")]
134pub fn decrypt_password(
135    ciphertext: &[u8],
136    password: &str,
137    salt: &[u8; 16],
138    nonce: &[u8; 12],
139) -> Result<Vec<u8>> {
140    use aes_gcm::{
141        aead::{Aead, KeyInit},
142        Aes256Gcm, Nonce,
143    };
144    use argon2::Argon2;
145
146    // Derive key using Argon2id
147    let mut key = [0u8; 32];
148    Argon2::default()
149        .hash_password_into(password.as_bytes(), salt, &mut key)
150        .map_err(|e| Error::Format(format!("Argon2 error: {e}")))?;
151
152    // Decrypt with AES-256-GCM
153    let cipher = Aes256Gcm::new_from_slice(&key)
154        .map_err(|e| Error::Format(format!("AES init error: {e}")))?;
155    let nonce_obj = Nonce::from_slice(nonce);
156    cipher.decrypt(nonce_obj, ciphertext).map_err(|_| {
157        Error::Format("Decryption failed: wrong password or corrupted data".to_string())
158    })
159}
160
161/// Encrypt data using recipient-based encryption (X25519 + HKDF + AES-256-GCM)
162///
163/// Returns: (mode_byte, ephemeral_public_key, nonce, ciphertext_with_tag)
164#[cfg(feature = "format-encryption")]
165pub fn encrypt_recipient(
166    plaintext: &[u8],
167    recipient_public_key: &[u8; 32],
168) -> Result<RecipientEncryptResult> {
169    use aes_gcm::{
170        aead::{Aead, KeyInit},
171        Aes256Gcm, Nonce,
172    };
173    use hkdf::Hkdf;
174    use sha2::Sha256;
175    use x25519_dalek::{EphemeralSecret, PublicKey};
176
177    // Generate ephemeral key pair
178    let mut rng_bytes = [0u8; 32];
179    getrandom::getrandom(&mut rng_bytes).map_err(|e| Error::Format(format!("RNG error: {e}")))?;
180    let ephemeral_secret = EphemeralSecret::random_from_rng(RngWrapper(rng_bytes));
181    let ephemeral_public = PublicKey::from(&ephemeral_secret);
182
183    // Perform X25519 key agreement
184    let recipient_pk = PublicKey::from(*recipient_public_key);
185    let shared_secret = ephemeral_secret.diffie_hellman(&recipient_pk);
186
187    // Derive encryption key using HKDF
188    let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
189    let mut key = [0u8; 32];
190    hkdf.expand(b"ald-v1-encrypt", &mut key)
191        .map_err(|e| Error::Format(format!("HKDF error: {e}")))?;
192
193    // Generate random nonce
194    let mut nonce = [0u8; NONCE_SIZE];
195    getrandom::getrandom(&mut nonce).map_err(|e| Error::Format(format!("RNG error: {e}")))?;
196
197    // Encrypt with AES-256-GCM
198    let cipher = Aes256Gcm::new_from_slice(&key)
199        .map_err(|e| Error::Format(format!("AES init error: {e}")))?;
200    let nonce_obj = Nonce::from_slice(&nonce);
201    let ciphertext = cipher
202        .encrypt(nonce_obj, plaintext)
203        .map_err(|e| Error::Format(format!("Encryption error: {e}")))?;
204
205    Ok((
206        mode::RECIPIENT,
207        ephemeral_public.to_bytes(),
208        nonce,
209        ciphertext,
210    ))
211}
212
213/// Decrypt data using recipient-based decryption
214#[cfg(feature = "format-encryption")]
215pub fn decrypt_recipient(
216    ciphertext: &[u8],
217    recipient_private_key: &[u8; 32],
218    ephemeral_public_key: &[u8; 32],
219    nonce: &[u8; 12],
220) -> Result<Vec<u8>> {
221    use aes_gcm::{
222        aead::{Aead, KeyInit},
223        Aes256Gcm, Nonce,
224    };
225    use hkdf::Hkdf;
226    use sha2::Sha256;
227    use x25519_dalek::{PublicKey, StaticSecret};
228
229    // Reconstruct the shared secret
230    let recipient_secret = StaticSecret::from(*recipient_private_key);
231    let ephemeral_pk = PublicKey::from(*ephemeral_public_key);
232    let shared_secret = recipient_secret.diffie_hellman(&ephemeral_pk);
233
234    // Derive encryption key using HKDF
235    let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
236    let mut key = [0u8; 32];
237    hkdf.expand(b"ald-v1-encrypt", &mut key)
238        .map_err(|e| Error::Format(format!("HKDF error: {e}")))?;
239
240    // Decrypt with AES-256-GCM
241    let cipher = Aes256Gcm::new_from_slice(&key)
242        .map_err(|e| Error::Format(format!("AES init error: {e}")))?;
243    let nonce_obj = Nonce::from_slice(nonce);
244    cipher
245        .decrypt(nonce_obj, ciphertext)
246        .map_err(|_| Error::Format("Decryption failed: wrong key or corrupted data".to_string()))
247}
248
249/// Wrapper to use getrandom bytes as RngCore
250#[cfg(feature = "format-encryption")]
251struct RngWrapper([u8; 32]);
252
253#[cfg(feature = "format-encryption")]
254impl rand_core::RngCore for RngWrapper {
255    fn next_u32(&mut self) -> u32 {
256        let mut buf = [0u8; 4];
257        self.fill_bytes(&mut buf);
258        u32::from_le_bytes(buf)
259    }
260
261    fn next_u64(&mut self) -> u64 {
262        let mut buf = [0u8; 8];
263        self.fill_bytes(&mut buf);
264        u64::from_le_bytes(buf)
265    }
266
267    fn fill_bytes(&mut self, dest: &mut [u8]) {
268        dest.copy_from_slice(&self.0[..dest.len()]);
269    }
270
271    fn try_fill_bytes(&mut self, dest: &mut [u8]) -> std::result::Result<(), rand_core::Error> {
272        self.fill_bytes(dest);
273        Ok(())
274    }
275}
276
277#[cfg(feature = "format-encryption")]
278impl rand_core::CryptoRng for RngWrapper {}
279
280#[cfg(all(test, feature = "format-encryption"))]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn test_password_encrypt_decrypt_roundtrip() {
286        let plaintext = b"Hello, World! This is a test message for encryption.";
287        let password = "my_secure_password_123";
288
289        let (mode_byte, salt, nonce, ciphertext) =
290            encrypt_password(plaintext, password).expect("encrypt failed");
291
292        assert_eq!(mode_byte, mode::PASSWORD);
293        assert_ne!(ciphertext, plaintext);
294
295        let decrypted =
296            decrypt_password(&ciphertext, password, &salt, &nonce).expect("decrypt failed");
297
298        assert_eq!(decrypted, plaintext);
299    }
300
301    #[test]
302    fn test_password_wrong_password_fails() {
303        let plaintext = b"Secret data";
304        let password = "correct_password";
305        let wrong_password = "wrong_password";
306
307        let (_, salt, nonce, ciphertext) =
308            encrypt_password(plaintext, password).expect("encrypt failed");
309
310        let result = decrypt_password(&ciphertext, wrong_password, &salt, &nonce);
311        assert!(result.is_err());
312    }
313
314    #[test]
315    fn test_recipient_encrypt_decrypt_roundtrip() {
316        use x25519_dalek::{PublicKey, StaticSecret};
317
318        let plaintext = b"Hello, recipient! This is a secure message.";
319
320        // Generate recipient key pair
321        let mut key_bytes = [0u8; 32];
322        getrandom::getrandom(&mut key_bytes).expect("rng failed");
323        let recipient_secret = StaticSecret::from(key_bytes);
324        let recipient_public = PublicKey::from(&recipient_secret);
325
326        let (mode_byte, ephemeral_pub, nonce, ciphertext) =
327            encrypt_recipient(plaintext, recipient_public.as_bytes()).expect("encrypt failed");
328
329        assert_eq!(mode_byte, mode::RECIPIENT);
330        assert_ne!(ciphertext, plaintext);
331
332        let decrypted = decrypt_recipient(&ciphertext, &key_bytes, &ephemeral_pub, &nonce)
333            .expect("decrypt failed");
334
335        assert_eq!(decrypted, plaintext);
336    }
337
338    #[test]
339    fn test_recipient_wrong_key_fails() {
340        use x25519_dalek::{PublicKey, StaticSecret};
341
342        let plaintext = b"Secret for specific recipient";
343
344        // Generate recipient key pair
345        let mut key_bytes = [0u8; 32];
346        getrandom::getrandom(&mut key_bytes).expect("rng failed");
347        let recipient_secret = StaticSecret::from(key_bytes);
348        let recipient_public = PublicKey::from(&recipient_secret);
349
350        let (_, ephemeral_pub, nonce, ciphertext) =
351            encrypt_recipient(plaintext, recipient_public.as_bytes()).expect("encrypt failed");
352
353        // Generate wrong key pair
354        let mut wrong_key_bytes = [0u8; 32];
355        getrandom::getrandom(&mut wrong_key_bytes).expect("rng failed");
356
357        let result = decrypt_recipient(&ciphertext, &wrong_key_bytes, &ephemeral_pub, &nonce);
358        assert!(result.is_err());
359    }
360
361    #[test]
362    fn test_encryption_produces_different_ciphertexts() {
363        let plaintext = b"Same message";
364        let password = "same_password";
365
366        let (_, _, _, ct1) = encrypt_password(plaintext, password).expect("encrypt 1 failed");
367        let (_, _, _, ct2) = encrypt_password(plaintext, password).expect("encrypt 2 failed");
368
369        // Due to random salt/nonce, ciphertexts should differ
370        assert_ne!(ct1, ct2);
371    }
372
373    #[test]
374    fn test_empty_plaintext_encryption() {
375        let plaintext = b"";
376        let password = "password";
377
378        let (mode_byte, salt, nonce, ciphertext) =
379            encrypt_password(plaintext, password).expect("encrypt failed");
380
381        assert_eq!(mode_byte, mode::PASSWORD);
382        // Even empty plaintext produces ciphertext (due to auth tag)
383        assert!(!ciphertext.is_empty());
384
385        let decrypted =
386            decrypt_password(&ciphertext, password, &salt, &nonce).expect("decrypt failed");
387        assert_eq!(decrypted.as_slice(), plaintext);
388    }
389
390    #[test]
391    fn test_large_plaintext_encryption() {
392        // Test with 1MB plaintext
393        let plaintext: Vec<u8> = (0u32..1_000_000).map(|i| (i % 256) as u8).collect();
394        let password = "large_data_password";
395
396        let (mode_byte, salt, nonce, ciphertext) =
397            encrypt_password(&plaintext, password).expect("encrypt failed");
398
399        assert_eq!(mode_byte, mode::PASSWORD);
400        assert!(ciphertext.len() >= plaintext.len());
401
402        let decrypted =
403            decrypt_password(&ciphertext, password, &salt, &nonce).expect("decrypt failed");
404        assert_eq!(decrypted, plaintext);
405    }
406
407    #[test]
408    fn test_special_characters_in_password() {
409        let plaintext = b"Test data";
410        let password = "p@$$w0rd!#%^&*()_+-=[]{}|;':\",./<>?`~";
411
412        let (_, salt, nonce, ciphertext) =
413            encrypt_password(plaintext, password).expect("encrypt failed");
414
415        let decrypted =
416            decrypt_password(&ciphertext, password, &salt, &nonce).expect("decrypt failed");
417        assert_eq!(decrypted.as_slice(), plaintext);
418    }
419
420    #[test]
421    fn test_unicode_password() {
422        let plaintext = b"Data with unicode password";
423        let password = "密码🔐пароль";
424
425        let (_, salt, nonce, ciphertext) =
426            encrypt_password(plaintext, password).expect("encrypt failed");
427
428        let decrypted =
429            decrypt_password(&ciphertext, password, &salt, &nonce).expect("decrypt failed");
430        assert_eq!(decrypted.as_slice(), plaintext);
431    }
432
433    #[test]
434    fn test_corrupted_ciphertext_fails() {
435        let plaintext = b"Original data";
436        let password = "password";
437
438        let (_, salt, nonce, mut ciphertext) =
439            encrypt_password(plaintext, password).expect("encrypt failed");
440
441        // Corrupt the ciphertext
442        if !ciphertext.is_empty() {
443            ciphertext[0] ^= 0xFF;
444        }
445
446        let result = decrypt_password(&ciphertext, password, &salt, &nonce);
447        assert!(result.is_err());
448    }
449
450    #[test]
451    fn test_corrupted_nonce_fails() {
452        let plaintext = b"Original data";
453        let password = "password";
454
455        let (_, salt, mut nonce, ciphertext) =
456            encrypt_password(plaintext, password).expect("encrypt failed");
457
458        // Corrupt the nonce
459        nonce[0] ^= 0xFF;
460
461        let result = decrypt_password(&ciphertext, password, &salt, &nonce);
462        assert!(result.is_err());
463    }
464
465    #[test]
466    fn test_corrupted_salt_fails() {
467        let plaintext = b"Original data";
468        let password = "password";
469
470        let (_, mut salt, nonce, ciphertext) =
471            encrypt_password(plaintext, password).expect("encrypt failed");
472
473        // Corrupt the salt
474        salt[0] ^= 0xFF;
475
476        let result = decrypt_password(&ciphertext, password, &salt, &nonce);
477        assert!(result.is_err());
478    }
479
480    #[test]
481    fn test_rng_wrapper() {
482        use rand_core::RngCore;
483
484        let mut rng = RngWrapper([0x42; 32]);
485
486        // Test next_u32
487        let val = rng.next_u32();
488        assert_eq!(val, 0x42424242);
489
490        // Test next_u64
491        let val64 = rng.next_u64();
492        assert_eq!(val64, 0x4242424242424242);
493
494        // Test fill_bytes
495        let mut buf = [0u8; 8];
496        rng.fill_bytes(&mut buf);
497        assert_eq!(buf, [0x42; 8]);
498
499        // Test try_fill_bytes
500        let mut buf2 = [0u8; 4];
501        rng.try_fill_bytes(&mut buf2).expect("should succeed");
502        assert_eq!(buf2, [0x42; 4]);
503    }
504
505    #[test]
506    fn test_mode_constants() {
507        // Verify mode constants are distinct
508        assert_ne!(mode::PASSWORD, mode::RECIPIENT);
509        assert_eq!(mode::PASSWORD, 0x00);
510        assert_eq!(mode::RECIPIENT, 0x01);
511    }
512}