Files
stegasoo/UNDER_THE_HOOD.md
2026-01-06 12:16:15 -05:00

29 KiB
Raw Blame History

Stegasoo Technical Deep Dive: Encoding & Decoding

A detailed breakdown of how Stegasoo's LSB and DCT steganography modes work under the hood.

Version 4.0 - Updated for simplified authentication (no date dependency)


Table of Contents

  1. High-Level Overview
  2. The Encoding Pipeline
  3. The Decoding Pipeline
  4. LSB Mode Deep Dive
  5. DCT Mode Deep Dive
  6. Comparison Table
  7. Security Considerations

High-Level Overview

┌─────────────────────────────────────────────────────────────────────────────┐
│                           STEGASOO ARCHITECTURE (v4.0)                      │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   INPUTS                    PROCESSING                      OUTPUT          │
│   ───────                   ──────────                      ──────          │
│                                                                             │
│   Reference Photo ─┐                                                        │
│   Passphrase ──────┼──► Argon2id KDF ──► AES-256 Key                        │
│   PIN/RSA Key ─────┘                           │                            │
│                                                ▼                            │
│   Message/File ────────────────────────► AES-256-GCM ──► Ciphertext         │
│                                          Encryption            │            │
│                                                                ▼            │
│   Carrier Image ───────────────────────────────────────► Embedding ─► Stego │
│                                                          (LSB/DCT)    Image │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

v4.0 Changes

Change v3.x v4.0
Authentication day_phrase + date passphrase (no date)
Default words 3 4
Header size 75 bytes 65 bytes (no date field)
Python support 3.10+ 3.10-3.12 only

Module Responsibilities

Module File Purpose
Crypto crypto.py Key derivation (Argon2id), AES-256-GCM encryption/decryption
Steganography steganography.py LSB pixel manipulation, capacity calculation
DCT Steganography dct_steganography.py Frequency-domain embedding, jpegio integration
Compression compression.py Optional LZ4 compression of payload
Validation validation.py Input validation, size limits
Utils utils.py Image hashing, format detection

The Encoding Pipeline

Step 1: Input Collection & Validation

# validation.py
def validate_encode_inputs(reference_photo, carrier, message, passphrase, pin, rsa_key):
    # Check image dimensions (max 24 megapixels)
    # Validate PIN format (6-9 digits)
    # Validate passphrase (3-12 words from BIP-39)
    # Check payload size vs carrier capacity
    # Ensure reference != carrier (security)

Step 2: Reference Photo Processing

# utils.py
def get_image_hash(image_bytes: bytes) -> bytes:
    """
    Generate deterministic hash from reference photo.
    This is the 'something you have' factor.
    """
    # Resize to 256x256 (normalize different resolutions)
    # Convert to grayscale (normalize color variations)
    # Apply slight blur (reduce JPEG artifact sensitivity)
    # SHA-256 hash of processed pixels
    return hashlib.sha256(processed_pixels).digest()  # 32 bytes

Why process the image? Minor variations (JPEG recompression, slight crops) in the reference photo between sender and receiver would produce different hashes, breaking decryption. The preprocessing makes the hash more resilient.

Step 3: Key Derivation (Argon2id)

# crypto.py
def derive_key(reference_hash: bytes, passphrase: str, pin: str, 
               rsa_signature: bytes = None) -> bytes:
    """
    Combine all authentication factors into one AES key.
    v4.0: No date parameter - simplified authentication.
    """
    # Concatenate all factors
    key_material = reference_hash + passphrase.encode() + pin.encode()
    
    if rsa_signature:
        key_material += rsa_signature
    
    # Argon2id parameters (memory-hard to resist GPU attacks)
    # - Memory: 256 MB
    # - Iterations: 4
    # - Parallelism: 4
    # - Output: 32 bytes (256 bits)
    
    key = argon2.hash_password_raw(
        password=key_material,
        salt=random_salt,  # 16 bytes, stored with ciphertext
        time_cost=4,
        memory_cost=262144,  # 256 MB
        parallelism=4,
        hash_len=32,
        type=argon2.Type.ID
    )
    return key  # 32-byte AES-256 key

