From ef7478b30a00644c7853d75599642f0b9ba0b187 Mon Sep 17 00:00:00 2001 From: "Aaron D. Lee" Date: Thu, 1 Jan 2026 22:18:13 -0500 Subject: [PATCH] A whoooole lotta 4.0.x fixes. --- .python-version | 1 + Dockerfile | 86 +- Dockerfile.base | 55 ++ INSTALL.md | 251 +++--- README.md | 177 ++-- SECURITY.md | 123 ++- UNDER_THE_HOOD.md | 679 +++++++------- build.sh | 61 ++ check_scipy.py | 170 ++++ debug_jpegio.py | 215 +++++ frontends/api/main.py | 16 +- frontends/cli/main.py | 36 +- frontends/web/README_subprocess.md | 62 ++ frontends/web/app.py | 201 ++++- frontends/web/stego_worker.py | 191 ++++ frontends/web/subprocess_stego.py | 425 +++++++++ frontends/web/templates/about.html | 226 ++--- frontends/web/templates/decode.html | 267 ++++-- frontends/web/templates/encode.html | 116 ++- frontends/web/templates/encode_result.html | 2 +- frontends/web/templates/generate.html | 1 - frontends/web/templates/index.html | 14 +- frontends/web/test_routes.py | 90 ++ minimal_flask_crash.py | 289 ++++++ pyproject.toml | 2 +- requirements.txt | 5 + src/stegasoo/constants.py | 2 +- src/stegasoo/dct_steganography.py | 638 +++++++------- src/stegasoo/dct_steganography.py_old | 974 +++++++++++++++++++++ src/stegasoo/steganography.py | 68 +- src/stegasoo/steganography.py_old | 878 +++++++++++++++++++ test_compare_capacity_flow.py | 205 +++++ test_data/1mb-jpg-example-file.jpg | Bin 0 -> 1155582 bytes test_data/2mb-jpg-example-file.jpg | Bin 0 -> 2423235 bytes test_dct_crash.py | 231 +++++ tests/RELEASE_CHECKLIST_V3.2.0.md | 525 ----------- tests/RELEASE_CHECKLIST_V4_0_0.md | 528 +++++++++++ tests/test_batch.py | 4 +- tests/test_stegasoo.py | 19 +- xx_2.jpg | Bin 0 -> 5299516 bytes 40 files changed, 6003 insertions(+), 1830 deletions(-) create mode 100644 .python-version create mode 100644 Dockerfile.base create mode 100755 build.sh create mode 100644 check_scipy.py create mode 100644 debug_jpegio.py create mode 100644 frontends/web/README_subprocess.md create mode 100644 frontends/web/stego_worker.py create mode 100644 frontends/web/subprocess_stego.py create mode 100644 frontends/web/test_routes.py create mode 100644 minimal_flask_crash.py create mode 100644 src/stegasoo/dct_steganography.py_old create mode 100644 src/stegasoo/steganography.py_old create mode 100644 test_compare_capacity_flow.py create mode 100644 test_data/1mb-jpg-example-file.jpg create mode 100644 test_data/2mb-jpg-example-file.jpg create mode 100644 test_dct_crash.py delete mode 100644 tests/RELEASE_CHECKLIST_V3.2.0.md create mode 100644 tests/RELEASE_CHECKLIST_V4_0_0.md create mode 100644 xx_2.jpg diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..92536a9 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12.0 diff --git a/Dockerfile b/Dockerfile index c6c642a..8f7bf95 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,32 @@ # Stegasoo Docker Image -# Multi-stage build for smaller image size +# Uses pre-built base image for fast rebuilds +# +# First time setup: +# docker build -f Dockerfile.base -t stegasoo-base:latest . +# +# Then build normally (fast!): +# docker-compose build +# +# Or if you don't have the base image, this falls back to building deps +# (slow, but works) -# Pin the base image digest for reproducibility -# To update: docker manifest inspect python:3.11-slim -v | jq -r '.[0].Descriptor.digest' -FROM python:3.11-slim@sha256:5501a4fe605abe24de87c2f3d6cf9fd760354416a0cad0296cf284fddcdca9e2 as base +# ============================================================================ +# ARG to switch between base image and full build +# ============================================================================ +ARG USE_BASE_IMAGE=true + +# ============================================================================ +# Base stage - use pre-built image if available +# ============================================================================ +FROM stegasoo-base:latest AS base-prebuilt + +FROM python:3.12-slim AS base-full -# Set environment variables ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 -# Suppress pip "running as root" warnings during build ENV PIP_ROOT_USER_ACTION=ignore # Install system dependencies -# NOTE: g++ is required for jpegio C++ compilation -# NOTE: libjpeg-dev is required for jpegio RUN apt-get update && apt-get install -y --no-install-recommends \ gcc \ g++ \ @@ -21,37 +34,30 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libffi-dev \ libzbar0 \ libjpeg-dev \ + zlib1g-dev \ && rm -rf /var/lib/apt/lists/* +# Install ALL dependencies (slow path) +RUN pip install --no-cache-dir \ + cython numpy scipy>=1.10.0 jpegio>=0.2.0 \ + argon2-cffi>=23.0.0 pillow>=10.0.0 cryptography>=41.0.0 \ + flask>=3.0.0 gunicorn>=21.0.0 \ + fastapi>=0.100.0 "uvicorn[standard]>=0.20.0" python-multipart>=0.0.6 \ + qrcode>=7.3.0 pyzbar>=0.1.9 click>=8.0.0 lz4>=4.0.0 + # ============================================================================ -# Builder stage - install Python packages +# Select which base to use (default: prebuilt) # ============================================================================ -FROM base as builder - -WORKDIR /build - -# Copy package files (including README.md which pyproject.toml references) -COPY pyproject.toml README.md ./ -COPY src/ src/ -COPY data/ data/ - -# Install build dependencies for jpegio, then install the package -# jpegio requires Cython and numpy to compile -RUN pip install --no-cache-dir cython numpy && \ - pip install --no-cache-dir ".[web]" +FROM base-prebuilt AS base # ============================================================================ # Production stage - Web UI # ============================================================================ -FROM base as web +FROM base AS web WORKDIR /app -# Copy installed packages from builder -COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages -COPY --from=builder /usr/local/bin /usr/local/bin - -# Copy application files +# Copy application files (this is all that rebuilds normally!) COPY src/ src/ COPY data/ data/ COPY frontends/web/ frontends/web/ @@ -75,25 +81,18 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ # Run with gunicorn WORKDIR /app/frontends/web -CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "--threads", "4", "--timeout", "60", "app:app"] +CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "--threads", "4", "--timeout", "120", "app:app"] # ============================================================================ # API stage - REST API # ============================================================================ -FROM base as api +FROM base AS api WORKDIR /app -# Install API extras (includes DCT dependencies) -COPY pyproject.toml README.md ./ +# Copy application files COPY src/ src/ COPY data/ data/ - -# Install build dependencies for jpegio, then install the package -RUN pip install --no-cache-dir cython numpy && \ - pip install --no-cache-dir ".[api]" - -# Copy API files COPY frontends/api/ frontends/api/ # Create non-root user @@ -117,20 +116,13 @@ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] # ============================================================================ # CLI stage - Command line tool # ============================================================================ -FROM base as cli +FROM base AS cli WORKDIR /app -# Install CLI extras -COPY pyproject.toml README.md ./ +# Copy application files COPY src/ src/ COPY data/ data/ - -# Install build dependencies for jpegio (if dct extras needed), then install -RUN pip install --no-cache-dir cython numpy && \ - pip install --no-cache-dir ".[cli,dct]" - -# Copy CLI files COPY frontends/cli/ frontends/cli/ # Create non-root user diff --git a/Dockerfile.base b/Dockerfile.base new file mode 100644 index 0000000..4fa4e2a --- /dev/null +++ b/Dockerfile.base @@ -0,0 +1,55 @@ +# Stegasoo Base Image +# Contains all slow-to-compile dependencies (jpegio, scipy, argon2) +# Build once: docker build -f Dockerfile.base -t stegasoo-base:latest . +# Push to registry for team use: docker push yourregistry/stegasoo-base:latest + +FROM python:3.12-slim + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV PIP_ROOT_USER_ACTION=ignore + +# Install system dependencies +# NOTE: g++ is required for jpegio C++ compilation +# NOTE: libjpeg-dev is required for jpegio +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + g++ \ + libc-dev \ + libffi-dev \ + libzbar0 \ + libjpeg-dev \ + zlib1g-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install the slow-to-compile packages +# These rarely change, so they get cached in this base image +RUN pip install --no-cache-dir \ + cython \ + numpy \ + scipy>=1.10.0 \ + jpegio>=0.2.0 \ + argon2-cffi>=23.0.0 \ + pillow>=10.0.0 \ + cryptography>=41.0.0 + +# Install web/api framework packages (also stable) +RUN pip install --no-cache-dir \ + flask>=3.0.0 \ + gunicorn>=21.0.0 \ + fastapi>=0.100.0 \ + "uvicorn[standard]>=0.20.0" \ + python-multipart>=0.0.6 \ + qrcode>=7.3.0 \ + pyzbar>=0.1.9 \ + click>=8.0.0 \ + lz4>=4.0.0 + +# Verify key packages work +RUN python -c "import jpegio; import scipy; import numpy; print('jpegio + scipy + numpy OK')" + +# Label for tracking +LABEL org.opencontainers.image.title="Stegasoo Base" +LABEL org.opencontainers.image.description="Pre-compiled dependencies for Stegasoo" +LABEL org.opencontainers.image.version="4.0.0" diff --git a/INSTALL.md b/INSTALL.md index b497525..4e49757 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -12,8 +12,6 @@ Complete installation instructions for all platforms and deployment methods. - [Docker](#docker) - [Docker Compose](#docker-compose) - [Optional Dependencies](#optional-dependencies) - - [DCT Steganography (scipy + jpegio)](#dct-steganography-scipy--jpegio) - - [Compression (lz4)](#compression-lz4) - [Platform-Specific Notes](#platform-specific-notes) - [Verification](#verification) - [Troubleshooting](#troubleshooting) @@ -22,11 +20,22 @@ Complete installation instructions for all platforms and deployment methods. ## Requirements +### ⚠️ Python Version Requirements + +| Python Version | Status | Notes | +|----------------|--------|-------| +| 3.10 | ✅ Supported | | +| 3.11 | ✅ Supported | Recommended | +| 3.12 | ✅ Supported | Recommended | +| 3.13 | ❌ **Not Supported** | jpegio C extension incompatible | + +**Important:** Python 3.13 (released October 2024) is **not compatible** with jpegio due to C extension ABI changes. Use Python 3.12 or earlier. + ### Minimum Requirements -| Requirement | Version | -|-------------|---------| -| Python | 3.10+ | +| Requirement | Value | +|-------------|-------| +| Python | 3.10-3.12 | | RAM | 512 MB minimum (256MB for Argon2) | | Disk | ~100 MB | @@ -36,7 +45,8 @@ Complete installation instructions for all platforms and deployment methods. ```bash sudo apt-get update sudo apt-get install -y \ - python3 \ + python3.12 \ + python3.12-venv \ python3-pip \ python3-dev \ libzbar0 \ @@ -44,14 +54,24 @@ sudo apt-get install -y \ build-essential ``` +**Linux (Arch):** +```bash +# Use pyenv for Python version management +curl https://pyenv.run | bash +pyenv install 3.12 +pyenv local 3.12 + +sudo pacman -S zbar libjpeg-turbo base-devel +``` + **macOS:** ```bash -brew install python@3.11 zbar jpeg +brew install python@3.12 zbar jpeg xcode-select --install # For compilation ``` **Windows:** -- Install Python 3.10+ from [python.org](https://python.org) +- Install Python 3.12 from [python.org](https://python.org) - Install Visual Studio Build Tools for compilation --- @@ -62,10 +82,18 @@ xcode-select --install # For compilation # Clone and install everything git clone https://github.com/adlee-was-taken/stegasoo.git cd stegasoo + +# Create venv with Python 3.12 (critical!) +python3.12 -m venv venv +source venv/bin/activate # Linux/macOS +# or: venv\Scripts\activate # Windows + +# Install all dependencies pip install -e ".[all]" # Verify stegasoo --version +python -c "from stegasoo import has_dct_support; print(f'DCT: {has_dct_support()}')" ``` --- @@ -81,11 +109,14 @@ Best for development or customization. git clone https://github.com/adlee-was-taken/stegasoo.git cd stegasoo -# Create virtual environment (recommended) -python -m venv venv +# Create virtual environment with Python 3.12 (recommended) +python3.12 -m venv venv source venv/bin/activate # Linux/macOS # or: venv\Scripts\activate # Windows +# Verify Python version +python -V # Should show 3.12.x + # Install core library only pip install -e . @@ -161,7 +192,7 @@ docker run -it --rm stegasoo-cli /bin/bash # Run commands directly docker run --rm stegasoo-cli --help -docker run --rm stegasoo-cli generate --pin --words 3 +docker run --rm stegasoo-cli generate --pin --words 4 # With volume for files docker run --rm \ @@ -169,7 +200,7 @@ docker run --rm \ stegasoo-cli encode \ -r /data/ref.jpg \ -c /data/carrier.png \ - -p "phrase words here" \ + -p "passphrase words here more" \ --pin 123456 \ -m "Secret message" \ -o /data/stego.png @@ -237,77 +268,6 @@ Adjust based on your available RAM: | 4 GB | 1G | 3 | | 8 GB+ | 1.5G | 4 | -#### Development Mode - -For development with hot-reload: - -```bash -# Create docker-compose.override.yml -cat > docker-compose.override.yml << 'EOF' -version: '3.8' -services: - web: - volumes: - - ./src:/app/src:ro - - ./frontends/web:/app/frontends/web:ro - environment: - - FLASK_ENV=development - command: ["python", "app.py"] - api: - volumes: - - ./src:/app/src:ro - - ./frontends/api:/app/frontends/api:ro - command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] -EOF - -# Start with override -docker-compose up -``` - -#### Production with Nginx - -For production deployment with SSL: - -```yaml -# docker-compose.prod.yml -version: '3.8' - -services: - web: - build: - context: . - target: web - expose: - - "5000" - restart: always - - api: - build: - context: . - target: api - expose: - - "8000" - restart: always - - nginx: - image: nginx:alpine - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx.conf:/etc/nginx/nginx.conf:ro - - ./certs:/etc/nginx/certs:ro - depends_on: - - web - - api - restart: always -``` - -Run with: -```bash -docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d -``` - --- ## Optional Dependencies @@ -320,9 +280,9 @@ DCT mode enables JPEG-resilient steganography. It's automatically included with ```bash # scipy is straightforward -pip install scipy +pip install scipy numpy -# jpegio - try pip first +# jpegio - MUST use Python 3.12 or earlier! pip install jpegio # If pip fails, build from source @@ -382,19 +342,37 @@ Most straightforward installation. Use your package manager for system dependenc **Ubuntu/Debian:** ```bash -sudo apt-get install python3-dev libzbar0 libjpeg-dev +sudo apt-get install python3.12 python3.12-venv python3-dev libzbar0 libjpeg-dev +python3.12 -m venv venv +source venv/bin/activate pip install stegasoo[all] ``` **Fedora/RHEL:** ```bash -sudo dnf install python3-devel zbar libjpeg-devel +sudo dnf install python3.12 python3-devel zbar libjpeg-devel +python3.12 -m venv venv +source venv/bin/activate pip install stegasoo[all] ``` -**Arch:** +**Arch (using pyenv):** ```bash -sudo pacman -S python zbar libjpeg-turbo +# Install pyenv +curl https://pyenv.run | bash + +# Add to ~/.bashrc or ~/.zshrc +export PATH="$HOME/.pyenv/bin:$PATH" +eval "$(pyenv init -)" + +# Install Python 3.12 +pyenv install 3.12 +cd ~/Sources/stegasoo +pyenv local 3.12 + +# Create venv and install +python -m venv venv +source venv/bin/activate pip install stegasoo[all] ``` @@ -405,46 +383,39 @@ pip install stegasoo[all] /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" # Install dependencies -brew install python@3.11 zbar jpeg +brew install python@3.12 zbar jpeg + +# Create venv +python3.12 -m venv venv +source venv/bin/activate # Install Stegasoo -pip3 install stegasoo[all] +pip install stegasoo[all] ``` **Apple Silicon (M1/M2/M3):** -jpegio may need Rosetta or native compilation: +jpegio may need native compilation: ```bash -# Try native first +# Ensure you have native Python +arch -arm64 brew install python@3.12 +arch -arm64 python3.12 -m venv venv +source venv/bin/activate pip install jpegio - -# If fails, ensure you have native Python -arch -arm64 brew install python@3.11 -arch -arm64 pip3 install jpegio ``` ### Windows -1. Install Python 3.10+ from [python.org](https://python.org) +1. Install Python 3.12 from [python.org](https://python.org) (NOT 3.13!) 2. Install Visual Studio Build Tools 3. Install from pip: ```powershell +python -m venv venv +.\venv\Scripts\activate pip install stegasoo[all] ``` -**For jpegio on Windows:** -```powershell -# May need pre-built wheel -pip install jpegio - -# If fails, install build tools and compile -pip install cython numpy -git clone https://github.com/dwgoon/jpegio.git -cd jpegio -python setup.py install -``` - ### Raspberry Pi Stegasoo works on Raspberry Pi 4 (2GB+ RAM recommended): @@ -474,6 +445,9 @@ stegasoo --version # Python import python -c "import stegasoo; print(stegasoo.__version__)" + +# Check Python version (must be 3.10-3.12) +python -V ``` ### Check All Features @@ -482,6 +456,8 @@ python -c "import stegasoo; print(stegasoo.__version__)" #!/usr/bin/env python3 """Verify Stegasoo installation.""" +import sys + def check_feature(name, check_fn): try: result = check_fn() @@ -495,9 +471,20 @@ def check_feature(name, check_fn): print("Stegasoo Installation Check") print("=" * 40) +# Python version check +py_version = sys.version_info +print(f"\nPython: {py_version.major}.{py_version.minor}.{py_version.micro}") +if py_version >= (3, 13): + print(" ⚠️ WARNING: Python 3.13+ not supported!") + print(" jpegio will not work. Use Python 3.12.") +elif py_version >= (3, 10): + print(" ✓ Python version OK") +else: + print(" ✗ Python 3.10+ required") + # Core import stegasoo -print(f"\nVersion: {stegasoo.__version__}") +print(f"\nStegasoo Version: {stegasoo.__version__}") print("\nCore Features:") check_feature("Argon2", lambda: stegasoo.has_argon2()) @@ -556,7 +543,7 @@ python check_install.py ```bash # Quick test with CLI -stegasoo generate --pin --words 3 --json > /tmp/creds.json +stegasoo generate --pin --words 4 --json > /tmp/creds.json # Create test image python -c " @@ -570,7 +557,7 @@ img.save('/tmp/test_ref.jpg') stegasoo encode \ -r /tmp/test_ref.jpg \ -c /tmp/test_carrier.png \ - -p "test phrase words" \ + -p "test phrase words here" \ --pin 123456 \ -m "Hello, Stegasoo!" \ -o /tmp/test_stego.png @@ -579,7 +566,7 @@ stegasoo encode \ stegasoo decode \ -r /tmp/test_ref.jpg \ -s /tmp/test_stego.png \ - -p "test phrase words" \ + -p "test phrase words here" \ --pin 123456 ``` @@ -589,6 +576,25 @@ stegasoo decode \ ### Common Issues +#### "jpegio crashes" / "free(): invalid size" / Core dump + +**This is the #1 issue!** You're using Python 3.13. + +```bash +# Check your Python version +python -V + +# If it shows 3.13, you need to use 3.12 +# Option 1: Use pyenv +pyenv install 3.12 +pyenv local 3.12 + +# Option 2: Use system Python 3.12 +python3.12 -m venv venv +source venv/bin/activate +pip install -e ".[all]" +``` + #### "No module named 'stegasoo'" ```bash @@ -611,7 +617,7 @@ sudo apt-get install libffi-dev pip install --force-reinstall argon2-cffi ``` -#### "jpegio not available" / DCT JPEG fails +#### "jpegio not available" (not crash, just missing) ```bash # Install build dependencies first @@ -656,23 +662,12 @@ deploy: memory: 768M ``` -#### Docker: Build fails on jpegio - -The Dockerfile includes jpegio build dependencies. If still failing: - -```bash -# Rebuild without cache -docker-compose build --no-cache - -# Or build manually -docker build --no-cache -t stegasoo-web --target web . -``` - #### Slow performance - **Argon2 is intentionally slow** - This is a security feature - Expected encode/decode time: 2-5 seconds - DCT mode adds ~1-2 seconds for transforms +- Large images (10MB+) may take 15-30 seconds #### "Carrier image too small" @@ -704,7 +699,7 @@ docker build --no-cache -t stegasoo-web --target web . After installation: -1. **Generate credentials**: `stegasoo generate --pin --words 3` +1. **Generate credentials**: `stegasoo generate --pin --words 4` 2. **Read the CLI docs**: [CLI.md](CLI.md) 3. **Try the Web UI**: `cd frontends/web && python app.py` 4. **Explore the API**: `cd frontends/api && python main.py` diff --git a/README.md b/README.md index 0fbfc54..3dc5ffa 100644 --- a/README.md +++ b/README.md @@ -2,39 +2,47 @@ A secure steganography system for hiding encrypted messages in images using hybrid authentication. -![Python](https://img.shields.io/badge/Python-3.10+-blue) +![Python](https://img.shields.io/badge/Python-3.10--3.12-blue) ![License](https://img.shields.io/badge/License-MIT-green) ![Security](https://img.shields.io/badge/Security-AES--256--GCM-red) -![Version](https://img.shields.io/badge/Version-3.0.2-purple) +![Version](https://img.shields.io/badge/Version-4.0.0-purple) ## Features - 🔐 **AES-256-GCM** authenticated encryption - 🧠 **Argon2id** memory-hard key derivation (256MB RAM requirement) - 🎲 **Pseudo-random pixel selection** defeats steganalysis -- 📅 **Daily key rotation** with BIP-39 passphrases - 🔑 **Multi-factor authentication**: PIN, RSA key, or both - 🖼️ **Reference photo** as "something you have" - 🌐 **Multiple interfaces**: CLI, Web UI, REST API - 📁 **File embedding** - Hide any file type (PDF, ZIP, documents) - 📱 **QR code support** - Encode/decode RSA keys via QR codes -- 🆕 **DCT steganography** - JPEG-resilient embedding for social media (v3.0+) +- 🆕 **DCT steganography** - JPEG-resilient embedding for social media +- 🆕 **Large image support** - Process images up to 14MB+ -## What's New in v3.0.2 +## What's New in v4.0.0 | Feature | Description | |---------|-------------| -| **DCT Mode** | Frequency-domain embedding survives JPEG recompression | -| **JPEG Output** | Native JPEG output using jpegio library | -| **Color Preservation** | DCT color mode preserves carrier image colors | -| **Auto-Detection** | Decoder automatically detects LSB vs DCT mode | +| **Simplified Auth** | Removed date dependency - encode/decode anytime without tracking dates | +| **Passphrase** | Renamed from "day phrase" to "passphrase" (no more daily rotation) | +| **Python 3.12** | Requires Python 3.10-3.12 (jpegio incompatible with 3.13) | +| **Large Image Fix** | JPEG normalization prevents crashes with quality=100 images | +| **Subprocess Isolation** | WebUI runs encode/decode in subprocesses for stability | +| **4-Word Default** | Default passphrase increased from 3 to 4 words | + +### Breaking Changes from v3.x + +- `day_phrase` parameter renamed to `passphrase` in all APIs +- `date_str` parameter removed from encode/decode functions +- Python 3.13 not supported (jpegio C extension incompatibility) ### Embedding Mode Comparison | Mode | Capacity (1080p) | JPEG Resilient | Best For | |------|------------------|----------------|----------| | **LSB** (default) | ~770 KB | ❌ No | Email, file transfer | -| **DCT** (experimental) | ~65 KB | ✅ Yes | Social media, messaging apps | +| **DCT** | ~65 KB | ✅ Yes | Social media, messaging apps | ## WebUI Preview @@ -45,35 +53,36 @@ A secure steganography system for hiding encrypted messages in images using hybr ## Quick Start ```bash -# Install with all features +# Install with all features (requires Python 3.10-3.12) pip install -e ".[all]" # Generate credentials (memorize these!) -stegasoo generate --pin --words 3 +stegasoo generate --pin --words 4 # Encode a message (LSB mode - default) stegasoo encode \ --ref photo.jpg \ --carrier meme.png \ - --phrase "apple forest thunder" \ + --passphrase "apple forest thunder mountain" \ --pin 123456 \ --message "Secret message" # Encode for social media (DCT mode) stegasoo encode \ --ref photo.jpg \ - --carrier meme.png \ - --phrase "apple forest thunder" \ + --carrier meme.jpg \ + --passphrase "apple forest thunder mountain" \ --pin 123456 \ --message "Secret message" \ --mode dct \ - --format jpeg + --dct-format jpeg \ + --dct-color color # Decode (auto-detects mode) stegasoo decode \ --ref photo.jpg \ --stego stego.png \ - --phrase "apple forest thunder" \ + --passphrase "apple forest thunder mountain" \ --pin 123456 ``` @@ -93,8 +102,8 @@ Stegasoo uses multiple authentication factors combined with strong cryptography: │ Reference Photo ──┐ │ │ (~80-256 bits) │ │ │ ├──► Argon2id KDF ──► AES-256-GCM Key │ -│ Day Phrase ───────┤ (256MB RAM) │ -│ (~33-132 bits) │ │ +│ Passphrase ───────┤ (256MB RAM) │ +│ (~43-132 bits) │ │ │ │ │ │ Static PIN ───────┤ │ │ (~20-30 bits) │ │ @@ -110,8 +119,8 @@ Stegasoo uses multiple authentication factors combined with strong cryptography: | Component | Entropy | Purpose | |-----------|---------|---------| | Reference Photo | ~80-256 bits | Something you have | -| Day Phrase (3-12 words) | ~33-132 bits | Something you know (rotates daily) | -| PIN (6-9 digits) | ~20-30 bits | Something you know (static) | +| Passphrase (3-12 words) | ~33-132 bits | Something you know | +| PIN (6-9 digits) | ~20-30 bits | Something you know | | RSA Key (2048-4096 bit) | ~112-128 bits | Something you have (optional) | | **Combined** | **133-400+ bits** | **Beyond brute force** | @@ -124,16 +133,16 @@ Stegasoo uses multiple authentication factors combined with strong cryptography: | Steganalysis | Pseudo-random pixel/coefficient selection | | GPU cracking | Argon2id requires 256MB RAM per attempt | | Side-channel | Constant-time operations in cryptography library | -| JPEG recompression | DCT mode embeds in frequency domain (v3.0+) | +| JPEG recompression | DCT mode embeds in frequency domain | ### Security Configurations | Configuration | Entropy | Use Case | |--------------|---------|----------| -| 3-word phrase + 6-digit PIN | ~133 bits | Casual private messaging | -| 6-word phrase + 9-digit PIN | ~176 bits | Standard security | -| 3-word phrase + RSA 2048 | ~241 bits | File-based authentication | -| 6-word phrase + PIN + RSA 4096 | ~304 bits | Maximum security | +| 3-word passphrase + 6-digit PIN | ~133 bits | Casual private messaging | +| 4-word passphrase + 9-digit PIN | ~176 bits | Standard security (recommended) | +| 4-word passphrase + RSA 2048 | ~241 bits | File-based authentication | +| 6-word passphrase + PIN + RSA 4096 | ~304 bits | Maximum security | --- @@ -148,17 +157,20 @@ Full-featured CLI with piping support: stegasoo generate --rsa --rsa-bits 4096 -o mykey.pem -p "password" # Encode from file -stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 -f secret.txt +stegasoo encode -r ref.jpg -c carrier.png -p "passphrase words here" --pin 123456 -f secret.txt -# Encode for social media (DCT + JPEG) -stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 \ - -m "Message" --mode dct --format jpeg +# Encode for social media (DCT + JPEG with color preservation) +stegasoo encode -r ref.jpg -c carrier.jpg -p "passphrase words here" --pin 123456 \ + -m "Message" --mode dct --dct-format jpeg --dct-color color # Decode to stdout (quiet mode) -stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q +stegasoo decode -r ref.jpg -s stego.png -p "passphrase words here" --pin 123456 -q -# Check image capacity (shows both LSB and DCT) -stegasoo info carrier.png +# Compare LSB vs DCT capacity for an image +stegasoo compare carrier.png + +# Check available modes +stegasoo modes ``` 📖 Full documentation: **[CLI.md](CLI.md)** @@ -179,7 +191,8 @@ Features: - Real-time entropy calculator - Native mobile sharing (Web Share API) - DCT mode with advanced options panel -- Automatic day-of-week detection +- Subprocess isolation for stability +- Large image support (14MB+ tested) 📖 Full documentation: **[WEB_UI.md](WEB_UI.md)** @@ -200,22 +213,23 @@ Example API calls: # Generate credentials curl -X POST http://localhost:8000/generate \ -H "Content-Type: application/json" \ - -d '{"use_pin": true, "words_per_phrase": 3}' + -d '{"use_pin": true, "passphrase_words": 4}' # Encode with DCT mode curl -X POST http://localhost:8000/encode/multipart \ -F "message=Secret" \ - -F "day_phrase=apple forest thunder" \ + -F "passphrase=apple forest thunder mountain" \ -F "pin=123456" \ - -F "embedding_mode=dct" \ - -F "output_format=jpeg" \ + -F "embed_mode=dct" \ + -F "dct_output_format=jpeg" \ + -F "dct_color_mode=color" \ -F "reference_photo=@photo.jpg" \ - -F "carrier=@meme.png" \ + -F "carrier=@meme.jpg" \ --output stego.jpg # Decode (auto-detects mode) curl -X POST http://localhost:8000/decode/multipart \ - -F "day_phrase=apple forest thunder" \ + -F "passphrase=apple forest thunder mountain" \ -F "pin=123456" \ -F "reference_photo=@photo.jpg" \ -F "stego_image=@stego.jpg" @@ -234,7 +248,7 @@ stegasoo/ │ ├── constants.py # Configuration │ ├── crypto.py # Encryption/decryption │ ├── steganography.py # LSB image embedding -│ ├── dct_steganography.py # DCT embedding (v3.0+) +│ ├── dct_steganography.py # DCT embedding │ ├── keygen.py # Credential generation │ ├── validation.py # Input validation │ ├── models.py # Data classes @@ -244,6 +258,9 @@ stegasoo/ │ ├── frontends/ │ ├── web/ # Flask web UI +│ │ ├── app.py +│ │ ├── subprocess_stego.py # Subprocess isolation +│ │ └── stego_worker.py # Worker script │ ├── cli/ # Command-line interface │ └── api/ # FastAPI REST API │ @@ -251,6 +268,7 @@ stegasoo/ │ └── bip39-words.txt # BIP-39 wordlist │ ├── pyproject.toml # Package configuration +├── requirements.txt # Dependencies ├── Dockerfile # Multi-stage Docker build ├── docker-compose.yml # Container orchestration │ @@ -258,22 +276,45 @@ stegasoo/ ├── INSTALL.md # Installation guide ├── CLI.md # CLI documentation ├── API.md # API documentation -└── WEB_UI.md # Web UI documentation +├── WEB_UI.md # Web UI documentation +├── SECURITY.md # Security documentation +└── UNDER_THE_HOOD.md # Technical deep-dive ``` --- +## Requirements + +| Requirement | Version | Notes | +|-------------|---------|-------| +| Python | 3.10-3.12 | **3.13 not supported** (jpegio incompatibility) | +| RAM | 512 MB+ | 256MB for Argon2 operations | +| Disk | ~100 MB | | + +### Key Dependencies + +| Package | Purpose | +|---------|---------| +| `cryptography` | AES-256-GCM encryption | +| `Pillow` | Image processing | +| `argon2-cffi` | Memory-hard key derivation | +| `scipy` | DCT transforms | +| `jpegio` | JPEG coefficient manipulation | +| `numpy` | Array operations | + +--- + ## Configuration ### Limits | Limit | Value | |-------|-------| -| Max image size | 4 megapixels | +| Max image size | Tested up to 14MB | | Max message size | 50 KB | | Max file upload | 5 MB | | PIN length | 6-9 digits | -| Phrase length | 3-12 words | +| Passphrase length | 3-12 words | | RSA key sizes | 2048, 3072, 4096 bits | ### Environment Variables @@ -302,8 +343,8 @@ ruff check src/ frontends/ mypy src/ # Check DCT support -python -c "from stegasoo import has_dct_support; print(has_dct_support())" -python -c "from stegasoo.dct_steganography import has_jpegio_support; print(has_jpegio_support())" +python -c "from stegasoo import has_dct_support; print(f'DCT: {has_dct_support()}')" +python -c "from stegasoo.dct_steganography import has_jpegio_support; print(f'jpegio: {has_jpegio_support()}')" ``` --- @@ -312,15 +353,51 @@ python -c "from stegasoo.dct_steganography import has_jpegio_support; print(has_ | Version | Changes | |---------|---------| -| **3.0.2** | Fixed JPEG output with jpegio integration | -| **3.0.1** | Added DCT color mode, JPEG output (broken) | -| **3.0.0** | Added DCT steganography mode | +| **4.0.0** | Removed date dependency, renamed day_phrase→passphrase, Python 3.12 requirement, JPEG normalization fix, subprocess isolation, large image support | +| **3.2.x** | DCT color mode, JPEG output fixes | +| **3.0.x** | Added DCT steganography mode | | **2.2.x** | QR code support, file embedding | | **2.0.x** | Web UI, REST API, RSA keys | | **1.0.x** | Initial release, CLI only | --- +## Upgrading from v3.x + +### Code Changes Required + +```python +# Old (v3.x) +result = encode( + message="secret", + day_phrase="apple forest thunder", + date_str="2024-01-15", + ... +) + +# New (v4.0) +result = encode( + message="secret", + passphrase="apple forest thunder mountain", + # No date_str needed! + ... +) +``` + +### CLI Changes + +```bash +# Old (v3.x) +stegasoo encode --phrase "words" --date 2024-01-15 ... + +# New (v4.0) +stegasoo encode --passphrase "words here more" ... +# or short form +stegasoo encode -p "words here more" ... +``` + +--- + ## License MIT License - Use responsibly. @@ -339,3 +416,5 @@ This tool is for educational and legitimate privacy purposes only. Users are res - **[CLI.md](CLI.md)** - Command-line interface reference - **[API.md](API.md)** - REST API documentation - **[WEB_UI.md](WEB_UI.md)** - Web interface guide +- **[SECURITY.md](SECURITY.md)** - Security model and threat analysis +- **[UNDER_THE_HOOD.md](UNDER_THE_HOOD.md)** - Technical implementation details diff --git a/SECURITY.md b/SECURITY.md index 5f3321b..0087517 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,10 +2,12 @@ ## Supported Versions -| Version | Supported | -| ------- | ------------------ | -| 2.x.x | :white_check_mark: | -| 1.x.x | :x: | +| Version | Supported | Notes | +| ------- | ------------------ | ----- | +| 4.x.x | ✅ Active | Current release | +| 3.x.x | ⚠️ Security fixes only | Upgrade recommended | +| 2.x.x | ❌ End of life | | +| 1.x.x | ❌ End of life | | ## Reporting a Vulnerability @@ -34,7 +36,7 @@ Stegasoo is designed to hide the **existence** of a secret message within an ord | Goal | How It's Achieved | |------|-------------------| | **Confidentiality** | AES-256-GCM encryption with Argon2id key derivation | -| **Steganography** | LSB embedding with pseudo-random pixel selection | +| **Steganography** | LSB/DCT embedding with pseudo-random pixel/coefficient selection | | **Authentication** | Multi-factor: reference photo + passphrase + PIN (or RSA key) | | **Integrity** | GCM authentication tag detects tampering | @@ -43,20 +45,43 @@ Stegasoo is designed to hide the **existence** of a secret message within an ord Stegasoo combines multiple authentication factors: ``` -┌─────────────────────────────────────────────────────────────┐ -│ Key Derivation │ -│ │ -│ Reference Photo ─────┐ │ -│ (something you have) │ │ -│ ├──► Argon2id ──► AES-256 Key │ -│ Day Passphrase ──────┤ (256MB RAM) │ -│ (something you know) │ │ -│ │ │ -│ PIN or RSA Key ──────┘ │ -│ (second factor) │ -└─────────────────────────────────────────────────────────────┘ +┌─────────────────────────────────────────────────────────────────┐ +│ Key Derivation │ +│ │ +│ Reference Photo ───────┐ │ +│ (something you have) │ │ +│ ├──► Argon2id ──► AES-256 Key │ +│ Passphrase ────────────┤ (256MB RAM) │ +│ (something you know) │ │ +│ │ │ +│ PIN or RSA Key ────────┘ │ +│ (second factor) │ +└─────────────────────────────────────────────────────────────────┘ ``` +## Changes in v4.0 + +### Removed: Date-Based Key Rotation + +**Previous versions (v3.x and earlier):** +- Required a date parameter for encode/decode +- Keys rotated daily based on "day phrase" +- Users had to remember which date they used + +**Version 4.0:** +- No date dependency +- Single passphrase (no rotation) +- Simpler but slightly reduced entropy per-message + +**Security Impact:** +- Minimal - the date only added ~10 bits of entropy +- Passphrase default increased from 3 to 4 words to compensate (+11 bits) +- Overall entropy remains similar or higher with 4-word default + +### Renamed: day_phrase → passphrase + +Terminology change only. No security impact. + ## What Stegasoo Does NOT Protect Against ### 1. Statistical Steganalysis @@ -68,7 +93,9 @@ Stegasoo combines multiple authentication factors: - RS analysis - Machine learning classifiers -**Mitigation:** Stegasoo uses pseudo-random pixel selection (not sequential), which helps but doesn't eliminate detectability. +**DCT mode is more resilient** but not undetectable. + +**Mitigation:** Stegasoo uses pseudo-random pixel/coefficient selection, which helps but doesn't eliminate detectability. **Recommendation:** Don't rely on Stegasoo if your adversary has: - Access to the original carrier image @@ -113,24 +140,28 @@ Stegasoo combines multiple authentication factors: **Recommendation:** - Use 8+ digit PINs -- Use 4+ word passphrases +- Use 4+ word passphrases (v4.0 default) - Consider RSA keys for high-security use cases ### 5. Image Modification **Risk:** Lossy compression destroys hidden data. -**Data is destroyed by:** +**LSB mode - data is destroyed by:** - JPEG compression - Resizing - Filters/effects - Screenshots -- Social media upload (Instagram, Twitter, etc.) +- Social media upload + +**DCT mode - more resilient but not immune:** +- Survives moderate JPEG recompression +- May fail with aggressive compression (quality < 70) +- Still destroyed by resizing, filters, screenshots **Recommendation:** -- Always use lossless formats (PNG, BMP) -- Transfer files directly (email, Signal, USB) -- Never upload stego images to social media +- LSB: Always use lossless formats (PNG, BMP), direct transfer +- DCT: Use for social media, but test with your specific platform ### 6. Metadata Leakage @@ -165,49 +196,52 @@ Stegasoo combines multiple authentication factors: | Encryption | AES-256-GCM | 12-byte IV, 16-byte tag | | Photo Hash | SHA-256 | Full image bytes | -### Pixel Selection +### Pixel/Coefficient Selection -Pixels are selected pseudo-randomly using a key derived from: +Selection key is derived from: ``` -pixel_key = SHA256(photo_hash || passphrase || date || pin/rsa_signature) +selection_key = SHA256(photo_hash || passphrase || pin/rsa_signature) ``` This prevents: - Sequential embedding patterns - Statistical detection of modified regions -### Format +### Message Format (v4.0) ``` -┌──────────────────────────────────────────────────────────────┐ -│ Magic (4B) │ Version (1B) │ Date (10B) │ Salt (32B) │ IV (12B) │ -├──────────────────────────────────────────────────────────────┤ -│ Encrypted Payload (AES-256-GCM) │ -│ ├── Type (1B): 0x01=text, 0x02=file │ -│ ├── Length (4B) │ -│ ├── Data (variable) │ -│ └── [Filename if file] (variable) │ -├──────────────────────────────────────────────────────────────┤ -│ GCM Auth Tag (16B) │ -└──────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────┐ +│ Magic (4B) │ Version (1B) │ Salt (32B) │ IV (12B) │ +├──────────────────────────────────────────────────────────────────┤ +│ Encrypted Payload (AES-256-GCM) │ +│ ├── Type (1B): 0x01=text, 0x02=file │ +│ ├── Length (4B) │ +│ ├── Data (variable) │ +│ └── [Filename if file] (variable) │ +├──────────────────────────────────────────────────────────────────┤ +│ GCM Auth Tag (16B) │ +└──────────────────────────────────────────────────────────────────┘ ``` +**Note:** v4.0 removed the date field from the header, reducing overhead by 10 bytes. + ## Best Practices ### For Maximum Security 1. **Use RSA keys** instead of PINs for authentication 2. **Use unique reference photos** not available online -3. **Use long passphrases** (4+ random words) +3. **Use long passphrases** (4+ random words, recommend 6+) 4. **Transfer via secure channels** (Signal, encrypted email) 5. **Delete stego images** after message is read 6. **Keep software updated** for security fixes +7. **Use DCT mode** for social media sharing ### For Casual Privacy 1. **6-digit PIN** is sufficient for non-adversarial use -2. **3-word passphrase** provides reasonable security -3. **PNG format** always for output +2. **4-word passphrase** provides reasonable security (v4.0 default) +3. **PNG format** for LSB mode output 4. **Direct file transfer** (email attachment, AirDrop) ## Known Limitations @@ -216,8 +250,8 @@ This prevents: |------------|--------|--------| | LSB is detectable | Statistical analysis can detect hidden data | By design (tradeoff for capacity) | | No forward secrecy | Compromised key decrypts all messages | Use different keys per message for high security | -| Date in header | Reveals when message was encoded | By design (enables day-specific passphrases) | | No deniability | Single password = single message | Future: plausible deniability layers | +| Python 3.13 incompatible | jpegio C extension crashes | Use Python 3.12 or earlier | ## Security Audit Status @@ -231,6 +265,9 @@ If you're a security researcher interested in auditing Stegasoo, please reach ou | Version | Security Changes | |---------|------------------| +| 4.0.0 | Removed date dependency, increased default passphrase to 4 words, added JPEG normalization | +| 3.2.0 | DCT color mode added | +| 3.0.0 | Added DCT steganography mode | | 2.2.0 | Added compression (no security impact) | | 2.1.0 | Upgraded to Argon2id, increased iterations | | 2.0.0 | Added RSA key support | diff --git a/UNDER_THE_HOOD.md b/UNDER_THE_HOOD.md index d121c57..42cbd80 100644 --- a/UNDER_THE_HOOD.md +++ b/UNDER_THE_HOOD.md @@ -2,6 +2,8 @@ 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 @@ -20,14 +22,14 @@ A detailed breakdown of how Stegasoo's LSB and DCT steganography modes work unde ``` ┌─────────────────────────────────────────────────────────────────────────────┐ -│ STEGASOO ARCHITECTURE │ +│ STEGASOO ARCHITECTURE (v4.0) │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ INPUTS PROCESSING OUTPUT │ │ ─────── ────────── ────── │ │ │ │ Reference Photo ─┐ │ -│ Day Phrase ──────┼──► Argon2id KDF ──► AES-256 Key │ +│ Passphrase ──────┼──► Argon2id KDF ──► AES-256 Key │ │ PIN/RSA Key ─────┘ │ │ │ ▼ │ │ Message/File ────────────────────────► AES-256-GCM ──► Ciphertext │ @@ -39,6 +41,15 @@ A detailed breakdown of how Stegasoo's LSB and DCT steganography modes work unde └─────────────────────────────────────────────────────────────────────────────┘ ``` +### 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 | @@ -58,10 +69,10 @@ A detailed breakdown of how Stegasoo's LSB and DCT steganography modes work unde ```python # validation.py -def validate_encode_inputs(reference_photo, carrier, message, day_phrase, pin, rsa_key): +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 day phrase (3-12 words from BIP-39) + # Validate passphrase (3-12 words from BIP-39) # Check payload size vs carrier capacity # Ensure reference != carrier (security) ``` @@ -88,27 +99,28 @@ def get_image_hash(image_bytes: bytes) -> bytes: ```python # crypto.py -def derive_key(reference_hash: bytes, day_phrase: str, pin: str, +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 + day_phrase.encode() + pin.encode() + 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: 3 + # - 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=3, + time_cost=4, memory_cost=262144, # 256 MB parallelism=4, hash_len=32, @@ -186,16 +198,19 @@ def encrypt(plaintext: bytes, key: bytes) -> bytes: 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: - # [8 bytes] - Magic number: "STGSOO" + version + mode + # [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'STGSOO\x03\x01' # v3, mode 1 (LSB) + magic = b'STGO\x04\x01' # v4, mode 1 (LSB) else: - magic = b'STGSOO\x03\x02' # v3, mode 2 (DCT) + magic = b'STGO\x04\x02' # v4, mode 2 (DCT) length = struct.pack('>I', len(encrypted_data)) @@ -210,452 +225,364 @@ This is where LSB and DCT diverge. See detailed sections below. ## The Decoding Pipeline -Decoding is essentially the reverse: - -``` -Stego Image ──► Extract Header ──► Detect Mode ──► Extract Data ──► Decrypt ──► Decompress ──► Output - │ │ - ▼ ▼ - Validate Magic LSB or DCT - Get Payload Size extraction -``` - ### Step 1: Mode Detection ```python -# __init__.py -def decode(stego_image: bytes, reference_photo: bytes, - day_phrase: str, pin: str, rsa_key: bytes = None) -> bytes: +def detect_mode(stego_image: bytes) -> str: """ - Auto-detect embedding mode and decode. + Detect which embedding mode was used. + Checks format and magic bytes. """ - # Try to read magic header from LSB positions first - header = extract_header_lsb(stego_image, seed=derive_seed(...)) + img = Image.open(io.BytesIO(stego_image)) - if header.startswith(b'STGSOO'): - version = header[6] - mode = header[7] - - if mode == 0x01: - return decode_lsb(...) - elif mode == 0x02: - return decode_dct(...) + # JPEG images with JPGS magic = DCT mode with jpegio + if img.format == 'JPEG': + # Check for jpegio magic + return 'dct' - raise InvalidStegoError("No valid Stegasoo header found") + # 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 -The receiver must provide the **exact same inputs** to derive the same key: -- Same reference photo (processed to same hash) -- Same day phrase -- Same PIN -- Same RSA key (if used) +```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) +``` -Any mismatch → wrong key → decryption fails (GCM tag mismatch) - -### Step 3: Extraction & Decryption +### Step 3: Data Extraction ```python -# crypto.py -def decrypt(encrypted_blob: bytes, key: bytes) -> bytes: +def extract_data(stego_image: bytes, mode: str) -> bytes: """ - Decrypt AES-256-GCM encrypted data. + Extract raw bytes from stego image. + Mode-specific extraction. """ - salt = encrypted_blob[0:16] - nonce = encrypted_blob[16:28] - tag = encrypted_blob[28:44] - ciphertext = encrypted_blob[44:] + 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) - try: - plaintext = cipher.decrypt_and_verify(ciphertext, tag) - return plaintext - except ValueError: - raise DecryptionError("Authentication failed - wrong key or corrupted data") + # Decompress if needed + if plaintext[0] & FLAG_COMPRESSED: + plaintext = lz4.frame.decompress(plaintext[5:]) + + # Extract payload + return parse_payload(plaintext) ``` --- ## LSB Mode Deep Dive -### What is LSB Steganography? +### How LSB Embedding Works -LSB (Least Significant Bit) hides data in the lowest bit of each color channel. Changing the LSB changes the pixel value by at most 1 (e.g., 142 → 141 or 143), which is imperceptible. +LSB (Least Significant Bit) embedding modifies the lowest bit of each color channel in selected pixels. ``` -Original pixel (RGB): [142, 87, 203] -Binary: [10001110, 01010111, 11001011] - ^ ^ ^ - LSB LSB LSB +Original Pixel (RGB): + R: 11010110 G: 01101001 B: 10110100 + ↓ ↓ ↓ + └─────────┴─────────┘ + 3 bits available -To embed bits [1, 0, 1]: -Modified: [10001111, 01010110, 11001011] -New pixel (RGB): [143, 86, 203] - -Difference: Imperceptible to human eye +After embedding "101": + R: 1101011[1] G: 0110100[0] B: 1011010[1] + ↑ ↑ ↑ + modified modified modified ``` -### LSB Embedding Process +### Pixel Selection Algorithm ```python -# steganography.py -def embed_lsb(carrier_image: bytes, payload: bytes, seed: bytes) -> bytes: +def select_pixels(carrier_shape, num_bits, seed: bytes) -> List[Tuple[int, int, int]]: """ - Embed payload using LSB steganography with pseudo-random pixel selection. + Generate pseudo-random pixel coordinates. + Distributes modifications across entire image. """ - img = Image.open(io.BytesIO(carrier_image)) - pixels = np.array(img) + height, width, channels = carrier_shape + total_positions = height * width * 3 # RGB channels - # 1. Calculate capacity - height, width, channels = pixels.shape - total_bits = height * width * channels # 3 bits per pixel (RGB) - capacity_bytes = total_bits // 8 + # 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) - if len(payload) > capacity_bytes: - raise CapacityError(f"Payload {len(payload)} > capacity {capacity_bytes}") + # 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)) - # 2. Generate pseudo-random pixel order (defeats steganalysis) - rng = np.random.default_rng(seed=int.from_bytes(seed[:8], 'big')) - - # Create list of all (y, x, channel) positions - positions = [(y, x, c) - for y in range(height) - for x in range(width) - for c in range(channels)] - - # Shuffle deterministically based on seed - rng.shuffle(positions) - - # 3. Convert payload to bits - payload_bits = np.unpackbits(np.frombuffer(payload, dtype=np.uint8)) - - # 4. Embed each bit - for i, bit in enumerate(payload_bits): - y, x, c = positions[i] - - # Clear LSB, then set to our bit - pixels[y, x, c] = (pixels[y, x, c] & 0xFE) | bit - - # 5. Save as PNG (lossless - preserves LSBs) - output = io.BytesIO() - Image.fromarray(pixels).save(output, format='PNG') - return output.getvalue() + return selected ``` -### Why Pseudo-Random Positions? - -Sequential embedding (left-to-right, top-to-bottom) creates statistical patterns detectable by steganalysis tools. Random scattering: - -``` -Sequential (detectable): Random (undetectable): -┌─────────────────────┐ ┌─────────────────────┐ -│█████████████........│ │.█..█.█...█..█.█..█.│ -│.....................│ │█...█..█.█..█...█.█.│ -│.....................│ │..█.█..█...█.█.█...█│ -│.....................│ │.█..█.█..█...█..█..█│ -└─────────────────────┘ └─────────────────────┘ - ↑ Pattern visible ↑ Uniform distribution -``` - -### LSB Extraction Process +### Embedding Process ```python -# steganography.py -def extract_lsb(stego_image: bytes, seed: bytes, length: int) -> bytes: +def embed_lsb(carrier: np.ndarray, data: bytes, seed: bytes) -> np.ndarray: """ - Extract payload from LSB positions. + Embed data using LSB substitution. """ - img = Image.open(io.BytesIO(stego_image)) - pixels = np.array(img) + bits = bytes_to_bits(data) + positions = select_pixels(carrier.shape, len(bits), seed) - # Regenerate same position sequence - rng = np.random.default_rng(seed=int.from_bytes(seed[:8], 'big')) - positions = [...] # Same as embedding - rng.shuffle(positions) + 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] - # Extract bits - bits = [] - for i in range(length * 8): - y, x, c = positions[i] - bits.append(pixels[y, x, c] & 1) # Get LSB - - # Convert bits to bytes - return np.packbits(bits).tobytes() + return stego ``` -### LSB Capacity Formula +### Capacity Calculation -``` -Capacity (bytes) = (Width × Height × 3 channels) / 8 bits - -Example: 1920×1080 image -= 1920 × 1080 × 3 / 8 -= 777,600 bytes (~759 KB) +```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 ``` -### LSB Limitations - -| Limitation | Description | -|------------|-------------| -| **JPEG destroys data** | JPEG recompression changes pixel values, destroying LSBs | -| **Screenshots may corrupt** | Screen capture may alter pixels | -| **Social media unusable** | All platforms recompress images | -| **Steganalysis vulnerable** | Chi-square analysis can detect LSB patterns | +**Example capacities:** +- 1920×1080: ~770 KB +- 4000×3000: ~4.5 MB +- 800×600: ~180 KB --- ## DCT Mode Deep Dive -### What is DCT Steganography? +### How DCT Embedding Works -DCT (Discrete Cosine Transform) hides data in the frequency coefficients of the image rather than raw pixels. JPEG images are already stored as DCT coefficients, making this approach survive JPEG recompression. - -### Understanding DCT Basics +DCT (Discrete Cosine Transform) mode embeds data in the frequency-domain coefficients, making it resilient to JPEG compression. ``` -Spatial Domain (pixels) Frequency Domain (DCT) -┌────────────────────┐ ┌────────────────────┐ -│ 52 55 61 66 ... │ │ 415 -30 -6 3 │ DC + low freq -│ 62 59 55 90 ... │ DCT │ -22 -17 5 -3 │ -│ 63 59 66 88 ... │ ────► │ -9 9 4 2 │ -│ 67 61 68 96 ... │ │ -4 2 1 -1 │ high freq -│ ... │ │ ... │ -└────────────────────┘ └────────────────────┘ - 8×8 pixel block 8×8 coefficient block +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 ``` -**Key insight**: Modifying mid-frequency coefficients is: -1. Less visible than modifying pixels -2. Survives JPEG recompression (coefficients are preserved) -3. Harder to detect statistically - -### DCT Embedding Process (scipy-based, PNG output) +### Coefficient Selection ```python # dct_steganography.py -def embed_in_dct(payload: bytes, carrier: bytes, seed: bytes, - output_format: str = 'png', color_mode: str = 'color') -> bytes: +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 payload in DCT coefficients. + Embed bits in a single 8×8 block. """ - img = Image.open(io.BytesIO(carrier)) + # Forward DCT + dct_block = dct_2d(block) - if color_mode == 'grayscale': - img = img.convert('L') - channels = [np.array(img)] - else: - # Convert to YCbCr (JPEG color space) - # Embed only in Y (luminance) channel - ycbcr = img.convert('YCbCr') - y, cb, cr = ycbcr.split() - channels = [np.array(y)] - color_channels = (cb, cr) # Preserve for reconstruction - - y_channel = channels[0].astype(float) - height, width = y_channel.shape - - # 1. Pad to multiple of 8 - pad_h = (8 - height % 8) % 8 - pad_w = (8 - width % 8) % 8 - y_padded = np.pad(y_channel, ((0, pad_h), (0, pad_w)), mode='edge') - - # 2. Process 8x8 blocks - blocks_h = y_padded.shape[0] // 8 - blocks_w = y_padded.shape[1] // 8 - total_blocks = blocks_h * blocks_w - - # 3. Generate random block order - rng = np.random.default_rng(seed=int.from_bytes(seed[:8], 'big')) - block_indices = list(range(total_blocks)) - rng.shuffle(block_indices) - - # 4. Embed using QIM (Quantization Index Modulation) - payload_bits = np.unpackbits(np.frombuffer(payload, dtype=np.uint8)) - bit_index = 0 - - # Positions within 8x8 block for embedding (mid-frequency) - # Avoid DC (0,0) and very high frequencies - embed_positions = [(1,2), (2,1), (2,2), (1,3), (3,1), (2,3), (3,2), (3,3), - (0,4), (4,0), (1,4), (4,1), (2,4), (4,2), (3,4), (4,3)] - - DELTA = 16 # Quantization step - - for block_idx in block_indices: - if bit_index >= len(payload_bits): + # Embed using quantization + for i, pos in enumerate(DEFAULT_EMBED_POSITIONS): + if i >= len(bits): break - - by = (block_idx // blocks_w) * 8 - bx = (block_idx % blocks_w) * 8 - # Extract 8x8 block - block = y_padded[by:by+8, bx:bx+8] - - # Apply DCT - dct_block = scipy.fftpack.dct( - scipy.fftpack.dct(block.T, norm='ortho').T, - norm='ortho' - ) - - # Embed bits in each position - for pos in embed_positions: - if bit_index >= len(payload_bits): - break - - coef = dct_block[pos] - bit = payload_bits[bit_index] - - # QIM embedding: - # Quantize coefficient, then adjust to encode bit - quantized = round(coef / DELTA) - if quantized % 2 != bit: - quantized += 1 if bit else -1 - dct_block[pos] = quantized * DELTA - - bit_index += 1 - - # Apply inverse DCT - block = scipy.fftpack.idct( - scipy.fftpack.idct(dct_block.T, norm='ortho').T, - norm='ortho' - ) - - y_padded[by:by+8, bx:bx+8] = block + 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 - # 5. Reconstruct image - y_modified = y_padded[:height, :width] # Remove padding - y_modified = np.clip(y_modified, 0, 255).astype(np.uint8) - - if color_mode == 'color': - # Merge modified Y with original Cb, Cr - result = Image.merge('YCbCr', ( - Image.fromarray(y_modified), - color_channels[0], - color_channels[1] - )).convert('RGB') - else: - result = Image.fromarray(y_modified) - - # 6. Save - output = io.BytesIO() - if output_format == 'jpeg': - result.save(output, format='JPEG', quality=95) - else: - result.save(output, format='PNG') - - return output.getvalue() + # Inverse DCT + return idct_2d(dct_block) ``` -### DCT Embedding Process (jpegio-based, native JPEG) +### jpegio Integration (Native JPEG Output) ```python -# dct_steganography.py -def embed_in_jpeg_native(payload: bytes, jpeg_carrier: bytes, seed: bytes) -> bytes: +def embed_jpegio(data: bytes, carrier_jpeg: bytes, seed: bytes) -> bytes: """ Embed directly in JPEG DCT coefficients using jpegio. - This preserves coefficients WITHOUT re-encoding. + Preserves JPEG structure perfectly. + + Note: Requires Python 3.12 or earlier (jpegio incompatible with 3.13) """ - import jpegio + import jpegio as jio - # 1. Read JPEG structure (coefficients, quantization tables, etc.) - jpeg = jpegio.read(io.BytesIO(jpeg_carrier)) + # Normalize problematic JPEGs (quality=100 causes crashes) + carrier_jpeg = normalize_jpeg_for_jpegio(carrier_jpeg) - # Y channel coefficients (component 0) - y_coefs = jpeg.coef_arrays[0] # Shape: (height/8, width/8, 8, 8) + # Read existing JPEG coefficients + jpeg = jio.read(temp_file_from_bytes(carrier_jpeg)) + coef_array = jpeg.coef_arrays[0] # Y channel - # 2. Find embeddable coefficients - # Rules: - # - Skip DC coefficient (index 0,0 in each block) - # - Skip zero coefficients (would become non-zero, visible) - # - Skip ±1 coefficients (LSB change might zero them) + # Find usable coefficients (magnitude >= 2, non-DC) + positions = get_usable_positions(coef_array) + order = generate_order(len(positions), seed) - embeddable = [] - for by in range(y_coefs.shape[0]): - for bx in range(y_coefs.shape[1]): - for i in range(8): - for j in range(8): - if i == 0 and j == 0: # Skip DC - continue - coef = y_coefs[by, bx, i, j] - if abs(coef) >= 2: # Safe to modify - embeddable.append((by, bx, i, j)) - - # 3. Shuffle positions - rng = np.random.default_rng(seed=int.from_bytes(seed[:8], 'big')) - rng.shuffle(embeddable) - - # 4. Embed bits in LSB of coefficients - payload_bits = np.unpackbits(np.frombuffer(payload, dtype=np.uint8)) - - for i, bit in enumerate(payload_bits): - if i >= len(embeddable): - raise CapacityError("Payload too large for carrier") + # 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] - by, bx, ci, cj = embeddable[i] - coef = y_coefs[by, bx, ci, cj] - - # Modify LSB - if coef > 0: - y_coefs[by, bx, ci, cj] = (abs(coef) & ~1) | bit - else: - y_coefs[by, bx, ci, cj] = -((abs(coef) & ~1) | bit) + 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 - # 5. Write JPEG (preserves structure, no re-encoding) - jpeg.coef_arrays[0] = y_coefs + # 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. - output = io.BytesIO() - jpegio.write(jpeg, output) - return output.getvalue() + 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() ``` -### Why jpegio Matters +### DCT Capacity Calculation -``` -WITHOUT jpegio (broken): -┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐ -│ JPEG Input │───►│ PIL Decode │───►│ Modify │───►│ PIL Encode │ -│ │ │ (decompress) │ │ Pixels │ │ (recompress) │ -└─────────────┘ └──────────────┘ └─────────────┘ └──────────────┘ - │ - ▼ - RE-QUANTIZATION - DESTROYS DATA! - -WITH jpegio (correct): -┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐ -│ JPEG Input │───►│ jpegio.read │───►│ Modify │───►│ jpegio.write │ -│ │ │ (raw coefs) │ │ Coefs │ │ (raw coefs) │ -└─────────────┘ └──────────────┘ └─────────────┘ └──────────────┘ - │ - ▼ - COEFFICIENTS - PRESERVED! +```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 ``` -### DCT Capacity Formula +**Example capacities:** +- 1920×1080: ~64 KB +- 4000×3000: ~375 KB +- 800×600: ~14 KB + +### Why DCT Survives JPEG Compression ``` -Capacity depends on image content (more texture = more non-zero coefficients) - -Rough estimate: -Capacity (bytes) ≈ (Width × Height × bits_per_block) / (64 × 8) - -Where bits_per_block ≈ 8-16 depending on image complexity - -Example: 1920×1080 image -≈ 1920 × 1080 × 12 / 512 -≈ 48,600 bytes (~47 KB) - -Actual capacity varies from ~30 KB to ~75 KB for 1080p +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 | |-----------|-------------| -| **Survives JPEG recompression** | Coefficients mostly preserved | -| **Social media compatible** | Works after Instagram/WhatsApp compression | +| **JPEG resilient** | Survives social media upload | | **Better steganalysis resistance** | Harder to detect statistically | | **Natural-looking output** | JPEG artifacts expected | @@ -667,6 +594,7 @@ Actual capacity varies from ~30 KB to ~75 KB for 1080p | **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 | --- @@ -683,6 +611,7 @@ Actual capacity varies from ~30 KB to ~75 KB for 1080p | **Color Support** | Full color | Color or Grayscale | | **Detection Resistance** | Moderate | Better | | **Best For** | Email, cloud storage | Social media, messaging | +| **Max Tested Image** | 14MB+ | 14MB+ | --- @@ -691,16 +620,16 @@ Actual capacity varies from ~30 KB to ~75 KB for 1080p ### What Makes Stegasoo Secure? ``` -MULTI-FACTOR AUTHENTICATION -──────────────────────────── +MULTI-FACTOR AUTHENTICATION (v4.0) +────────────────────────────────── Factor 1: Reference Photo ─┐ • 80-256 bits entropy │ • "Something you have" │ ├──► Combined entropy: 133-400+ bits -Factor 2: Day Phrase │ (Beyond brute force) - • 33-132 bits entropy │ +Factor 2: Passphrase │ (Beyond brute force) + • 43-132 bits entropy │ • "Something you know" │ - • Rotates daily │ + • 4 words default (v4.0) │ │ Factor 3: PIN │ • 20-30 bits entropy │ @@ -755,11 +684,11 @@ AUTHENTICATED ENCRYPTION (AES-256-GCM) ## Data Flow Diagrams -### Complete Encode Flow +### Complete Encode Flow (v4.0) ``` ┌──────────────────────────────────────────────────────────────────────────────┐ -│ ENCODE FLOW │ +│ ENCODE FLOW (v4.0) │ └──────────────────────────────────────────────────────────────────────────────┘ User Inputs Processing Output @@ -768,7 +697,7 @@ User Inputs Processing Output Reference Photo ──────┐ ├──► get_image_hash() ──► ref_hash (32 bytes) │ │ -Day Phrase ───────────┤ ▼ +Passphrase ───────────┤ ▼ ├──► derive_key() ──────► aes_key (32 bytes) PIN ──────────────────┤ (Argon2id) │ │ │ @@ -801,11 +730,11 @@ Carrier Image ────────────────────── (downloadable) ``` -### Complete Decode Flow +### Complete Decode Flow (v4.0) ``` ┌──────────────────────────────────────────────────────────────────────────────┐ -│ DECODE FLOW │ +│ DECODE FLOW (v4.0) │ └──────────────────────────────────────────────────────────────────────────────┘ User Inputs Processing Output @@ -814,7 +743,7 @@ User Inputs Processing Output Reference Photo ──────┐ ├──► get_image_hash() ──► ref_hash (32 bytes) │ │ -Day Phrase ───────────┤ ▼ +Passphrase ───────────┤ ▼ ├──► derive_key() ──────► aes_key (32 bytes) PIN ──────────────────┤ (Argon2id) │ │ (MUST MATCH!) │ @@ -866,3 +795,11 @@ Both modes share the same cryptographic foundation (Argon2id + AES-256-GCM) and 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 diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..049936d --- /dev/null +++ b/build.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# Stegasoo Build Script +# Usage: ./build.sh [base|fast|full|clean] + +set -e + +case "${1:-fast}" in + base) + # Build base image with all dependencies (run once, or when deps change) + echo "🔨 Building base image (this takes 5-10 minutes)..." + docker build -f Dockerfile.base -t stegasoo-base:latest . + echo "✅ Base image built! Future builds will be fast." + echo "" + echo "Optional: Push to registry for team use:" + echo " docker tag stegasoo-base:latest yourregistry/stegasoo-base:latest" + echo " docker push yourregistry/stegasoo-base:latest" + ;; + + fast) + # Fast build using pre-built base image + if ! docker image inspect stegasoo-base:latest >/dev/null 2>&1; then + echo "⚠️ Base image not found. Building it first (one-time)..." + $0 base + fi + echo "🚀 Fast build using base image..." + docker-compose build + echo "✅ Done! Start with: docker-compose up -d" + ;; + + full) + # Full rebuild from scratch (slow, but no base image needed) + echo "🐢 Full build from scratch (slow)..." + docker-compose build --no-cache + echo "✅ Done! Start with: docker-compose up -d" + ;; + + clean) + # Clean up everything + echo "🧹 Cleaning up..." + docker-compose down --rmi local -v 2>/dev/null || true + docker rmi stegasoo-base:latest 2>/dev/null || true + echo "✅ Cleaned!" + ;; + + *) + echo "Stegasoo Build Script" + echo "" + echo "Usage: $0 [command]" + echo "" + echo "Commands:" + echo " base Build the base image (one-time, 5-10 min)" + echo " fast Fast build using base image (default, ~10 sec)" + echo " full Full rebuild from scratch (slow, no base needed)" + echo " clean Remove all images and volumes" + echo "" + echo "Typical workflow:" + echo " 1. First time: $0 base" + echo " 2. Daily dev: $0 fast (or just 'docker-compose build')" + echo " 3. Deps change: $0 base (rebuild base image)" + ;; +esac diff --git a/check_scipy.py b/check_scipy.py new file mode 100644 index 0000000..75abb7d --- /dev/null +++ b/check_scipy.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +""" +Diagnostic script to check for scipy/numpy issues. +Run this BEFORE starting the web app. + +Usage: + python check_scipy.py +""" + +import sys +print(f"Python version: {sys.version}") +print() + +# Check numpy +try: + import numpy as np + print(f"NumPy version: {np.__version__}") + print(f"NumPy config:") + np.show_config() +except ImportError as e: + print(f"NumPy not installed: {e}") +except Exception as e: + print(f"NumPy error: {e}") + +print() +print("-" * 50) +print() + +# Check scipy +try: + import scipy + print(f"SciPy version: {scipy.__version__}") +except ImportError as e: + print(f"SciPy not installed: {e}") + +print() + +# Check PIL +try: + from PIL import Image + print(f"Pillow version: {Image.__version__}") +except ImportError as e: + print(f"Pillow not installed: {e}") + +print() +print("-" * 50) +print() + +# Test scipy DCT directly +print("Testing scipy DCT...") +try: + from scipy.fftpack import dct, idct + import numpy as np + + # Create test array + test = np.random.rand(8, 8).astype(np.float64) + print(f"Input array shape: {test.shape}, dtype: {test.dtype}") + + # Test 1D DCT + row = test[0, :] + result = dct(row, norm='ortho') + print(f"1D DCT result shape: {result.shape}, dtype: {result.dtype}") + + # Test 2D DCT (the potentially problematic operation) + result2d = dct(dct(test.T, norm='ortho').T, norm='ortho') + print(f"2D DCT result shape: {result2d.shape}, dtype: {result2d.dtype}") + + # Test inverse + recovered = idct(idct(result2d.T, norm='ortho').T, norm='ortho') + error = np.max(np.abs(test - recovered)) + print(f"Round-trip error: {error}") + + if error < 1e-10: + print("✓ scipy DCT working correctly") + else: + print("⚠ scipy DCT has precision issues") + +except Exception as e: + print(f"✗ scipy DCT failed: {e}") + import traceback + traceback.print_exc() + +print() +print("-" * 50) +print() + +# Test with larger array (more like real image processing) +print("Testing with larger arrays (512x512)...") +try: + from scipy.fftpack import dct, idct + import numpy as np + import gc + + # Simulate processing many 8x8 blocks + large_array = np.random.rand(512, 512).astype(np.float64) + print(f"Large array shape: {large_array.shape}, size: {large_array.nbytes} bytes") + + count = 0 + for y in range(0, 512, 8): + for x in range(0, 512, 8): + block = large_array[y:y+8, x:x+8].copy() + dct_block = dct(dct(block.T, norm='ortho').T, norm='ortho') + recovered = idct(idct(dct_block.T, norm='ortho').T, norm='ortho') + large_array[y:y+8, x:x+8] = recovered + count += 1 + + print(f"Processed {count} blocks successfully") + + del large_array + gc.collect() + + print("✓ Large array processing completed") + +except Exception as e: + print(f"✗ Large array processing failed: {e}") + import traceback + traceback.print_exc() + +print() +print("-" * 50) +print() + +# Test PIL with large image +print("Testing PIL with large image...") +try: + from PIL import Image + import io + + # Create a large test image + img = Image.new('RGB', (4000, 3000), color=(128, 128, 128)) + + # Save to bytes + buffer = io.BytesIO() + img.save(buffer, format='PNG') + img_bytes = buffer.getvalue() + print(f"Test image size: {len(img_bytes)} bytes") + + # Re-open and process + buffer2 = io.BytesIO(img_bytes) + img2 = Image.open(buffer2) + print(f"Re-opened image: {img2.size}, mode: {img2.mode}") + + # Convert to numpy array + import numpy as np + arr = np.array(img2) + print(f"NumPy array: {arr.shape}, dtype: {arr.dtype}") + + # Clean up + img.close() + img2.close() + buffer.close() + buffer2.close() + del arr + gc.collect() + + print("✓ PIL large image test completed") + +except Exception as e: + print(f"✗ PIL test failed: {e}") + import traceback + traceback.print_exc() + +print() +print("=" * 50) +print("Diagnostics complete") +print() +print("If no errors above but web app still crashes, try:") +print("1. pip install --upgrade scipy numpy pillow") +print("2. pip install scipy==1.11.4 numpy==1.26.4 # Known stable versions") +print("3. Check if using conda vs pip (mixing can cause issues)") diff --git a/debug_jpegio.py b/debug_jpegio.py new file mode 100644 index 0000000..2a7ab89 --- /dev/null +++ b/debug_jpegio.py @@ -0,0 +1,215 @@ +#!/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 ") + 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() diff --git a/frontends/api/main.py b/frontends/api/main.py index a2b8af7..3307b21 100644 --- a/frontends/api/main.py +++ b/frontends/api/main.py @@ -1,10 +1,15 @@ #!/usr/bin/env python3 """ -Stegasoo REST API (v3.2.0) +Stegasoo REST API (v4.0.0) FastAPI-based REST API for steganography operations. Supports both text messages and file embedding. +CHANGES in v4.0.0: +- Updated from v3.2.0 with no functional API changes +- Internal: JPEG normalization for jpegio compatibility +- Internal: Python 3.12 recommended + CHANGES in v3.2.0: - Removed date dependency from all operations - Renamed day_phrase → passphrase @@ -32,7 +37,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'src')) import stegasoo from stegasoo import ( encode, decode, generate_credentials, - validate_image, calculate_capacity, + validate_image, __version__, StegasooError, DecryptionError, CapacityError, has_argon2, @@ -75,6 +80,11 @@ app = FastAPI( description=""" Secure steganography with hybrid authentication. Supports text messages and file embedding. +## Version 4.0.0 Changes + +- **Python 3.12 recommended** - jpegio compatibility improvements +- **JPEG normalization** - Handles quality=100 images automatically + ## Version 3.2.0 Changes - **No date parameters needed** - Encode and decode anytime without tracking dates @@ -1052,7 +1062,7 @@ async def api_image_info( if not result.is_valid: raise HTTPException(400, result.error_message) - capacity = calculate_capacity(image_data) + capacity = calculate_capacity_by_mode(image_data, 'lsb') response = ImageInfoResponse( width=result.details['width'], diff --git a/frontends/cli/main.py b/frontends/cli/main.py index 9b7f051..4f08fa2 100644 --- a/frontends/cli/main.py +++ b/frontends/cli/main.py @@ -64,15 +64,37 @@ from stegasoo import ( # Models FilePayload, - - # Constants - EMBED_MODE_LSB, - EMBED_MODE_DCT, - EMBED_MODE_AUTO, - DEFAULT_PASSPHRASE_WORDS, - DEFAULT_PIN_LENGTH, ) +# Import constants - try main module first, then constants submodule +try: + from stegasoo import ( + EMBED_MODE_LSB, + EMBED_MODE_DCT, + EMBED_MODE_AUTO, + ) +except ImportError: + from stegasoo.constants import ( + EMBED_MODE_LSB, + EMBED_MODE_DCT, + EMBED_MODE_AUTO, + ) + +# Import constants that may not be in main __init__ +try: + from stegasoo.constants import ( + DEFAULT_PASSPHRASE_WORDS, + DEFAULT_PIN_LENGTH, + MIN_PIN_LENGTH, + MAX_PIN_LENGTH, + ) +except ImportError: + # Fallback defaults if constants not available + DEFAULT_PASSPHRASE_WORDS = 4 + DEFAULT_PIN_LENGTH = 6 + MIN_PIN_LENGTH = 6 + MAX_PIN_LENGTH = 9 + # Optional: strip_image_metadata from utils try: from stegasoo.utils import strip_image_metadata diff --git a/frontends/web/README_subprocess.md b/frontends/web/README_subprocess.md new file mode 100644 index 0000000..9b8ab7f --- /dev/null +++ b/frontends/web/README_subprocess.md @@ -0,0 +1,62 @@ +# Subprocess Isolation for Stegasoo WebUI + +This update runs encode/decode/compare operations in isolated subprocesses +to prevent jpegio/scipy crashes from taking down the Flask server. + +## Files + +- **app.py** - Updated Flask app using subprocess isolation +- **subprocess_stego.py** - Flask-side wrapper with clean API +- **stego_worker.py** - Subprocess script that does actual stegasoo operations + +## Setup + +1. Place all three files in your `webui/` directory (same level as templates/) + +2. Make sure stego_worker.py is executable (optional): + ```bash + chmod +x stego_worker.py + ``` + +3. Run the Flask app: + ```bash + python app.py + ``` + +## How It Works + +Instead of calling stegasoo functions directly in the Flask process: + +```python +# OLD (crashes could kill Flask) +result = encode(...) +``` + +We now run them in subprocesses: + +```python +# NEW (crashes only kill the subprocess) +result = subprocess_stego.encode(...) +``` + +If jpegio or scipy crashes due to memory corruption, only the subprocess +dies. Flask logs the error and continues running. The next request spawns +a fresh subprocess. + +## Configuration + +In `app.py`, you can adjust the timeout: + +```python +subprocess_stego = SubprocessStego(timeout=180) # 3 minutes +``` + +Larger images may need longer timeouts. + +## Troubleshooting + +If you see "Worker script not found" errors, make sure `stego_worker.py` +is in the same directory as `app.py`. + +If subprocess operations fail, check the Flask logs for error details. +The subprocess wrapper captures both stdout and stderr from the worker. diff --git a/frontends/web/app.py b/frontends/web/app.py index 6b3c0d8..db4e294 100644 --- a/frontends/web/app.py +++ b/frontends/web/app.py @@ -29,12 +29,16 @@ from flask import ( jsonify, flash, redirect, url_for ) +import os +os.environ['NUMPY_MADVISE_HUGEPAGE'] = '0' +os.environ['OMP_NUM_THREADS'] = '1' + # Add parent to path for development sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'src')) import stegasoo from stegasoo import ( - encode, decode, generate_credentials, + generate_credentials, export_rsa_key_pem, load_rsa_key, validate_pin, validate_message, validate_image, validate_rsa_key, validate_security_factors, @@ -48,8 +52,7 @@ from stegasoo import ( EMBED_MODE_DCT, EMBED_MODE_AUTO, has_dct_support, - compare_modes, - will_fit_by_mode, + # NOTE: encode, decode, compare_modes, will_fit_by_mode now use subprocess isolation ) from stegasoo.constants import ( __version__, @@ -90,6 +93,17 @@ from stegasoo.qr_utils import ( QR_MAX_BINARY, COMPRESSION_PREFIX ) +# ============================================================================ +# SUBPROCESS ISOLATION FOR STEGASOO OPERATIONS +# ============================================================================ +# Runs encode/decode/compare in subprocesses to prevent jpegio/scipy crashes +# from taking down the Flask server. + +from subprocess_stego import SubprocessStego + +# Initialize subprocess wrapper (worker script must be in same directory) +subprocess_stego = SubprocessStego(timeout=180) # 3 minute timeout for large images + # ============================================================================ # FLASK APP CONFIGURATION @@ -436,6 +450,7 @@ def api_compare_capacity(): """ Compare LSB and DCT capacity for an uploaded carrier image. Returns JSON with capacity info for both modes. + Uses subprocess isolation to prevent crashes. """ carrier = request.files.get('carrier') if not carrier: @@ -443,23 +458,28 @@ def api_compare_capacity(): try: carrier_data = carrier.read() - comparison = compare_modes(carrier_data) + + # Use subprocess-isolated compare_modes + result = subprocess_stego.compare_modes(carrier_data) + + if not result.success: + return jsonify({'error': result.error or 'Comparison failed'}), 500 return jsonify({ 'success': True, - 'width': comparison['width'], - 'height': comparison['height'], + 'width': result.width, + 'height': result.height, 'lsb': { - 'capacity_bytes': comparison['lsb']['capacity_bytes'], - 'capacity_kb': round(comparison['lsb']['capacity_kb'], 1), - 'output': comparison['lsb']['output'], + 'capacity_bytes': result.lsb['capacity_bytes'], + 'capacity_kb': round(result.lsb['capacity_kb'], 1), + 'output': result.lsb.get('output', 'PNG'), }, 'dct': { - 'capacity_bytes': comparison['dct']['capacity_bytes'], - 'capacity_kb': round(comparison['dct']['capacity_kb'], 1), - 'output': comparison['dct']['output'], - 'available': comparison['dct']['available'], - 'ratio': round(comparison['dct']['ratio_vs_lsb'], 1), + 'capacity_bytes': result.dct['capacity_bytes'], + 'capacity_kb': round(result.dct['capacity_kb'], 1), + 'output': result.dct.get('output', 'JPEG'), + 'available': result.dct.get('available', True), + 'ratio': round(result.dct.get('ratio_vs_lsb', 0), 1), } }) except Exception as e: @@ -471,6 +491,7 @@ def api_check_fit(): """ Check if a payload will fit in the carrier with selected mode. Returns JSON with fit status and details. + Uses subprocess isolation to prevent crashes. """ carrier = request.files.get('carrier') payload_size = request.form.get('payload_size', type=int) @@ -487,16 +508,25 @@ def api_check_fit(): try: carrier_data = carrier.read() - result = will_fit_by_mode(payload_size, carrier_data, embed_mode=embed_mode) + + # Use subprocess-isolated capacity check + result = subprocess_stego.check_capacity( + carrier_data=carrier_data, + payload_size=payload_size, + embed_mode=embed_mode, + ) + + if not result.success: + return jsonify({'error': result.error or 'Capacity check failed'}), 500 return jsonify({ 'success': True, - 'fits': result['fits'], - 'payload_size': result['payload_size'], - 'capacity': result['capacity'], - 'usage_percent': round(result['usage_percent'], 1), - 'headroom': result['headroom'], - 'mode': embed_mode, + 'fits': result.fits, + 'payload_size': result.payload_size, + 'capacity': result.capacity, + 'usage_percent': round(result.usage_percent, 1), + 'headroom': result.headroom, + 'mode': result.mode, }) except Exception as e: return jsonify({'error': str(e)}), 500 @@ -641,37 +671,63 @@ def encode_page(): return render_template('encode.html', has_qrcode_read=HAS_QRCODE_READ) # v3.2.0: No date parameter needed - encode_result = encode( - message=payload, - reference_photo=ref_data, - carrier_image=carrier_data, - passphrase=passphrase, # v3.2.0: Renamed from day_phrase - pin=pin, - rsa_key_data=rsa_key_data, - rsa_password=key_password, - # date_str removed in v3.2.0 - embed_mode=embed_mode, - dct_output_format=dct_output_format if embed_mode == 'dct' else None, - dct_color_mode=dct_color_mode if embed_mode == 'dct' else None, - ) + # Use subprocess-isolated encode to prevent crashes + if payload_type == 'file' and payload_file and payload_file.filename: + encode_result = subprocess_stego.encode( + carrier_data=carrier_data, + reference_data=ref_data, + file_data=payload.data, + file_name=payload.filename, + file_mime=payload.mime_type, + passphrase=passphrase, + pin=pin if pin else None, + rsa_key_data=rsa_key_data, + rsa_password=key_password, + embed_mode=embed_mode, + dct_output_format=dct_output_format if embed_mode == 'dct' else 'png', + dct_color_mode=dct_color_mode if embed_mode == 'dct' else 'color', + ) + else: + encode_result = subprocess_stego.encode( + carrier_data=carrier_data, + reference_data=ref_data, + message=payload, + passphrase=passphrase, + pin=pin if pin else None, + rsa_key_data=rsa_key_data, + rsa_password=key_password, + embed_mode=embed_mode, + dct_output_format=dct_output_format if embed_mode == 'dct' else 'png', + dct_color_mode=dct_color_mode if embed_mode == 'dct' else 'color', + ) + + # Check for subprocess errors + if not encode_result.success: + error_msg = encode_result.error or 'Encoding failed' + if 'capacity' in error_msg.lower(): + raise CapacityError(error_msg) + raise StegasooError(error_msg) # Determine actual output format for filename and storage if embed_mode == 'dct' and dct_output_format == 'jpeg': output_ext = '.jpg' output_mime = 'image/jpeg' - filename = encode_result.filename - if filename.endswith('.png'): - filename = filename[:-4] + '.jpg' else: output_ext = '.png' output_mime = 'image/png' - filename = encode_result.filename + + # Use filename from result or generate one + filename = encode_result.filename + if not filename: + filename = generate_filename('stego', output_ext) + elif embed_mode == 'dct' and dct_output_format == 'jpeg' and filename.endswith('.png'): + filename = filename[:-4] + '.jpg' # Store temporarily file_id = secrets.token_urlsafe(16) cleanup_temp_files() TEMP_FILES[file_id] = { - 'data': encode_result.stego_image, + 'data': encode_result.stego_data, 'filename': filename, 'timestamp': time.time(), 'embed_mode': embed_mode, @@ -864,17 +920,24 @@ def decode_page(): return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ) # v3.2.0: No date_str parameter needed - decode_result = decode( - stego_image=stego_data, - reference_photo=ref_data, - passphrase=passphrase, # v3.2.0: Renamed from day_phrase - pin=pin, + # Use subprocess-isolated decode to prevent crashes + decode_result = subprocess_stego.decode( + stego_data=stego_data, + reference_data=ref_data, + passphrase=passphrase, + pin=pin if pin else None, rsa_key_data=rsa_key_data, rsa_password=key_password, - # date_str removed in v3.2.0 embed_mode=embed_mode, ) + # Check for subprocess errors + if not decode_result.success: + error_msg = decode_result.error or 'Decoding failed' + if 'decrypt' in error_msg.lower() or decode_result.error_type == 'DecryptionError': + raise DecryptionError(error_msg) + raise StegasooError(error_msg) + if decode_result.is_file: # File content - store temporarily for download file_id = secrets.token_urlsafe(16) @@ -942,6 +1005,54 @@ def about(): ) +# Add these two test routes anywhere in app.py after the app = Flask(...) line: + +@app.route('/test-capacity', methods=['POST']) +def test_capacity(): + """Minimal capacity test - no stegasoo code, just PIL.""" + carrier = request.files.get('carrier') + if not carrier: + return jsonify({'error': 'No carrier image provided'}), 400 + + try: + carrier_data = carrier.read() + buffer = io.BytesIO(carrier_data) + img = Image.open(buffer) + width, height = img.size + fmt = img.format + img.close() + buffer.close() + + pixels = width * height + lsb_bytes = (pixels * 3) // 8 + dct_bytes = ((width // 8) * (height // 8) * 16) // 8 - 10 + + return jsonify({ + 'success': True, + 'width': width, + 'height': height, + 'format': fmt, + 'lsb_kb': round(lsb_bytes / 1024, 1), + 'dct_kb': round(dct_bytes / 1024, 1), + }) + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@app.route('/test-capacity-nopil', methods=['POST']) +def test_capacity_nopil(): + """Ultra-minimal test - no PIL, no stegasoo.""" + carrier = request.files.get('carrier') + if not carrier: + return jsonify({'error': 'No carrier image provided'}), 400 + + carrier_data = carrier.read() + return jsonify({ + 'success': True, + 'data_size': len(carrier_data), + }) + + # ============================================================================ # MAIN # ============================================================================ diff --git a/frontends/web/stego_worker.py b/frontends/web/stego_worker.py new file mode 100644 index 0000000..eeb224e --- /dev/null +++ b/frontends/web/stego_worker.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +""" +Stegasoo Subprocess Worker + +This script runs in a subprocess and handles encode/decode operations. +If it crashes due to jpegio/scipy issues, the parent Flask process survives. + +Communication is via JSON over stdin/stdout: +- Input: JSON object with operation parameters +- Output: JSON object with results or error + +Usage: + echo '{"operation": "encode", ...}' | python stego_worker.py +""" + +import sys +import json +import base64 +import traceback +from pathlib import Path + +# Ensure stegasoo is importable +sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'src')) +sys.path.insert(0, str(Path(__file__).parent)) + + +def encode_operation(params: dict) -> dict: + """Handle encode operation.""" + from stegasoo import encode, FilePayload + + # Decode base64 inputs + carrier_data = base64.b64decode(params['carrier_b64']) + reference_data = base64.b64decode(params['reference_b64']) + + # Optional RSA key + rsa_key_data = None + if params.get('rsa_key_b64'): + rsa_key_data = base64.b64decode(params['rsa_key_b64']) + + # Determine payload type + if params.get('file_b64'): + file_data = base64.b64decode(params['file_b64']) + payload = FilePayload( + data=file_data, + filename=params.get('file_name', 'file'), + mime_type=params.get('file_mime', 'application/octet-stream'), + ) + else: + payload = params.get('message', '') + + # Call encode with correct parameter names + result = encode( + message=payload, + reference_photo=reference_data, + carrier_image=carrier_data, + passphrase=params.get('passphrase', ''), + pin=params.get('pin'), + rsa_key_data=rsa_key_data, + rsa_password=params.get('rsa_password'), + embed_mode=params.get('embed_mode', 'lsb'), + dct_output_format=params.get('dct_output_format', 'png'), + dct_color_mode=params.get('dct_color_mode', 'color'), + ) + + # Build stats dict if available + stats = None + if hasattr(result, 'stats') and result.stats: + stats = { + 'pixels_modified': getattr(result.stats, 'pixels_modified', 0), + 'capacity_used': getattr(result.stats, 'capacity_used', 0), + 'bytes_embedded': getattr(result.stats, 'bytes_embedded', 0), + } + + return { + 'success': True, + 'stego_b64': base64.b64encode(result.stego_image).decode('ascii'), + 'filename': getattr(result, 'filename', None), + 'stats': stats, + } + + +def decode_operation(params: dict) -> dict: + """Handle decode operation.""" + from stegasoo import decode + + # Decode base64 inputs + stego_data = base64.b64decode(params['stego_b64']) + reference_data = base64.b64decode(params['reference_b64']) + + # Optional RSA key + rsa_key_data = None + if params.get('rsa_key_b64'): + rsa_key_data = base64.b64decode(params['rsa_key_b64']) + + # Call decode with correct parameter names + result = decode( + stego_image=stego_data, + reference_photo=reference_data, + passphrase=params.get('passphrase', ''), + pin=params.get('pin'), + rsa_key_data=rsa_key_data, + rsa_password=params.get('rsa_password'), + embed_mode=params.get('embed_mode', 'auto'), + ) + + if result.is_file: + return { + 'success': True, + 'is_file': True, + 'file_b64': base64.b64encode(result.file_data).decode('ascii'), + 'filename': result.filename, + 'mime_type': result.mime_type, + } + else: + return { + 'success': True, + 'is_file': False, + 'message': result.message, + } + + +def compare_operation(params: dict) -> dict: + """Handle compare_modes operation.""" + from stegasoo import compare_modes + + carrier_data = base64.b64decode(params['carrier_b64']) + result = compare_modes(carrier_data) + + return { + 'success': True, + 'comparison': result, + } + + +def capacity_check_operation(params: dict) -> dict: + """Handle will_fit_by_mode operation.""" + from stegasoo import will_fit_by_mode + + carrier_data = base64.b64decode(params['carrier_b64']) + + result = will_fit_by_mode( + payload=params['payload_size'], + carrier_image=carrier_data, + embed_mode=params.get('embed_mode', 'lsb'), + ) + + return { + 'success': True, + 'result': result, + } + + +def main(): + """Main entry point - read JSON from stdin, write JSON to stdout.""" + try: + # Read all input + input_text = sys.stdin.read() + + if not input_text.strip(): + output = {'success': False, 'error': 'No input provided'} + else: + params = json.loads(input_text) + operation = params.get('operation') + + if operation == 'encode': + output = encode_operation(params) + elif operation == 'decode': + output = decode_operation(params) + elif operation == 'compare': + output = compare_operation(params) + elif operation == 'capacity': + output = capacity_check_operation(params) + else: + output = {'success': False, 'error': f'Unknown operation: {operation}'} + + except json.JSONDecodeError as e: + output = {'success': False, 'error': f'Invalid JSON: {e}'} + except Exception as e: + output = { + 'success': False, + 'error': str(e), + 'error_type': type(e).__name__, + 'traceback': traceback.format_exc(), + } + + # Write output as JSON + print(json.dumps(output), flush=True) + + +if __name__ == '__main__': + main() diff --git a/frontends/web/subprocess_stego.py b/frontends/web/subprocess_stego.py new file mode 100644 index 0000000..edd3c1b --- /dev/null +++ b/frontends/web/subprocess_stego.py @@ -0,0 +1,425 @@ +""" +Subprocess Steganography Wrapper + +Runs stegasoo operations in isolated subprocesses to prevent crashes +from taking down the Flask server. + +Usage: + from subprocess_stego import SubprocessStego + + stego = SubprocessStego() + + # Encode + result = stego.encode( + carrier_data=carrier_bytes, + reference_data=ref_bytes, + message="secret message", + passphrase="my passphrase", + pin="123456", + embed_mode="dct", + ) + + if result.success: + stego_bytes = result.stego_data + extension = result.extension + else: + error_message = result.error + + # Decode + result = stego.decode( + stego_data=stego_bytes, + reference_data=ref_bytes, + passphrase="my passphrase", + pin="123456", + ) + + # Compare modes (capacity) + result = stego.compare_modes(carrier_bytes) +""" + +import json +import base64 +import subprocess +import sys +from pathlib import Path +from dataclasses import dataclass +from typing import Optional, Dict, Any, Union + + +# Default timeout for operations (seconds) +DEFAULT_TIMEOUT = 120 + +# Path to worker script - adjust if needed +WORKER_SCRIPT = Path(__file__).parent / 'stego_worker.py' + + +@dataclass +class EncodeResult: + """Result from encode operation.""" + success: bool + stego_data: Optional[bytes] = None + filename: Optional[str] = None + stats: Optional[Dict[str, Any]] = None + error: Optional[str] = None + error_type: Optional[str] = None + + +@dataclass +class DecodeResult: + """Result from decode operation.""" + success: bool + is_file: bool = False + message: Optional[str] = None + file_data: Optional[bytes] = None + filename: Optional[str] = None + mime_type: Optional[str] = None + error: Optional[str] = None + error_type: Optional[str] = None + + +@dataclass +class CompareResult: + """Result from compare_modes operation.""" + success: bool + width: int = 0 + height: int = 0 + lsb: Optional[Dict[str, Any]] = None + dct: Optional[Dict[str, Any]] = None + error: Optional[str] = None + + +@dataclass +class CapacityResult: + """Result from capacity check operation.""" + success: bool + fits: bool = False + payload_size: int = 0 + capacity: int = 0 + usage_percent: float = 0.0 + headroom: int = 0 + mode: str = "" + error: Optional[str] = None + + +class SubprocessStego: + """ + Subprocess-isolated steganography operations. + + All operations run in a separate Python process. If jpegio or scipy + crashes, only the subprocess dies - Flask keeps running. + """ + + def __init__( + self, + worker_path: Optional[Path] = None, + python_executable: Optional[str] = None, + timeout: int = DEFAULT_TIMEOUT, + ): + """ + Initialize subprocess wrapper. + + Args: + worker_path: Path to stego_worker.py (default: same directory) + python_executable: Python interpreter to use (default: same as current) + timeout: Default timeout in seconds + """ + self.worker_path = worker_path or WORKER_SCRIPT + self.python = python_executable or sys.executable + self.timeout = timeout + + if not self.worker_path.exists(): + raise FileNotFoundError(f"Worker script not found: {self.worker_path}") + + def _run_worker(self, params: Dict[str, Any], timeout: Optional[int] = None) -> Dict[str, Any]: + """ + Run the worker subprocess with given parameters. + + Args: + params: Dictionary of parameters (will be JSON-encoded) + timeout: Operation timeout in seconds + + Returns: + Dictionary with results from worker + """ + timeout = timeout or self.timeout + input_json = json.dumps(params) + + try: + result = subprocess.run( + [self.python, str(self.worker_path)], + input=input_json, + capture_output=True, + text=True, + timeout=timeout, + cwd=str(self.worker_path.parent), + ) + + if result.returncode != 0: + # Worker crashed + return { + 'success': False, + 'error': f'Worker crashed (exit code {result.returncode})', + 'stderr': result.stderr, + } + + if not result.stdout.strip(): + return { + 'success': False, + 'error': 'Worker returned empty output', + 'stderr': result.stderr, + } + + return json.loads(result.stdout) + + except subprocess.TimeoutExpired: + return { + 'success': False, + 'error': f'Operation timed out after {timeout} seconds', + 'error_type': 'TimeoutError', + } + except json.JSONDecodeError as e: + return { + 'success': False, + 'error': f'Invalid JSON from worker: {e}', + 'raw_output': result.stdout if 'result' in dir() else None, + } + except Exception as e: + return { + 'success': False, + 'error': str(e), + 'error_type': type(e).__name__, + } + + def encode( + self, + carrier_data: bytes, + reference_data: bytes, + message: Optional[str] = None, + file_data: Optional[bytes] = None, + file_name: Optional[str] = None, + file_mime: Optional[str] = None, + passphrase: str = "", + pin: Optional[str] = None, + rsa_key_data: Optional[bytes] = None, + rsa_password: Optional[str] = None, + embed_mode: str = "lsb", + dct_output_format: str = "png", + dct_color_mode: str = "color", + timeout: Optional[int] = None, + ) -> EncodeResult: + """ + Encode a message or file into an image. + + Args: + carrier_data: Carrier image bytes + reference_data: Reference photo bytes + message: Text message to encode (if not file) + file_data: File bytes to encode (if not message) + file_name: Original filename (for file payload) + file_mime: MIME type (for file payload) + passphrase: Encryption passphrase + pin: Optional PIN + rsa_key_data: Optional RSA key PEM bytes + rsa_password: RSA key password if encrypted + embed_mode: 'lsb' or 'dct' + dct_output_format: 'png' or 'jpeg' (for DCT mode) + dct_color_mode: 'grayscale' or 'color' (for DCT mode) + timeout: Operation timeout in seconds + + Returns: + EncodeResult with stego_data and extension on success + """ + params = { + 'operation': 'encode', + 'carrier_b64': base64.b64encode(carrier_data).decode('ascii'), + 'reference_b64': base64.b64encode(reference_data).decode('ascii'), + 'message': message, + 'passphrase': passphrase, + 'pin': pin, + 'embed_mode': embed_mode, + 'dct_output_format': dct_output_format, + 'dct_color_mode': dct_color_mode, + } + + if file_data: + params['file_b64'] = base64.b64encode(file_data).decode('ascii') + params['file_name'] = file_name + params['file_mime'] = file_mime + + if rsa_key_data: + params['rsa_key_b64'] = base64.b64encode(rsa_key_data).decode('ascii') + params['rsa_password'] = rsa_password + + result = self._run_worker(params, timeout) + + if result.get('success'): + return EncodeResult( + success=True, + stego_data=base64.b64decode(result['stego_b64']), + filename=result.get('filename'), + stats=result.get('stats'), + ) + else: + return EncodeResult( + success=False, + error=result.get('error', 'Unknown error'), + error_type=result.get('error_type'), + ) + + def decode( + self, + stego_data: bytes, + reference_data: bytes, + passphrase: str = "", + pin: Optional[str] = None, + rsa_key_data: Optional[bytes] = None, + rsa_password: Optional[str] = None, + embed_mode: str = "auto", + timeout: Optional[int] = None, + ) -> DecodeResult: + """ + Decode a message or file from a stego image. + + Args: + stego_data: Stego image bytes + reference_data: Reference photo bytes + passphrase: Decryption passphrase + pin: Optional PIN + rsa_key_data: Optional RSA key PEM bytes + rsa_password: RSA key password if encrypted + embed_mode: 'auto', 'lsb', or 'dct' + timeout: Operation timeout in seconds + + Returns: + DecodeResult with message or file_data on success + """ + params = { + 'operation': 'decode', + 'stego_b64': base64.b64encode(stego_data).decode('ascii'), + 'reference_b64': base64.b64encode(reference_data).decode('ascii'), + 'passphrase': passphrase, + 'pin': pin, + 'embed_mode': embed_mode, + } + + if rsa_key_data: + params['rsa_key_b64'] = base64.b64encode(rsa_key_data).decode('ascii') + params['rsa_password'] = rsa_password + + result = self._run_worker(params, timeout) + + if result.get('success'): + if result.get('is_file'): + return DecodeResult( + success=True, + is_file=True, + file_data=base64.b64decode(result['file_b64']), + filename=result.get('filename'), + mime_type=result.get('mime_type'), + ) + else: + return DecodeResult( + success=True, + is_file=False, + message=result.get('message'), + ) + else: + return DecodeResult( + success=False, + error=result.get('error', 'Unknown error'), + error_type=result.get('error_type'), + ) + + def compare_modes( + self, + carrier_data: bytes, + timeout: Optional[int] = None, + ) -> CompareResult: + """ + Compare LSB and DCT capacity for a carrier image. + + Args: + carrier_data: Carrier image bytes + timeout: Operation timeout in seconds + + Returns: + CompareResult with capacity information + """ + params = { + 'operation': 'compare', + 'carrier_b64': base64.b64encode(carrier_data).decode('ascii'), + } + + result = self._run_worker(params, timeout) + + if result.get('success'): + comparison = result.get('comparison', {}) + return CompareResult( + success=True, + width=comparison.get('width', 0), + height=comparison.get('height', 0), + lsb=comparison.get('lsb'), + dct=comparison.get('dct'), + ) + else: + return CompareResult( + success=False, + error=result.get('error', 'Unknown error'), + ) + + def check_capacity( + self, + carrier_data: bytes, + payload_size: int, + embed_mode: str = "lsb", + timeout: Optional[int] = None, + ) -> CapacityResult: + """ + Check if a payload will fit in the carrier. + + Args: + carrier_data: Carrier image bytes + payload_size: Size of payload in bytes + embed_mode: 'lsb' or 'dct' + timeout: Operation timeout in seconds + + Returns: + CapacityResult with fit information + """ + params = { + 'operation': 'capacity', + 'carrier_b64': base64.b64encode(carrier_data).decode('ascii'), + 'payload_size': payload_size, + 'embed_mode': embed_mode, + } + + result = self._run_worker(params, timeout) + + if result.get('success'): + r = result.get('result', {}) + return CapacityResult( + success=True, + fits=r.get('fits', False), + payload_size=r.get('payload_size', 0), + capacity=r.get('capacity', 0), + usage_percent=r.get('usage_percent', 0.0), + headroom=r.get('headroom', 0), + mode=r.get('mode', embed_mode), + ) + else: + return CapacityResult( + success=False, + error=result.get('error', 'Unknown error'), + ) + + +# Convenience function for quick usage +_default_stego: Optional[SubprocessStego] = None + + +def get_subprocess_stego() -> SubprocessStego: + """Get or create default SubprocessStego instance.""" + global _default_stego + if _default_stego is None: + _default_stego = SubprocessStego() + return _default_stego diff --git a/frontends/web/templates/about.html b/frontends/web/templates/about.html index ef64701..9d65c24 100644 --- a/frontends/web/templates/about.html +++ b/frontends/web/templates/about.html @@ -11,11 +11,11 @@

