Files
2026-02-24 23:49:46 +03:00

14 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
02-core-archiver 01 execute 1
Cargo.toml
src/main.rs
src/cli.rs
src/key.rs
src/format.rs
src/crypto.rs
src/compression.rs
true
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
truths artifacts key_links
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
path provides contains
Cargo.toml Project manifest with all dependencies aes
path provides contains
src/main.rs CLI entry point with clap dispatch clap
path provides exports
src/cli.rs Clap derive structs for Pack/Unpack/Inspect subcommands
Cli
Commands
path provides exports
src/key.rs Hardcoded 32-byte encryption key
KEY
path provides exports
src/format.rs Header and TocEntry structs with serialize/deserialize
Header
TocEntry
MAGIC
VERSION
path provides exports
src/crypto.rs AES-256-CBC encrypt/decrypt, HMAC-SHA-256, SHA-256
encrypt_data
decrypt_data
compute_hmac
verify_hmac
sha256_hash
path provides exports
src/compression.rs Gzip compress/decompress and should_compress heuristic
compress
decompress
should_compress
from to via pattern
src/crypto.rs src/key.rs imports KEY constant use crate::key::KEY
from to via pattern
src/format.rs docs/FORMAT.md byte-for-byte field layout match 0x00.*0xEA.*0x72.*0x63
from to via pattern
src/main.rs src/cli.rs clap parse and dispatch 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.

<execution_context> @/home/nick/.claude/get-shit-done/workflows/execute-plan.md @/home/nick/.claude/get-shit-done/templates/summary.md </execution_context>

@.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.
  1. Set up Cargo.toml with exact dependency versions from research:

    [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"
    
  2. 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<PathBuf>, output: PathBuf, no_compress: Vec<String> }
      • 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)
  3. Create src/key.rs with the hardcoded 32-byte key:

    /// 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).

  4. 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 <sampling_rate>run after this task commits</sampling_rate> - 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:

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<Header>: 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<TocEntry>: 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<Vec<TocEntry>>: 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.

  1. Create src/crypto.rs implementing the encryption pipeline:

    Type aliases:

    use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
    type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
    type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
    type HmacSha256 = hmac::Hmac<sha2::Sha256>;
    

    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<u8>: 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::<Pkcs7>(&mut buf, plaintext.len()). Returns the buffer (full encrypted_size bytes).
    • decrypt_data(ciphertext: &[u8], key: &[u8; 32], iv: &[u8; 16]) -> anyhow::Result<Vec<u8>>: Allocates mutable buffer from ciphertext. Calls Aes256CbcDec::new(key.into(), iv.into()).decrypt_padded_mut::<Pkcs7>(&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.

  2. Create src/compression.rs:

    Functions:

    • compress(data: &[u8]) -> anyhow::Result<Vec<u8>>: 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<Vec<u8>>: 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 <sampling_rate>run after this task commits</sampling_rate> - 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

<success_criteria> 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. </success_criteria>

After completion, create `.planning/phases/02-core-archiver/02-01-SUMMARY.md`