216 lines
7.6 KiB
Python
216 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Debug script for DCT/jpegio extraction issues.
|
|
Run from the stegasoo directory.
|
|
"""
|
|
|
|
import sys
|
|
import struct
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent / 'src'))
|
|
|
|
import hashlib
|
|
import numpy as np
|
|
|
|
# Check for jpegio
|
|
try:
|
|
import jpegio as jio
|
|
print("✓ jpegio available")
|
|
except ImportError:
|
|
print("✗ jpegio NOT available")
|
|
sys.exit(1)
|
|
|
|
def get_usable_positions(coef_array, min_magnitude=2):
|
|
"""Get positions of usable coefficients."""
|
|
positions = []
|
|
h, w = coef_array.shape
|
|
for row in range(h):
|
|
for col in range(w):
|
|
# Skip DC coefficients (top-left of each 8x8 block)
|
|
if (row % 8 == 0) and (col % 8 == 0):
|
|
continue
|
|
if abs(coef_array[row, col]) >= min_magnitude:
|
|
positions.append((row, col))
|
|
return positions
|
|
|
|
def generate_order(num_positions, seed):
|
|
"""Generate pseudo-random order for coefficient selection."""
|
|
hash_bytes = hashlib.sha256(seed + b"jpeg_coef_order").digest()
|
|
rng = np.random.RandomState(int.from_bytes(hash_bytes[:4], 'big'))
|
|
order = list(range(num_positions))
|
|
rng.shuffle(order)
|
|
return order
|
|
|
|
def extract_bits(coef_array, positions, order, num_bits):
|
|
"""Extract bits from coefficients."""
|
|
bits = []
|
|
for i, pos_idx in enumerate(order):
|
|
if i >= num_bits:
|
|
break
|
|
row, col = positions[pos_idx]
|
|
coef = coef_array[row, col]
|
|
bits.append(coef & 1)
|
|
return bits
|
|
|
|
def bits_to_bytes(bits):
|
|
"""Convert list of bits to bytes."""
|
|
result = []
|
|
for i in range(0, len(bits), 8):
|
|
byte_bits = bits[i:i+8]
|
|
if len(byte_bits) == 8:
|
|
byte_val = sum(byte_bits[j] << (7-j) for j in range(8))
|
|
result.append(byte_val)
|
|
return bytes(result)
|
|
|
|
def main():
|
|
if len(sys.argv) < 3:
|
|
print("Usage: python debug_jpegio.py <stego_image.jpg> <reference_photo>")
|
|
print("\nOptional: add passphrase, pin, key path")
|
|
print(" python debug_jpegio.py stego.jpg ref.jpg 'passphrase' '123456' key.pem")
|
|
sys.exit(1)
|
|
|
|
stego_path = sys.argv[1]
|
|
ref_path = sys.argv[2]
|
|
passphrase = sys.argv[3] if len(sys.argv) > 3 else "test"
|
|
pin = sys.argv[4] if len(sys.argv) > 4 else ""
|
|
key_path = sys.argv[5] if len(sys.argv) > 5 else None
|
|
|
|
print(f"\n{'='*60}")
|
|
print("JPEGIO DCT EXTRACTION DEBUG")
|
|
print(f"{'='*60}")
|
|
print(f"Stego image: {stego_path}")
|
|
print(f"Reference: {ref_path}")
|
|
print(f"Passphrase: '{passphrase}'")
|
|
print(f"PIN: '{pin}'")
|
|
print(f"Key: {key_path}")
|
|
|
|
# Load stego image with jpegio
|
|
print(f"\n[1] Loading stego image with jpegio...")
|
|
try:
|
|
jpeg = jio.read(stego_path)
|
|
print(f" ✓ jpegio.read() succeeded")
|
|
print(f" Number of components: {len(jpeg.coef_arrays)}")
|
|
for i, arr in enumerate(jpeg.coef_arrays):
|
|
print(f" Component {i}: shape={arr.shape}, dtype={arr.dtype}")
|
|
except Exception as e:
|
|
print(f" ✗ Failed: {e}")
|
|
sys.exit(1)
|
|
|
|
# Get coefficient array (channel 0)
|
|
coef_array = jpeg.coef_arrays[0]
|
|
print(f"\n[2] Coefficient array analysis...")
|
|
print(f" Shape: {coef_array.shape}")
|
|
print(f" Non-zero coefficients: {np.count_nonzero(coef_array)}")
|
|
print(f" Min value: {coef_array.min()}")
|
|
print(f" Max value: {coef_array.max()}")
|
|
|
|
# Get usable positions
|
|
print(f"\n[3] Finding usable positions (|coef| >= 2, non-DC)...")
|
|
positions = get_usable_positions(coef_array)
|
|
print(f" Usable positions: {len(positions)}")
|
|
print(f" Capacity: ~{len(positions) // 8} bytes")
|
|
|
|
# Generate seed (this needs to match the encode seed!)
|
|
print(f"\n[4] Generating seed...")
|
|
|
|
# Load reference photo
|
|
ref_data = Path(ref_path).read_bytes()
|
|
ref_hash = hashlib.sha256(ref_data).digest()
|
|
print(f" Reference hash: {ref_hash[:8].hex()}...")
|
|
|
|
# Load RSA key if provided
|
|
rsa_component = b""
|
|
if key_path:
|
|
try:
|
|
from stegasoo import load_rsa_key
|
|
key_data = Path(key_path).read_bytes()
|
|
# Try without password first
|
|
try:
|
|
rsa_key = load_rsa_key(key_data, password=None)
|
|
except:
|
|
rsa_key = load_rsa_key(key_data, password="testpass")
|
|
|
|
# Get public key bytes for seed
|
|
from cryptography.hazmat.primitives import serialization
|
|
pub_bytes = rsa_key.public_key().public_bytes(
|
|
encoding=serialization.Encoding.DER,
|
|
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
|
)
|
|
rsa_component = hashlib.sha256(pub_bytes).digest()
|
|
print(f" RSA key loaded, hash: {rsa_component[:8].hex()}...")
|
|
except Exception as e:
|
|
print(f" ✗ Could not load RSA key: {e}")
|
|
|
|
# Build seed like stegasoo does
|
|
# This is the critical part - must match encoding!
|
|
seed_parts = [
|
|
ref_hash,
|
|
passphrase.encode('utf-8'),
|
|
pin.encode('utf-8') if pin else b"",
|
|
rsa_component,
|
|
]
|
|
seed = hashlib.sha256(b"".join(seed_parts)).digest()
|
|
print(f" Combined seed: {seed[:8].hex()}...")
|
|
|
|
# Generate order
|
|
print(f"\n[5] Generating coefficient order...")
|
|
order = generate_order(len(positions), seed)
|
|
print(f" First 10 indices: {order[:10]}")
|
|
|
|
# Try to extract header
|
|
print(f"\n[6] Extracting header (first 80 bits = 10 bytes)...")
|
|
HEADER_SIZE = 10
|
|
header_bits = extract_bits(coef_array, positions, order, HEADER_SIZE * 8)
|
|
header_bytes = bits_to_bytes(header_bits)
|
|
print(f" Raw header bytes: {header_bytes.hex()}")
|
|
print(f" As ASCII (if printable): {repr(header_bytes)}")
|
|
|
|
# Check for JPGS magic
|
|
JPEGIO_MAGIC = b'JPGS'
|
|
if header_bytes[:4] == JPEGIO_MAGIC:
|
|
print(f" ✓ Found JPEGIO magic bytes!")
|
|
version = header_bytes[4]
|
|
flags = header_bytes[5]
|
|
data_length = struct.unpack('>I', header_bytes[6:10])[0]
|
|
print(f" Version: {version}")
|
|
print(f" Flags: {flags}")
|
|
print(f" Data length: {data_length} bytes")
|
|
|
|
if data_length > 0 and data_length < len(positions) // 8:
|
|
print(f"\n[7] Extracting payload ({data_length} bytes)...")
|
|
total_bits = (HEADER_SIZE + data_length) * 8
|
|
all_bits = extract_bits(coef_array, positions, order, total_bits)
|
|
data_bits = all_bits[HEADER_SIZE * 8:]
|
|
payload = bits_to_bytes(data_bits)
|
|
print(f" Payload (first 64 bytes): {payload[:64].hex()}")
|
|
print(f" This should be encrypted data starting with salt/IV")
|
|
else:
|
|
print(f" ✗ Invalid data length: {data_length}")
|
|
else:
|
|
print(f" ✗ No JPEGIO magic found")
|
|
print(f" Expected: {JPEGIO_MAGIC.hex()} ('JPGS')")
|
|
print(f" Got: {header_bytes[:4].hex()} ('{header_bytes[:4]}')")
|
|
|
|
# Try alternate interpretations
|
|
print(f"\n[7] Trying alternate header interpretations...")
|
|
|
|
# Maybe it's scipy DCT format?
|
|
DCT_MAGIC = b'DCTS'
|
|
if header_bytes[:4] == DCT_MAGIC:
|
|
print(f" Found SCIPY DCT magic - wrong extraction method!")
|
|
else:
|
|
# Show bit distribution
|
|
print(f" First 32 extracted bits: {header_bits[:32]}")
|
|
|
|
# Check if bits look random or patterned
|
|
ones = sum(header_bits[:80])
|
|
print(f" Bit distribution: {ones}/80 ones ({100*ones/80:.1f}%)")
|
|
|
|
print(f"\n{'='*60}")
|
|
print("DEBUG COMPLETE")
|
|
print(f"{'='*60}\n")
|
|
|
|
if __name__ == '__main__':
|
|
main()
|