- Stegasoo is a secure steganography tool that hides encrypted messages and files + Stegasoo is a steganography tool that hides encrypted messages and files inside ordinary images using multi-factor authentication.

-
Key Features
+
Features
    @@ -32,39 +32,37 @@
  • AES-256-GCM Encryption -
    Military-grade authenticated encryption +
    Authenticated encryption with integrity verification
  • - Single Passphrase - v3.2.0 -
    Stronger default security + LSB & DCT Modes +
    Choose capacity (LSB) or JPEG resilience (DCT)
    -
  • - - DCT Mode - v3.0 -
    Survives JPEG recompression for social media -
  • Random Pixel Embedding -
    Defeats statistical steganalysis +
    Key-derived selection defeats statistical analysis
  • - Large Capacity -
    Up to {{ max_payload_kb }} KB payload, 24MP images + Large Image Support +
    Up to {{ max_payload_kb }} KB payload, tested with 14MB+ images
  • Zero Server Storage
    Nothing saved, files auto-expire
  • +
  • + + QR Code Keys +
    Import/export RSA keys via QR codes +
@@ -78,8 +76,7 @@

- New in v3.0 - Stegasoo now supports two embedding modes, each optimized for different use cases. + Stegasoo supports two embedding modes, each optimized for different use cases.

