diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index a3a9dc0..ff3e481 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -103,10 +103,11 @@ Plans:
2. All headers are XOR-obfuscated with a fixed key -- no recognizable structure patterns in first 256 bytes
3. Random decoy padding exists between data blocks -- file boundaries are not detectable by size analysis
4. All three decoders (Rust, Kotlin, Shell) still produce byte-identical output after obfuscation is applied
-**Plans**: TBD
+**Plans**: 2 plans
Plans:
-- [ ] 06-01: TBD
+- [ ] 06-01-PLAN.md -- Rust archiver/unpacker obfuscation (XOR header + encrypted TOC + decoy padding + updated tests)
+- [ ] 06-02-PLAN.md -- Kotlin and Shell decoder obfuscation support + cross-validation tests
## Progress
@@ -120,4 +121,4 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6
| 3. Round-Trip Verification | 2/2 | Complete | 2026-02-24 |
| 4. Kotlin Decoder | 1/1 | Complete | 2026-02-24 |
| 5. Shell Decoder | 2/2 | Complete | 2026-02-24 |
-| 6. Obfuscation Hardening | 0/1 | Not started | - |
+| 6. Obfuscation Hardening | 0/2 | Not started | - |
diff --git a/.planning/phases/06-obfuscation-hardening/06-01-PLAN.md b/.planning/phases/06-obfuscation-hardening/06-01-PLAN.md
new file mode 100644
index 0000000..7d22bef
--- /dev/null
+++ b/.planning/phases/06-obfuscation-hardening/06-01-PLAN.md
@@ -0,0 +1,186 @@
+---
+phase: 06-obfuscation-hardening
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - src/format.rs
+ - src/archive.rs
+ - src/crypto.rs
+ - tests/golden_vectors.rs
+autonomous: true
+requirements:
+ - FMT-06
+ - FMT-07
+ - FMT-08
+
+must_haves:
+ truths:
+ - "Rust archiver pack() produces archives with XOR-obfuscated headers (magic bytes not visible in raw hex)"
+ - "Rust archiver pack() encrypts the TOC with AES-256-CBC using a random toc_iv stored in header"
+ - "Rust archiver pack() inserts random decoy padding between data blocks"
+ - "Rust unpack() and inspect() correctly decode obfuscated archives (XOR de-obfuscation + TOC decryption)"
+ - "All existing cargo test pass (unit tests + integration tests + golden vectors)"
+ - "Flags byte is 0x0F when compression + all 3 obfuscation features are active"
+ artifacts:
+ - path: "src/format.rs"
+ provides: "XOR_KEY constant, xor_header_buf() function, read_header_auto() with XOR bootstrapping"
+ contains: "XOR_KEY"
+ - path: "src/archive.rs"
+ provides: "Updated pack() with TOC encryption + decoy padding + XOR header; updated unpack()/inspect() with de-obfuscation"
+ contains: "xor_header_buf"
+ - path: "src/crypto.rs"
+ provides: "generate_iv (unchanged) used for toc_iv"
+ key_links:
+ - from: "src/archive.rs pack()"
+ to: "src/format.rs xor_header_buf()"
+ via: "XOR applied to 40-byte header buffer after write_header"
+ pattern: "xor_header_buf"
+ - from: "src/archive.rs pack()"
+ to: "src/crypto.rs encrypt_data()"
+ via: "TOC plaintext buffer encrypted with toc_iv"
+ pattern: "encrypt_data.*toc"
+ - from: "src/archive.rs unpack()/inspect()"
+ to: "src/format.rs"
+ via: "XOR bootstrapping on header read, then TOC decryption"
+ pattern: "xor_header_buf|decrypt_data"
+---
+
+
+Implement all three obfuscation features (XOR headers, encrypted TOC, decoy padding) in the Rust archiver and unpacker, with all existing tests passing.
+
+Purpose: Make the archive format resist casual analysis by hiding the header structure, encrypting all metadata, and inserting random noise between data blocks. This is the encoder-side implementation that the Kotlin and Shell decoders will build against.
+
+Output: Updated src/format.rs, src/archive.rs with full obfuscation pipeline. All `cargo test` pass including existing unit, golden vector, and round-trip integration tests.
+
+
+
+@/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/06-obfuscation-hardening/06-RESEARCH.md
+@docs/FORMAT.md (Sections 9.1-9.3 and Section 10 for decode order)
+@src/format.rs
+@src/crypto.rs
+@src/archive.rs
+@src/key.rs
+
+
+
+
+
+ Task 1: Add XOR header obfuscation and TOC encryption to format.rs
+ src/format.rs
+
+Add the following to format.rs:
+
+1. **XOR_KEY constant** (FORMAT.md Section 9.1):
+```rust
+pub const XOR_KEY: [u8; 8] = [0xA5, 0x3C, 0x96, 0x0F, 0xE1, 0x7B, 0x4D, 0xC8];
+```
+
+2. **xor_header_buf()** function that XORs a mutable byte slice (first 40 bytes) with the cyclic 8-byte key. XOR is its own inverse, so the same function encodes and decodes.
+
+3. **read_header_auto()** function (replaces or wraps read_header for external use):
+ - Read 40 raw bytes.
+ - Check bytes 0-3 against MAGIC.
+ - If match: parse header normally from the buffer.
+ - If NO match: apply xor_header_buf to all 40 bytes, re-check magic. If still wrong, return error.
+ - Parse header fields from the (possibly de-XORed) buffer.
+ - This function should accept `&mut impl (Read + Seek)` or work from a `[u8; 40]` buffer passed in. The simplest approach: accept a `[u8; 40]` buffer and return a Header (factoring out the parsing from read_header into a parse_header_from_buf helper).
+
+4. **write_header_to_buf()** helper that serializes header to a `[u8; 40]` buffer (instead of directly to writer), so the caller can XOR it before writing.
+
+5. **write_toc_entry_to_vec() / serialize_toc()** helper that serializes all TOC entries to a `Vec` buffer, so the caller can encrypt the buffer. This can reuse write_toc_entry with a Vec writer.
+
+6. **read_toc_from_buf()** helper that parses TOC entries from a byte slice (using a Cursor), so the caller can pass in the decrypted TOC buffer.
+
+Keep the existing read_header() and write_header() functions for backward compatibility with existing tests, but the new pack/unpack code will use the _buf variants.
+
+Add unit tests:
+- XOR round-trip: write header to buf, XOR, XOR again, verify identical to original.
+- XOR changes magic: write header to buf, XOR, verify bytes 0-3 are NOT 0x00 0xEA 0x72 0x63.
+- read_header_auto works with both plain and XOR'd headers.
+
+
+ cd /home/nick/Projects/Rust/encrypted_archive && cargo test --lib format -- --nocapture 2>&1 | tail -5
+ Verify XOR_KEY constant matches FORMAT.md Section 9.1 exactly
+
+ format.rs has XOR_KEY, xor_header_buf(), read_header_auto() with bootstrapping, and helper functions for buffer-based header/TOC serialization/parsing. All format unit tests pass.
+
+
+
+ Task 2: Update pack/unpack/inspect with full obfuscation pipeline
+ src/archive.rs
+
+Update archive.rs to implement all three obfuscation features. Follow the encoder order from 06-RESEARCH.md:
+
+**pack() changes:**
+
+1. **Generate decoy padding** for each file: `let padding_after: u16 = rng.random_range(64..=4096);` using `rand::Rng`. Generate the random bytes too: `let mut padding_bytes = vec![0u8; padding_after as usize]; rand::Fill::fill(&mut padding_bytes[..], &mut rng);`. Store padding_after and padding_bytes in ProcessedFile struct (add fields).
+
+2. **Compute data offsets accounting for padding**: After computing toc_offset + toc_size (which will now be the ENCRYPTED toc size), compute data offsets as `current_offset += pf.encrypted_size + pf.padding_after as u32` for each file.
+
+3. **Serialize TOC entries to a buffer**: Use the new serialize_toc helper. Include padding_after values in entries.
+
+4. **Encrypt serialized TOC**: Generate `toc_iv = crypto::generate_iv()`. Call `crypto::encrypt_data(&toc_plaintext, &KEY, &toc_iv)`. The `toc_size` in the header becomes `encrypted_toc.len() as u32`.
+
+5. **Build header**: Set flags bits 1-3 in addition to bit 0 (compression). When all obfuscation is active and files are compressed, flags = 0x0F. Set toc_iv in header.
+
+6. **Compute toc_offset and data offsets**: `toc_offset = HEADER_SIZE`. Data block start = `toc_offset + encrypted_toc_size`. Then compute per-file data_offset accounting for preceding files' `encrypted_size + padding_after`.
+
+7. **Serialize header to buffer and XOR**: Use write_header_to_buf, then xor_header_buf on the resulting 40-byte buffer.
+
+8. **Write archive**: XOR'd header bytes || encrypted TOC bytes || (for each file: ciphertext || padding_bytes).
+
+**unpack() changes:**
+
+1. Read 40 bytes raw. Use read_header_auto (XOR bootstrapping).
+2. Check flags bit 1 (0x02) for TOC encryption. If set: seek to toc_offset, read toc_size bytes, decrypt with `crypto::decrypt_data(&encrypted_toc, &KEY, &header.toc_iv)`. Parse TOC from decrypted buffer using read_toc_from_buf.
+3. If TOC not encrypted (backward compat): read TOC directly as before.
+4. Rest of unpack is unchanged -- each file uses data_offset from TOC entries, which already accounts for padding.
+
+**inspect() changes:**
+
+Apply the same header and TOC de-obfuscation as unpack. Factor out a shared `read_archive_metadata()` helper that returns (Header, Vec) with all de-obfuscation applied. Both unpack() and inspect() call this helper.
+
+**Important notes:**
+- Use `use rand::Rng;` for `random_range()`.
+- Padding range 64..=4096 bytes per file.
+- The `--no-compress` flag behavior is unchanged.
+- Do NOT add a `--no-obfuscate` flag yet (always obfuscate).
+
+
+ cd /home/nick/Projects/Rust/encrypted_archive && cargo test 2>&1 | tail -10
+ Run `cargo run -- pack test_file.txt -o /tmp/test.bin && xxd /tmp/test.bin | head -3` and verify first 4 bytes are NOT 00 ea 72 63
+
+ pack() produces fully obfuscated archives (XOR header + encrypted TOC + decoy padding). unpack() and inspect() correctly de-obfuscate. All `cargo test` pass including existing integration tests and round-trip tests (which now exercise the full obfuscation pipeline end-to-end).
+
+
+
+
+
+1. `cargo test` -- all existing unit, golden, and integration tests pass
+2. `cargo run -- pack -o /tmp/obf.bin` produces an archive where `xxd /tmp/obf.bin | head -3` shows no recognizable magic bytes
+3. `cargo run -- inspect /tmp/obf.bin` correctly displays metadata after de-obfuscation
+4. `cargo run -- unpack /tmp/obf.bin -o /tmp/obf_out/` extracts files byte-identically to originals
+5. `binwalk /tmp/obf.bin` and `file /tmp/obf.bin` show no recognized signatures
+
+
+
+- All three obfuscation features (FMT-06, FMT-07, FMT-08) are implemented in Rust archiver
+- Flags byte is 0x0F for archives with compression + all obfuscation
+- XOR bootstrapping allows decoders to detect both plain and obfuscated archives
+- All `cargo test` pass (0 failures)
+- Archives are unrecognizable by file/binwalk/strings
+
+
+
diff --git a/.planning/phases/06-obfuscation-hardening/06-02-PLAN.md b/.planning/phases/06-obfuscation-hardening/06-02-PLAN.md
new file mode 100644
index 0000000..4c24baa
--- /dev/null
+++ b/.planning/phases/06-obfuscation-hardening/06-02-PLAN.md
@@ -0,0 +1,291 @@
+---
+phase: 06-obfuscation-hardening
+plan: 02
+type: execute
+wave: 2
+depends_on:
+ - "06-01"
+files_modified:
+ - kotlin/ArchiveDecoder.kt
+ - shell/decode.sh
+ - kotlin/test_decoder.sh
+ - shell/test_decoder.sh
+autonomous: true
+requirements:
+ - FMT-06
+ - FMT-07
+ - FMT-08
+
+must_haves:
+ truths:
+ - "Kotlin decoder extracts files from obfuscated archives (XOR header + encrypted TOC + decoy padding) producing byte-identical output"
+ - "Shell decoder extracts files from obfuscated archives producing byte-identical output"
+ - "All 6 Kotlin cross-validation tests pass (Rust pack with obfuscation -> Kotlin decode -> SHA-256 match)"
+ - "All 6 Shell cross-validation tests pass (Rust pack with obfuscation -> Shell decode -> SHA-256 match)"
+ - "Both decoders handle XOR bootstrapping (check magic, if mismatch XOR 40 bytes and re-check)"
+ - "Both decoders decrypt encrypted TOC before parsing entries when flags bit 1 is set"
+ artifacts:
+ - path: "kotlin/ArchiveDecoder.kt"
+ provides: "XOR_KEY constant, xorHeader() function, TOC decryption, updated decode() with obfuscation support"
+ contains: "XOR_KEY"
+ - path: "shell/decode.sh"
+ provides: "XOR de-obfuscation loop, TOC decryption via openssl, updated TOC parsing from decrypted temp file"
+ contains: "XOR_KEY_HEX"
+ - path: "kotlin/test_decoder.sh"
+ provides: "Cross-validation tests using obfuscated archives"
+ - path: "shell/test_decoder.sh"
+ provides: "Cross-validation tests using obfuscated archives"
+ key_links:
+ - from: "kotlin/ArchiveDecoder.kt decode()"
+ to: "xorHeader()"
+ via: "XOR bootstrapping on header bytes before parseHeader"
+ pattern: "xorHeader"
+ - from: "kotlin/ArchiveDecoder.kt decode()"
+ to: "decryptAesCbc()"
+ via: "Encrypted TOC bytes decrypted with toc_iv before parseToc"
+ pattern: "decryptAesCbc.*toc"
+ - from: "shell/decode.sh"
+ to: "openssl enc -d"
+ via: "Encrypted TOC extracted to temp file, decrypted, then parsed from decrypted file"
+ pattern: "openssl enc.*toc"
+---
+
+
+Update Kotlin and Shell decoders to handle obfuscated archives (XOR header + encrypted TOC + decoy padding) and verify all three decoders produce byte-identical output via cross-validation tests.
+
+Purpose: Complete the obfuscation hardening by ensuring all decoder implementations correctly handle the new format. This is the final piece -- the Rust archiver (Plan 01) produces obfuscated archives, and now all decoders must read them.
+
+Output: Updated ArchiveDecoder.kt and decode.sh with obfuscation support. All cross-validation tests pass.
+
+
+
+@/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/06-obfuscation-hardening/06-RESEARCH.md
+@.planning/phases/06-obfuscation-hardening/06-01-SUMMARY.md
+@docs/FORMAT.md (Sections 9.1-9.3 and Section 10)
+@kotlin/ArchiveDecoder.kt
+@shell/decode.sh
+@kotlin/test_decoder.sh
+@shell/test_decoder.sh
+
+
+
+
+
+ Task 1: Update Kotlin decoder with XOR header + encrypted TOC support
+ kotlin/ArchiveDecoder.kt, kotlin/test_decoder.sh
+
+Update ArchiveDecoder.kt to handle obfuscated archives. Follow the decoder order from FORMAT.md Section 10 and 06-RESEARCH.md patterns.
+
+**Add XOR_KEY constant and xorHeader() function:**
+```kotlin
+val XOR_KEY = byteArrayOf(
+ 0xA5.toByte(), 0x3C, 0x96.toByte(), 0x0F,
+ 0xE1.toByte(), 0x7B, 0x4D, 0xC8.toByte()
+)
+
+fun xorHeader(buf: ByteArray) {
+ for (i in 0 until minOf(buf.size, 40)) {
+ buf[i] = ((buf[i].toInt() and 0xFF) xor (XOR_KEY[i % 8].toInt() and 0xFF)).toByte()
+ }
+}
+```
+Note: MUST use `and 0xFF` on BOTH operands to avoid Kotlin signed byte issues (06-RESEARCH.md Pitfall 4).
+
+**Update decode() function:**
+
+1. **XOR bootstrapping** (after reading 40-byte headerBytes):
+ - Check if first 4 bytes match MAGIC.
+ - If NO match: call `xorHeader(headerBytes)`.
+ - Then call `parseHeader(headerBytes)` (which validates magic).
+
+2. **TOC decryption** (before parsing TOC entries):
+ - After parsing header, check `header.flags and 0x02 != 0` (bit 1 = TOC encrypted).
+ - If set: seek to `header.tocOffset`, read `header.tocSize.toInt()` bytes, decrypt with `decryptAesCbc(encryptedToc, header.tocIv, KEY)`.
+ - Parse TOC from decrypted bytes: `parseToc(decryptedToc, header.fileCount)`.
+ - If NOT set (backward compat): read raw TOC bytes as before and parse directly.
+
+3. **parseToc() adjustment for encrypted TOC:**
+ - Currently parseToc() asserts `pos == data.size`. After TOC encryption, the decrypted buffer may have PKCS7 padding bytes stripped, so the size should match the sum of entry sizes. Keep the assertion -- it validates that the decrypted plaintext is correct.
+
+4. **Decoy padding** requires NO decoder changes -- decoders already use absolute `data_offset` from TOC entries to seek to each file's ciphertext. Padding is naturally skipped.
+
+**Re-run cross-validation tests** (kotlin/test_decoder.sh). The test script already:
+- Builds the Rust archiver (`cargo build --release`)
+- Creates test files, packs with Rust, decodes with Kotlin, compares SHA-256
+- Now the Rust archiver produces obfuscated archives, so the Kotlin decoder must handle them.
+
+No changes needed to test_decoder.sh unless the test script has hardcoded assumptions about archive format. Read it first and verify.
+
+
+ cd /home/nick/Projects/Rust/encrypted_archive && bash kotlin/test_decoder.sh 2>&1 | tail -10
+ Check that kotlin/ArchiveDecoder.kt contains xorHeader function and TOC decryption logic
+
+ Kotlin decoder handles XOR-obfuscated headers, encrypted TOC, and archives with decoy padding. All 6 cross-validation tests pass (Rust pack -> Kotlin decode -> SHA-256 match).
+
+
+
+ Task 2: Update Shell decoder with XOR header + encrypted TOC support
+ shell/decode.sh, shell/test_decoder.sh
+
+Update decode.sh to handle obfuscated archives. This is the most complex change because shell has no native XOR and TOC parsing must switch from reading the archive file to reading a decrypted temp file.
+
+**1. Add XOR de-obfuscation (after reading magic, before parsing header fields):**
+
+Replace the current magic check block (lines ~108-113) with XOR bootstrapping:
+
+```sh
+XOR_KEY_HEX="a53c960fe17b4dc8"
+
+# Read 40-byte header as hex string (80 hex chars)
+raw_header_hex=$(read_hex "$ARCHIVE" 0 40)
+magic_hex=$(printf '%.8s' "$raw_header_hex")
+
+if [ "$magic_hex" != "00ea7263" ]; then
+ # Attempt XOR de-obfuscation
+ header_hex=""
+ byte_idx=0
+ while [ "$byte_idx" -lt 40 ]; do
+ hex_pos=$((byte_idx * 2))
+ # Extract this byte from raw header (2 hex chars)
+ raw_byte=$(printf '%s' "$raw_header_hex" | cut -c$((hex_pos + 1))-$((hex_pos + 2)))
+ # Extract key byte (cyclic)
+ key_pos=$(( (byte_idx % 8) * 2 ))
+ key_byte=$(printf '%s' "$XOR_KEY_HEX" | cut -c$((key_pos + 1))-$((key_pos + 2)))
+ # XOR
+ xored=$(printf '%02x' "$(( 0x$raw_byte ^ 0x$key_byte ))")
+ header_hex="${header_hex}${xored}"
+ byte_idx=$((byte_idx + 1))
+ done
+
+ # Verify magic after XOR
+ magic_hex=$(printf '%.8s' "$header_hex")
+ if [ "$magic_hex" != "00ea7263" ]; then
+ printf 'Invalid archive: bad magic bytes\n' >&2
+ exit 1
+ fi
+else
+ header_hex="$raw_header_hex"
+fi
+
+# Write de-XORed header to temp file for field parsing
+printf '%s' "$header_hex" | xxd -r -p > "$TMPDIR/header.bin"
+```
+
+If xxd is not available (HAS_XXD=0), use an od-based approach to write the binary header from hex. For the `xxd -r -p` replacement when only od is available, use printf with octal escapes or a python one-liner. However, since the existing code already checks for xxd availability and falls back to od for reading, check if `xxd -r -p` is available. If not, use:
+```sh
+# Fallback: write binary from hex using printf with octal
+i=0
+: > "$TMPDIR/header.bin"
+while [ $i -lt 80 ]; do
+ byte_hex=$(printf '%s' "$header_hex" | cut -c$((i + 1))-$((i + 2)))
+ printf "\\$(printf '%03o' "0x$byte_hex")" >> "$TMPDIR/header.bin"
+ i=$((i + 2))
+done
+```
+
+**2. Parse header fields from temp file instead of archive:**
+
+Change all header field reads to use `$TMPDIR/header.bin`:
+```sh
+version_hex=$(read_hex "$TMPDIR/header.bin" 4 1)
+version=$(printf '%d' "0x${version_hex}")
+flags_hex=$(read_hex "$TMPDIR/header.bin" 5 1)
+flags=$(printf '%d' "0x${flags_hex}")
+file_count=$(read_le_u16 "$TMPDIR/header.bin" 6)
+toc_offset=$(read_le_u32 "$TMPDIR/header.bin" 8)
+toc_size=$(read_le_u32 "$TMPDIR/header.bin" 12)
+toc_iv_hex=$(read_hex "$TMPDIR/header.bin" 16 16)
+```
+
+**3. TOC decryption (when flags bit 1 is set):**
+
+After reading header fields, check TOC encryption flag:
+```sh
+toc_encrypted=$(( flags & 2 ))
+
+if [ "$toc_encrypted" -ne 0 ]; then
+ # Extract encrypted TOC to temp file
+ dd if="$ARCHIVE" bs=1 skip="$toc_offset" count="$toc_size" of="$TMPDIR/toc_enc.bin" 2>/dev/null
+
+ # Decrypt TOC
+ openssl enc -d -aes-256-cbc -nosalt \
+ -K "$KEY_HEX" -iv "$toc_iv_hex" \
+ -in "$TMPDIR/toc_enc.bin" -out "$TMPDIR/toc_dec.bin"
+
+ TOC_FILE="$TMPDIR/toc_dec.bin"
+ TOC_BASE_OFFSET=0
+else
+ TOC_FILE="$ARCHIVE"
+ TOC_BASE_OFFSET=$toc_offset
+fi
+```
+
+**4. Update TOC parsing loop to use TOC_FILE and TOC_BASE_OFFSET:**
+
+Change `pos=$toc_offset` to `pos=$TOC_BASE_OFFSET`.
+
+Change ALL references to `"$ARCHIVE"` in the TOC field reads to `"$TOC_FILE"`:
+- `read_le_u16 "$TOC_FILE" "$pos"` instead of `read_le_u16 "$ARCHIVE" "$pos"`
+- `dd if="$TOC_FILE" ...` for filename read
+- `read_le_u32 "$TOC_FILE" "$pos"` for all u32 fields
+- `read_hex "$TOC_FILE" "$pos" N` for IV, HMAC, SHA-256, compression_flag
+
+This is the biggest refactor (06-RESEARCH.md Pitfall 1). Every field read in the TOC loop (lines ~141-183) must change from `$ARCHIVE` to `$TOC_FILE`.
+
+**IMPORTANT HMAC exception:** The HMAC verification reads IV bytes from `$ARCHIVE` at `$iv_toc_pos` (the absolute archive position). After TOC encryption, IV is stored in the TOC entries (which are now in the decrypted file). The HMAC input is still IV || ciphertext from the archive data block. So for HMAC computation:
+- IV comes from the TOC entry (already parsed as `$iv_hex`).
+- Ciphertext comes from `$ARCHIVE` at `$data_offset`.
+- The HMAC input must be constructed from the parsed iv_hex and the raw ciphertext from the archive.
+
+Change the HMAC verification to construct IV from the parsed hex variable instead of reading from the archive at the TOC position:
+```sh
+computed_hmac=$( {
+ printf '%s' "$iv_hex" | xxd -r -p
+ cat "$TMPDIR/ct.bin"
+} | openssl dgst -sha256 -mac HMAC -macopt "hexkey:${KEY_HEX}" -hex 2>/dev/null | awk '{print $NF}' )
+```
+With od fallback for `xxd -r -p` if needed.
+
+**5. No changes needed for decoy padding:** The decoder uses `data_offset` from TOC entries (absolute offsets), so padding between blocks is naturally skipped.
+
+**Re-run cross-validation tests** (shell/test_decoder.sh). No changes should be needed to the test script since it already tests Rust pack -> Shell decode -> SHA-256 comparison.
+
+
+ cd /home/nick/Projects/Rust/encrypted_archive && sh shell/test_decoder.sh 2>&1 | tail -10
+ Check that decode.sh has XOR_KEY_HEX variable, XOR loop, and TOC decryption section
+
+ Shell decoder handles XOR-obfuscated headers, encrypted TOC, and archives with decoy padding. All 6 cross-validation tests pass (Rust pack -> Shell decode -> SHA-256 match). HMAC verification works with IV from parsed TOC entry.
+
+
+
+
+
+1. `bash kotlin/test_decoder.sh` -- all 6 Kotlin cross-validation tests pass
+2. `sh shell/test_decoder.sh` -- all 6 Shell cross-validation tests pass
+3. Kotlin decoder correctly applies XOR bootstrapping + TOC decryption
+4. Shell decoder correctly applies XOR bootstrapping + TOC decryption from temp file
+5. Both decoders produce byte-identical output to Rust unpack on the same obfuscated archive
+6. `strings obfuscated_archive.bin | grep -i "hello\|test\|file"` returns nothing (no plaintext metadata leaks)
+
+
+
+- All three decoders (Rust, Kotlin, Shell) produce byte-identical output from obfuscated archives
+- 12 cross-validation tests pass (6 Kotlin + 6 Shell)
+- Phase 6 success criteria from ROADMAP.md are fully met:
+ 1. File table encrypted with its own IV -- hex dump reveals no plaintext metadata
+ 2. Headers XOR-obfuscated -- no recognizable structure in first 256 bytes
+ 3. Random decoy padding between blocks -- file boundaries not detectable
+ 4. All three decoders still produce byte-identical output
+
+
+