---
phase: 05-shell-decoder
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - shell/decode.sh
autonomous: true
requirements:
  - SHL-01
  - SHL-02
  - SHL-03

must_haves:
  truths:
    - "Shell script extracts all files from a Rust-created archive, byte-identical to originals"
    - "Script uses only dd, xxd/od, openssl, gunzip, sha256sum -- no bash-specific syntax"
    - "Script decrypts files using openssl enc -aes-256-cbc with raw hex key (-K/-iv/-nosalt)"
    - "Script correctly handles files with Cyrillic UTF-8 names"
    - "Script verifies HMAC-SHA-256 before decryption (graceful degradation if openssl lacks HMAC support)"
    - "Script verifies SHA-256 after decompression"
  artifacts:
    - path: "shell/decode.sh"
      provides: "Busybox-compatible archive decoder shell script"
      min_lines: 150
      contains: "openssl enc -d -aes-256-cbc"
  key_links:
    - from: "shell/decode.sh"
      to: "docs/FORMAT.md Section 13"
      via: "read_hex, read_le_u16, read_le_u32 functions from spec"
      pattern: "read_le_u32|read_le_u16|read_hex"
    - from: "shell/decode.sh"
      to: "src/key.rs"
      via: "Hardcoded KEY_HEX constant matching Rust key bytes"
      pattern: "7a35c1d94fe82b6a910df358bc74a61e428fd063e5179b2cfa8406cd3e79b550"
    - from: "shell/decode.sh"
      to: "openssl enc"
      via: "AES-256-CBC decryption with raw key mode"
      pattern: "openssl enc -d -aes-256-cbc -nosalt -K.*-iv"
---

<objective>
Create the busybox-compatible shell decoder script that extracts files from archives created by the Rust archiver.

Purpose: Provide a fallback extraction path when Kotlin/Android is unavailable. The script must work on minimal busybox systems with only dd, xxd/od, openssl, gunzip, and sha256sum.

Output: `shell/decode.sh` -- a single self-contained POSIX shell script implementing the full decode pipeline from FORMAT.md Section 10.
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@docs/FORMAT.md
@src/key.rs
@.planning/phases/05-shell-decoder/05-RESEARCH.md
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create shell/decode.sh with full decode pipeline</name>
  <files>shell/decode.sh</files>
  <action>
Create `shell/decode.sh` -- a single self-contained POSIX shell script that decodes archives created by the Rust archiver. The script MUST be compatible with busybox ash/sh (NO bash-specific syntax: no `[[ ]]`, no arrays, no `$((16#FF))`, no process substitution `<()`).

**Script structure (follow this order):**

1. **Shebang and usage:**
   - `#!/bin/sh` (NOT `#!/bin/bash`)
   - Usage: `decode.sh <archive_file> <output_dir>`
   - Validate exactly 2 arguments; print usage and exit 1 otherwise
   - Create output directory if it doesn't exist: `mkdir -p "$OUTPUT_DIR"`

2. **Hardcoded key constant:**
   ```sh
   KEY_HEX="7a35c1d94fe82b6a910df358bc74a61e428fd063e5179b2cfa8406cd3e79b550"
   ```
   Verify this matches src/key.rs bytes: 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

3. **Prerequisite checks:**
   - Check for `dd`, `openssl`, `sha256sum` -- exit with error if missing
   - Do NOT require `gunzip` at startup (only needed if compressed files exist)
   - Detect `xxd` vs `od` fallback using `command -v xxd`

4. **Temporary directory with cleanup trap:**
   ```sh
   TMPDIR=$(mktemp -d)
   trap 'rm -rf "$TMPDIR"' EXIT
   ```

5. **Hex reading functions (from FORMAT.md Section 13.1 + 13.2):**
   - `read_hex "$file" "$offset" "$count"` -- returns lowercase hex string
   - If xxd available: `dd ... | xxd -p | tr -d '\n'`
   - If xxd NOT available: `dd ... | od -A n -t x1 | tr -d ' \n'`
   - `read_le_u16 "$file" "$offset"` -- reads 2 bytes LE, prints decimal
   - `read_le_u32 "$file" "$offset"` -- reads 4 bytes LE, prints decimal
   - Use `printf '%d' "0x${swapped_hex}"` for hex-to-decimal conversion (POSIX compatible)

6. **HMAC availability detection (from FORMAT.md Section 13.3):**
   ```sh
   SKIP_HMAC=0
   if ! echo -n "test" | openssl dgst -sha256 -mac HMAC -macopt hexkey:00 >/dev/null 2>&1; then
     echo "WARNING: openssl HMAC not supported, skipping HMAC verification"
     SKIP_HMAC=1
   fi
   ```

7. **Header parsing (FORMAT.md Section 4):**
   - Read magic bytes at offset 0, count 4. Verify equals "00ea7263" (lowercase hex). If not, error "Invalid archive: bad magic bytes"
   - Read version at offset 4. Verify equals 1. If not, error "Unsupported version"
   - Read flags at offset 5 (1 byte)
   - Read file_count at offset 6 (u16 LE)
   - Read toc_offset at offset 8 (u32 LE)
   - Read toc_size at offset 12 (u32 LE)
   - Print: "Archive: N files"

