1use aes_gcm::{
4 aead::{Aead, KeyInit, OsRng},
5 Aes256Gcm, Nonce,
6};
7use argon2::Argon2;
8use rand::RngCore;
9use sha2::{Digest, Sha256};
10use thiserror::Error;
11use zeroize::Zeroize;
12use std::path::Path;
13
14#[derive(Error, Debug)]
16pub enum CryptoError {
17 #[error("Invalid password")]
18 InvalidPassword,
19 #[error("Encryption failed: {0}")]
20 EncryptionFailed(String),
21 #[error("Decryption failed: {0}")]
22 DecryptionFailed(String),
23 #[error("Key derivation failed: {0}")]
24 KeyDerivationFailed(String),
25 #[error("Invalid encrypted data format")]
26 InvalidFormat,
27}
28
29pub struct SecureKey {
31 key: [u8; 32], }
33
34impl SecureKey {
35 pub fn new(key: [u8; 32]) -> Self {
37 Self { key }
38 }
39
40 pub fn as_bytes(&self) -> &[u8; 32] {
42 &self.key
43 }
44}
45
46impl Drop for SecureKey {
47 fn drop(&mut self) {
48 self.key.zeroize();
49 }
50}
51
52pub const SALT_SIZE: usize = 32;
54pub const NONCE_SIZE: usize = 12;
55pub const CHECKSUM_SIZE: usize = 32; #[derive(Debug, Clone)]
59pub struct FileMetadata {
60 pub filename: String,
62 pub checksum: [u8; CHECKSUM_SIZE],
64 pub compressed: bool,
66}
67
68impl FileMetadata {
69 pub fn new(filename: String, checksum: [u8; CHECKSUM_SIZE], compressed: bool) -> Self {
71 Self {
72 filename,
73 checksum,
74 compressed,
75 }
76 }
77
78 pub fn from_file(file_path: &Path, data: &[u8], compressed: bool) -> Self {
80 let filename = file_path
81 .file_name()
82 .and_then(|n| n.to_str())
83 .unwrap_or("unknown")
84 .to_string();
85
86 let mut hasher = Sha256::new();
87 hasher.update(data);
88 let checksum: [u8; CHECKSUM_SIZE] = hasher.finalize().into();
89
90 Self::new(filename, checksum, compressed)
91 }
92
93 pub fn to_bytes(&self) -> Vec<u8> {
95 let filename_bytes = self.filename.as_bytes();
96 let filename_len = filename_bytes.len() as u16;
97
98 let mut bytes = Vec::new();
99 bytes.extend_from_slice(&filename_len.to_le_bytes());
100 bytes.extend_from_slice(filename_bytes);
101 bytes.extend_from_slice(&self.checksum);
102 bytes.push(if self.compressed { 1 } else { 0 });
103
104 bytes
105 }
106
107 pub fn from_bytes(bytes: &[u8]) -> Result<(Self, usize), CryptoError> {
109 if bytes.len() < 2 {
110 return Err(CryptoError::InvalidFormat);
111 }
112
113 let filename_len = u16::from_le_bytes([bytes[0], bytes[1]]) as usize;
114 let required_size = 2 + filename_len + CHECKSUM_SIZE + 1;
115
116 if bytes.len() < required_size {
117 return Err(CryptoError::InvalidFormat);
118 }
119
120 let filename = String::from_utf8(bytes[2..2 + filename_len].to_vec())
121 .map_err(|_| CryptoError::InvalidFormat)?;
122
123 let mut checksum = [0u8; CHECKSUM_SIZE];
124 checksum.copy_from_slice(&bytes[2 + filename_len..2 + filename_len + CHECKSUM_SIZE]);
125
126 let compressed = bytes[2 + filename_len + CHECKSUM_SIZE] != 0;
127
128 Ok((Self::new(filename, checksum, compressed), required_size))
129 }
130
131 pub fn verify_checksum(&self, data: &[u8]) -> bool {
133 let mut hasher = Sha256::new();
134 hasher.update(data);
135 let computed_checksum: [u8; CHECKSUM_SIZE] = hasher.finalize().into();
136 computed_checksum == self.checksum
137 }
138}
139
140#[derive(Debug)]
142pub struct EncryptionHeader {
143 pub salt: [u8; SALT_SIZE],
144 pub nonce: [u8; NONCE_SIZE],
145 pub metadata: FileMetadata,
146}
147
148impl EncryptionHeader {
149 pub fn new(metadata: FileMetadata) -> Self {
151 let mut salt = [0u8; SALT_SIZE];
152 let mut nonce = [0u8; NONCE_SIZE];
153
154 OsRng.fill_bytes(&mut salt);
155 OsRng.fill_bytes(&mut nonce);
156
157 Self { salt, nonce, metadata }
158 }
159
160 pub fn to_bytes(&self) -> Vec<u8> {
162 let mut bytes = Vec::new();
163 bytes.extend_from_slice(&self.salt);
164 bytes.extend_from_slice(&self.nonce);
165
166 let metadata_bytes = self.metadata.to_bytes();
167 let metadata_len = metadata_bytes.len() as u32;
168 bytes.extend_from_slice(&metadata_len.to_le_bytes());
169 bytes.extend_from_slice(&metadata_bytes);
170
171 bytes
172 }
173
174 pub fn from_bytes(bytes: &[u8]) -> Result<(Self, usize), CryptoError> {
176 let min_size = SALT_SIZE + NONCE_SIZE + 4; if bytes.len() < min_size {
178 return Err(CryptoError::InvalidFormat);
179 }
180
181 let mut salt = [0u8; SALT_SIZE];
182 let mut nonce = [0u8; NONCE_SIZE];
183
184 salt.copy_from_slice(&bytes[..SALT_SIZE]);
185 nonce.copy_from_slice(&bytes[SALT_SIZE..SALT_SIZE + NONCE_SIZE]);
186
187 let metadata_len_offset = SALT_SIZE + NONCE_SIZE;
188 let metadata_len = u32::from_le_bytes([
189 bytes[metadata_len_offset],
190 bytes[metadata_len_offset + 1],
191 bytes[metadata_len_offset + 2],
192 bytes[metadata_len_offset + 3],
193 ]) as usize;
194
195 let metadata_start = metadata_len_offset + 4;
196 if bytes.len() < metadata_start + metadata_len {
197 return Err(CryptoError::InvalidFormat);
198 }
199
200 let (metadata, _) = FileMetadata::from_bytes(&bytes[metadata_start..metadata_start + metadata_len])?;
201 let total_size = metadata_start + metadata_len;
202
203 Ok((Self { salt, nonce, metadata }, total_size))
204 }
205}
206
207pub struct CryptoEngine {
209 argon2: Argon2<'static>,
210}
211
212impl Default for CryptoEngine {
213 fn default() -> Self {
214 Self::new()
215 }
216}
217
218impl CryptoEngine {
219 pub fn new() -> Self {
221 Self {
222 argon2: Argon2::default(),
223 }
224 }
225
226 pub fn derive_key(&self, password: &str, salt: &[u8; SALT_SIZE]) -> Result<SecureKey, CryptoError> {
228 let mut key = [0u8; 32];
229 self.argon2
230 .hash_password_into(password.as_bytes(), salt, &mut key)
231 .map_err(|e| CryptoError::KeyDerivationFailed(e.to_string()))?;
232
233 Ok(SecureKey::new(key))
234 }
235
236 pub fn encrypt(&self, data: &[u8], password: &str, metadata: FileMetadata) -> Result<Vec<u8>, CryptoError> {
238 let header = EncryptionHeader::new(metadata);
239 let key = self.derive_key(password, &header.salt)?;
240
241 let cipher = Aes256Gcm::new_from_slice(key.as_bytes())
242 .map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;
243
244 let nonce = Nonce::from_slice(&header.nonce);
245 let ciphertext = cipher
246 .encrypt(nonce, data)
247 .map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;
248
249 let header_bytes = header.to_bytes();
250 let mut result = Vec::with_capacity(header_bytes.len() + ciphertext.len());
251 result.extend_from_slice(&header_bytes);
252 result.extend_from_slice(&ciphertext);
253
254 Ok(result)
255 }
256
257 pub fn decrypt(&self, encrypted_data: &[u8], password: &str) -> Result<(Vec<u8>, FileMetadata), CryptoError> {
259 if encrypted_data.len() < SALT_SIZE + NONCE_SIZE + 4 {
260 return Err(CryptoError::InvalidFormat);
261 }
262
263 let (header, header_size) = EncryptionHeader::from_bytes(encrypted_data)?;
264 let ciphertext = &encrypted_data[header_size..];
265
266 let key = self.derive_key(password, &header.salt)?;
267
268 let cipher = Aes256Gcm::new_from_slice(key.as_bytes())
269 .map_err(|e| CryptoError::DecryptionFailed(e.to_string()))?;
270
271 let nonce = Nonce::from_slice(&header.nonce);
272 let plaintext = cipher
273 .decrypt(nonce, ciphertext)
274 .map_err(|_| CryptoError::InvalidPassword)?;
275
276 Ok((plaintext, header.metadata))
277 }
278
279 pub fn decrypt_legacy(&self, encrypted_data: &[u8], password: &str) -> Result<Vec<u8>, CryptoError> {
281 const LEGACY_HEADER_SIZE: usize = SALT_SIZE + NONCE_SIZE;
282
283 if encrypted_data.len() < LEGACY_HEADER_SIZE {
284 return Err(CryptoError::InvalidFormat);
285 }
286
287 let is_legacy = encrypted_data.len() >= LEGACY_HEADER_SIZE && {
289 if encrypted_data.len() > SALT_SIZE + NONCE_SIZE + 4 {
291 let metadata_len_offset = SALT_SIZE + NONCE_SIZE;
292 let metadata_len = u32::from_le_bytes([
293 encrypted_data[metadata_len_offset],
294 encrypted_data[metadata_len_offset + 1],
295 encrypted_data[metadata_len_offset + 2],
296 encrypted_data[metadata_len_offset + 3],
297 ]) as usize;
298
299 metadata_len > 1024 || metadata_len_offset + 4 + metadata_len > encrypted_data.len()
301 } else {
302 true
303 }
304 };
305
306 if is_legacy {
307 let mut salt = [0u8; SALT_SIZE];
308 let mut nonce = [0u8; NONCE_SIZE];
309
310 salt.copy_from_slice(&encrypted_data[..SALT_SIZE]);
311 nonce.copy_from_slice(&encrypted_data[SALT_SIZE..LEGACY_HEADER_SIZE]);
312
313 let ciphertext = &encrypted_data[LEGACY_HEADER_SIZE..];
314
315 let key = self.derive_key(password, &salt)?;
316
317 let cipher = Aes256Gcm::new_from_slice(key.as_bytes())
318 .map_err(|e| CryptoError::DecryptionFailed(e.to_string()))?;
319
320 let nonce_obj = Nonce::from_slice(&nonce);
321 let plaintext = cipher
322 .decrypt(nonce_obj, ciphertext)
323 .map_err(|_| CryptoError::InvalidPassword)?;
324
325 Ok(plaintext)
326 } else {
327 let (plaintext, _) = self.decrypt(encrypted_data, password)?;
329 Ok(plaintext)
330 }
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn test_encryption_decryption() {
340 let engine = CryptoEngine::new();
341 let data = b"Hello, World! This is a test message.";
342 let password = "strong_password_123";
343 let metadata = FileMetadata::new("test.txt".to_string(), [0u8; 32], false);
344
345 let encrypted = engine.encrypt(data, password, metadata.clone()).unwrap();
346 assert_ne!(encrypted.as_slice(), data);
347 assert!(encrypted.len() > data.len());
348
349 let (decrypted, recovered_metadata) = engine.decrypt(&encrypted, password).unwrap();
350 assert_eq!(decrypted.as_slice(), data);
351 assert_eq!(recovered_metadata.filename, metadata.filename);
352 assert_eq!(recovered_metadata.compressed, metadata.compressed);
353 }
354
355 #[test]
356 fn test_wrong_password() {
357 let engine = CryptoEngine::new();
358 let data = b"Secret message";
359 let password = "correct_password";
360 let wrong_password = "wrong_password";
361 let metadata = FileMetadata::new("secret.txt".to_string(), [0u8; 32], false);
362
363 let encrypted = engine.encrypt(data, password, metadata).unwrap();
364 let result = engine.decrypt(&encrypted, wrong_password);
365
366 assert!(matches!(result, Err(CryptoError::InvalidPassword)));
367 }
368
369 #[test]
370 fn test_legacy_format_compatibility() {
371 let engine = CryptoEngine::new();
372 let _data = b"Legacy test data";
373 let password = "test_password";
374
375 let result = engine.decrypt_legacy(b"invalid_short_data", password);
378 assert!(matches!(result, Err(CryptoError::InvalidFormat)));
379 }
380
381 #[test]
382 fn test_file_metadata() {
383 use std::path::Path;
384
385 let data = b"Test file content";
386 let path = Path::new("test.txt");
387 let metadata = FileMetadata::from_file(path, data, false);
388
389 assert_eq!(metadata.filename, "test.txt");
390 assert!(!metadata.compressed);
391 assert!(metadata.verify_checksum(data));
392
393 let bytes = metadata.to_bytes();
395 let (recovered, size) = FileMetadata::from_bytes(&bytes).unwrap();
396 assert_eq!(size, bytes.len());
397 assert_eq!(recovered.filename, metadata.filename);
398 assert_eq!(recovered.checksum, metadata.checksum);
399 assert_eq!(recovered.compressed, metadata.compressed);
400 }
401
402 #[test]
403 fn test_invalid_format() {
404 let engine = CryptoEngine::new();
405 let invalid_data = b"not_encrypted_data";
406 let password = "password";
407
408 let result = engine.decrypt(invalid_data, password);
409 assert!(matches!(result, Err(CryptoError::InvalidFormat)));
410 }
411
412 #[test]
413 fn test_encryption_header() {
414 let metadata = FileMetadata::new("test.txt".to_string(), [42u8; 32], true);
415 let header = EncryptionHeader::new(metadata.clone());
416
417 let bytes = header.to_bytes();
419 let (deserialized, size) = EncryptionHeader::from_bytes(&bytes).unwrap();
420
421 assert_eq!(size, bytes.len());
422 assert_eq!(header.salt, deserialized.salt);
423 assert_eq!(header.nonce, deserialized.nonce);
424 assert_eq!(header.metadata.filename, deserialized.metadata.filename);
425 assert_eq!(header.metadata.checksum, deserialized.metadata.checksum);
426 assert_eq!(header.metadata.compressed, deserialized.metadata.compressed);
427 }
428
429 #[test]
430 fn test_secure_key_zeroization() {
431 let key_bytes = [42u8; 32];
432 let key = SecureKey::new(key_bytes);
433 assert_eq!(key.as_bytes(), &key_bytes);
434
435 drop(key);
437 }
439}