Why Argon2id?

  • Memory-hard: Requires 256MB RAM per attempt, defeating GPU/ASIC attacks
  • Time-hard: ~2-3 seconds per derivation
  • Side-channel resistant: ID variant protects against timing attacks

Step 4: Payload Preparation

# compression.py (optional)
def prepare_payload(data: bytes, filename: str = None) -> bytes:
    """
    Prepare the payload with metadata header.
    """
    # Header format (variable length):
    # [1 byte]  - Flags (compression, file mode, etc.)
    # [4 bytes] - Original data length (big-endian)
    # [2 bytes] - Filename length (if file mode)
    # [N bytes] - Filename (if file mode)
    # [N bytes] - Data (possibly compressed)
    
    header = struct.pack('>BI', flags, len(data))
    
    if filename:
        header += struct.pack('>H', len(filename)) + filename.encode()
    
    # Optional LZ4 compression
    if should_compress(data):
        data = lz4.frame.compress(data)
        flags |= FLAG_COMPRESSED
    
    return header + data

Step 5: AES-256-GCM Encryption

# crypto.py
def encrypt(plaintext: bytes, key: bytes) -> bytes:
    """
    Encrypt payload with AES-256-GCM.
    Returns: salt + nonce + ciphertext + tag
    """
    salt = os.urandom(16)       # Random salt for key derivation
    nonce = os.urandom(12)      # Random nonce for GCM
    
    cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
    ciphertext, tag = cipher.encrypt_and_digest(plaintext)
    
    # Final encrypted blob:
    # [16 bytes] Salt
    # [12 bytes] Nonce  
    # [16 bytes] Auth Tag
    # [N bytes]  Ciphertext
    
    return salt + nonce + tag + ciphertext

Why GCM?

  • Authenticated encryption: Detects tampering
  • No padding oracle: Stream cipher mode
  • Built-in integrity: 128-bit authentication tag

Step 6: Stego Header Construction

# steganography.py / dct_steganography.py
def build_stego_header(encrypted_data: bytes, mode: str) -> bytes:
    """
    Build the header that precedes embedded data.
    v4.0: Simplified header (no date field)
    """
    # Header format:
    # [4 bytes]  - Magic number: "STGO" (v4)
    # [1 byte]   - Version (0x04)
    # [1 byte]   - Mode (0x01=LSB, 0x02=DCT)
    # [4 bytes]  - Payload length
    # [N bytes]  - Encrypted payload
    
    if mode == 'lsb':
        magic = b'STGO\x04\x01'  # v4, mode 1 (LSB)
    else:
        magic = b'STGO\x04\x02'  # v4, mode 2 (DCT)
    
    length = struct.pack('>I', len(encrypted_data))
    
    return magic + length + encrypted_data

Step 7: Embedding (Mode-Specific)

This is where LSB and DCT diverge. See detailed sections below.


The Decoding Pipeline

Step 1: Mode Detection

def detect_mode(stego_image: bytes) -> str:
    """
    Detect which embedding mode was used.
    Checks format and magic bytes.
    """
    img = Image.open(io.BytesIO(stego_image))
    
    # JPEG images with JPGS magic = DCT mode with jpegio
    if img.format == 'JPEG':
        # Check for jpegio magic
        return 'dct'
    
    # PNG/BMP: Read first few bytes from LSB
    # Check for STGO or DCTS magic
    magic = extract_header_lsb(stego_image, 6)
    
    if magic.startswith(b'STGO'):
        mode_byte = magic[5]
        return 'lsb' if mode_byte == 0x01 else 'dct'
    elif magic.startswith(b'DCTS'):
        return 'dct'
    
    return 'lsb'  # Default fallback

Step 2: Key Re-derivation

# Same process as encoding
def derive_key_for_decode(reference_hash, passphrase, pin, rsa_signature=None):
    # Must use SAME parameters as encoding
    # No date parameter in v4.0
    return derive_key(reference_hash, passphrase, pin, rsa_signature)

