sf_cli/
file_ops.rs

1//! File and folder operations for encryption/decryption
2
3use crate::{
4    compression::{CompressionEngine, CompressionError},
5    crypto::{CryptoEngine, CryptoError, FileMetadata},
6    hybrid_crypto::{HybridCryptoEngine, HybridCryptoError},
7    models::{OperationParams, OperationResult, OperationType, TargetType},
8    progress::ProgressTracker,
9};
10use std::{
11    fs::{self, File},
12    io::{self, BufReader, BufWriter, Read, Write},
13    path::Path,
14};
15use thiserror::Error;
16
17/// File operation errors
18#[derive(Error, Debug)]
19pub enum FileOperationError {
20    #[error("IO error: {0}")]
21    IoError(#[from] io::Error),
22    #[error("Crypto error: {0}")]
23    CryptoError(#[from] CryptoError),
24    #[error("Hybrid crypto error: {0}")]
25    HybridCryptoError(#[from] HybridCryptoError),
26    #[error("Compression error: {0}")]
27    CompressionError(#[from] CompressionError),
28    #[error("Invalid path: {0}")]
29    InvalidPath(String),
30    #[error("Path does not exist: {0}")]
31    PathNotFound(String),
32    #[error("Permission denied: {0}")]
33    PermissionDenied(String),
34}
35
36/// File operations engine
37pub struct FileOperator {
38    crypto: CryptoEngine,
39    hybrid_crypto: HybridCryptoEngine,
40    compression: CompressionEngine,
41}
42
43impl Default for FileOperator {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49impl FileOperator {
50    /// Create a new file operator
51    pub fn new() -> Self {
52        Self {
53            crypto: CryptoEngine::new(),
54            hybrid_crypto: HybridCryptoEngine::new(),
55            compression: CompressionEngine::new(),
56        }
57    }
58
59    /// Process a file or directory based on operation parameters
60    pub async fn process(&self, params: &OperationParams, password: &str) -> OperationResult {
61        let source = &params.source;
62        
63        // Validate source path
64        if !source.exists() {
65            return OperationResult::failure(
66                source.clone(),
67                params.operation.clone(),
68                format!("Source path does not exist: {}", source.display()),
69            );
70        }
71
72        match params.target_type {
73            TargetType::File => self.process_file(params, password).await,
74            TargetType::Directory => self.process_directory(params, password).await,
75        }
76    }
77
78    /// Process a single file
79    async fn process_file(&self, params: &OperationParams, password: &str) -> OperationResult {
80        let source = &params.source;
81        let destination = params.get_destination();
82
83        // Ensure destination directory exists
84        if let Some(parent) = destination.parent() {
85            if let Err(e) = fs::create_dir_all(parent) {
86                return OperationResult::failure(
87                    source.clone(),
88                    params.operation.clone(),
89                    format!("Failed to create destination directory: {}", e),
90                );
91            }
92        }
93
94        let result = match params.operation {
95            OperationType::Encrypt => {
96                match self.encrypt_file(source, &destination, password, params).await {
97                    Ok(bytes_processed) => OperationResult::success(
98                        source.clone(),
99                        destination,
100                        bytes_processed,
101                        params.operation.clone(),
102                        params.compress,
103                    ),
104                    Err(e) => OperationResult::failure(
105                        source.clone(),
106                        params.operation.clone(),
107                        e.to_string(),
108                    ),
109                }
110            }
111            OperationType::Decrypt => {
112                match self.decrypt_file(source, &destination, password, params).await {
113                    Ok((bytes_processed, metadata, checksum_verified)) => {
114                        OperationResult::success_with_metadata(
115                            source.clone(),
116                            destination,
117                            bytes_processed,
118                            params.operation.clone(),
119                            metadata.compressed,
120                            Some(metadata.filename),
121                            Some(checksum_verified),
122                        )
123                    }
124                    Err(e) => OperationResult::failure(
125                        source.clone(),
126                        params.operation.clone(),
127                        e.to_string(),
128                    ),
129                }
130            }
131            OperationType::HybridEncrypt => {
132                match self.hybrid_encrypt_file(source, &destination, params).await {
133                    Ok(bytes_processed) => OperationResult::success(
134                        source.clone(),
135                        destination,
136                        bytes_processed,
137                        params.operation.clone(),
138                        params.compress,
139                    ),
140                    Err(e) => OperationResult::failure(
141                        source.clone(),
142                        params.operation.clone(),
143                        e.to_string(),
144                    ),
145                }
146            }
147            OperationType::HybridDecrypt => {
148                match self.hybrid_decrypt_file(source, &destination, params).await {
149                    Ok((bytes_processed, metadata)) => OperationResult::success_with_metadata(
150                        source.clone(),
151                        destination,
152                        bytes_processed,
153                        params.operation.clone(),
154                        metadata.compressed,
155                        Some(metadata.filename),
156                        Some(true), // Assume checksum is verified for hybrid decryption
157                    ),
158                    Err(e) => OperationResult::failure(
159                        source.clone(),
160                        params.operation.clone(),
161                        e.to_string(),
162                    ),
163                }
164            }
165        };
166
167        result
168    }
169
170    /// Process a directory (compress to tar.gz then encrypt/decrypt)
171    async fn process_directory(&self, params: &OperationParams, password: &str) -> OperationResult {
172        let source = &params.source;
173        let destination = params.get_destination();
174
175        match params.operation {
176            OperationType::Encrypt => {
177                // For directories, we always compress (tar.gz) before encrypting
178                match self.encrypt_directory(source, &destination, password, params).await {
179                    Ok(bytes_processed) => OperationResult::success(
180                        source.clone(),
181                        destination,
182                        bytes_processed,
183                        params.operation.clone(),
184                        true, // Always compressed for directories
185                    ),
186                    Err(e) => OperationResult::failure(
187                        source.clone(),
188                        params.operation.clone(),
189                        e.to_string(),
190                    ),
191                }
192            }
193            OperationType::Decrypt => {
194                match self.decrypt_directory(&source, &destination, password, params).await {
195                    Ok(bytes_processed) => OperationResult::success(
196                        source.clone(),
197                        destination,
198                        bytes_processed,
199                        params.operation.clone(),
200                        true, // Always compressed for directories
201                    ),
202                    Err(e) => OperationResult::failure(
203                        source.clone(),
204                        params.operation.clone(),
205                        e.to_string(),
206                    ),
207                }
208            }
209            OperationType::HybridEncrypt => {
210                // Hybrid directory encryption
211                println!("📁 Hybrid Directory Encryption");
212                println!("==============================");
213                println!("🔑 This will compress the directory and encrypt with hybrid encryption");
214                
215                OperationResult::failure(
216                    source.clone(),
217                    params.operation.clone(),
218                    "Hybrid directory encryption implementation pending".to_string(),
219                )
220            }
221            OperationType::HybridDecrypt => {
222                // Hybrid directory decryption
223                println!("📁 Hybrid Directory Decryption");
224                println!("==============================");
225                
226                OperationResult::failure(
227                    source.clone(),
228                    params.operation.clone(),
229                    "Hybrid directory decryption implementation pending".to_string(),
230                )
231            }
232        }
233    }
234
235    /// Encrypt a single file
236    async fn encrypt_file(
237        &self,
238        source: &Path,
239        destination: &Path,
240        password: &str,
241        params: &OperationParams,
242    ) -> Result<u64, FileOperationError> {
243        let file_size = fs::metadata(source)?.len();
244        let progress = if params.show_progress {
245            Some(ProgressTracker::new(file_size, "Encrypting"))
246        } else {
247            None
248        };
249
250        let mut input = BufReader::new(File::open(source)?);
251        let mut file_data = Vec::new();
252
253        // Read entire file
254        input.read_to_end(&mut file_data)?;
255
256        // Apply compression if requested
257        let data_to_encrypt = if params.compress {
258            progress.as_ref().map(|p| p.set_message("Compressing..."));
259            self.compression.compress(&file_data)?
260        } else {
261            file_data.clone()
262        };
263
264        // Create metadata
265        let metadata = FileMetadata::from_file(source, &file_data, params.compress);
266
267        // Encrypt the data
268        progress.as_ref().map(|p| p.set_message("Encrypting..."));
269        let encrypted_data = self.crypto.encrypt(&data_to_encrypt, password, metadata)?;
270        
271        // Write to destination
272        let mut output = BufWriter::new(File::create(destination)?);
273        output.write_all(&encrypted_data)?;
274        output.flush()?;
275
276        if let Some(progress) = &progress {
277            progress.inc(file_size);
278            progress.finish("Encryption complete");
279        }
280
281        // Delete source file if requested
282        if params.delete_source {
283            fs::remove_file(source)?;
284        }
285
286        Ok(encrypted_data.len() as u64)
287    }
288
289    /// Encrypt a single file using hybrid encryption
290    async fn hybrid_encrypt_file(
291        &self,
292        source: &Path,
293        destination: &Path,
294        params: &OperationParams,
295    ) -> Result<u64, FileOperationError> {
296        // Display helpful message about SSH keys and show discovered keys
297        println!("🔑 Hybrid Encryption Mode");
298        println!("=========================");
299        
300        let file_size = fs::metadata(source)?.len();
301        let progress = if params.show_progress {
302            Some(ProgressTracker::new(file_size, "Hybrid Encrypting"))
303        } else {
304            None
305        };
306
307        let mut input = BufReader::new(File::open(source)?);
308        let mut file_data = Vec::new();
309
310        // Read entire file
311        input.read_to_end(&mut file_data)?;
312
313        // Apply compression if requested
314        let data_to_encrypt = if params.compress {
315            progress.as_ref().map(|p| p.set_message("Compressing..."));
316            self.compression.compress(&file_data)?
317        } else {
318            file_data.clone()
319        };
320
321        // Create metadata
322        let metadata = FileMetadata::from_file(source, &file_data, params.compress);
323
324        // Encrypt the data using hybrid encryption
325        progress.as_ref().map(|p| p.set_message("Hybrid encrypting..."));
326        let encrypted_data = self.hybrid_crypto.encrypt(&data_to_encrypt, params.public_key_path.as_deref(), metadata)?;
327        
328        // Create destination with .hsf extension
329        let final_destination = if destination.extension().is_none() {
330            destination.with_extension("hsf")
331        } else {
332            destination.to_path_buf()
333        };
334        
335        // Write to destination
336        let mut output = BufWriter::new(File::create(&final_destination)?);
337        output.write_all(&encrypted_data)?;
338        output.flush()?;
339
340        if let Some(progress) = &progress {
341            progress.inc(file_size);
342            progress.finish("Hybrid encryption complete");
343        }
344
345        // Show success message
346        println!("✅ Successfully encrypted file using hybrid encryption");
347        println!("📁 Output: {}", final_destination.display());
348
349        // Delete source file if requested
350        if params.delete_source {
351            fs::remove_file(source)?;
352        }
353
354        Ok(encrypted_data.len() as u64)
355    }
356
357    /// Decrypt a hybrid encrypted file
358    async fn hybrid_decrypt_file(
359        &self,
360        source: &Path,
361        destination: &Path,
362        params: &OperationParams,
363    ) -> Result<(u64, FileMetadata), FileOperationError> {
364        // Display helpful message about hybrid decryption
365        println!("🔓 Hybrid Decryption Mode");
366        println!("=========================");
367        
368        let file_size = fs::metadata(source)?.len();
369        let progress = if params.show_progress {
370            Some(ProgressTracker::new(file_size, "Hybrid Decrypting"))
371        } else {
372            None
373        };
374
375        // Read the encrypted file
376        let mut input = BufReader::new(File::open(source)?);
377        let mut encrypted_data = Vec::new();
378        input.read_to_end(&mut encrypted_data)?;
379
380        // Decrypt the data using hybrid decryption
381        progress.as_ref().map(|p| p.set_message("Hybrid decrypting..."));
382        let (decrypted_data, metadata) = self.hybrid_crypto.decrypt(&encrypted_data, params.private_key_path.as_deref())?;
383        
384        // Apply decompression if the data was compressed
385        let final_data = if metadata.compressed {
386            progress.as_ref().map(|p| p.set_message("Decompressing..."));
387            self.compression.decompress(&decrypted_data)?
388        } else {
389            decrypted_data
390        };
391
392        // Create destination (remove .hsf extension if present)
393        let final_destination = if source.extension() == Some(std::ffi::OsStr::new("hsf")) {
394            destination.with_file_name(source.file_stem().unwrap_or(source.as_os_str()))
395        } else {
396            destination.to_path_buf()
397        };
398        
399        // Write to destination
400        let mut output = BufWriter::new(File::create(&final_destination)?);
401        output.write_all(&final_data)?;
402        output.flush()?;
403
404        if let Some(progress) = &progress {
405            progress.inc(file_size);
406            progress.finish("Hybrid decryption complete");
407        }
408
409        // Show success message
410        println!("✅ Successfully decrypted file using hybrid decryption");
411        println!("📁 Output: {}", final_destination.display());
412
413        // Delete source file if requested
414        if params.delete_source {
415            fs::remove_file(source)?;
416        }
417
418        Ok((final_data.len() as u64, metadata))
419    }
420
421    /// Decrypt a single file
422    async fn decrypt_file(
423        &self,
424        source: &Path,
425        destination: &Path,
426        password: &str,
427        params: &OperationParams,
428    ) -> Result<(u64, FileMetadata, bool), FileOperationError> {
429        let file_size = fs::metadata(source)?.len();
430        let progress = if params.show_progress {
431            Some(ProgressTracker::new(file_size, "Decrypting"))
432        } else {
433            None
434        };
435
436        let mut input = BufReader::new(File::open(source)?);
437        let mut encrypted_data = Vec::new();
438        
439        // Read the encrypted file
440        input.read_to_end(&mut encrypted_data)?;
441        
442        if let Some(progress) = &progress {
443            progress.inc(file_size);
444            progress.set_message("Decrypting...");
445        }
446
447        // Try new format first, fall back to legacy if needed
448        let (decrypted_data, metadata) = match self.crypto.decrypt(&encrypted_data, password) {
449            Ok((data, meta)) => (data, Some(meta)),
450            Err(_) => {
451                // Try legacy format
452                let data = self.crypto.decrypt_legacy(&encrypted_data, password)?;
453                (data, None)
454            }
455        };
456        
457        // Decompress if needed
458        let final_data = if let Some(ref meta) = metadata {
459            if meta.compressed {
460                if let Some(progress) = &progress {
461                    progress.set_message("Decompressing...");
462                }
463                self.compression.decompress(&decrypted_data)?
464            } else {
465                decrypted_data
466            }
467        } else if params.compress {
468            // Legacy: try decompression based on params
469            if let Some(progress) = &progress {
470                progress.set_message("Decompressing...");
471            }
472            match self.compression.decompress(&decrypted_data) {
473                Ok(decompressed) => decompressed,
474                Err(_) => decrypted_data, // Not compressed, use as-is
475            }
476        } else {
477            decrypted_data
478        };
479
480        // Write the final data
481        let mut output = BufWriter::new(File::create(destination)?);
482        output.write_all(&final_data)?;
483        output.flush()?;
484
485        if let Some(progress) = &progress {
486            progress.finish("Decryption complete");
487        }
488
489        // Delete source file if requested
490        if params.delete_source {
491            fs::remove_file(source)?;
492        }
493
494        // Verify checksum if metadata is available and verification is enabled
495        let checksum_verified = if let Some(ref meta) = metadata {
496            if params.verify_checksum {
497                meta.verify_checksum(&final_data)
498            } else {
499                true // Skip verification if disabled
500            }
501        } else {
502            true // Legacy format, no checksum available
503        };
504
505        let result_metadata = metadata.unwrap_or_else(|| {
506            // Create placeholder metadata for legacy files
507            FileMetadata::new("unknown".to_string(), [0u8; 32], params.compress)
508        });
509
510        Ok((final_data.len() as u64, result_metadata, checksum_verified))
511    }
512
513    /// Encrypt a directory (tar.gz then encrypt)
514    async fn encrypt_directory(
515        &self,
516        source: &Path,
517        destination: &Path,
518        password: &str,
519        params: &OperationParams,
520    ) -> Result<u64, FileOperationError> {
521        let progress = if params.show_progress {
522            Some(ProgressTracker::new_spinner("Encrypting directory"))
523        } else {
524            None
525        };
526
527        // Create a tar.gz archive of the directory in memory
528        progress.as_ref().map(|p| p.set_message("Creating archive..."));
529        let archive_data = self.create_directory_archive(source)?;
530        
531        // Create metadata for directory
532        let metadata = FileMetadata::from_file(source, &archive_data, true); // Always compressed for directories
533        
534        // Encrypt the archive
535        progress.as_ref().map(|p| p.set_message("Encrypting archive..."));
536        let encrypted_data = self.crypto.encrypt(&archive_data, password, metadata)?;
537        
538        // Write encrypted data to destination
539        let mut output = BufWriter::new(File::create(destination)?);
540        output.write_all(&encrypted_data)?;
541        output.flush()?;
542
543        if let Some(progress) = &progress {
544            progress.finish("Directory encryption complete");
545        }
546
547        Ok(encrypted_data.len() as u64)
548    }
549
550    /// Decrypt a directory (decrypt then extract tar.gz)
551    async fn decrypt_directory(
552        &self,
553        source: &Path,
554        destination: &Path,
555        password: &str,
556        params: &OperationParams,
557    ) -> Result<u64, FileOperationError> {
558        let progress = if params.show_progress {
559            Some(ProgressTracker::new_spinner("Decrypting directory"))
560        } else {
561            None
562        };
563
564        // Read and decrypt the file
565        progress.as_ref().map(|p| p.set_message("Reading encrypted file..."));
566        let mut encrypted_data = Vec::new();
567        let mut input = BufReader::new(File::open(source)?);
568        input.read_to_end(&mut encrypted_data)?;
569
570        progress.as_ref().map(|p| p.set_message("Decrypting..."));
571        let (archive_data, _metadata) = match self.crypto.decrypt(&encrypted_data, password) {
572            Ok((data, meta)) => (data, Some(meta)),
573            Err(_) => {
574                // Try legacy format
575                let data = self.crypto.decrypt_legacy(&encrypted_data, password)?;
576                (data, None)
577            }
578        };
579
580        // Extract the archive
581        progress.as_ref().map(|p| p.set_message("Extracting archive..."));
582        self.extract_directory_archive(&archive_data, destination)?;
583
584        if let Some(progress) = &progress {
585            progress.finish("Directory decryption complete");
586        }
587
588        Ok(archive_data.len() as u64)
589    }
590
591    /// Create a tar.gz archive of a directory
592    fn create_directory_archive(&self, source: &Path) -> Result<Vec<u8>, FileOperationError> {
593        use flate2::write::GzEncoder;
594
595        let archive_buffer = Vec::new();
596        let encoder = GzEncoder::new(archive_buffer, flate2::Compression::default());
597        let mut tar = tar::Builder::new(encoder);
598
599        // Add the directory to the tar archive
600        tar.append_dir_all(".", source)
601            .map_err(|e| FileOperationError::IoError(e))?;
602
603        let encoder = tar.into_inner()
604            .map_err(|e| FileOperationError::IoError(e))?;
605        
606        let archive_data = encoder.finish()
607            .map_err(|e| FileOperationError::IoError(e))?;
608
609        Ok(archive_data)
610    }
611
612    /// Extract a tar.gz archive to a directory
613    fn extract_directory_archive(&self, archive_data: &[u8], destination: &Path) -> Result<(), FileOperationError> {
614        use flate2::read::GzDecoder;
615
616        // Create destination directory
617        fs::create_dir_all(destination)?;
618
619        let decoder = GzDecoder::new(archive_data);
620        let mut archive = tar::Archive::new(decoder);
621        
622        archive.unpack(destination)
623            .map_err(|e| FileOperationError::IoError(e))?;
624
625        Ok(())
626    }
627}
628
629#[cfg(test)]
630mod tests {
631    use super::*;
632    use tempfile::TempDir;
633    use std::fs;
634
635    #[tokio::test]
636    async fn test_file_encryption_decryption() {
637        let temp_dir = TempDir::new().unwrap();
638        let source_file = temp_dir.path().join("test.txt");
639        let encrypted_file = temp_dir.path().join("test.sf");
640        let decrypted_file = temp_dir.path().join("test_decrypted.txt");
641
642        // Create test file
643        fs::write(&source_file, b"Hello, World! This is a test file.").unwrap();
644
645        let operator = FileOperator::new();
646        let password = "test_password_123";
647
648        // Encrypt
649        let encrypt_params = OperationParams::new(
650            OperationType::Encrypt,
651            TargetType::File,
652            source_file.clone(),
653        ).with_destination(encrypted_file.clone())
654         .with_progress(false);
655
656        let result = operator.process(&encrypt_params, password).await;
657        assert!(result.success, "Encryption failed: {:?}", result.error);
658        assert!(encrypted_file.exists());
659
660        // Decrypt
661        let decrypt_params = OperationParams::new(
662            OperationType::Decrypt,
663            TargetType::File,
664            encrypted_file,
665        ).with_destination(decrypted_file.clone())
666         .with_progress(false);
667
668        let result = operator.process(&decrypt_params, password).await;
669        assert!(result.success, "Decryption failed: {:?}", result.error);
670        assert!(decrypted_file.exists());
671
672        // Verify content
673        let original_content = fs::read(&source_file).unwrap();
674        let decrypted_content = fs::read(&decrypted_file).unwrap();
675        assert_eq!(original_content, decrypted_content);
676    }
677
678    #[tokio::test]
679    async fn test_file_encryption_with_compression() {
680        let temp_dir = TempDir::new().unwrap();
681        let source_file = temp_dir.path().join("test.txt");
682        let encrypted_file = temp_dir.path().join("test.sf.gz");
683        let decrypted_file = temp_dir.path().join("test_decrypted.txt");
684
685        // Create test file with repetitive content (compresses well)
686        let content = "Hello, World! ".repeat(1000);
687        fs::write(&source_file, content.as_bytes()).unwrap();
688
689        let operator = FileOperator::new();
690        let password = "test_password_123";
691
692        // Encrypt with compression
693        let encrypt_params = OperationParams::new(
694            OperationType::Encrypt,
695            TargetType::File,
696            source_file.clone(),
697        ).with_destination(encrypted_file.clone())
698         .with_compression(true)
699         .with_progress(false);
700
701        let result = operator.process(&encrypt_params, password).await;
702        assert!(result.success);
703        assert!(encrypted_file.exists());
704
705        // Decrypt with compression
706        let decrypt_params = OperationParams::new(
707            OperationType::Decrypt,
708            TargetType::File,
709            encrypted_file,
710        ).with_destination(decrypted_file.clone())
711         .with_compression(true)
712         .with_progress(false);
713
714        let result = operator.process(&decrypt_params, password).await;
715        assert!(result.success);
716        assert!(decrypted_file.exists());
717
718        // Verify content
719        let original_content = fs::read(&source_file).unwrap();
720        let decrypted_content = fs::read(&decrypted_file).unwrap();
721        assert_eq!(original_content, decrypted_content);
722    }
723
724    #[tokio::test]
725    async fn test_wrong_password() {
726        let temp_dir = TempDir::new().unwrap();
727        let source_file = temp_dir.path().join("test.txt");
728        let encrypted_file = temp_dir.path().join("test.sf");
729
730        fs::write(&source_file, b"Secret content").unwrap();
731
732        let operator = FileOperator::new();
733
734        // Encrypt with one password
735        let encrypt_params = OperationParams::new(
736            OperationType::Encrypt,
737            TargetType::File,
738            source_file,
739        ).with_destination(encrypted_file.clone())
740         .with_progress(false);
741
742        let result = operator.process(&encrypt_params, "correct_password").await;
743        assert!(result.success);
744
745        // Try to decrypt with wrong password
746        let decrypt_params = OperationParams::new(
747            OperationType::Decrypt,
748            TargetType::File,
749            encrypted_file,
750        ).with_progress(false);
751
752        let result = operator.process(&decrypt_params, "wrong_password").await;
753        assert!(!result.success);
754        assert!(result.error.is_some());
755    }
756}