8. **TOC parsing loop (FORMAT.md Section 5):**
   - Start at `pos=$toc_offset`
   - Loop `i` from 0 to `$((file_count - 1))` using a while loop (not seq, for POSIX compat; actually `seq` IS available in busybox so either is fine)
   - For each entry, parse sequentially:
     - name_length (u16 LE at pos), advance pos by 2
     - filename: `dd if="$ARCHIVE" bs=1 skip="$pos" count="$name_length" 2>/dev/null` -- raw UTF-8 bytes go directly into variable (handles Cyrillic per SHL-03)
     - advance pos by name_length
     - original_size (u32 LE), advance pos by 4
     - compressed_size (u32 LE), advance pos by 4
     - encrypted_size (u32 LE), advance pos by 4
     - data_offset (u32 LE), advance pos by 4
     - iv_hex: `read_hex "$ARCHIVE" "$pos" 16`, advance pos by 16
     - hmac_hex: `read_hex "$ARCHIVE" "$pos" 32`, advance pos by 32
     - sha256_hex: `read_hex "$ARCHIVE" "$pos" 32`, advance pos by 32
     - compression_flag: `read_hex "$ARCHIVE" "$pos" 1`, advance pos by 1
     - padding_after (u16 LE), advance pos by 2
   - Store iv_toc_offset separately (the offset where IV was read) for HMAC verification

9. **Per-file decode pipeline (FORMAT.md Section 10):**
   For each file in the TOC loop:

   a. **Extract ciphertext to temp file:**
      ```sh
      dd if="$ARCHIVE" bs=1 skip="$data_offset" count="$encrypted_size" of="$TMPDIR/ct.bin" 2>/dev/null
      ```

   b. **Verify HMAC (if available):**
      - Only if `SKIP_HMAC=0`
      - HMAC input = IV bytes (from archive, NOT from hex variable) || ciphertext bytes
      - Extract IV bytes from archive at the iv_toc_offset position (16 bytes) and cat with ciphertext:
        ```sh
        computed_hmac=$( { dd if="$ARCHIVE" bs=1 skip="$iv_toc_pos" count=16 2>/dev/null; cat "$TMPDIR/ct.bin"; } | openssl dgst -sha256 -mac HMAC -macopt "hexkey:${KEY_HEX}" -hex 2>/dev/null | awk '{print $NF}' )
        ```
      - Normalize both to lowercase: `echo "$hex" | tr 'A-F' 'a-f'`
      - If mismatch: print "HMAC FAILED for <filename>, skipping" to stderr and `continue` to next file

   c. **Decrypt:**
      ```sh
      openssl enc -d -aes-256-cbc -nosalt -K "$KEY_HEX" -iv "$iv_hex" -in "$TMPDIR/ct.bin" -out "$TMPDIR/dec.bin"
      ```

   d. **Decompress (if compression_flag = "01"):**
      ```sh
      if [ "$compression_flag" = "01" ]; then
        gunzip -c "$TMPDIR/dec.bin" > "$TMPDIR/out.bin"
      else
        mv "$TMPDIR/dec.bin" "$TMPDIR/out.bin"
      fi
      ```
      Handle special case: if original_size is 0, create empty file directly (`touch "$TMPDIR/out.bin"`)

   e. **Verify SHA-256:**
      ```sh
      actual_sha=$(sha256sum "$TMPDIR/out.bin" | awk '{print $1}')
      ```
      If mismatch: print "WARNING: SHA-256 mismatch for <filename>" to stderr (but still write the file, matching Rust/Kotlin behavior)

   f. **Write output file:**
      ```sh
      mv "$TMPDIR/out.bin" "$OUTPUT_DIR/$filename"
      ```
      Print progress: "Extracted: <filename> (<original_size> bytes)"

10. **Final summary:**
    Print "Done: extracted N files to <output_dir>"

**CRITICAL anti-patterns to avoid:**
- NO `[[ ]]` -- use `[ ]` only
- NO bash arrays
- NO `$((16#FF))` -- use `printf '%d' "0x..."` instead
- NO process substitution `<()`
- NO `echo -e` -- use `printf` for anything requiring escape sequences
- ALL `dd` commands must have `2>/dev/null`
- Set `export LC_ALL=C` near the top (for predictable byte handling)

Make the script executable: `chmod +x shell/decode.sh`
  </action>
  <verify>
    <automated>cd /home/nick/Projects/Rust/encrypted_archive && test -f shell/decode.sh && test -x shell/decode.sh && sh -n shell/decode.sh && echo "SYNTAX OK" && grep -q 'openssl enc -d -aes-256-cbc' shell/decode.sh && grep -q '7a35c1d94fe82b6a910df358bc74a61e428fd063e5179b2cfa8406cd3e79b550' shell/decode.sh && echo "KEY OK" && ! grep -E '\[\[|BASH_SOURCE|\$\(\(16#' shell/decode.sh && echo "NO BASH-ISMS"</automated>
    <manual>Review shell/decode.sh for POSIX compliance, correct FORMAT.md field offsets, and complete pipeline</manual>
  </verify>
  <done>shell/decode.sh exists, is executable, passes sh -n syntax check, contains the correct KEY_HEX, uses openssl enc -d -aes-256-cbc, and has no bash-specific syntax</done>
</task>

</tasks>

<verification>
1. `sh -n shell/decode.sh` passes (valid POSIX shell syntax)
2. Script contains correct hardcoded key matching src/key.rs
3. Script contains openssl enc -d -aes-256-cbc -nosalt -K -iv invocation
4. Script has xxd/od fallback detection
5. Script has HMAC graceful degradation
6. No bash-isms: no `[[`, no `BASH_SOURCE`, no `$((16#...))`, no arrays
</verification>

<success_criteria>
- shell/decode.sh is a valid POSIX shell script (passes sh -n)
- Script implements complete decode pipeline: header parse -> TOC parse -> HMAC verify -> decrypt -> decompress -> SHA-256 verify -> write
- Hardcoded key matches src/key.rs
- xxd/od fallback for hex conversion
- Graceful HMAC degradation
- UTF-8 filename preservation for Cyrillic names
</success_criteria>

<output>
After completion, create `.planning/phases/05-shell-decoder/05-01-SUMMARY.md`
</output>