Step 3: Data Extraction

def extract_data(stego_image: bytes, mode: str) -> bytes:
    """
    Extract raw bytes from stego image.
    Mode-specific extraction.
    """
    if mode == 'dct':
        return extract_from_dct(stego_image, pixel_key)
    else:
        return extract_from_lsb(stego_image, pixel_key)

Step 4: Decryption & Payload Recovery

def decrypt_and_recover(encrypted_data: bytes, key: bytes) -> Union[str, bytes]:
    """
    Decrypt and extract original message/file.
    """
    # Parse header
    salt = encrypted_data[:16]
    nonce = encrypted_data[16:28]
    tag = encrypted_data[28:44]
    ciphertext = encrypted_data[44:]
    
    # Decrypt
    cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
    plaintext = cipher.decrypt_and_verify(ciphertext, tag)
    
    # Decompress if needed
    if plaintext[0] & FLAG_COMPRESSED:
        plaintext = lz4.frame.decompress(plaintext[5:])
    
    # Extract payload
    return parse_payload(plaintext)

LSB Mode Deep Dive

How LSB Embedding Works

LSB (Least Significant Bit) embedding modifies the lowest bit of each color channel in selected pixels.

Original Pixel (RGB):
  R: 11010110  G: 01101001  B: 10110100
              ↓         ↓         ↓
              └─────────┴─────────┘
                 3 bits available

After embedding "101":
  R: 1101011[1]  G: 0110100[0]  B: 1011010[1]
              ↑         ↑         ↑
           modified  modified  modified

Pixel Selection Algorithm

def select_pixels(carrier_shape, num_bits, seed: bytes) -> List[Tuple[int, int, int]]:
    """
    Generate pseudo-random pixel coordinates.
    Distributes modifications across entire image.
    """
    height, width, channels = carrier_shape
    total_positions = height * width * 3  # RGB channels
    
    # Use seed to generate reproducible random order
    rng = np.random.RandomState(int.from_bytes(seed[:4], 'big'))
    all_positions = np.arange(total_positions)
    rng.shuffle(all_positions)
    
    # Convert flat indices to (y, x, channel)
    selected = []
    for idx in all_positions[:num_bits]:
        y = idx // (width * 3)
        x = (idx % (width * 3)) // 3
        c = idx % 3
        selected.append((y, x, c))
    
    return selected

Embedding Process

def embed_lsb(carrier: np.ndarray, data: bytes, seed: bytes) -> np.ndarray:
    """
    Embed data using LSB substitution.
    """
    bits = bytes_to_bits(data)
    positions = select_pixels(carrier.shape, len(bits), seed)
    
    stego = carrier.copy()
    for i, (y, x, c) in enumerate(positions):
        # Clear LSB and set to our bit
        stego[y, x, c] = (stego[y, x, c] & 0xFE) | bits[i]
    
    return stego

Capacity Calculation

def calculate_lsb_capacity(width: int, height: int) -> int:
    """
    Calculate maximum payload size for LSB mode.
    """
    total_bits = width * height * 3  # 3 bits per pixel (RGB)
    header_bits = 10 * 8  # 10-byte stego header
    available_bits = total_bits - header_bits
    
    return available_bits // 8  # Convert to bytes

Example capacities:

  • 1920×1080: ~770 KB
  • 4000×3000: ~4.5 MB
  • 800×600: ~180 KB

DCT Mode Deep Dive

How DCT Embedding Works

DCT (Discrete Cosine Transform) mode embeds data in the frequency-domain coefficients, making it resilient to JPEG compression.

Image Block (8×8 pixels)
         ↓
    DCT Transform
         ↓
DCT Coefficients (8×8)
┌────────────────────┐
│ DC  AC₁ AC₂ AC₃ ...│  ← Lower frequencies (top-left)
│ AC₄ AC₅ AC₆ ...    │
│ ...        ...     │  ← Mid frequencies (embed here)
│ ...            ... │
│           AC₆₃ ────│  ← Higher frequencies (bottom-right)
└────────────────────┘
         ↓
   Modify select ACs
         ↓
    IDCT Transform
         ↓
