Files
android-encrypted-archiver/.planning/phases/05-shell-decoder/05-01-PLAN.md
2026-02-25 01:33:31 +03:00

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
shell/decode.sh
true
SHL-01
SHL-02
SHL-03
truths artifacts key_links
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
path provides min_lines contains
shell/decode.sh Busybox-compatible archive decoder shell script 150 openssl enc -d -aes-256-cbc
from to via pattern
shell/decode.sh docs/FORMAT.md Section 13 read_hex, read_le_u16, read_le_u32 functions from spec read_le_u32|read_le_u16|read_hex
from to via pattern
shell/decode.sh src/key.rs Hardcoded KEY_HEX constant matching Rust key bytes 7a35c1d94fe82b6a910df358bc74a61e428fd063e5179b2cfa8406cd3e79b550
from to via pattern
shell/decode.sh openssl enc AES-256-CBC decryption with raw key mode openssl enc -d -aes-256-cbc -nosalt -K.*-iv
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.

<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):

  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:

    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:

    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):

    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:

    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:
      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 continue to 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"
    fi
    

    Handle 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)"

  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 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

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

<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>
After completion, create `.planning/phases/05-shell-decoder/05-01-SUMMARY.md`