@@ -120,7 +117,6 @@
DCT Mode - v3.0

@@ -200,7 +196,7 @@

How Security Works
-

Stegasoo uses hybrid multi-factor authentication to derive encryption keys:

+

Stegasoo uses multi-factor authentication to derive encryption keys:

@@ -215,7 +211,6 @@
Passphrase - v3.2.0
Something you know
~44 bits (4 words)
@@ -224,7 +219,7 @@
Static PIN -
Something you know (fixed)
+
Something you know
~20 bits (6 digits)
@@ -233,7 +228,7 @@ RSA Key
Something you have (optional)
-
~128 bits (2048-bit)
+
~128 bits
@@ -247,148 +242,77 @@
Key Derivation

{% if has_argon2 %} - Argon2id Available - Using Argon2id with 256MB memory cost — the winner of the Password Hashing Competition - and current best practice for key derivation. This makes GPU/ASIC attacks infeasible. + Argon2id + Using Argon2id with 256MB memory cost — memory-hard KDF that + makes GPU/ASIC attacks infeasible. {% else %} Argon2 Not Available Falling back to PBKDF2-SHA512 with 600,000 iterations. Install argon2-cffi for stronger security. {% endif %}

- -
Steganography Techniques
-

- LSB Mode: Uses Least Significant Bit embedding with pseudo-random pixel selection. - The pixel locations are determined by a key derived from your credentials, making the - hidden data's location unpredictable without the correct inputs. -

