diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 1ddea6a..0f9dd32 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -45,11 +45,11 @@ Plans: 3. Each file in the archive is independently compressed (gzip) and encrypted (AES-256-CBC) with a unique random IV 4. HMAC-SHA256 is computed over IV+ciphertext for each file (encrypt-then-MAC) 5. Running `encrypted_archive inspect archive.bin` shows file count, names, and sizes without decrypting content -**Plans**: TBD +**Plans**: 2 plans Plans: -- [ ] 02-01: TBD -- [ ] 02-02: TBD +- [ ] 02-01-PLAN.md -- Project scaffolding, binary format types, crypto pipeline, and compression module +- [ ] 02-02-PLAN.md -- Pack, inspect, and unpack commands with full archive orchestration ### Phase 3: Round-Trip Verification **Goal**: Proven byte-identical round-trips through the Rust unpack command, backed by golden test vectors diff --git a/.planning/phases/02-core-archiver/02-01-PLAN.md b/.planning/phases/02-core-archiver/02-01-PLAN.md new file mode 100644 index 0000000..b736b94 --- /dev/null +++ b/.planning/phases/02-core-archiver/02-01-PLAN.md @@ -0,0 +1,288 @@ +--- +phase: 02-core-archiver +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - Cargo.toml + - src/main.rs + - src/cli.rs + - src/key.rs + - src/format.rs + - src/crypto.rs + - src/compression.rs +autonomous: true +requirements: [FMT-01, FMT-02, FMT-03, FMT-04, ENC-01, ENC-02, ENC-03, ENC-04, ENC-05, CMP-01, CMP-02, INT-01, CLI-01] + +must_haves: + truths: + - "cargo build compiles the project with zero errors and zero warnings" + - "All binary format types (Header, TocEntry) match FORMAT.md byte-for-byte field definitions" + - "encrypt then decrypt with known key/IV produces original data" + - "compress then decompress produces original data" + - "HMAC computed over IV||ciphertext matches recomputation" + - "SHA-256 of original data is correctly computed and stored" + artifacts: + - path: "Cargo.toml" + provides: "Project manifest with all dependencies" + contains: "aes" + - path: "src/main.rs" + provides: "CLI entry point with clap dispatch" + contains: "clap" + - path: "src/cli.rs" + provides: "Clap derive structs for Pack/Unpack/Inspect subcommands" + exports: ["Cli", "Commands"] + - path: "src/key.rs" + provides: "Hardcoded 32-byte encryption key" + exports: ["KEY"] + - path: "src/format.rs" + provides: "Header and TocEntry structs with serialize/deserialize" + exports: ["Header", "TocEntry", "MAGIC", "VERSION"] + - path: "src/crypto.rs" + provides: "AES-256-CBC encrypt/decrypt, HMAC-SHA-256, SHA-256" + exports: ["encrypt_data", "decrypt_data", "compute_hmac", "verify_hmac", "sha256_hash"] + - path: "src/compression.rs" + provides: "Gzip compress/decompress and should_compress heuristic" + exports: ["compress", "decompress", "should_compress"] + key_links: + - from: "src/crypto.rs" + to: "src/key.rs" + via: "imports KEY constant" + pattern: "use crate::key::KEY" + - from: "src/format.rs" + to: "docs/FORMAT.md" + via: "byte-for-byte field layout match" + pattern: "0x00.*0xEA.*0x72.*0x63" + - from: "src/main.rs" + to: "src/cli.rs" + via: "clap parse and dispatch" + pattern: "Cli::parse" +--- + + +Create the Rust project foundation with all library modules: CLI skeleton, binary format types, crypto pipeline, and compression. + +Purpose: Establish the complete module structure and all building-block functions that the pack/unpack/inspect commands will orchestrate. Every individual operation (encrypt, decrypt, compress, decompress, HMAC, SHA-256, serialize header, serialize TOC entry) must work correctly in isolation before being wired together. + +Output: A compiling Rust project with 7 source files covering CLI parsing, binary format types, cryptographic operations, compression, and the hardcoded key. + + + +@/home/nick/.claude/get-shit-done/workflows/execute-plan.md +@/home/nick/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/02-core-archiver/02-RESEARCH.md +@docs/FORMAT.md + + + + + + Task 1: Project scaffolding with Cargo, CLI skeleton, and key module + Cargo.toml, src/main.rs, src/cli.rs, src/key.rs + +1. Initialize the Rust project: + ``` + cargo init --name encrypted_archive /home/nick/Projects/Rust/encrypted_archive + ``` + If Cargo.toml already exists, just update it. + +2. Set up Cargo.toml with exact dependency versions from research: + ```toml + [package] + name = "encrypted_archive" + version = "0.1.0" + edition = "2021" + + [dependencies] + aes = "0.8" + cbc = "0.1" + hmac = "0.12" + sha2 = "0.10" + flate2 = "1.1" + clap = { version = "4.5", features = ["derive"] } + rand = "0.9" + anyhow = "1.0" + ``` + +3. Create `src/cli.rs` with clap derive structs matching the research pattern: + - `Cli` struct with `#[command(subcommand)]` + - `Commands` enum with three variants: + - `Pack { files: Vec, output: PathBuf, no_compress: Vec }` + - `Unpack { archive: PathBuf, output_dir: PathBuf }` (output_dir defaults to ".") + - `Inspect { archive: PathBuf }` + - Use `#[arg(required = true)]` for files in Pack + - Use `#[arg(short, long)]` for output paths + - Use `#[arg(long)]` for no_compress (Vec of filename patterns to skip compression for) + +4. Create `src/key.rs` with the hardcoded 32-byte key: + ```rust + /// Hardcoded 32-byte AES-256 key. + /// Same key is used for AES-256-CBC encryption and HMAC-SHA-256 authentication (v1). + /// v2 will derive separate subkeys using HKDF. + pub const KEY: [u8; 32] = [ + 0x7A, 0x35, 0xC1, 0xD9, 0x4F, 0xE8, 0x2B, 0x6A, + 0x91, 0x0D, 0xF3, 0x58, 0xBC, 0x74, 0xA6, 0x1E, + 0x42, 0x8F, 0xD0, 0x63, 0xE5, 0x17, 0x9B, 0x2C, + 0xFA, 0x84, 0x06, 0xCD, 0x3E, 0x79, 0xB5, 0x50, + ]; + ``` + Use a non-trivial key (not the example key 00 01 02 ... 1F from FORMAT.md worked example). + +5. Create `src/main.rs`: + - Parse CLI with `Cli::parse()` + - Match on `Commands` variants, calling placeholder functions that print "not implemented yet" and return `Ok(())` + - Use `anyhow::Result<()>` as the return type for main + - Declare modules: `mod cli; mod key; mod format; mod crypto; mod compression; mod archive;` + +IMPORTANT: Use Context7 to verify clap 4.5 derive API before writing cli.rs. Call `mcp__context7__resolve-library-id` for "clap" and then `mcp__context7__query-docs` for the derive subcommand pattern. + +Do NOT use `rand::thread_rng()` -- it was renamed to `rand::rng()` in rand 0.9. +Do NOT use `block-modes` crate -- it is deprecated; use `cbc` directly. + + + cd /home/nick/Projects/Rust/encrypted_archive && cargo build 2>&1 + Verify Cargo.toml has correct dependencies, src/main.rs declares all modules, CLI help text shows pack/unpack/inspect + run after this task commits + + + - cargo build succeeds with no errors + - `cargo run -- --help` shows three subcommands: pack, unpack, inspect + - `cargo run -- pack --help` shows files (required), --output, --no-compress arguments + - All 7 module files exist (main.rs, cli.rs, key.rs, format.rs, crypto.rs, compression.rs, archive.rs) even if some are stubs + + + + + Task 2: Format types, crypto pipeline, and compression module + src/format.rs, src/crypto.rs, src/compression.rs + +1. Create `src/format.rs` implementing the binary format from FORMAT.md: + + Constants: + ```rust + pub const MAGIC: [u8; 4] = [0x00, 0xEA, 0x72, 0x63]; + pub const VERSION: u8 = 1; + pub const HEADER_SIZE: u32 = 40; + ``` + + Structs: + - `Header` with fields: version (u8), flags (u8), file_count (u16), toc_offset (u32), toc_size (u32), toc_iv ([u8; 16]), reserved ([u8; 8]) + - `TocEntry` with fields: name (String), original_size (u32), compressed_size (u32), encrypted_size (u32), data_offset (u32), iv ([u8; 16]), hmac ([u8; 32]), sha256 ([u8; 32]), compression_flag (u8), padding_after (u16) + + Serialization (write functions): + - `write_header(writer: &mut impl Write, header: &Header) -> anyhow::Result<()>`: + Writes all 40 bytes in exact FORMAT.md order. Magic bytes first, then version, flags, file_count (LE), toc_offset (LE), toc_size (LE), toc_iv (16 bytes), reserved (8 bytes of zero). + - `write_toc_entry(writer: &mut impl Write, entry: &TocEntry) -> anyhow::Result<()>`: + Writes: name_length (u16 LE) + name bytes + original_size (u32 LE) + compressed_size (u32 LE) + encrypted_size (u32 LE) + data_offset (u32 LE) + iv (16 bytes) + hmac (32 bytes) + sha256 (32 bytes) + compression_flag (u8) + padding_after (u16 LE). + Entry size = 101 + name.len() bytes. + + Deserialization (read functions): + - `read_header(reader: &mut impl Read) -> anyhow::Result
`: + Reads 40 bytes, verifies magic == MAGIC, verifies version == 1, checks flags bits 4-7 are zero (reject if not). Returns parsed Header. + - `read_toc_entry(reader: &mut impl Read) -> anyhow::Result`: + Reads name_length (u16 LE), then name_length bytes as UTF-8 string, then all fixed fields. Uses `from_le_bytes()` for all multi-byte integers. + - `read_toc(reader: &mut impl Read, file_count: u16) -> anyhow::Result>`: + Reads file_count entries sequentially. + + Helper: + - `entry_size(entry: &TocEntry) -> u32`: Returns `101 + entry.name.len() as u32` + - `compute_toc_size(entries: &[TocEntry]) -> u32`: Sum of all entry_size values + + ALL multi-byte fields MUST use `to_le_bytes()` for writing and `from_le_bytes()` for reading. Do NOT use `to_ne_bytes()` or `to_be_bytes()`. + Filenames: Use `name.len()` (byte count) NOT `name.chars().count()` (character count). FORMAT.md specifies byte count. + +2. Create `src/crypto.rs` implementing the encryption pipeline: + + Type aliases: + ```rust + use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit}; + type Aes256CbcEnc = cbc::Encryptor; + type Aes256CbcDec = cbc::Decryptor; + type HmacSha256 = hmac::Hmac; + ``` + + Functions: + - `generate_iv() -> [u8; 16]`: + Uses `rand::rng().fill(&mut iv)` (NOT thread_rng -- renamed in rand 0.9). + - `encrypt_data(plaintext: &[u8], key: &[u8; 32], iv: &[u8; 16]) -> Vec`: + Computes encrypted_size = ((plaintext.len() / 16) + 1) * 16. + Allocates buffer of encrypted_size, copies plaintext to start. + Calls `Aes256CbcEnc::new(key.into(), iv.into()).encrypt_padded_mut::(&mut buf, plaintext.len())`. + Returns the buffer (full encrypted_size bytes). + - `decrypt_data(ciphertext: &[u8], key: &[u8; 32], iv: &[u8; 16]) -> anyhow::Result>`: + Allocates mutable buffer from ciphertext. + Calls `Aes256CbcDec::new(key.into(), iv.into()).decrypt_padded_mut::(&mut buf)`. + Returns decrypted data as Vec. + - `compute_hmac(key: &[u8; 32], iv: &[u8; 16], ciphertext: &[u8]) -> [u8; 32]`: + Creates HmacSha256, updates with iv then ciphertext. Returns `finalize().into_bytes().into()`. + HMAC input = IV (16 bytes) || ciphertext (encrypted_size bytes). Nothing else. + - `verify_hmac(key: &[u8; 32], iv: &[u8; 16], ciphertext: &[u8], expected: &[u8; 32]) -> bool`: + Creates HmacSha256, updates with iv then ciphertext. Uses `verify_slice(expected)` for constant-time comparison. Returns true on success. + - `sha256_hash(data: &[u8]) -> [u8; 32]`: + Returns `sha2::Sha256::digest(data).into()`. + + CRITICAL: Use `hmac::Mac` trait for `new_from_slice()`, `update()`, `finalize()`, `verify_slice()`. + CRITICAL: encrypted_size formula: `((input_len / 16) + 1) * 16` -- PKCS7 ALWAYS adds at least 1 byte. + +3. Create `src/compression.rs`: + + Functions: + - `compress(data: &[u8]) -> anyhow::Result>`: + Uses `flate2::write::GzEncoder` with `Compression::default()`. + IMPORTANT: Use `GzBuilder::new().mtime(0)` to zero the gzip timestamp for reproducible output in tests. + Writes all data, finishes encoder, returns compressed bytes. + - `decompress(data: &[u8]) -> anyhow::Result>`: + Uses `flate2::read::GzDecoder`. Reads all bytes to a Vec. + - `should_compress(filename: &str, no_compress_list: &[String]) -> bool`: + Returns false if filename matches any entry in no_compress_list (by suffix or exact match). + Returns false for known compressed extensions: apk, zip, gz, bz2, xz, zst, png, jpg, jpeg, gif, webp, mp4, mp3, aac, ogg, flac, 7z, rar, jar. + Returns true otherwise. + Uses `filename.rsplit('.').next()` for extension extraction. + +IMPORTANT: Use Context7 to verify the `aes`, `cbc`, `hmac`, `sha2`, `flate2`, and `rand` crate APIs before writing. Resolve library IDs and query docs for encrypt/decrypt patterns, HMAC usage, and GzEncoder/GzDecoder usage. + + + cd /home/nick/Projects/Rust/encrypted_archive && cargo build 2>&1 && cargo run -- --help 2>&1 + Review format.rs field order against FORMAT.md sections 4 and 5 to confirm byte-level match + run after this task commits + + + - cargo build succeeds with no errors + - format.rs exports Header, TocEntry, MAGIC, VERSION, HEADER_SIZE, and all read/write functions + - crypto.rs exports encrypt_data, decrypt_data, compute_hmac, verify_hmac, sha256_hash, generate_iv + - compression.rs exports compress, decompress, should_compress + - Header serialization writes exactly 40 bytes with correct field order per FORMAT.md Section 4 + - TocEntry serialization writes exactly (101 + name_length) bytes per FORMAT.md Section 5 + - All multi-byte integers use little-endian encoding + - encrypt_data output size matches formula: ((input_len / 16) + 1) * 16 + + + + + + +- `cargo build` succeeds with zero errors +- `cargo run -- --help` shows pack, unpack, inspect subcommands +- All 7 source files exist: main.rs, cli.rs, key.rs, format.rs, crypto.rs, compression.rs, archive.rs +- format.rs Header struct has fields matching FORMAT.md Section 4 (magic, version, flags, file_count, toc_offset, toc_size, toc_iv, reserved) +- format.rs TocEntry struct has fields matching FORMAT.md Section 5 (name, original_size, compressed_size, encrypted_size, data_offset, iv, hmac, sha256, compression_flag, padding_after) +- crypto.rs uses `cbc::Encryptor` (NOT deprecated block-modes) +- crypto.rs uses `rand::rng()` (NOT thread_rng) +- crypto.rs HMAC input is IV || ciphertext only +- compression.rs uses `GzBuilder::new().mtime(0)` for reproducible gzip + + + +A compiling Rust project with complete module structure where every building-block operation (format read/write, encrypt/decrypt, HMAC compute/verify, SHA-256 hash, compress/decompress) is implemented and ready for the pack/unpack/inspect commands to orchestrate. + + + +After completion, create `.planning/phases/02-core-archiver/02-01-SUMMARY.md` + diff --git a/.planning/phases/02-core-archiver/02-02-PLAN.md b/.planning/phases/02-core-archiver/02-02-PLAN.md new file mode 100644 index 0000000..a36198e --- /dev/null +++ b/.planning/phases/02-core-archiver/02-02-PLAN.md @@ -0,0 +1,225 @@ +--- +phase: 02-core-archiver +plan: 02 +type: execute +wave: 2 +depends_on: ["02-01"] +files_modified: + - src/archive.rs + - src/main.rs +autonomous: true +requirements: [CLI-02, CLI-03] + +must_haves: + truths: + - "Running `encrypted_archive pack file1 file2 -o out.bin` produces a valid archive file" + - "The output archive file starts with magic bytes 0x00 0xEA 0x72 0x63" + - "Running `encrypted_archive inspect out.bin` shows file count, names, and sizes without decryption" + - "Running `encrypted_archive unpack out.bin -o outdir/` extracts files identical to originals" + - "Each file in the archive has a unique random IV (no IV reuse)" + - "HMAC is verified before decryption during unpack (reject tampered files)" + - "SHA-256 is verified after decompression during unpack (detect corruption)" + - "The output file is not recognized by `file` command (no standard signatures)" + - "Already-compressed files (APK) are stored without gzip compression" + artifacts: + - path: "src/archive.rs" + provides: "pack(), unpack(), inspect() orchestration functions" + exports: ["pack", "unpack", "inspect"] + min_lines: 150 + - path: "src/main.rs" + provides: "CLI dispatch wiring commands to archive functions" + contains: "archive::pack" + key_links: + - from: "src/archive.rs" + to: "src/format.rs" + via: "writes header and TOC entries using format module" + pattern: "format::write_header|format::write_toc_entry|format::read_header" + - from: "src/archive.rs" + to: "src/crypto.rs" + via: "encrypts/decrypts file data and computes/verifies HMAC" + pattern: "crypto::encrypt_data|crypto::decrypt_data|crypto::compute_hmac|crypto::verify_hmac" + - from: "src/archive.rs" + to: "src/compression.rs" + via: "compresses/decompresses file data based on should_compress" + pattern: "compression::compress|compression::decompress|compression::should_compress" + - from: "src/main.rs" + to: "src/archive.rs" + via: "dispatches CLI commands to archive functions" + pattern: "archive::pack|archive::unpack|archive::inspect" +--- + + +Implement the three archive commands (pack, unpack, inspect) and wire them to the CLI, producing a fully functional encrypted archive tool. + +Purpose: This is the core deliverable of Phase 2 -- the working archiver. Pack produces archives conforming to FORMAT.md, inspect reads metadata without decryption, and unpack extracts files with full HMAC and SHA-256 verification. + +Output: A working `encrypted_archive` binary with pack, unpack, and inspect commands that produce and consume archives matching the FORMAT.md specification. + + + +@/home/nick/.claude/get-shit-done/workflows/execute-plan.md +@/home/nick/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/02-core-archiver/02-RESEARCH.md +@.planning/phases/02-core-archiver/02-01-SUMMARY.md +@docs/FORMAT.md + + + + + + Task 1: Implement pack and inspect commands + src/archive.rs, src/main.rs + +1. Implement `pack()` in `src/archive.rs`: + + Signature: `pub fn pack(files: &[PathBuf], output: &Path, no_compress: &[String]) -> anyhow::Result<()>` + + Algorithm (two-pass archive writing per research Pattern 2): + + **Pass 1 -- Process all files:** + For each input file: + a. Read entire file into memory (per Out of Scope: no streaming). + b. Validate file size <= u32::MAX. If exceeded, return error: "File too large: {name} ({size} bytes exceeds 4 GB limit)". + c. Compute SHA-256 of original data: `crypto::sha256_hash(&data)`. + d. Determine compression: `compression::should_compress(&filename, no_compress)`. + e. If compressing: `compressed = compression::compress(&data)?`. Else: `compressed = data.clone()`, set compression_flag = 0. + f. Generate random IV: `crypto::generate_iv()`. + g. Encrypt: `ciphertext = crypto::encrypt_data(&compressed, &KEY, &iv)`. + h. Compute HMAC: `hmac = crypto::compute_hmac(&KEY, &iv, &ciphertext)`. + i. Store results in a `ProcessedFile` struct (or equivalent) with: name, original_size, compressed_size, encrypted_size, iv, hmac, sha256, compression_flag, ciphertext. + + **Pass 2 -- Compute offsets and write archive:** + a. Compute TOC size: sum of (101 + name.len()) for each file. + b. Set toc_offset = HEADER_SIZE (40). + c. Compute data offsets: + - First file: data_offset = toc_offset + toc_size + - Each subsequent: previous data_offset + previous encrypted_size + d. Determine flags byte: if ANY file has compression_flag == 1, set bit 0 (0x01). Bits 1-3 are 0 (no obfuscation in Phase 2). Bits 4-7 MUST be 0. + e. Create Header: version=1, flags, file_count, toc_offset, toc_size, toc_iv=[0u8;16], reserved=[0u8;8]. + f. Open output file for writing. + g. Write header using `format::write_header()`. + h. Write TOC entries using `format::write_toc_entry()` for each file (with computed data_offset). + i. Write data blocks: for each file, write ciphertext bytes. + j. Print summary: "Packed {N} files into {output} ({total_bytes} bytes)". + + Import KEY from `crate::key::KEY`. + +2. Implement `inspect()` in `src/archive.rs`: + + Signature: `pub fn inspect(archive: &Path) -> anyhow::Result<()>` + + Algorithm: + a. Open archive file for reading. + b. Read header using `format::read_header()`. This validates magic, version, flags. + c. Read TOC entries using `format::read_toc()`. + d. Print header info: + ``` + Archive: {filename} + Version: {version} + Flags: 0x{flags:02X} + Files: {file_count} + TOC offset: {toc_offset} + TOC size: {toc_size} + ``` + e. For each file entry, print: + ``` + [{i}] {name} + Original: {original_size} bytes + Compressed: {compressed_size} bytes + Encrypted: {encrypted_size} bytes + Offset: {data_offset} + Compression: {yes/no} + IV: {hex} + HMAC: {hex} + SHA-256: {hex} + ``` + f. Print total: "Total original size: {sum} bytes". + +3. Implement `unpack()` in `src/archive.rs`: + + Signature: `pub fn unpack(archive: &Path, output_dir: &Path) -> anyhow::Result<()>` + + Algorithm (follows FORMAT.md Section 10 decode order): + a. Open archive file for reading. + b. Read header using `format::read_header()`. Validates magic, version, flags. + c. Read TOC entries using `format::read_toc()`. + d. Create output directory if it doesn't exist. + e. Track error count for per-file failures. + f. For each file entry: + - Seek to data_offset. Read encrypted_size bytes (ciphertext). + - **Verify HMAC FIRST**: `crypto::verify_hmac(&KEY, &entry.iv, &ciphertext, &entry.hmac)`. + If HMAC fails: print error "HMAC verification failed for {name}, skipping", increment error count, continue to next file. + - Decrypt: `decrypted = crypto::decrypt_data(&ciphertext, &KEY, &entry.iv)?`. + - Decompress if compression_flag == 1: `decompressed = compression::decompress(&decrypted)?`. + Else: `decompressed = decrypted`. + - **Verify SHA-256**: Compare `crypto::sha256_hash(&decompressed)` with `entry.sha256`. + If mismatch: print warning "SHA-256 mismatch for {name} (data may be corrupted)", increment error count. Still write the file. + - Create parent directories if name contains `/`. + - Write decompressed data to `output_dir/entry.name`. + - Print "Extracted: {name} ({original_size} bytes)". + g. Print summary: "Extracted {success_count}/{file_count} files". + h. If error_count > 0: return Err with message "{error_count} file(s) had verification errors". + + Per research open question #2: Abort on header/TOC errors. For per-file errors (HMAC/SHA-256), report and continue with non-zero exit code. + +4. Wire up `src/main.rs`: + Replace placeholder functions with actual calls: + ```rust + match cli.command { + Commands::Pack { files, output, no_compress } => { + archive::pack(&files, &output, &no_compress)?; + } + Commands::Unpack { archive, output_dir } => { + archive::unpack(&archive, &output_dir)?; + } + Commands::Inspect { archive } => { + archive::inspect(&archive)?; + } + } + ``` + +IMPORTANT: Use `std::io::Seek` and `SeekFrom::Start(offset as u64)` for seeking to data_offset during unpack. +IMPORTANT: Use `std::fs::create_dir_all()` for creating output directories. +IMPORTANT: filename from TocEntry may contain path separators -- sanitize to prevent directory traversal (reject names starting with `/` or containing `..`). + + + cd /home/nick/Projects/Rust/encrypted_archive && cargo build 2>&1 && echo "--- Build OK ---" && echo "Hello, World!" > /tmp/ea_test_hello.txt && printf '\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10' > /tmp/ea_test_binary.bin && cargo run -- pack /tmp/ea_test_hello.txt /tmp/ea_test_binary.bin -o /tmp/ea_test_archive.bin 2>&1 && echo "--- Pack OK ---" && cargo run -- inspect /tmp/ea_test_archive.bin 2>&1 && echo "--- Inspect OK ---" && mkdir -p /tmp/ea_test_out && cargo run -- unpack /tmp/ea_test_archive.bin -o /tmp/ea_test_out 2>&1 && echo "--- Unpack OK ---" && diff /tmp/ea_test_hello.txt /tmp/ea_test_out/ea_test_hello.txt && diff /tmp/ea_test_binary.bin /tmp/ea_test_out/ea_test_binary.bin && echo "--- Round-trip OK: files identical ---" && file /tmp/ea_test_archive.bin 2>&1 && echo "--- file command check (should say 'data' not a known format) ---" && rm -rf /tmp/ea_test_* + Verify inspect output shows correct file metadata, verify round-trip produces identical files + run after this task commits, before declaring plan complete + + + - `encrypted_archive pack` accepts multiple input files and produces a single archive + - `encrypted_archive inspect` displays file metadata (names, sizes, offsets, IVs, HMACs, SHA-256) without decryption + - `encrypted_archive unpack` extracts all files, verifying HMAC before decryption and SHA-256 after decompression + - Round-trip test: pack 2 files, unpack, diff shows files are byte-identical + - Archive file is not recognized by `file` command (shows "data" not a known format) + - HMAC verification happens before decryption (encrypt-then-MAC correctly implemented) + - Files with compressed extensions (apk, zip, etc.) are stored without gzip compression + - Error handling: HMAC failure skips file and continues, SHA-256 mismatch warns but still writes + + + + + + +- `cargo build` succeeds +- Pack 2 test files (text + binary), unpack to different directory, diff shows byte-identical +- `file archive.bin` does NOT show a recognized format (should show "data") +- `inspect` shows correct file count, names, sizes, and hex-encoded IVs/HMACs/SHA-256 +- Compression auto-detection: `.apk` file should have compression_flag=0 in inspect output +- Unpack with tampered archive (flip a byte in ciphertext) should report HMAC failure but continue + + + +A fully functional `encrypted_archive` binary where `pack` creates archives matching FORMAT.md, `inspect` displays metadata, and `unpack` extracts with full HMAC and SHA-256 verification. Round-trip fidelity: packed files are byte-identical after unpacking. + + + +After completion, create `.planning/phases/02-core-archiver/02-02-SUMMARY.md` +