#!/bin/sh # decode.sh -- Busybox-compatible archive decoder # Extracts files from archives created by the Rust archiver (encrypted_archive). # Uses only: dd, xxd/od, openssl, gunzip, sha256sum # # Usage: decode.sh set -e export LC_ALL=C # ------------------------------------------------------- # Usage check # ------------------------------------------------------- if [ $# -ne 2 ]; then printf 'Usage: %s \n' "$0" >&2 exit 1 fi ARCHIVE="$1" OUTPUT_DIR="$2" if [ ! -f "$ARCHIVE" ]; then printf 'Error: archive file not found: %s\n' "$ARCHIVE" >&2 exit 1 fi mkdir -p "$OUTPUT_DIR" # ------------------------------------------------------- # Hardcoded 32-byte AES-256 key (matches src/key.rs) # ------------------------------------------------------- KEY_HEX="7a35c1d94fe82b6a910df358bc74a61e428fd063e5179b2cfa8406cd3e79b550" # ------------------------------------------------------- # Prerequisite checks # ------------------------------------------------------- for tool in dd openssl sha256sum; do if ! command -v "$tool" >/dev/null 2>&1; then printf 'Error: required tool not found: %s\n' "$tool" >&2 exit 1 fi done # ------------------------------------------------------- # Detect xxd vs od fallback # ------------------------------------------------------- if command -v xxd >/dev/null 2>&1; then HAS_XXD=1 else HAS_XXD=0 fi # ------------------------------------------------------- # Temporary directory with cleanup trap # ------------------------------------------------------- TMPDIR=$(mktemp -d) trap 'rm -rf "$TMPDIR"' EXIT # ------------------------------------------------------- # Hex reading functions # ------------------------------------------------------- # read_hex # Returns lowercase hex string of bytes at read_hex() { if [ "$HAS_XXD" = "1" ]; then dd if="$1" bs=1 skip="$2" count="$3" 2>/dev/null | xxd -p | tr -d '\n' else dd if="$1" bs=1 skip="$2" count="$3" 2>/dev/null | od -A n -t x1 | tr -d ' \n' fi } # read_le_u16 # Reads 2 bytes little-endian, prints decimal read_le_u16() { _hex=$(read_hex "$1" "$2" 2) _b0=$(printf '%.2s' "$_hex") _b1=$(printf '%.2s' "${_hex#??}") printf '%d' "0x${_b1}${_b0}" } # read_le_u32 # Reads 4 bytes little-endian, prints decimal read_le_u32() { _hex=$(read_hex "$1" "$2" 4) _b0=$(printf '%.2s' "$_hex") _b1=$(printf '%.2s' "${_hex#??}") _rest="${_hex#????}" _b2=$(printf '%.2s' "$_rest") _b3=$(printf '%.2s' "${_rest#??}") printf '%d' "0x${_b3}${_b2}${_b1}${_b0}" } # ------------------------------------------------------- # HMAC availability detection # ------------------------------------------------------- SKIP_HMAC=0 if ! printf 'test' | openssl dgst -sha256 -mac HMAC -macopt hexkey:00 >/dev/null 2>&1; then printf 'WARNING: openssl HMAC not supported, skipping HMAC verification\n' >&2 SKIP_HMAC=1 fi # ------------------------------------------------------- # XOR obfuscation key (FORMAT.md Section 9.1) # ------------------------------------------------------- XOR_KEY_HEX="a53c960fe17b4dc8" # ------------------------------------------------------- # hex_to_bin # Write binary data from a hex string to a file. # ------------------------------------------------------- hex_to_bin() { if [ "$HAS_XXD" = "1" ]; then printf '%s' "$1" | xxd -r -p > "$2" else _htb_hex="$1" _htb_i=0 _htb_len=${#_htb_hex} : > "$2" while [ "$_htb_i" -lt "$_htb_len" ]; do _htb_byte=$(printf '%s' "$_htb_hex" | cut -c$((_htb_i + 1))-$((_htb_i + 2))) printf "\\$(printf '%03o' "0x$_htb_byte")" >> "$2" _htb_i=$((_htb_i + 2)) done fi } # ------------------------------------------------------- # Header parsing with XOR bootstrapping (FORMAT.md Section 9.1, Section 10) # ------------------------------------------------------- # 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 (got %s)\n' "$magic_hex" >&2 exit 1 fi else header_hex="$raw_header_hex" fi # Write de-XORed header to temp file for field parsing hex_to_bin "$header_hex" "$TMPDIR/header.bin" # Parse header fields from de-XORed temp file version_hex=$(read_hex "$TMPDIR/header.bin" 4 1) version=$(printf '%d' "0x${version_hex}") if [ "$version" -ne 1 ]; then printf 'Unsupported version: %d\n' "$version" >&2 exit 1 fi 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) printf 'Archive: %d files\n' "$file_count" # ------------------------------------------------------- # TOC decryption (FORMAT.md Section 9.2) # ------------------------------------------------------- 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 # ------------------------------------------------------- # TOC parsing loop (FORMAT.md Section 5) # ------------------------------------------------------- pos=$TOC_BASE_OFFSET extracted=0 i=0 while [ "$i" -lt "$file_count" ]; do # -- name_length (u16 LE) -- name_length=$(read_le_u16 "$TOC_FILE" "$pos") pos=$((pos + 2)) # -- filename (raw UTF-8 bytes) -- filename=$(dd if="$TOC_FILE" bs=1 skip="$pos" count="$name_length" 2>/dev/null) pos=$((pos + name_length)) # -- original_size (u32 LE) -- original_size=$(read_le_u32 "$TOC_FILE" "$pos") pos=$((pos + 4)) # -- compressed_size (u32 LE) -- compressed_size=$(read_le_u32 "$TOC_FILE" "$pos") pos=$((pos + 4)) # -- encrypted_size (u32 LE) -- encrypted_size=$(read_le_u32 "$TOC_FILE" "$pos") pos=$((pos + 4)) # -- data_offset (u32 LE) -- data_offset=$(read_le_u32 "$TOC_FILE" "$pos") pos=$((pos + 4)) # -- iv (16 bytes as hex) -- iv_hex=$(read_hex "$TOC_FILE" "$pos" 16) pos=$((pos + 16)) # -- hmac (32 bytes as hex) -- hmac_hex=$(read_hex "$TOC_FILE" "$pos" 32) pos=$((pos + 32)) # -- sha256 (32 bytes as hex) -- sha256_hex=$(read_hex "$TOC_FILE" "$pos" 32) pos=$((pos + 32)) # -- compression_flag (1 byte as hex) -- compression_flag=$(read_hex "$TOC_FILE" "$pos" 1) pos=$((pos + 1)) # -- padding_after (u16 LE) -- padding_after=$(read_le_u16 "$TOC_FILE" "$pos") pos=$((pos + 2)) # ======================================================= # Per-file decode pipeline (FORMAT.md Section 10) # ======================================================= # 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) # HMAC input = IV (16 bytes) || ciphertext # IV comes from the parsed TOC entry (iv_hex), not from an archive position if [ "$SKIP_HMAC" = "0" ]; then # Write IV bytes to temp file from parsed hex hex_to_bin "$iv_hex" "$TMPDIR/iv.bin" computed_hmac=$( { cat "$TMPDIR/iv.bin" cat "$TMPDIR/ct.bin" } | openssl dgst -sha256 -mac HMAC -macopt "hexkey:${KEY_HEX}" -hex 2>/dev/null | awk '{print $NF}' ) # Normalize both to lowercase computed_hmac=$(printf '%s' "$computed_hmac" | tr 'A-F' 'a-f') hmac_hex_lc=$(printf '%s' "$hmac_hex" | tr 'A-F' 'a-f') if [ "$computed_hmac" != "$hmac_hex_lc" ]; then printf 'HMAC FAILED for %s, skipping\n' "$filename" >&2 i=$((i + 1)) continue fi fi # 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 [ "$original_size" -eq 0 ]; then # Special case: empty file : > "$TMPDIR/out.bin" elif [ "$compression_flag" = "01" ]; then if ! command -v gunzip >/dev/null 2>&1; then printf 'Error: gunzip required for compressed file: %s\n' "$filename" >&2 i=$((i + 1)) continue fi gunzip -c "$TMPDIR/dec.bin" > "$TMPDIR/out.bin" else mv "$TMPDIR/dec.bin" "$TMPDIR/out.bin" fi # e. Verify SHA-256 actual_sha=$(sha256sum "$TMPDIR/out.bin" | awk '{print $1}') if [ "$actual_sha" != "$sha256_hex" ]; then printf 'WARNING: SHA-256 mismatch for %s\n' "$filename" >&2 fi # f. Write output file mv "$TMPDIR/out.bin" "$OUTPUT_DIR/$filename" printf 'Extracted: %s (%d bytes)\n' "$filename" "$original_size" extracted=$((extracted + 1)) # Clean up temp files for next iteration rm -f "$TMPDIR/ct.bin" "$TMPDIR/dec.bin" "$TMPDIR/iv.bin" i=$((i + 1)) done # ------------------------------------------------------- # Final summary # ------------------------------------------------------- printf 'Done: extracted %d files to %s\n' "$extracted" "$OUTPUT_DIR"