-

- DCT Mode: Uses Discrete Cosine Transform embedding with Quantization Index Modulation (QIM). - Data is hidden in mid-frequency coefficients of 8×8 blocks, making it resilient to JPEG recompression. - {% if has_dct %} - DCT Available - {% else %} - DCT Requires scipy - {% endif %} -

+
-
File Embedding
+
Version History
-

- Stegasoo supports embedding any file type, not just text messages. -

- -
-
-
Supported
-
    -
  • PDF documents
  • -
  • ZIP/RAR archives
  • -
  • Office documents (DOCX, XLSX, PPTX)
  • -
  • Source code files
  • -
  • Any binary file up to {{ max_payload_kb }} KB
  • -
-
-
-
How It Works
-
    -
  • Original filename is preserved
  • -
  • MIME type is stored for proper handling
  • -
  • File is encrypted identically to text
  • -
  • Decoding auto-detects text vs. file
  • -
-
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VersionChanges
4.0.0 + Simplified auth (no date dependency), passphrase replaces day_phrase, + 4-word default, JPEG normalization fix, large image support (14MB+ tested), + subprocess isolation for stability, Python 3.10-3.12 required +
3.2.0Single passphrase (removed day-of-week rotation), increased default words
3.0.0DCT steganography mode, JPEG output, color preservation option
2.2.0QR code RSA key import/export
2.1.0File embedding, compression support
2.0.0Web UI, REST API, RSA key support
1.0.0Initial release, CLI only, LSB mode
-
- - Tip: For larger files, compress them first (ZIP) to maximize capacity. - Note that DCT mode has ~10× less capacity than LSB mode. +
+ + Compatibility: v4.0 cannot decode messages from v3.1 or earlier (different format). + Messages encoded with v3.2 should decode correctly.
- -
-
-
REST API
-
-
-