Modified Image Block

Coefficient Selection

# dct_steganography.py
EMBED_POSITIONS = [
    (0, 1), (1, 0), (2, 0), (1, 1), (0, 2), (0, 3), (1, 2), (2, 1), (3, 0),
    (4, 0), (3, 1), (2, 2), (1, 3), (0, 4), (0, 5), (1, 4), (2, 3), (3, 2),
    (4, 1), (5, 0), (5, 1), (4, 2), (3, 3), (2, 4), (1, 5), (0, 6), (0, 7),
    (1, 6), (2, 5), (3, 4), (4, 3), (5, 2), (6, 1), (7, 0),
]

# Use positions 4-20 (mid-frequency, good balance)
DEFAULT_EMBED_POSITIONS = EMBED_POSITIONS[4:20]  # 16 positions per block

Why mid-frequency?

  • DC coefficient (0,0): Too visible, contains brightness
  • Low AC: Visible changes, but survives compression
  • Mid AC: Best balance of invisibility + resilience
  • High AC: Invisible but destroyed by compression

Block Processing

def embed_in_block(block: np.ndarray, bits: List[int]) -> np.ndarray:
    """
    Embed bits in a single 8×8 block.
    """
    # Forward DCT
    dct_block = dct_2d(block)
    
    # Embed using quantization
    for i, pos in enumerate(DEFAULT_EMBED_POSITIONS):
        if i >= len(bits):
            break
        
        coef = dct_block[pos[0], pos[1]]
        # Quantize and modify LSB
        quantized = round(coef / QUANT_STEP)
        if (quantized % 2) != bits[i]:
            quantized += 1 if coef > 0 else -1
        dct_block[pos[0], pos[1]] = quantized * QUANT_STEP
    
    # Inverse DCT
    return idct_2d(dct_block)

jpegio Integration (Native JPEG Output)

def embed_jpegio(data: bytes, carrier_jpeg: bytes, seed: bytes) -> bytes:
    """
    Embed directly in JPEG DCT coefficients using jpegio.
    Preserves JPEG structure perfectly.
    
    Note: Requires Python 3.12 or earlier (jpegio incompatible with 3.13)
    """
    import jpegio as jio
    
    # Normalize problematic JPEGs (quality=100 causes crashes)
    carrier_jpeg = normalize_jpeg_for_jpegio(carrier_jpeg)
    
    # Read existing JPEG coefficients
    jpeg = jio.read(temp_file_from_bytes(carrier_jpeg))
    coef_array = jpeg.coef_arrays[0]  # Y channel
    
    # Find usable coefficients (magnitude >= 2, non-DC)
    positions = get_usable_positions(coef_array)
    order = generate_order(len(positions), seed)
    
    # Embed by modifying coefficient LSBs
    bits = bytes_to_bits(data)
    for i, pos_idx in enumerate(order[:len(bits)]):
        row, col = positions[pos_idx]
        coef = coef_array[row, col]
        
        if (coef & 1) != bits[i]:
            # Flip LSB while preserving sign
            if coef > 0:
                coef_array[row, col] = coef - 1 if (coef & 1) else coef + 1
            else:
                coef_array[row, col] = coef + 1 if (coef & 1) else coef - 1
    
    # Write modified JPEG
    jio.write(jpeg, output_path)
    return read_bytes(output_path)

JPEG Normalization (v4.0)

def normalize_jpeg_for_jpegio(image_data: bytes) -> bytes:
    """
    Normalize problematic JPEGs before jpegio processing.
    
    JPEGs with quality=100 have quantization tables with all values=1,
    which causes jpegio to crash. Re-save at quality 95.
    """
    img = Image.open(io.BytesIO(image_data))
    
    if img.format != 'JPEG':
        return image_data
    
    # Check if any quantization table has all values <= 1
    needs_normalization = False
    if hasattr(img, 'quantization'):
        for table in img.quantization.values():
            if max(table) <= 1:
                needs_normalization = True
                break
    
    if not needs_normalization:
        return image_data
    
    # Re-save at safe quality
    buffer = io.BytesIO()
    img.save(buffer, format='JPEG', quality=95, subsampling=0)
    return buffer.getvalue()

