feat(06-02): add XOR header bootstrapping and encrypted TOC support to Shell decoder
- Add XOR_KEY_HEX constant and hex_to_bin() helper (xxd + od fallback) - Replace magic check with XOR bootstrapping: read 40 bytes, XOR if mismatch - Write de-XORed header to temp file for field parsing - Add TOC decryption via openssl enc when flags bit 1 is set - Switch TOC parsing loop from $ARCHIVE to $TOC_FILE variable - Update HMAC verification to construct IV from parsed hex (not archive position) - All 7 cross-validation tests pass (Rust pack -> Shell decode -> SHA-256 match)
This commit is contained in:
127
shell/decode.sh
127
shell/decode.sh
@@ -102,17 +102,69 @@ if ! printf 'test' | openssl dgst -sha256 -mac HMAC -macopt hexkey:00 >/dev/null
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# -------------------------------------------------------
|
# -------------------------------------------------------
|
||||||
# Header parsing (FORMAT.md Section 4)
|
# XOR obfuscation key (FORMAT.md Section 9.1)
|
||||||
# -------------------------------------------------------
|
# -------------------------------------------------------
|
||||||
# Header is 40 bytes at offset 0x00
|
XOR_KEY_HEX="a53c960fe17b4dc8"
|
||||||
magic_hex=$(read_hex "$ARCHIVE" 0 4)
|
|
||||||
|
# -------------------------------------------------------
|
||||||
|
# hex_to_bin <hex_string> <output_file>
|
||||||
|
# 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
|
if [ "$magic_hex" != "00ea7263" ]; then
|
||||||
printf 'Invalid archive: bad magic bytes (got %s)\n' "$magic_hex" >&2
|
# Attempt XOR de-obfuscation
|
||||||
exit 1
|
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
|
fi
|
||||||
|
|
||||||
version_hex=$(read_hex "$ARCHIVE" 4 1)
|
# 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}")
|
version=$(printf '%d' "0x${version_hex}")
|
||||||
|
|
||||||
if [ "$version" -ne 1 ]; then
|
if [ "$version" -ne 1 ]; then
|
||||||
@@ -120,66 +172,87 @@ if [ "$version" -ne 1 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
flags_hex=$(read_hex "$ARCHIVE" 5 1)
|
flags_hex=$(read_hex "$TMPDIR/header.bin" 5 1)
|
||||||
flags=$(printf '%d' "0x${flags_hex}")
|
flags=$(printf '%d' "0x${flags_hex}")
|
||||||
|
|
||||||
file_count=$(read_le_u16 "$ARCHIVE" 6)
|
file_count=$(read_le_u16 "$TMPDIR/header.bin" 6)
|
||||||
toc_offset=$(read_le_u32 "$ARCHIVE" 8)
|
toc_offset=$(read_le_u32 "$TMPDIR/header.bin" 8)
|
||||||
toc_size=$(read_le_u32 "$ARCHIVE" 12)
|
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"
|
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)
|
# TOC parsing loop (FORMAT.md Section 5)
|
||||||
# -------------------------------------------------------
|
# -------------------------------------------------------
|
||||||
pos=$toc_offset
|
pos=$TOC_BASE_OFFSET
|
||||||
extracted=0
|
extracted=0
|
||||||
i=0
|
i=0
|
||||||
|
|
||||||
while [ "$i" -lt "$file_count" ]; do
|
while [ "$i" -lt "$file_count" ]; do
|
||||||
# -- name_length (u16 LE) --
|
# -- name_length (u16 LE) --
|
||||||
name_length=$(read_le_u16 "$ARCHIVE" "$pos")
|
name_length=$(read_le_u16 "$TOC_FILE" "$pos")
|
||||||
pos=$((pos + 2))
|
pos=$((pos + 2))
|
||||||
|
|
||||||
# -- filename (raw UTF-8 bytes) --
|
# -- filename (raw UTF-8 bytes) --
|
||||||
filename=$(dd if="$ARCHIVE" bs=1 skip="$pos" count="$name_length" 2>/dev/null)
|
filename=$(dd if="$TOC_FILE" bs=1 skip="$pos" count="$name_length" 2>/dev/null)
|
||||||
pos=$((pos + name_length))
|
pos=$((pos + name_length))
|
||||||
|
|
||||||
# -- original_size (u32 LE) --
|
# -- original_size (u32 LE) --
|
||||||
original_size=$(read_le_u32 "$ARCHIVE" "$pos")
|
original_size=$(read_le_u32 "$TOC_FILE" "$pos")
|
||||||
pos=$((pos + 4))
|
pos=$((pos + 4))
|
||||||
|
|
||||||
# -- compressed_size (u32 LE) --
|
# -- compressed_size (u32 LE) --
|
||||||
compressed_size=$(read_le_u32 "$ARCHIVE" "$pos")
|
compressed_size=$(read_le_u32 "$TOC_FILE" "$pos")
|
||||||
pos=$((pos + 4))
|
pos=$((pos + 4))
|
||||||
|
|
||||||
# -- encrypted_size (u32 LE) --
|
# -- encrypted_size (u32 LE) --
|
||||||
encrypted_size=$(read_le_u32 "$ARCHIVE" "$pos")
|
encrypted_size=$(read_le_u32 "$TOC_FILE" "$pos")
|
||||||
pos=$((pos + 4))
|
pos=$((pos + 4))
|
||||||
|
|
||||||
# -- data_offset (u32 LE) --
|
# -- data_offset (u32 LE) --
|
||||||
data_offset=$(read_le_u32 "$ARCHIVE" "$pos")
|
data_offset=$(read_le_u32 "$TOC_FILE" "$pos")
|
||||||
pos=$((pos + 4))
|
pos=$((pos + 4))
|
||||||
|
|
||||||
# -- iv (16 bytes as hex) --
|
# -- iv (16 bytes as hex) --
|
||||||
iv_toc_pos=$pos
|
iv_hex=$(read_hex "$TOC_FILE" "$pos" 16)
|
||||||
iv_hex=$(read_hex "$ARCHIVE" "$pos" 16)
|
|
||||||
pos=$((pos + 16))
|
pos=$((pos + 16))
|
||||||
|
|
||||||
# -- hmac (32 bytes as hex) --
|
# -- hmac (32 bytes as hex) --
|
||||||
hmac_hex=$(read_hex "$ARCHIVE" "$pos" 32)
|
hmac_hex=$(read_hex "$TOC_FILE" "$pos" 32)
|
||||||
pos=$((pos + 32))
|
pos=$((pos + 32))
|
||||||
|
|
||||||
# -- sha256 (32 bytes as hex) --
|
# -- sha256 (32 bytes as hex) --
|
||||||
sha256_hex=$(read_hex "$ARCHIVE" "$pos" 32)
|
sha256_hex=$(read_hex "$TOC_FILE" "$pos" 32)
|
||||||
pos=$((pos + 32))
|
pos=$((pos + 32))
|
||||||
|
|
||||||
# -- compression_flag (1 byte as hex) --
|
# -- compression_flag (1 byte as hex) --
|
||||||
compression_flag=$(read_hex "$ARCHIVE" "$pos" 1)
|
compression_flag=$(read_hex "$TOC_FILE" "$pos" 1)
|
||||||
pos=$((pos + 1))
|
pos=$((pos + 1))
|
||||||
|
|
||||||
# -- padding_after (u16 LE) --
|
# -- padding_after (u16 LE) --
|
||||||
padding_after=$(read_le_u16 "$ARCHIVE" "$pos")
|
padding_after=$(read_le_u16 "$TOC_FILE" "$pos")
|
||||||
pos=$((pos + 2))
|
pos=$((pos + 2))
|
||||||
|
|
||||||
# =======================================================
|
# =======================================================
|
||||||
@@ -190,9 +263,13 @@ while [ "$i" -lt "$file_count" ]; do
|
|||||||
dd if="$ARCHIVE" bs=1 skip="$data_offset" count="$encrypted_size" of="$TMPDIR/ct.bin" 2>/dev/null
|
dd if="$ARCHIVE" bs=1 skip="$data_offset" count="$encrypted_size" of="$TMPDIR/ct.bin" 2>/dev/null
|
||||||
|
|
||||||
# b. Verify HMAC (if available)
|
# 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
|
if [ "$SKIP_HMAC" = "0" ]; then
|
||||||
|
# Write IV bytes to temp file from parsed hex
|
||||||
|
hex_to_bin "$iv_hex" "$TMPDIR/iv.bin"
|
||||||
computed_hmac=$( {
|
computed_hmac=$( {
|
||||||
dd if="$ARCHIVE" bs=1 skip="$iv_toc_pos" count=16 2>/dev/null
|
cat "$TMPDIR/iv.bin"
|
||||||
cat "$TMPDIR/ct.bin"
|
cat "$TMPDIR/ct.bin"
|
||||||
} | openssl dgst -sha256 -mac HMAC -macopt "hexkey:${KEY_HEX}" -hex 2>/dev/null | awk '{print $NF}' )
|
} | openssl dgst -sha256 -mac HMAC -macopt "hexkey:${KEY_HEX}" -hex 2>/dev/null | awk '{print $NF}' )
|
||||||
|
|
||||||
@@ -239,7 +316,7 @@ while [ "$i" -lt "$file_count" ]; do
|
|||||||
extracted=$((extracted + 1))
|
extracted=$((extracted + 1))
|
||||||
|
|
||||||
# Clean up temp files for next iteration
|
# Clean up temp files for next iteration
|
||||||
rm -f "$TMPDIR/ct.bin" "$TMPDIR/dec.bin"
|
rm -f "$TMPDIR/ct.bin" "$TMPDIR/dec.bin" "$TMPDIR/iv.bin"
|
||||||
|
|
||||||
i=$((i + 1))
|
i=$((i + 1))
|
||||||
done
|
done
|
||||||
|
|||||||
Reference in New Issue
Block a user