- FastAPI - Stegasoo includes a complete REST API with automatic documentation and type validation. -

- -
Endpoints
-
-
-
    -
  • POST /generate – Generate credentials
  • -
  • POST /encode – Encode text (JSON)
  • -
  • POST /encode/multipart – Encode with uploads
  • -
  • POST /decode – Decode message (JSON)
  • -
  • POST /decode/multipart – Decode with uploads
  • -
-
-
-
    -
  • POST /image/info – Get image capacity
  • -
  • POST /extract-key-from-qr – Extract RSA from QR
  • -
  • GET / – API status and capabilities
  • -
  • GET /docs – Swagger documentation
  • -
  • GET /redoc – ReDoc documentation
  • -
-
-
- -
Example: DCT Encode
-
# Encode with DCT mode for social media
-curl -X POST "http://localhost:8000/encode/multipart" \
-  -F "passphrase=apple forest thunder mountain" \
-  -F "pin=123456" \
-  -F "embed_mode=dct" \
-  -F "dct_output_format=jpeg" \
-  -F "reference_photo=@photo.jpg" \
-  -F "carrier=@meme.png" \
-  -F "message=secret message" \
-  --output stego.jpg
- -
Command Line
-
# Generate credentials
-stegasoo generate --pin --words 4
-
-# Encode with LSB (default)
-stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder mountain" \
-  --pin 123456 -m "secret"
-
-# Encode with DCT for social media
-stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder mountain" \
-  --pin 123456 -m "secret" --mode dct --dct-format jpeg
-
-# Decode (auto-detects mode)
-stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder mountain" \
-  --pin 123456
- -

