16 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 12-user-key-input | 01 | execute | 1 |
|
true |
|
|
Purpose: Remove the hardcoded key dependency so the archive tool is parameterized by user input, which is the foundation for all three key input methods.
Output: Working --key and --key-file support with all existing tests passing via explicit key args.
<execution_context> @/home/nick/.claude/get-shit-done/workflows/execute-plan.md @/home/nick/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/ROADMAP.md @.planning/STATE.md @.planning/REQUIREMENTS.mdFrom src/key.rs (CURRENT -- will be replaced):
pub const KEY: [u8; 32] = [ ... ];
From src/cli.rs (CURRENT -- will be extended):
#[derive(Parser)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
Pack { files: Vec<PathBuf>, output: PathBuf, no_compress: Vec<String> },
Unpack { archive: PathBuf, output_dir: PathBuf },
Inspect { archive: PathBuf },
}
From src/archive.rs (CURRENT signatures -- will add key param):
pub fn pack(files: &[PathBuf], output: &Path, no_compress: &[String]) -> anyhow::Result<()>
pub fn unpack(archive: &Path, output_dir: &Path) -> anyhow::Result<()>
pub fn inspect(archive: &Path) -> anyhow::Result<()>
From src/crypto.rs (unchanged -- already takes key as param):
pub fn encrypt_data(plaintext: &[u8], key: &[u8; 32], iv: &[u8; 16]) -> Vec<u8>
pub fn decrypt_data(ciphertext: &[u8], key: &[u8; 32], iv: &[u8; 16]) -> anyhow::Result<Vec<u8>>
pub fn compute_hmac(key: &[u8; 32], iv: &[u8; 16], ciphertext: &[u8]) -> [u8; 32]
pub fn verify_hmac(key: &[u8; 32], iv: &[u8; 16], ciphertext: &[u8], expected: &[u8; 32]) -> bool
Hardcoded KEY hex value (for test migration):
7a35c1d94fe82b6a910df358bc74a61e428fd063e5179b2cfa8406cd3e79b550
-
Cargo.toml: Add
hex = "0.4"dependency (for hex decoding of --key arg). Verify version:cargo search hex --limit 1. -
src/cli.rs: Add key source arguments as a clap arg group on the top-level
Clistruct (NOT on each subcommand -- the key applies globally to all commands):
use clap::{Parser, Subcommand, Args};
#[derive(Args, Clone)]
#[group(required = false, multiple = false)]
pub struct KeyArgs {
/// Raw 32-byte key as 64-character hex string
#[arg(long, value_name = "HEX")]
pub key: Option<String>,
/// Path to file containing raw 32-byte key
#[arg(long, value_name = "PATH")]
pub key_file: Option<PathBuf>,
/// Password for key derivation (interactive prompt if no value given)
#[arg(long, value_name = "PASSWORD")]
pub password: Option<Option<String>>,
}
#[derive(Parser)]
#[command(name = "encrypted_archive")]
#[command(about = "Custom encrypted archive tool")]
pub struct Cli {
#[command(flatten)]
pub key_args: KeyArgs,
#[command(subcommand)]
pub command: Commands,
}
Note: password uses Option<Option<String>> so that --password with no value gives Some(None) (interactive prompt) and --password mypass gives Some(Some("mypass")). The group is required = false because inspect does not require a key (it only reads TOC metadata). pack and unpack will enforce key presence in main.rs.
- src/key.rs: Replace the hardcoded KEY constant with key resolution functions. Keep the old KEY constant available as
LEGACY_KEYfor golden tests only:
use std::path::Path;
/// Legacy hardcoded key (used only in golden test vectors).
/// Do NOT use in production code.
#[cfg(test)]
pub const LEGACY_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,
];
/// Resolved key source for the archive operation.
pub enum KeySource {
Hex(String),
File(std::path::PathBuf),
Password(Option<String>), // None = interactive prompt
}
/// Resolve a KeySource into a 32-byte AES-256 key.
///
/// For Hex: decode 64-char hex string into [u8; 32].
/// For File: read exactly 32 bytes from file.
/// For Password: placeholder that returns error (implemented in Plan 02).
pub fn resolve_key(source: &KeySource) -> anyhow::Result<[u8; 32]> {
match source {
KeySource::Hex(hex_str) => {
let bytes = hex::decode(hex_str)
.map_err(|e| anyhow::anyhow!("Invalid hex key: {}", e))?;
anyhow::ensure!(
bytes.len() == 32,
"Key must be exactly 32 bytes (64 hex chars), got {} bytes ({} hex chars)",
bytes.len(),
hex_str.len()
);
let mut key = [0u8; 32];
key.copy_from_slice(&bytes);
Ok(key)
}
KeySource::File(path) => {
let bytes = std::fs::read(path)
.map_err(|e| anyhow::anyhow!("Failed to read key file '{}': {}", path.display(), e))?;
anyhow::ensure!(
bytes.len() == 32,
"Key file must be exactly 32 bytes, got {} bytes: {}",
bytes.len(),
path.display()
);
let mut key = [0u8; 32];
key.copy_from_slice(&bytes);
Ok(key)
}
KeySource::Password(_) => {
anyhow::bail!("Password-based key derivation not yet implemented (coming in Plan 02)")
}
}
}
-
src/archive.rs: Refactor all three public functions to accept a
keyparameter:pub fn pack(files: &[PathBuf], output: &Path, no_compress: &[String], key: &[u8; 32])pub fn unpack(archive: &Path, output_dir: &Path, key: &[u8; 32])pub fn inspect(archive: &Path, key: Option<&[u8; 32]>)-- key is optional for inspect (KEY-07)- Remove
use crate::key::KEY;import - Change
read_archive_metadatato acceptkey: Option<&[u8; 32]>parameter - Update
process_fileto acceptkey: &[u8; 32]parameter - Replace all
&KEYreferences with the passed-inkeyparameter - For
inspectwhen key isNone: read and display header fields (version, flags, file_count, toc_offset, whether salt/KDF is present) WITHOUT attempting TOC decryption. If the TOC is encrypted (flags bit 1), print "TOC is encrypted, provide a key to see entry listing". If the TOC is NOT encrypted, parse and display entries normally. - For
inspectwhen key isSome(k): decrypt TOC and show full entry listing (file names, sizes, compression flags, etc.).
-
src/main.rs: Wire CLI args to key resolution and archive functions. CRITICAL:
inspectmust work WITHOUT a key (KEY-07). Onlypackandunpackrequire a key argument.
use encrypted_archive::key::{KeySource, resolve_key};
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
// Determine key source from CLI args (may be None for inspect)
let key_source = if let Some(hex) = &cli.key_args.key {
Some(KeySource::Hex(hex.clone()))
} else if let Some(path) = &cli.key_args.key_file {
Some(KeySource::File(path.clone()))
} else if let Some(password_opt) = &cli.key_args.password {
Some(KeySource::Password(password_opt.clone()))
} else {
None
};
match cli.command {
Commands::Pack { files, output, no_compress } => {
let source = key_source
.ok_or_else(|| anyhow::anyhow!("One of --key, --key-file, or --password is required for pack"))?;
let key = resolve_key(&source)?;
archive::pack(&files, &output, &no_compress, &key)?;
}
Commands::Unpack { archive: arch, output_dir } => {
let source = key_source
.ok_or_else(|| anyhow::anyhow!("One of --key, --key-file, or --password is required for unpack"))?;
let key = resolve_key(&source)?;
archive::unpack(&arch, &output_dir, &key)?;
}
Commands::Inspect { archive: arch } => {
// Inspect works without a key (shows header metadata only).
// With a key, it also decrypts and shows the TOC entry listing.
let key = key_source
.map(|s| resolve_key(&s))
.transpose()?;
archive::inspect(&arch, key.as_ref())?;
}
}
Ok(())
}
- Verify build compiles: Run
cargo buildto confirm all wiring is correct before moving to tests. cd /home/nick/Projects/Rust/encrypted_archive && cargo build 2>&1cargo buildsucceeds with no errors- archive.rs no longer imports KEY from key.rs
- All three archive functions accept a key parameter
- CLI accepts --key, --key-file, --password as mutually exclusive args
- main.rs resolves key source and threads it to archive functions
-
src/crypto.rs tests: Replace
use crate::key::KEY;with a local constant:#[cfg(test)] mod tests { use super::*; use hex_literal::hex; /// Test key matching legacy hardcoded value const TEST_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, ]; // Replace all &KEY with &TEST_KEY in existing tests -
tests/round_trip.rs: All CLI tests now need
--key <hex>argument. Define a constant at the top:/// Hex-encoded 32-byte key for test archives (matches legacy hardcoded key) const TEST_KEY_HEX: &str = "7a35c1d94fe82b6a910df358bc74a61e428fd063e5179b2cfa8406cd3e79b550";Then update the
cmd()helper or each test to pass--keybefore the subcommand:fn cmd_with_key() -> Command { let mut c = Command::new(assert_cmd::cargo::cargo_bin!("encrypted_archive")); c.args(["--key", TEST_KEY_HEX]); c }Replace all
cmd()calls withcmd_with_key()in existing tests. This ensures all pack/unpack/inspect invocations pass the key.IMPORTANT: The
--keyarg is on the top-level CLI struct, so it goes BEFORE the subcommand:encrypted_archive --key <hex> pack ... -
Add new tests in tests/round_trip.rs:
test_key_file_roundtrip: Create a 32-byte key file, pack with--key-file, unpack with--key-file, verify byte-identical.test_rejects_wrong_key: Pack with one key, try unpack with different key, expect HMAC failure.test_rejects_bad_hex: Run with--key abcd(too short), expect error.test_rejects_missing_key: Runpack file -o outwithout any key arg, expect error about "required for pack".test_inspect_without_key: Pack with --key, then runinspectWITHOUT any key arg. Should succeed and print header metadata (version, flags, file_count). Should NOT show decrypted TOC entries.test_inspect_with_key: Pack with --key, then runinspect --key <hex>. Should succeed and print both header metadata AND full TOC entry listing.
-
Run full test suite:
cargo test-- all tests must pass. cd /home/nick/Projects/Rust/encrypted_archive && cargo test 2>&1- All existing golden tests pass with local KEY constant
- All existing round_trip tests pass with --key hex argument
- New test: key file round-trip works
- New test: wrong key causes HMAC failure
- New test: bad hex rejected with clear error
- New test: missing key arg rejected with clear error for pack/unpack
- New test: inspect without key shows header metadata only
- New test: inspect with key shows full TOC entry listing
cargo testreports 0 failures
<success_criteria>
- Hardcoded KEY constant is no longer used in production code (only in test constants)
--key <HEX>and--key-file <PATH>work for pack/unpack and optionally for inspectinspectworks without any key argument (shows header metadata), and with a key (shows full TOC listing)--passwordis accepted by CLI but returns "not yet implemented" error- All existing tests pass with explicit key arguments
- New tests verify key-file, wrong-key rejection, bad-hex rejection, missing-key rejection </success_criteria>