git_crypt/
crypto.rs

1//! # Cryptographic Operations
2//!
3//! This module provides core encryption and decryption functionality using AES-256-GCM.
4//!
5//! ## Algorithm
6//!
7//! - **Cipher**: AES-256-GCM (Galois/Counter Mode)
8//! - **Key size**: 256 bits (32 bytes)
9//! - **Nonce size**: 96 bits (12 bytes)
10//! - **Authentication**: Built into GCM mode (16-byte tag)
11//!
12//! ## Encrypted Data Format
13//!
14//! ```text
15//! [GITCRYPT][12-byte nonce][variable-length ciphertext + 16-byte GCM tag]
16//! ```
17//!
18//! The magic header ensures reliable detection of encrypted data and provides
19//! versioning capability for future format changes.
20//!
21//! ## Security Properties
22//!
23//! - **Confidentiality**: AES-256 provides strong encryption
24//! - **Authentication**: GCM mode ensures tamper detection
25//! - **Nonce uniqueness**: Random nonces prevent pattern detection
26//! - **Key derivation**: Keys generated from OS random number generator
27//!
28//! ## Unit Tests
29//!
30//! Run crypto module tests:
31//! ```bash
32//! cargo test crypto::
33//! ```
34//!
35//! Tests cover:
36//! - Basic encryption/decryption round-trips
37//! - Empty and large data handling
38//! - Binary data with all byte values
39//! - Unicode content
40//! - Key uniqueness and nonce randomness
41//! - Authentication with wrong keys
42//! - Tamper detection on corrupted data
43//! - Invalid key size rejection
44
45use aes_gcm::{
46    aead::{Aead, KeyInit, OsRng},
47    Aes256Gcm, Nonce,
48};
49use rand::RngCore;
50use crate::error::{GitCryptError, Result};
51
52pub const KEY_SIZE: usize = 32; // 256 bits
53pub const NONCE_SIZE: usize = 12; // 96 bits for GCM
54
55// Magic header to identify encrypted data
56const MAGIC_HEADER: &[u8] = b"GITCRYPT";
57
58#[derive(Clone)]
59pub struct CryptoKey {
60    key: [u8; KEY_SIZE],
61}
62
63impl CryptoKey {
64    /// Generate a new random key
65    pub fn generate() -> Self {
66        let mut key = [0u8; KEY_SIZE];
67        OsRng.fill_bytes(&mut key);
68        Self { key }
69    }
70
71    /// Create a key from existing bytes
72    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
73        if bytes.len() != KEY_SIZE {
74            return Err(GitCryptError::InvalidKeyFormat);
75        }
76        let mut key = [0u8; KEY_SIZE];
77        key.copy_from_slice(bytes);
78        Ok(Self { key })
79    }
80
81    /// Get the key as bytes
82    pub fn as_bytes(&self) -> &[u8] {
83        &self.key
84    }
85
86    /// Encrypt data
87    pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
88        let cipher = Aes256Gcm::new_from_slice(&self.key)
89            .map_err(|e| GitCryptError::Crypto(e.to_string()))?;
90
91        // Generate random nonce
92        let mut nonce_bytes = [0u8; NONCE_SIZE];
93        OsRng.fill_bytes(&mut nonce_bytes);
94        let nonce = Nonce::from_slice(&nonce_bytes);
95
96        // Encrypt
97        let ciphertext = cipher
98            .encrypt(nonce, plaintext)
99            .map_err(|e| GitCryptError::Crypto(e.to_string()))?;
100
101        // Format: MAGIC_HEADER + nonce + ciphertext
102        let mut result = Vec::with_capacity(MAGIC_HEADER.len() + NONCE_SIZE + ciphertext.len());
103        result.extend_from_slice(MAGIC_HEADER);
104        result.extend_from_slice(&nonce_bytes);
105        result.extend_from_slice(&ciphertext);
106
107        Ok(result)
108    }
109
110    /// Decrypt data
111    pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
112        let min_size = MAGIC_HEADER.len() + NONCE_SIZE;
113        if ciphertext.len() < min_size {
114            return Err(GitCryptError::Crypto("Ciphertext too short".into()));
115        }
116
117        // Check magic header
118        if &ciphertext[..MAGIC_HEADER.len()] != MAGIC_HEADER {
119            return Err(GitCryptError::Crypto("Invalid encrypted data format".into()));
120        }
121
122        let cipher = Aes256Gcm::new_from_slice(&self.key)
123            .map_err(|e| GitCryptError::Crypto(e.to_string()))?;
124
125        // Extract nonce and ciphertext (skip magic header)
126        let data = &ciphertext[MAGIC_HEADER.len()..];
127        let (nonce_bytes, encrypted_data) = data.split_at(NONCE_SIZE);
128        let nonce = Nonce::from_slice(nonce_bytes);
129
130        // Decrypt
131        let plaintext = cipher
132            .decrypt(nonce, encrypted_data)
133            .map_err(|e| GitCryptError::Crypto(e.to_string()))?;
134
135        Ok(plaintext)
136    }
137
138    /// Check if data has our magic header
139    pub fn is_encrypted(data: &[u8]) -> bool {
140        data.len() >= MAGIC_HEADER.len() && &data[..MAGIC_HEADER.len()] == MAGIC_HEADER
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_encrypt_decrypt() {
150        let key = CryptoKey::generate();
151        let plaintext = b"Hello, World!";
152
153        let ciphertext = key.encrypt(plaintext).unwrap();
154        assert_ne!(plaintext.as_slice(), &ciphertext[..]);
155
156        let decrypted = key.decrypt(&ciphertext).unwrap();
157        assert_eq!(plaintext.as_slice(), &decrypted[..]);
158    }
159
160    #[test]
161    fn test_empty_data() {
162        let key = CryptoKey::generate();
163        let plaintext = b"";
164
165        let ciphertext = key.encrypt(plaintext).unwrap();
166        let decrypted = key.decrypt(&ciphertext).unwrap();
167        assert_eq!(plaintext.as_slice(), &decrypted[..]);
168    }
169
170    #[test]
171    fn test_large_data() {
172        let key = CryptoKey::generate();
173        let plaintext = vec![0x42u8; 1024 * 1024]; // 1MB
174
175        let ciphertext = key.encrypt(&plaintext).unwrap();
176        let decrypted = key.decrypt(&ciphertext).unwrap();
177        assert_eq!(plaintext, decrypted);
178    }
179
180    #[test]
181    fn test_binary_data() {
182        let key = CryptoKey::generate();
183        let plaintext: Vec<u8> = (0..=255).collect();
184
185        let ciphertext = key.encrypt(&plaintext).unwrap();
186        let decrypted = key.decrypt(&ciphertext).unwrap();
187        assert_eq!(plaintext, decrypted);
188    }
189
190    #[test]
191    fn test_different_keys_produce_different_ciphertext() {
192        let key1 = CryptoKey::generate();
193        let key2 = CryptoKey::generate();
194        let plaintext = b"Same plaintext";
195
196        let ciphertext1 = key1.encrypt(plaintext).unwrap();
197        let ciphertext2 = key2.encrypt(plaintext).unwrap();
198
199        // Different keys should produce different ciphertext
200        assert_ne!(ciphertext1, ciphertext2);
201    }
202
203    #[test]
204    fn test_same_key_different_nonces() {
205        let key = CryptoKey::generate();
206        let plaintext = b"Same plaintext and key";
207
208        let ciphertext1 = key.encrypt(plaintext).unwrap();
209        let ciphertext2 = key.encrypt(plaintext).unwrap();
210
211        // Same key but different nonces should produce different ciphertext
212        assert_ne!(ciphertext1, ciphertext2);
213
214        // But both should decrypt to same plaintext
215        assert_eq!(key.decrypt(&ciphertext1).unwrap(), plaintext.as_slice());
216        assert_eq!(key.decrypt(&ciphertext2).unwrap(), plaintext.as_slice());
217    }
218
219    #[test]
220    fn test_wrong_key_fails_decryption() {
221        let key1 = CryptoKey::generate();
222        let key2 = CryptoKey::generate();
223        let plaintext = b"Secret message";
224
225        let ciphertext = key1.encrypt(plaintext).unwrap();
226
227        // Decryption with wrong key should fail
228        let result = key2.decrypt(&ciphertext);
229        assert!(result.is_err());
230    }
231
232    #[test]
233    fn test_corrupted_ciphertext_fails() {
234        let key = CryptoKey::generate();
235        let plaintext = b"Secret message";
236
237        let mut ciphertext = key.encrypt(plaintext).unwrap();
238
239        // Corrupt a byte in the ciphertext (not the nonce)
240        if ciphertext.len() > NONCE_SIZE {
241            ciphertext[NONCE_SIZE] ^= 0xFF;
242        }
243
244        // Decryption should fail due to authentication
245        let result = key.decrypt(&ciphertext);
246        assert!(result.is_err());
247    }
248
249    #[test]
250    fn test_truncated_ciphertext_fails() {
251        let key = CryptoKey::generate();
252        let plaintext = b"Secret message";
253
254        let ciphertext = key.encrypt(plaintext).unwrap();
255
256        // Try to decrypt truncated ciphertext
257        let truncated = &ciphertext[..5];
258        let result = key.decrypt(truncated);
259        assert!(result.is_err());
260    }
261
262    #[test]
263    fn test_key_from_bytes() {
264        let key_bytes = [0x42u8; KEY_SIZE];
265        let key = CryptoKey::from_bytes(&key_bytes).unwrap();
266        assert_eq!(key.as_bytes(), &key_bytes);
267    }
268
269    #[test]
270    fn test_key_from_invalid_length() {
271        let too_short = vec![0x42u8; KEY_SIZE - 1];
272        let result = CryptoKey::from_bytes(&too_short);
273        assert!(result.is_err());
274
275        let too_long = vec![0x42u8; KEY_SIZE + 1];
276        let result = CryptoKey::from_bytes(&too_long);
277        assert!(result.is_err());
278    }
279
280    #[test]
281    fn test_key_roundtrip() {
282        let key1 = CryptoKey::generate();
283        let key_bytes = key1.as_bytes();
284        let key2 = CryptoKey::from_bytes(key_bytes).unwrap();
285
286        // Both keys should encrypt/decrypt the same way
287        let plaintext = b"Test message";
288        let ciphertext = key1.encrypt(plaintext).unwrap();
289        let decrypted = key2.decrypt(&ciphertext).unwrap();
290        assert_eq!(plaintext.as_slice(), &decrypted[..]);
291    }
292
293    #[test]
294    fn test_unicode_data() {
295        let key = CryptoKey::generate();
296        let plaintext = "Hello, δΈ–η•Œ! πŸ”πŸ¦€".as_bytes();
297
298        let ciphertext = key.encrypt(plaintext).unwrap();
299        let decrypted = key.decrypt(&ciphertext).unwrap();
300        assert_eq!(plaintext, &decrypted[..]);
301        assert_eq!(
302            String::from_utf8(decrypted).unwrap(),
303            "Hello, δΈ–η•Œ! πŸ”πŸ¦€"
304        );
305    }
306
307    #[test]
308    fn test_ciphertext_has_nonce() {
309        let key = CryptoKey::generate();
310        let plaintext = b"Test";
311
312        let ciphertext = key.encrypt(plaintext).unwrap();
313
314        // Ciphertext should be longer than plaintext (nonce + tag)
315        assert!(ciphertext.len() >= plaintext.len() + NONCE_SIZE);
316
317        // First NONCE_SIZE bytes should be the nonce
318        assert_eq!(&ciphertext[..NONCE_SIZE].len(), &NONCE_SIZE);
319    }
320}