- - {% if has_argon2 %}Argon2{% else %}PBKDF2{% endif %} - - - {% if has_dct %}DCT Available{% else %}DCT Unavailable{% endif %} - - - {% if has_qrcode_read %}QR Reading{% else %}No QR Reading{% endif %} - -

-
-
-
Usage Guide
@@ -470,7 +394,7 @@ stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder mountain" \
-
+
Limits & Specifications
@@ -514,11 +438,13 @@ stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder mountain" \ 2048, 3072, 4096 bits - Passphrase length - v3.2.0 - + Passphrase length 3-12 words (BIP-39, recommended: 4+ words) + + Python version + 3.10-3.12 (3.13 not supported) +
@@ -528,7 +454,7 @@ stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder mountain" \

Stegasoo v{{ version }} • Open Source • - Built with Python, Flask/FastAPI, and cryptography + Built with Python, Flask, and cryptography

diff --git a/frontends/web/templates/decode.html b/frontends/web/templates/decode.html index 0d7bd38..17279a7 100644 --- a/frontends/web/templates/decode.html +++ b/frontends/web/templates/decode.html @@ -3,6 +3,53 @@ {% block title %}Decode Message - Stegasoo{% endblock %} {% block content %} + +
@@ -17,14 +64,14 @@
-
-
{{ decoded_message }}
- +
{{ decoded_message }}
+
+
- + Decode Another @@ -99,7 +146,7 @@ -
The passphrase used during encoding (typically 4 words) @@ -114,64 +161,62 @@
-
- +
-
- +
+
-
- If PIN was used during encoding -
- +
If PIN was used during encoding
-
+
- -
-
- -
-
- -
PNG, JPG, or other image of QR code
+ + +
+ + + + + +
+ + +
+ +
+ + +
+
+ +
+ + Drop QR image or click to browse +
+
-
- If RSA key was used during encoding (file or QR image) + + +
+ +
- -
- - -
- Leave blank if your key file is not password-protected -
-
- @@ -276,7 +321,7 @@
  • - Format compatibility: v3.2.0 cannot decode messages from v3.1.0 (different format) + Format compatibility: v4.0 cannot decode messages from v3.1 or earlier (different format)
  • @@ -304,22 +349,6 @@ document.getElementById('decodeForm')?.addEventListener('submit', function() { btn.disabled = true; }); -// Show RSA password field when key is selected -const rsaKeyInput = document.getElementById('rsaKeyInput'); -const rsaKeyQrInput = document.getElementById('rsaKeyQrInput'); -const rsaPasswordGroup = document.getElementById('rsaPasswordGroup'); - -function checkRsaKeySelected() { - const hasFile = (rsaKeyInput && rsaKeyInput.files.length > 0) || - (rsaKeyQrInput && rsaKeyQrInput.files.length > 0); - if (rsaPasswordGroup) { - rsaPasswordGroup.classList.toggle('d-none', !hasFile); - } -} - -rsaKeyInput?.addEventListener('change', checkRsaKeySelected); -rsaKeyQrInput?.addEventListener('change', checkRsaKeySelected); - // PIN Toggle document.getElementById('togglePin')?.addEventListener('click', function() { const input = document.getElementById('pinInput'); @@ -333,6 +362,35 @@ document.getElementById('togglePin')?.addEventListener('click', function() { } }); +// RSA Password Toggle +document.getElementById('toggleRsaPassword')?.addEventListener('click', function() { + const input = document.getElementById('rsaPasswordInput'); + const icon = this.querySelector('i'); + if (input.type === 'password') { + input.type = 'text'; + icon.classList.replace('bi-eye', 'bi-eye-slash'); + } else { + input.type = 'password'; + icon.classList.replace('bi-eye-slash', 'bi-eye'); + } +}); + +// RSA Input Method Toggle (File vs QR) +const rsaMethodFile = document.getElementById('rsaMethodFile'); +const rsaMethodQr = document.getElementById('rsaMethodQr'); +const rsaFileSection = document.getElementById('rsaFileSection'); +const rsaQrSection = document.getElementById('rsaQrSection'); + +function updateRsaInputMethod() { + if (!rsaMethodFile || !rsaFileSection || !rsaQrSection) return; + const isFile = rsaMethodFile.checked; + rsaFileSection.classList.toggle('d-none', !isFile); + rsaQrSection.classList.toggle('d-none', isFile); +} + +rsaMethodFile?.addEventListener('change', updateRsaInputMethod); +rsaMethodQr?.addEventListener('change', updateRsaInputMethod); + // Mode card highlighting const autoModeCard = document.getElementById('autoModeCard'); const lsbModeCardDec = document.getElementById('lsbModeCardDec'); @@ -428,12 +486,85 @@ document.querySelectorAll('.drop-zone').forEach(zone => { const reader = new FileReader(); reader.onload = e => { - preview.src = e.target.result; - preview.classList.remove('d-none'); + if (preview) { + preview.src = e.target.result; + preview.classList.remove('d-none'); + } label.innerHTML = '' + file.name; }; reader.readAsDataURL(file); } }); + +// QR Code RSA Key scanning +const rsaKeyQrInput = document.getElementById('rsaKeyQrInput'); +const qrPreview = document.getElementById('qrPreview'); +if (rsaKeyQrInput) { + rsaKeyQrInput.addEventListener('change', function() { + if (this.files && this.files[0]) { + const file = this.files[0]; + + // Show image preview + if (file.type.startsWith('image/')) { + const reader = new FileReader(); + reader.onload = e => { + if (qrPreview) { + qrPreview.src = e.target.result; + qrPreview.classList.remove('d-none'); + } + }; + reader.readAsDataURL(file); + } + + // Extract key from QR + const formData = new FormData(); + formData.append('qr_image', file); + + fetch('/extract-key-from-qr', { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(data => { + if (!data.success) { + alert('QR decode failed: ' + data.error); + return; + } + // Visual feedback + document.querySelector('#qrDropZone .drop-zone-label').innerHTML = + 'RSA Key loaded from QR'; + }) + .catch(err => { + alert('QR decode failed: ' + err); + }); + } + }); +} + +// Auto-resize passphrase input font to fit long passphrases +const passphraseInput = document.getElementById('passphraseInput'); +if (passphraseInput) { + // Stepped font sizes (characters -> rem) + const fontSizeSteps = [ + { maxChars: 30, size: 1.1 }, + { maxChars: 45, size: 1.0 }, + { maxChars: 60, size: 0.95 }, + { maxChars: Infinity, size: 0.9 } + ]; + + function adjustPassphraseFontSize() { + const len = passphraseInput.value.length; + + for (const step of fontSizeSteps) { + if (len <= step.maxChars) { + passphraseInput.style.fontSize = step.size + 'rem'; + break; + } + } + } + + passphraseInput.addEventListener('input', adjustPassphraseFontSize); + adjustPassphraseFontSize(); // Initial call in case of pre-filled value +} {% endblock %} diff --git a/frontends/web/templates/encode.html b/frontends/web/templates/encode.html index cfde38f..ff77521 100644 --- a/frontends/web/templates/encode.html +++ b/frontends/web/templates/encode.html @@ -3,6 +3,57 @@ {% block title %}Encode Message - Stegasoo{% endblock %} {% block content %} + +
    @@ -11,7 +62,7 @@
    - +
    @@ -182,15 +233,16 @@
    - +
    - +
    + +
    Your passphrase for this message
    @@ -210,8 +262,8 @@
    -
    - +
    + @@ -250,6 +302,7 @@ Drop QR image or click to browse
    +
    @@ -406,12 +459,33 @@ function updatePayloadSection() { payloadTextRadio.addEventListener('change', updatePayloadSection); payloadFileRadio.addEventListener('change', updatePayloadSection); -// Passphrase validation (v3.2.0) +// Passphrase validation and auto-resize font const passphraseInput = document.getElementById('passphraseInput'); const passphraseWarning = document.getElementById('passphraseWarning'); +// Stepped font sizes (characters -> rem) +const fontSizeSteps = [ + { maxChars: 30, size: 1.1 }, + { maxChars: 45, size: 1.0 }, + { maxChars: 60, size: 0.95 }, + { maxChars: Infinity, size: 0.9 } +]; + +function adjustPassphraseFontSize() { + if (!passphraseInput) return; + const len = passphraseInput.value.length; + + for (const step of fontSizeSteps) { + if (len <= step.maxChars) { + passphraseInput.style.fontSize = step.size + 'rem'; + break; + } + } +} + if (passphraseInput) { passphraseInput.addEventListener('input', function() { + // Word count warning const words = this.value.trim().split(/\s+/).filter(w => w.length > 0); const recommendedWords = {{ recommended_passphrase_words }}; @@ -420,7 +494,13 @@ if (passphraseInput) { } else { passphraseWarning.style.display = 'none'; } + + // Auto-resize font + adjustPassphraseFontSize(); }); + + // Initial font size adjustment + adjustPassphraseFontSize(); } // Payload file info display @@ -728,11 +808,27 @@ document.addEventListener('paste', function(e) { // QR Code RSA Key scanning const rsaQrInput = document.getElementById('rsaQrInput'); +const qrPreview = document.getElementById('qrPreview'); if (rsaQrInput) { rsaQrInput.addEventListener('change', function() { if (this.files && this.files[0]) { + const file = this.files[0]; + + // Show image preview + if (file.type.startsWith('image/')) { + const reader = new FileReader(); + reader.onload = e => { + if (qrPreview) { + qrPreview.src = e.target.result; + qrPreview.classList.remove('d-none'); + } + }; + reader.readAsDataURL(file); + } + + // Extract key from QR const formData = new FormData(); - formData.append('qr_image', this.files[0]); + formData.append('qr_image', file); fetch('/extract-key-from-qr', { method: 'POST', diff --git a/frontends/web/templates/encode_result.html b/frontends/web/templates/encode_result.html index 479c682..4c8225d 100644 --- a/frontends/web/templates/encode_result.html +++ b/frontends/web/templates/encode_result.html @@ -120,7 +120,7 @@ {% if embed_mode == 'dct' %}
  • Recipient needs DCT mode or Auto detection to decode
  • {% if color_mode == 'color' %} -
  • v3.0.1 Color preserved - extraction works on both color and grayscale
  • +
  • Color preserved - extraction works on both color and grayscale
  • {% endif %} {% endif %} diff --git a/frontends/web/templates/generate.html b/frontends/web/templates/generate.html index fc433bb..f00830f 100644 --- a/frontends/web/templates/generate.html +++ b/frontends/web/templates/generate.html @@ -112,7 +112,6 @@
    PASSPHRASE - v3.2.0
    diff --git a/frontends/web/templates/index.html b/frontends/web/templates/index.html index 194110d..1069615 100644 --- a/frontends/web/templates/index.html +++ b/frontends/web/templates/index.html @@ -11,7 +11,7 @@

    Stegasoo - v3.2.0 + v4.0

    Hide encrypted data in plain sight.

    @@ -94,7 +94,6 @@
    DCT Mode - v3.0
    Survives JPEG recompression
    Best for social media @@ -108,7 +107,7 @@
    How It Works
    - Learn More + Learn More
    @@ -117,21 +116,20 @@
    • - Reference Photo – shared secret image + Reference Photo — shared secret image
    • - Passphrase – 4+ words - v3.2.0 + Passphrase — 4+ words
    • - PIN – 6-9 digits (and/or RSA key) + PIN — 6-9 digits (and/or RSA key)
    -
    We Provide
    +
    Security