envelope_cli/crypto/
encryption.rs

1//! AES-256-GCM encryption/decryption
2//!
3//! Provides authenticated encryption for data at rest using AES-256-GCM.
4//! Each encryption operation generates a unique nonce for security.
5
6use aes_gcm::aead::rand_core::RngCore;
7use aes_gcm::{
8    aead::{Aead, KeyInit, OsRng},
9    Aes256Gcm, Nonce,
10};
11use serde::{Deserialize, Serialize};
12
13use crate::error::{EnvelopeError, EnvelopeResult};
14
15use super::DerivedKey;
16
17/// Size of the AES-GCM nonce in bytes (96 bits)
18const NONCE_SIZE: usize = 12;
19
20/// Encrypted data with associated metadata
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct EncryptedData {
23    /// The nonce used for this encryption (base64 encoded)
24    pub nonce: String,
25    /// The encrypted ciphertext with authentication tag (base64 encoded)
26    pub ciphertext: String,
27    /// Version for future algorithm upgrades
28    #[serde(default = "default_version")]
29    pub version: u8,
30}
31
32fn default_version() -> u8 {
33    1
34}
35
36impl EncryptedData {
37    /// Create a new EncryptedData from raw bytes
38    fn new(nonce: &[u8], ciphertext: &[u8]) -> Self {
39        use base64::{engine::general_purpose::STANDARD, Engine};
40        Self {
41            nonce: STANDARD.encode(nonce),
42            ciphertext: STANDARD.encode(ciphertext),
43            version: 1,
44        }
45    }
46
47    /// Decode the nonce from base64
48    fn decode_nonce(&self) -> EnvelopeResult<Vec<u8>> {
49        use base64::{engine::general_purpose::STANDARD, Engine};
50        STANDARD
51            .decode(&self.nonce)
52            .map_err(|e| EnvelopeError::Encryption(format!("Invalid nonce encoding: {}", e)))
53    }
54
55    /// Decode the ciphertext from base64
56    fn decode_ciphertext(&self) -> EnvelopeResult<Vec<u8>> {
57        use base64::{engine::general_purpose::STANDARD, Engine};
58        STANDARD
59            .decode(&self.ciphertext)
60            .map_err(|e| EnvelopeError::Encryption(format!("Invalid ciphertext encoding: {}", e)))
61    }
62}
63
64/// Encrypt plaintext data using AES-256-GCM
65///
66/// Generates a random nonce for each encryption operation.
67pub fn encrypt(plaintext: &[u8], key: &DerivedKey) -> EnvelopeResult<EncryptedData> {
68    // Create cipher from key
69    let cipher = Aes256Gcm::new_from_slice(key.as_bytes())
70        .map_err(|e| EnvelopeError::Encryption(format!("Failed to create cipher: {}", e)))?;
71
72    // Generate random nonce
73    let mut nonce_bytes = [0u8; NONCE_SIZE];
74    OsRng.fill_bytes(&mut nonce_bytes);
75    let nonce = Nonce::from_slice(&nonce_bytes);
76
77    // Encrypt
78    let ciphertext = cipher
79        .encrypt(nonce, plaintext)
80        .map_err(|e| EnvelopeError::Encryption(format!("Encryption failed: {}", e)))?;
81
82    Ok(EncryptedData::new(&nonce_bytes, &ciphertext))
83}
84
85/// Decrypt ciphertext using AES-256-GCM
86pub fn decrypt(encrypted: &EncryptedData, key: &DerivedKey) -> EnvelopeResult<Vec<u8>> {
87    // Verify version
88    if encrypted.version != 1 {
89        return Err(EnvelopeError::Encryption(format!(
90            "Unsupported encryption version: {}",
91            encrypted.version
92        )));
93    }
94
95    // Create cipher from key
96    let cipher = Aes256Gcm::new_from_slice(key.as_bytes())
97        .map_err(|e| EnvelopeError::Encryption(format!("Failed to create cipher: {}", e)))?;
98
99    // Decode nonce and ciphertext
100    let nonce_bytes = encrypted.decode_nonce()?;
101    if nonce_bytes.len() != NONCE_SIZE {
102        return Err(EnvelopeError::Encryption(format!(
103            "Invalid nonce size: expected {}, got {}",
104            NONCE_SIZE,
105            nonce_bytes.len()
106        )));
107    }
108    let nonce = Nonce::from_slice(&nonce_bytes);
109
110    let ciphertext = encrypted.decode_ciphertext()?;
111
112    // Decrypt
113    let plaintext = cipher.decrypt(nonce, ciphertext.as_ref()).map_err(|_| {
114        EnvelopeError::Encryption("Decryption failed: invalid key or corrupted data".to_string())
115    })?;
116
117    Ok(plaintext)
118}
119
120/// Encrypt a string
121pub fn encrypt_string(plaintext: &str, key: &DerivedKey) -> EnvelopeResult<EncryptedData> {
122    encrypt(plaintext.as_bytes(), key)
123}
124
125/// Decrypt to a string
126pub fn decrypt_string(encrypted: &EncryptedData, key: &DerivedKey) -> EnvelopeResult<String> {
127    let plaintext = decrypt(encrypted, key)?;
128    String::from_utf8(plaintext)
129        .map_err(|e| EnvelopeError::Encryption(format!("Invalid UTF-8 in decrypted data: {}", e)))
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use crate::crypto::key_derivation::{derive_key, KeyDerivationParams};
136
137    fn test_key() -> DerivedKey {
138        let params = KeyDerivationParams::new();
139        derive_key("test_passphrase", &params).unwrap()
140    }
141
142    #[test]
143    fn test_encrypt_decrypt() {
144        let key = test_key();
145        let plaintext = b"Hello, World!";
146
147        let encrypted = encrypt(plaintext, &key).unwrap();
148        let decrypted = decrypt(&encrypted, &key).unwrap();
149
150        assert_eq!(plaintext, decrypted.as_slice());
151    }
152
153    #[test]
154    fn test_encrypt_decrypt_string() {
155        let key = test_key();
156        let plaintext = "Hello, World!";
157
158        let encrypted = encrypt_string(plaintext, &key).unwrap();
159        let decrypted = decrypt_string(&encrypted, &key).unwrap();
160
161        assert_eq!(plaintext, decrypted);
162    }
163
164    #[test]
165    fn test_different_nonces() {
166        let key = test_key();
167        let plaintext = b"Hello, World!";
168
169        let encrypted1 = encrypt(plaintext, &key).unwrap();
170        let encrypted2 = encrypt(plaintext, &key).unwrap();
171
172        // Same plaintext should produce different ciphertext (different nonces)
173        assert_ne!(encrypted1.nonce, encrypted2.nonce);
174        assert_ne!(encrypted1.ciphertext, encrypted2.ciphertext);
175    }
176
177    #[test]
178    fn test_wrong_key_fails() {
179        let key1 = test_key();
180        let params2 = KeyDerivationParams::new();
181        let key2 = derive_key("different_passphrase", &params2).unwrap();
182
183        let plaintext = b"Hello, World!";
184        let encrypted = encrypt(plaintext, &key1).unwrap();
185
186        // Decryption with wrong key should fail
187        let result = decrypt(&encrypted, &key2);
188        assert!(result.is_err());
189    }
190
191    #[test]
192    fn test_tampered_ciphertext_fails() {
193        let key = test_key();
194        let plaintext = b"Hello, World!";
195
196        let mut encrypted = encrypt(plaintext, &key).unwrap();
197
198        // Tamper with ciphertext
199        use base64::{engine::general_purpose::STANDARD, Engine};
200        let mut ciphertext = STANDARD.decode(&encrypted.ciphertext).unwrap();
201        if !ciphertext.is_empty() {
202            ciphertext[0] ^= 0xFF;
203        }
204        encrypted.ciphertext = STANDARD.encode(&ciphertext);
205
206        // Decryption should fail due to authentication
207        let result = decrypt(&encrypted, &key);
208        assert!(result.is_err());
209    }
210
211    #[test]
212    fn test_empty_plaintext() {
213        let key = test_key();
214        let plaintext = b"";
215
216        let encrypted = encrypt(plaintext, &key).unwrap();
217        let decrypted = decrypt(&encrypted, &key).unwrap();
218
219        assert_eq!(plaintext, decrypted.as_slice());
220    }
221
222    #[test]
223    fn test_large_plaintext() {
224        let key = test_key();
225        let plaintext: Vec<u8> = (0..10000).map(|i| (i % 256) as u8).collect();
226
227        let encrypted = encrypt(&plaintext, &key).unwrap();
228        let decrypted = decrypt(&encrypted, &key).unwrap();
229
230        assert_eq!(plaintext, decrypted);
231    }
232}