use std::fs; use std::io::{Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; use crate::compression; use crate::crypto; use crate::format::{self, Header, TocEntry, HEADER_SIZE}; use crate::key::KEY; /// Processed file data collected during Pass 1 of pack. struct ProcessedFile { name: String, original_size: u32, compressed_size: u32, encrypted_size: u32, iv: [u8; 16], hmac: [u8; 32], sha256: [u8; 32], compression_flag: u8, ciphertext: Vec, } /// Pack files into an encrypted archive. /// /// Two-pass algorithm: /// Pass 1: Read, hash, compress, encrypt each file. /// Pass 2: Compute offsets, write header + TOC + data blocks. pub fn pack(files: &[PathBuf], output: &Path, no_compress: &[String]) -> anyhow::Result<()> { anyhow::ensure!(!files.is_empty(), "No input files specified"); // --- Pass 1: Process all files --- let mut processed: Vec = Vec::with_capacity(files.len()); for file_path in files { let data = fs::read(file_path)?; // Validate file size <= u32::MAX anyhow::ensure!( data.len() <= u32::MAX as usize, "File too large: {} ({} bytes exceeds 4 GB limit)", file_path.display(), data.len() ); // Use just the filename (not the full path) as the archive entry name let name = file_path .file_name() .ok_or_else(|| anyhow::anyhow!("Invalid file path: {}", file_path.display()))? .to_str() .ok_or_else(|| anyhow::anyhow!("Non-UTF-8 filename: {}", file_path.display()))? .to_string(); // Step 1: SHA-256 of original data let sha256 = crypto::sha256_hash(&data); // Step 2: Determine compression and compress if needed let should_compress = compression::should_compress(&name, no_compress); let (compressed_data, compression_flag) = if should_compress { let compressed = compression::compress(&data)?; (compressed, 1u8) } else { (data.clone(), 0u8) }; let original_size = data.len() as u32; let compressed_size = compressed_data.len() as u32; // Step 3: Generate random IV let iv = crypto::generate_iv(); // Step 4: Encrypt let ciphertext = crypto::encrypt_data(&compressed_data, &KEY, &iv); let encrypted_size = ciphertext.len() as u32; // Step 5: Compute HMAC over IV || ciphertext let hmac = crypto::compute_hmac(&KEY, &iv, &ciphertext); processed.push(ProcessedFile { name, original_size, compressed_size, encrypted_size, iv, hmac, sha256, compression_flag, ciphertext, }); } // --- Pass 2: Compute offsets and write archive --- // Build TOC entries (without data_offset yet) to compute TOC size let toc_size: u32 = processed .iter() .map(|pf| 101 + pf.name.len() as u32) .sum(); let toc_offset = HEADER_SIZE; // Compute data offsets let mut data_offsets: Vec = Vec::with_capacity(processed.len()); let mut current_offset = toc_offset + toc_size; for pf in &processed { data_offsets.push(current_offset); current_offset += pf.encrypted_size; } // Determine flags byte: bit 0 if any file is compressed let any_compressed = processed.iter().any(|pf| pf.compression_flag == 1); let flags: u8 = if any_compressed { 0x01 } else { 0x00 }; // Create header let header = Header { version: format::VERSION, flags, file_count: processed.len() as u16, toc_offset, toc_size, toc_iv: [0u8; 16], // TOC not encrypted in v1 Phase 2 reserved: [0u8; 8], }; // Open output file let mut out_file = fs::File::create(output)?; // Write header format::write_header(&mut out_file, &header)?; // Write TOC entries for (i, pf) in processed.iter().enumerate() { let entry = TocEntry { name: pf.name.clone(), original_size: pf.original_size, compressed_size: pf.compressed_size, encrypted_size: pf.encrypted_size, data_offset: data_offsets[i], iv: pf.iv, hmac: pf.hmac, sha256: pf.sha256, compression_flag: pf.compression_flag, padding_after: 0, // No decoy padding in Phase 2 }; format::write_toc_entry(&mut out_file, &entry)?; } // Write data blocks for pf in &processed { out_file.write_all(&pf.ciphertext)?; } let total_bytes = current_offset; println!( "Packed {} files into {} ({} bytes)", processed.len(), output.display(), total_bytes ); Ok(()) } /// Inspect archive metadata without decryption. /// /// Reads and displays the header and all TOC entries. pub fn inspect(archive: &Path) -> anyhow::Result<()> { let mut file = fs::File::open(archive)?; // Read header let header = format::read_header(&mut file)?; // Read TOC entries let entries = format::read_toc(&mut file, header.file_count)?; // Print header info let filename = archive .file_name() .map(|s| s.to_string_lossy().to_string()) .unwrap_or_else(|| archive.display().to_string()); println!("Archive: {}", filename); println!("Version: {}", header.version); println!("Flags: 0x{:02X}", header.flags); println!("Files: {}", header.file_count); println!("TOC offset: {}", header.toc_offset); println!("TOC size: {}", header.toc_size); println!(); // Print each file entry let mut total_original: u64 = 0; for (i, entry) in entries.iter().enumerate() { let compression_str = if entry.compression_flag == 1 { "yes" } else { "no" }; println!("[{}] {}", i, entry.name); println!(" Original: {} bytes", entry.original_size); println!(" Compressed: {} bytes", entry.compressed_size); println!(" Encrypted: {} bytes", entry.encrypted_size); println!(" Offset: {}", entry.data_offset); println!(" Compression: {}", compression_str); println!( " IV: {}", entry.iv.iter().map(|b| format!("{:02x}", b)).collect::() ); println!( " HMAC: {}", entry.hmac.iter().map(|b| format!("{:02x}", b)).collect::() ); println!( " SHA-256: {}", entry.sha256.iter().map(|b| format!("{:02x}", b)).collect::() ); total_original += entry.original_size as u64; } println!(); println!("Total original size: {} bytes", total_original); Ok(()) } /// Unpack an encrypted archive, extracting all files with HMAC and SHA-256 verification. /// /// Follows FORMAT.md Section 10 decode order: /// 1. Read header (validates magic, version, flags) /// 2. Read TOC entries /// 3. For each file: verify HMAC, decrypt, decompress, verify SHA-256, write pub fn unpack(archive: &Path, output_dir: &Path) -> anyhow::Result<()> { let mut file = fs::File::open(archive)?; // Read header let header = format::read_header(&mut file)?; // Read TOC entries let entries = format::read_toc(&mut file, header.file_count)?; // Create output directory fs::create_dir_all(output_dir)?; let file_count = entries.len(); let mut error_count: usize = 0; let mut success_count: usize = 0; for entry in &entries { // Sanitize filename: reject directory traversal if entry.name.starts_with('/') || entry.name.contains("..") { eprintln!( "Skipping file with unsafe name: {} (directory traversal attempt)", entry.name ); error_count += 1; continue; } // Seek to data_offset and read ciphertext file.seek(SeekFrom::Start(entry.data_offset as u64))?; let mut ciphertext = vec![0u8; entry.encrypted_size as usize]; file.read_exact(&mut ciphertext)?; // Step 1: Verify HMAC FIRST (encrypt-then-MAC) if !crypto::verify_hmac(&KEY, &entry.iv, &ciphertext, &entry.hmac) { eprintln!("HMAC verification failed for {}, skipping", entry.name); error_count += 1; continue; } // Step 2: Decrypt let decrypted = match crypto::decrypt_data(&ciphertext, &KEY, &entry.iv) { Ok(data) => data, Err(e) => { eprintln!("Decryption failed for {}: {}", entry.name, e); error_count += 1; continue; } }; // Step 3: Decompress if compressed let decompressed = if entry.compression_flag == 1 { match compression::decompress(&decrypted) { Ok(data) => data, Err(e) => { eprintln!("Decompression failed for {}: {}", entry.name, e); error_count += 1; continue; } } } else { decrypted }; // Step 4: Verify SHA-256 let computed_sha256 = crypto::sha256_hash(&decompressed); if computed_sha256 != entry.sha256 { eprintln!( "SHA-256 mismatch for {} (data may be corrupted)", entry.name ); error_count += 1; // Still write the file per spec } // Step 5: Create parent directories if name contains path separators let output_path = output_dir.join(&entry.name); if let Some(parent) = output_path.parent() { fs::create_dir_all(parent)?; } // Step 6: Write file fs::write(&output_path, &decompressed)?; println!("Extracted: {} ({} bytes)", entry.name, entry.original_size); success_count += 1; } println!( "Extracted {}/{} files", success_count, file_count ); if error_count > 0 { anyhow::bail!("{} file(s) had verification errors", error_count); } Ok(()) }