DCT Capacity Calculation

def calculate_dct_capacity(width: int, height: int) -> int:
    """
    Calculate maximum payload for DCT mode.
    """
    blocks_x = width // 8
    blocks_y = height // 8
    total_blocks = blocks_x * blocks_y
    
    bits_per_block = len(DEFAULT_EMBED_POSITIONS)  # 16
    total_bits = total_blocks * bits_per_block
    
    header_bits = 10 * 8  # Stego header
    available_bits = total_bits - header_bits
    
    return available_bits // 8

Example capacities:

  • 1920×1080: ~64 KB
  • 4000×3000: ~375 KB
  • 800×600: ~14 KB

Why DCT Survives JPEG Compression

Original JPEG:    Stego JPEG:      Re-compressed:
                                   
DCT coefficients  Modified DCT     Coefficients 
preserved in      coefficients     re-quantized
file format       still valid      
      │                 │                │
      ▼                 ▼                ▼
   [DCT] ──────►  [Modified] ──────►  [Still
   [coefs]        [DCT coefs]         Modified!]
                                   
LSB changes survive because they're embedded in 
the frequency domain, not spatial pixel values.

DCT Advantages

Advantage Description
JPEG resilient Survives social media upload
Better steganalysis resistance Harder to detect statistically
Natural-looking output JPEG artifacts expected

DCT Limitations

Limitation Description
Lower capacity ~10% of LSB capacity
Slower processing DCT transforms are compute-intensive
Requires scipy/jpegio Additional dependencies
Quality-dependent Heavy recompression still degrades data
Python version jpegio requires Python 3.12 or earlier

Comparison Table

Aspect LSB Mode DCT Mode
Capacity (1080p) ~770 KB ~50 KB
Output Format PNG only PNG or JPEG
Survives JPEG No Yes
Social Media Broken Works
Processing Speed Fast (~0.5s) Slower (~2s)
Dependencies Pillow, NumPy + scipy, jpegio
Color Support Full color Color or Grayscale
Detection Resistance Moderate Better
Best For Email, cloud storage Social media, messaging
Max Tested Image 14MB+ 14MB+

Security Considerations

What Makes Stegasoo Secure?

MULTI-FACTOR AUTHENTICATION (v4.0)
──────────────────────────────────
Factor 1: Reference Photo    ─┐
  • 80-256 bits entropy       │
  • "Something you have"      │
                              ├──► Combined entropy: 133-400+ bits
Factor 2: Passphrase          │    (Beyond brute force)
  • 43-132 bits entropy       │
  • "Something you know"      │
  • 4 words default (v4.0)    │
                              │
Factor 3: PIN                 │
  • 20-30 bits entropy        │
  • "Something you know"      │
                              │
Factor 4: RSA Key (optional) ─┘
  • 112-128 bits entropy
  • "Something you have"

MEMORY-HARD KDF (Argon2id)
──────────────────────────
• 256 MB RAM per attempt
• ~3 seconds per attempt
• Defeats GPU/ASIC attacks
• 10 attempts = 30 seconds, not 0.00001 seconds

AUTHENTICATED ENCRYPTION (AES-256-GCM)
──────────────────────────────────────
• 256-bit key (unbreakable)
• Built-in integrity check
• Detects tampering
• No padding oracle attacks

Attack Surface Analysis

Attack LSB Protection DCT Protection
Visual inspection Imperceptible Imperceptible
File size analysis ⚠️ PNG larger JPEG natural
Histogram analysis ⚠️ Slight anomalies Normal JPEG
Chi-square attack ⚠️ Detectable at scale Resistant
RS steganalysis ⚠️ Detectable Resistant
JPEG recompression Destroyed Survives

Threat Model

Stegasoo protects against:

  • Passive eavesdropping
  • Casual inspection of images
  • Basic forensic analysis
  • Brute force key guessing
  • JPEG recompression (DCT mode)

