diff --git a/src/archive.rs b/src/archive.rs index 8221da8..a102565 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -2,6 +2,8 @@ use std::fs; use std::io::{Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; +use rand::Rng; + use crate::compression; use crate::crypto; use crate::format::{self, Header, TocEntry, HEADER_SIZE}; @@ -18,16 +20,46 @@ struct ProcessedFile { sha256: [u8; 32], compression_flag: u8, ciphertext: Vec, + padding_after: u16, + padding_bytes: Vec, +} + +/// Read and de-obfuscate archive header and TOC entries. +/// +/// Handles XOR header bootstrapping (FORMAT.md Section 10 steps 1-3) +/// and TOC decryption (Section 10 step 4) automatically. +/// Used by both unpack() and inspect(). +fn read_archive_metadata(file: &mut fs::File) -> anyhow::Result<(Header, Vec)> { + // Step 1-3: Read header with XOR bootstrapping + let header = format::read_header_auto(file)?; + + // Step 4: Read TOC (possibly encrypted) + file.seek(SeekFrom::Start(header.toc_offset as u64))?; + let mut toc_raw = vec![0u8; header.toc_size as usize]; + file.read_exact(&mut toc_raw)?; + + let entries = if header.flags & 0x02 != 0 { + // TOC is encrypted: decrypt with toc_iv, then parse + let toc_plaintext = crypto::decrypt_data(&toc_raw, &KEY, &header.toc_iv)?; + format::read_toc_from_buf(&toc_plaintext, header.file_count)? + } else { + // TOC is plaintext: parse directly + format::read_toc_from_buf(&toc_raw, header.file_count)? + }; + + Ok((header, entries)) } /// 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. +/// Two-pass algorithm with full obfuscation: +/// Pass 1: Read, hash, compress, encrypt each file; generate decoy padding. +/// Pass 2: Encrypt TOC, compute offsets, XOR header, write archive. pub fn pack(files: &[PathBuf], output: &Path, no_compress: &[String]) -> anyhow::Result<()> { anyhow::ensure!(!files.is_empty(), "No input files specified"); + let mut rng = rand::rng(); + // --- Pass 1: Process all files --- let mut processed: Vec = Vec::with_capacity(files.len()); @@ -75,6 +107,11 @@ pub fn pack(files: &[PathBuf], output: &Path, no_compress: &[String]) -> anyhow: // Step 5: Compute HMAC over IV || ciphertext let hmac = crypto::compute_hmac(&KEY, &iv, &ciphertext); + // Step 6: Generate decoy padding (FORMAT.md Section 9.3) + let padding_after: u16 = rng.random_range(64..=4096); + let mut padding_bytes = vec![0u8; padding_after as usize]; + rand::Fill::fill(&mut padding_bytes[..], &mut rng); + processed.push(ProcessedFile { name, original_size, @@ -85,51 +122,62 @@ pub fn pack(files: &[PathBuf], output: &Path, no_compress: &[String]) -> anyhow: sha256, compression_flag, ciphertext, + padding_after, + padding_bytes, }); } // --- Pass 2: Compute offsets and write archive --- - // Build TOC entries (without data_offset yet) to compute TOC size - let toc_size: u32 = processed + // Determine flags byte: bit 0 if any file is compressed, bits 1-3 for obfuscation + let any_compressed = processed.iter().any(|pf| pf.compression_flag == 1); + let mut flags: u8 = if any_compressed { 0x01 } else { 0x00 }; + // Enable all three obfuscation features + flags |= 0x02; // bit 1: TOC encrypted + flags |= 0x04; // bit 2: XOR header + flags |= 0x08; // bit 3: decoy padding + + // Build TOC entries (with placeholder data_offset=0, will be set after toc_size known) + let toc_entries: Vec = processed .iter() - .map(|pf| 101 + pf.name.len() as u32) - .sum(); + .map(|pf| TocEntry { + name: pf.name.clone(), + original_size: pf.original_size, + compressed_size: pf.compressed_size, + encrypted_size: pf.encrypted_size, + data_offset: 0, // placeholder + iv: pf.iv, + hmac: pf.hmac, + sha256: pf.sha256, + compression_flag: pf.compression_flag, + padding_after: pf.padding_after, + }) + .collect(); + + // Serialize TOC to get plaintext size, then encrypt to get final toc_size + let toc_plaintext = format::serialize_toc(&toc_entries)?; + + // Generate TOC IV and encrypt + let toc_iv = crypto::generate_iv(); + let encrypted_toc = crypto::encrypt_data(&toc_plaintext, &KEY, &toc_iv); + let encrypted_toc_size = encrypted_toc.len() as u32; let toc_offset = HEADER_SIZE; - // Compute data offsets + // Compute data offsets (accounting for encrypted TOC size and padding) + let data_block_start = toc_offset + encrypted_toc_size; let mut data_offsets: Vec = Vec::with_capacity(processed.len()); - let mut current_offset = toc_offset + toc_size; + let mut current_offset = data_block_start; for pf in &processed { data_offsets.push(current_offset); - current_offset += pf.encrypted_size; + current_offset += pf.encrypted_size + pf.padding_after as u32; } - // 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 { + // Now re-serialize TOC with correct data_offsets + let final_toc_entries: Vec = processed + .iter() + .enumerate() + .map(|(i, pf)| TocEntry { name: pf.name.clone(), original_size: pf.original_size, compressed_size: pf.compressed_size, @@ -139,14 +187,48 @@ pub fn pack(files: &[PathBuf], output: &Path, no_compress: &[String]) -> anyhow: 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)?; - } + padding_after: pf.padding_after, + }) + .collect(); - // Write data blocks + let final_toc_plaintext = format::serialize_toc(&final_toc_entries)?; + let final_encrypted_toc = crypto::encrypt_data(&final_toc_plaintext, &KEY, &toc_iv); + let final_encrypted_toc_size = final_encrypted_toc.len() as u32; + + // Sanity check: encrypted TOC size should not change (same plaintext length) + assert_eq!( + encrypted_toc_size, final_encrypted_toc_size, + "TOC encrypted size changed unexpectedly" + ); + + // Create header + let header = Header { + version: format::VERSION, + flags, + file_count: processed.len() as u16, + toc_offset, + toc_size: final_encrypted_toc_size, + toc_iv, + reserved: [0u8; 8], + }; + + // Serialize header to buffer and XOR + let mut header_buf = format::write_header_to_buf(&header); + format::xor_header_buf(&mut header_buf); + + // Open output file + let mut out_file = fs::File::create(output)?; + + // Write XOR'd header + out_file.write_all(&header_buf)?; + + // Write encrypted TOC + out_file.write_all(&final_encrypted_toc)?; + + // Write data blocks with interleaved decoy padding for pf in &processed { out_file.write_all(&pf.ciphertext)?; + out_file.write_all(&pf.padding_bytes)?; } let total_bytes = current_offset; @@ -163,14 +245,12 @@ pub fn pack(files: &[PathBuf], output: &Path, no_compress: &[String]) -> anyhow: /// Inspect archive metadata without decryption. /// /// Reads and displays the header and all TOC entries. +/// Handles XOR header de-obfuscation and TOC decryption. 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)?; + // Read header and TOC with full de-obfuscation + let (header, entries) = read_archive_metadata(&mut file)?; // Print header info let filename = archive @@ -201,6 +281,7 @@ pub fn inspect(archive: &Path) -> anyhow::Result<()> { println!(" Encrypted: {} bytes", entry.encrypted_size); println!(" Offset: {}", entry.data_offset); println!(" Compression: {}", compression_str); + println!(" Padding after: {} bytes", entry.padding_after); println!( " IV: {}", entry.iv.iter().map(|b| format!("{:02x}", b)).collect::() @@ -226,17 +307,14 @@ pub fn inspect(archive: &Path) -> anyhow::Result<()> { /// 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 +/// 1. Read header with XOR bootstrapping +/// 2. Read and decrypt TOC entries +/// 3. For each file: seek to data_offset, 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)?; + // Read header and TOC with full de-obfuscation + let (_header, entries) = read_archive_metadata(&mut file)?; // Create output directory fs::create_dir_all(output_dir)?;