# 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](#high-level-overview)
2. [The Encoding Pipeline](#the-encoding-pipeline)
3. [The Decoding Pipeline](#the-decoding-pipeline)
4. [LSB Mode Deep Dive](#lsb-mode-deep-dive)
5. [DCT Mode Deep Dive](#dct-mode-deep-dive)
6. [Comparison Table](#comparison-table)
7. [Security Considerations](#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

```python
# 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

```python
# 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)

```python
# 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

```python
# 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

```python
# 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

```python
# 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

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

```python
# 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

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

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

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

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

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

```python
# 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

```python
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)

```python
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)

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

```python
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:
- **Private channel?** → LSB (maximum capacity)
- **Public platform?** → DCT (maximum compatibility)

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