Stegasoo does NOT protect against:

  • ⚠️ Targeted forensic analysis with original carrier
  • ⚠️ Nation-state level steganalysis
  • ⚠️ Rubber hose cryptanalysis (coercion)
  • ⚠️ Compromise of reference photo or credentials

Data Flow Diagrams

Complete Encode Flow (v4.0)

┌──────────────────────────────────────────────────────────────────────────────┐
│                              ENCODE FLOW (v4.0)                              │
└──────────────────────────────────────────────────────────────────────────────┘

User Inputs                    Processing                           Output
───────────                    ──────────                           ──────

Reference Photo ──────┐
                      ├──► get_image_hash() ──► ref_hash (32 bytes)
                      │         │
Passphrase ───────────┤         ▼
                      ├──► derive_key() ──────► aes_key (32 bytes)
PIN ──────────────────┤    (Argon2id)               │
                      │                             │
RSA Key (optional) ───┘                             │
                                                    ▼
Message/File ──────────► prepare_payload() ──► encrypt() ──► ciphertext
                         (compress, header)    (AES-GCM)         │
                                                                 │
                                                                 ▼
                                                    build_stego_header()
                                                    (magic + length)
                                                                 │
Carrier Image ─────────────────────────────────────────►  embed()
                                                          │     │
                                              ┌───────────┴─────┴────────────┐
                                              │                              │
                                           LSB Mode                       DCT Mode
                                              │                              │
                                              ▼                              ▼
                                         embed_lsb()                    embed_in_dct()
                                         (pixel LSBs)                 (DCT coefficients)
                                              │                              │
                                              ▼                              ▼
                                          PNG Output                    PNG or JPEG
                                              │                              │
                                              └──────────┬───────────────────┘
                                                         │
                                                         ▼
                                                   Stego Image
                                                   (downloadable)

Complete Decode Flow (v4.0)

┌──────────────────────────────────────────────────────────────────────────────┐
│                              DECODE FLOW (v4.0)                               │
└──────────────────────────────────────────────────────────────────────────────┘

User Inputs                    Processing                           Output
───────────                    ──────────                           ──────

Reference Photo ──────┐
                      ├──► get_image_hash() ──► ref_hash (32 bytes)
                      │         │
Passphrase ───────────┤         ▼
                      ├──► derive_key() ──────► aes_key (32 bytes)
PIN ──────────────────┤    (Argon2id)               │
                      │    (MUST MATCH!)            │
RSA Key (optional) ───┘                             │
                                                    │
                                                    ▼
Stego Image ──────────► detect_mode() ──────► extract()
                        (read magic)          │     │
                              │     ┌─────────┴─────┴──────────┐
                              │     │                          │
                              │  LSB Mode                DCT Mode
                              │     │                          │
                              │     ▼                          ▼
                              │  extract_lsb()          extract_from_dct()
                              │     │                          │
                              │     └────────┬─────────────────┘
                              │              │
                              │              ▼
                              │     parse_stego_header()
                              │     (magic, length)
                              │              │
                              │              ▼
                              └────────► decrypt()
                                         (AES-GCM)
                                              │
                                              ▼
                                     decompress()
                                     (if compressed)
                                              │
                                              ▼
                                     extract_payload()
                                     (handle file/text)
                                              │
                                              ▼
                                     Original Message
                                     or File

Summary

LSB Mode is simpler, faster, and higher capacity - perfect for controlled channels where images won't be modified.

DCT Mode is more complex but survives real-world image processing - essential for social media and messaging apps.

Both modes share the same cryptographic foundation (Argon2id + AES-256-GCM) and multi-factor authentication, ensuring security regardless of embedding method.

The choice comes down to your use case:

  • Public platform? → DCT (maximum compatibility)
  • Private channel? → LSB (maximum capacity)

v4.0 Simplifications

  • No more date tracking - encode/decode anytime without remembering dates
  • Single passphrase - no daily rotation to manage
  • Default 4 words - better security out of the box
  • JPEG normalization - handles quality=100 images automatically
  • Large image support - tested with 14MB+ images