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 |
|
true |
|
|
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.-
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" -
Create
src/cli.rswith clap derive structs matching the research pattern:Clistruct with#[command(subcommand)]Commandsenum 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)
-
Create
src/key.rswith 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).
-
Create
src/main.rs:- Parse CLI with
Cli::parse() - Match on
Commandsvariants, calling placeholder functions that print "not implemented yet" and returnOk(()) - Use
anyhow::Result<()>as the return type for main - Declare modules:
mod cli; mod key; mod format; mod crypto; mod compression; mod archive;
- Parse CLI with
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
Constants:
pub const MAGIC: [u8; 4] = [0x00, 0xEA, 0x72, 0x63];
pub const VERSION: u8 = 1;
pub const HEADER_SIZE: u32 = 40;
Structs:
Headerwith fields: version (u8), flags (u8), file_count (u16), toc_offset (u32), toc_size (u32), toc_iv ([u8; 16]), reserved ([u8; 8])TocEntrywith 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. Usesfrom_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: Returns101 + entry.name.len() as u32compute_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.
-
Create
src/crypto.rsimplementing 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]: Usesrand::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. CallsAes256CbcEnc::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. CallsAes256CbcDec::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. Returnsfinalize().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. Usesverify_slice(expected)for constant-time comparison. Returns true on success.sha256_hash(data: &[u8]) -> [u8; 32]: Returnssha2::Sha256::digest(data).into().
CRITICAL: Use
hmac::Mactrait fornew_from_slice(),update(),finalize(),verify_slice(). CRITICAL: encrypted_size formula:((input_len / 16) + 1) * 16-- PKCS7 ALWAYS adds at least 1 byte. -
Create
src/compression.rs:Functions:
compress(data: &[u8]) -> anyhow::Result<Vec<u8>>: Usesflate2::write::GzEncoderwithCompression::default(). IMPORTANT: UseGzBuilder::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>>: Usesflate2::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. Usesfilename.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
<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`