9.8 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 05-shell-decoder | 01 | execute | 1 |
|
true |
|
|
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.
<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 @docs/FORMAT.md @src/key.rs @.planning/phases/05-shell-decoder/05-RESEARCH.md Task 1: Create shell/decode.sh with full decode pipeline shell/decode.sh 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):
-
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"
-
Hardcoded key constant:
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
-
Prerequisite checks:
- Check for
dd,openssl,sha256sum-- exit with error if missing - Do NOT require
gunzipat startup (only needed if compressed files exist) - Detect
xxdvsodfallback usingcommand -v xxd
- Check for
-
Temporary directory with cleanup trap:
TMPDIR=$(mktemp -d) trap 'rm -rf "$TMPDIR"' EXIT -
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 decimalread_le_u32 "$file" "$offset"-- reads 4 bytes LE, prints decimal- Use
printf '%d' "0x${swapped_hex}"for hex-to-decimal conversion (POSIX compatible)
-
HMAC availability detection (from FORMAT.md Section 13.3):
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 -
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"
-
TOC parsing loop (FORMAT.md Section 5):
- Start at
pos=$toc_offset - Loop
ifrom 0 to$((file_count - 1))using a while loop (not seq, for POSIX compat; actuallyseqIS 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
- Start at
-
Per-file decode pipeline (FORMAT.md Section 10): For each file in the TOC loop:
a. Extract ciphertext to temp file:
dd if="$ARCHIVE" bs=1 skip="$data_offset" count="$encrypted_size" of="$TMPDIR/ct.bin" 2>/dev/nullb. 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:
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 , skipping" to stderr and
continueto next file
c. Decrypt:
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"):
if [ "$compression_flag" = "01" ]; then gunzip -c "$TMPDIR/dec.bin" > "$TMPDIR/out.bin" else mv "$TMPDIR/dec.bin" "$TMPDIR/out.bin" fiHandle special case: if original_size is 0, create empty file directly (
touch "$TMPDIR/out.bin")e. Verify SHA-256:
actual_sha=$(sha256sum "$TMPDIR/out.bin" | awk '{print $1}')If mismatch: print "WARNING: SHA-256 mismatch for " to stderr (but still write the file, matching Rust/Kotlin behavior)
f. Write output file:
mv "$TMPDIR/out.bin" "$OUTPUT_DIR/$filename"Print progress: "Extracted: (<original_size> bytes)"
- Only if
-
Final summary: Print "Done: extracted N files to <output_dir>"
CRITICAL anti-patterns to avoid:
- NO
[[ ]]-- use[ ]only - NO bash arrays
- NO
$((16#FF))-- useprintf '%d' "0x..."instead - NO process substitution
<() - NO
echo -e-- useprintffor anything requiring escape sequences - ALL
ddcommands must have2>/dev/null - Set
export LC_ALL=Cnear the top (for predictable byte handling)
Make the script executable: chmod +x shell/decode.sh
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"
Review shell/decode.sh for POSIX compliance, correct FORMAT.md field offsets, and complete pipeline
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
<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>