Version 3.0.2 full expirimental DCT support, jpegio for better jpg manipulation, etc.

This commit is contained in:
Aaron D. Lee
2025-12-31 15:43:29 -05:00
parent 4eefc946c4
commit 34376b2dfe
19 changed files with 2954 additions and 2200 deletions

View File

@@ -12,11 +12,13 @@ ENV PYTHONUNBUFFERED=1
ENV PIP_ROOT_USER_ACTION=ignore ENV PIP_ROOT_USER_ACTION=ignore
# Install system dependencies # Install system dependencies
# NOTE: libjpeg-dev is required for jpegio compilation
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \ gcc \
libc-dev \ libc-dev \
libffi-dev \ libffi-dev \
libzbar0 \ libzbar0 \
libjpeg-dev \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# ============================================================================ # ============================================================================
@@ -31,8 +33,10 @@ COPY pyproject.toml README.md ./
COPY src/ src/ COPY src/ src/
COPY data/ data/ COPY data/ data/
# Install the package with web extras # Install build dependencies for jpegio, then install the package
RUN pip install --no-cache-dir ".[web]" # jpegio requires Cython and numpy to compile
RUN pip install --no-cache-dir cython numpy && \
pip install --no-cache-dir ".[web]"
# ============================================================================ # ============================================================================
# Production stage - Web UI # Production stage - Web UI
@@ -78,11 +82,14 @@ FROM base as api
WORKDIR /app WORKDIR /app
# Install API extras # Install API extras (includes DCT dependencies)
COPY pyproject.toml README.md ./ COPY pyproject.toml README.md ./
COPY src/ src/ COPY src/ src/
COPY data/ data/ COPY data/ data/
RUN pip install --no-cache-dir ".[api]"
# 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 API files
COPY frontends/api/ frontends/api/ COPY frontends/api/ frontends/api/
@@ -116,7 +123,10 @@ WORKDIR /app
COPY pyproject.toml README.md ./ COPY pyproject.toml README.md ./
COPY src/ src/ COPY src/ src/
COPY data/ data/ COPY data/ data/
RUN pip install --no-cache-dir ".[cli]"
# 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 CLI files
COPY frontends/cli/ frontends/cli/ COPY frontends/cli/ frontends/cli/

712
INSTALL.md Normal file
View File

@@ -0,0 +1,712 @@
# Stegasoo Installation Guide
Complete installation instructions for all platforms and deployment methods.
## Table of Contents
- [Requirements](#requirements)
- [Quick Install](#quick-install)
- [Installation Methods](#installation-methods)
- [From Source (Development)](#from-source-development)
- [From PyPI](#from-pypi)
- [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)
---
## Requirements
### Minimum Requirements
| Requirement | Version |
|-------------|---------|
| Python | 3.10+ |
| RAM | 512 MB minimum (256MB for Argon2) |
| Disk | ~100 MB |
### System Dependencies
**Linux (Debian/Ubuntu):**
```bash
sudo apt-get update
sudo apt-get install -y \
python3 \
python3-pip \
python3-dev \
libzbar0 \
libjpeg-dev \
build-essential
```
**macOS:**
```bash
brew install python@3.11 zbar jpeg
xcode-select --install # For compilation
```
**Windows:**
- Install Python 3.10+ from [python.org](https://python.org)
- Install Visual Studio Build Tools for compilation
---
## Quick Install
```bash
# Clone and install everything
git clone https://github.com/adlee-was-taken/stegasoo.git
cd stegasoo
pip install -e ".[all]"
# Verify
stegasoo --version
```
---
## Installation Methods
### From Source (Development)
Best for development or customization.
```bash
# Clone the repository
git clone https://github.com/adlee-was-taken/stegasoo.git
cd stegasoo
# Create virtual environment (recommended)
python -m venv venv
source venv/bin/activate # Linux/macOS
# or: venv\Scripts\activate # Windows
# Install core library only
pip install -e .
# Install with specific extras
pip install -e ".[cli]" # Command-line interface
pip install -e ".[web]" # Flask web UI + DCT support
pip install -e ".[api]" # FastAPI REST API + DCT support
pip install -e ".[dct]" # DCT steganography only
pip install -e ".[compression]" # LZ4 compression
# Install everything
pip install -e ".[all]"
# Install with development tools
pip install -e ".[dev]"
```
### From PyPI
```bash
# Core only
pip install stegasoo
# With extras
pip install stegasoo[cli]
pip install stegasoo[web]
pip install stegasoo[api]
pip install stegasoo[all]
```
### Docker
Build and run individual containers.
#### Build Images
```bash
# Build all targets
docker build -t stegasoo-web --target web .
docker build -t stegasoo-api --target api .
docker build -t stegasoo-cli --target cli .
```
#### Run Web UI
```bash
docker run -d \
--name stegasoo-web \
-p 5000:5000 \
--memory=768m \
stegasoo-web
# Visit http://localhost:5000
```
#### Run REST API
```bash
docker run -d \
--name stegasoo-api \
-p 8000:8000 \
--memory=768m \
stegasoo-api
# Docs at http://localhost:8000/docs
```
#### Run CLI
```bash
# Interactive shell
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
# With volume for files
docker run --rm \
-v $(pwd)/images:/data \
stegasoo-cli encode \
-r /data/ref.jpg \
-c /data/carrier.png \
-p "phrase words here" \
--pin 123456 \
-m "Secret message" \
-o /data/stego.png
```
### Docker Compose
The easiest way to run all services.
#### Start All Services
```bash
# Start in background
docker-compose up -d
# Start specific service
docker-compose up -d web
docker-compose up -d api
# View logs
docker-compose logs -f
# Stop all
docker-compose down
```
#### Services
| Service | URL | Description |
|---------|-----|-------------|
| `web` | http://localhost:5000 | Flask Web UI |
| `api` | http://localhost:8000 | FastAPI REST API |
#### Build and Start
```bash
# Build images and start
docker-compose up -d --build
# Force rebuild (no cache)
docker-compose build --no-cache
docker-compose up -d
```
#### Resource Configuration
The `docker-compose.yml` includes resource limits:
```yaml
services:
web:
deploy:
resources:
limits:
memory: 768M # For Argon2 + scipy
reservations:
memory: 384M
```
Adjust based on your available RAM:
| Available RAM | Recommended Limit | Workers |
|---------------|-------------------|---------|
| 2 GB | 768M | 2 |
| 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
### DCT Steganography (scipy + jpegio)
DCT mode enables JPEG-resilient steganography. It's automatically included with `[web]`, `[api]`, and `[all]` extras.
#### Install via pip
```bash
# scipy is straightforward
pip install scipy
# jpegio - try pip first
pip install jpegio
# If pip fails, build from source
pip install cython numpy
git clone https://github.com/dwgoon/jpegio.git
cd jpegio
python setup.py install
```
#### Linux Build Dependencies
```bash
sudo apt-get install -y \
build-essential \
python3-dev \
libjpeg-dev \
cython3
```
#### macOS Build Dependencies
```bash
brew install jpeg cython
```
#### Verify DCT Support
```python
from stegasoo import has_dct_support
from stegasoo.dct_steganography import has_jpegio_support
print(f"DCT support (scipy): {has_dct_support()}")
print(f"JPEG native (jpegio): {has_jpegio_support()}")
```
Expected output:
```
DCT support (scipy): True
JPEG native (jpegio): True
```
### Compression (lz4)
Optional LZ4 compression for messages:
```bash
pip install lz4
```
---
## Platform-Specific Notes
### Linux
Most straightforward installation. Use your package manager for system dependencies.
**Ubuntu/Debian:**
```bash
sudo apt-get install python3-dev libzbar0 libjpeg-dev
pip install stegasoo[all]
```
**Fedora/RHEL:**
```bash
sudo dnf install python3-devel zbar libjpeg-devel
pip install stegasoo[all]
```
**Arch:**
```bash
sudo pacman -S python zbar libjpeg-turbo
pip install stegasoo[all]
```
### macOS
```bash
# Install Homebrew if needed
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Install dependencies
brew install python@3.11 zbar jpeg
# Install Stegasoo
pip3 install stegasoo[all]
```
**Apple Silicon (M1/M2/M3):**
jpegio may need Rosetta or native compilation:
```bash
# Try native first
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)
2. Install Visual Studio Build Tools
3. Install from pip:
```powershell
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):
```bash
# System dependencies
sudo apt-get install python3-dev libzbar0 libjpeg-dev
# Install (may take a while to compile)
pip install stegasoo[cli]
# For web/api, ensure enough RAM
pip install stegasoo[web] # Needs ~768MB free
```
**Note:** Argon2 operations will be slower on Pi due to memory-hardness.
---
## Verification
### Check Installation
```bash
# CLI version
stegasoo --version
# Python import
python -c "import stegasoo; print(stegasoo.__version__)"
```
### Check All Features
```python
#!/usr/bin/env python3
"""Verify Stegasoo installation."""
def check_feature(name, check_fn):
try:
result = check_fn()
status = "" if result else ""
print(f" {status} {name}: {result}")
return result
except Exception as e:
print(f"{name}: Error - {e}")
return False
print("Stegasoo Installation Check")
print("=" * 40)
# Core
import stegasoo
print(f"\nVersion: {stegasoo.__version__}")
print("\nCore Features:")
check_feature("Argon2", lambda: stegasoo.has_argon2())
check_feature("Pillow", lambda: True) # Required, would fail import
print("\nOptional Features:")
check_feature("DCT (scipy)", stegasoo.has_dct_support)
try:
from stegasoo.dct_steganography import has_jpegio_support
check_feature("JPEG native (jpegio)", has_jpegio_support)
except ImportError:
print(" ✗ JPEG native (jpegio): Not installed")
try:
import lz4
check_feature("Compression (lz4)", lambda: True)
except ImportError:
print(" - Compression (lz4): Not installed (optional)")
try:
import pyzbar
check_feature("QR codes (pyzbar)", lambda: True)
except ImportError:
print(" - QR codes (pyzbar): Not installed (optional)")
print("\nInterfaces:")
try:
import click
check_feature("CLI", lambda: True)
except ImportError:
print(" ✗ CLI: Not installed")
try:
import flask
check_feature("Web UI", lambda: True)
except ImportError:
print(" - Web UI: Not installed")
try:
import fastapi
check_feature("REST API", lambda: True)
except ImportError:
print(" - REST API: Not installed")
print("\n" + "=" * 40)
print("Installation check complete!")
```
Save as `check_install.py` and run:
```bash
python check_install.py
```
### Test Encoding/Decoding
```bash
# Quick test with CLI
stegasoo generate --pin --words 3 --json > /tmp/creds.json
# Create test image
python -c "
from PIL import Image
img = Image.new('RGB', (256, 256), 'blue')
img.save('/tmp/test_carrier.png')
img.save('/tmp/test_ref.jpg')
"
# Encode
stegasoo encode \
-r /tmp/test_ref.jpg \
-c /tmp/test_carrier.png \
-p "test phrase words" \
--pin 123456 \
-m "Hello, Stegasoo!" \
-o /tmp/test_stego.png
# Decode
stegasoo decode \
-r /tmp/test_ref.jpg \
-s /tmp/test_stego.png \
-p "test phrase words" \
--pin 123456
```
---
## Troubleshooting
### Common Issues
#### "No module named 'stegasoo'"
```bash
# Ensure you're in the right environment
which python
pip list | grep stegasoo
# Reinstall
pip install -e ".[all]"
```
#### "Argon2 not available"
```bash
# Install argon2-cffi
pip install argon2-cffi
# On Linux, may need:
sudo apt-get install libffi-dev
pip install --force-reinstall argon2-cffi
```
#### "jpegio not available" / DCT JPEG fails
```bash
# Install build dependencies first
sudo apt-get install libjpeg-dev # Linux
brew install jpeg # macOS
# Then install jpegio
pip install cython numpy
pip install jpegio
# If still fails, build from source
git clone https://github.com/dwgoon/jpegio.git
cd jpegio
python setup.py install
```
#### "libzbar not found" (QR codes)
```bash
# Linux
sudo apt-get install libzbar0
# macOS
brew install zbar
# Then reinstall pyzbar
pip install --force-reinstall pyzbar
```
#### Docker: "Cannot allocate memory"
Argon2 needs 256MB per operation. Increase container memory:
```bash
# Docker run
docker run --memory=768m ...
# Docker Compose - edit docker-compose.yml
deploy:
resources:
limits:
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
#### "Carrier image too small"
- LSB needs ~3 bits per pixel
- DCT needs ~0.25 bits per pixel
- For 50KB message: LSB needs ~136K pixels, DCT needs ~1.6M pixels
- Use larger carrier images or shorter messages
### Getting Help
1. Check the documentation:
- [README.md](README.md)
- [CLI.md](CLI.md)
- [API.md](API.md)
- [WEB_UI.md](WEB_UI.md)
2. Check existing issues on GitHub
3. Open a new issue with:
- Python version (`python --version`)
- OS and version
- Installation method
- Full error message
- Steps to reproduce
---
## Next Steps
After installation:
1. **Generate credentials**: `stegasoo generate --pin --words 3`
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`
Happy steganography! 🦕

323
README.md
View File

@@ -5,6 +5,7 @@ A secure steganography system for hiding encrypted messages in images using hybr
![Python](https://img.shields.io/badge/Python-3.10+-blue) ![Python](https://img.shields.io/badge/Python-3.10+-blue)
![License](https://img.shields.io/badge/License-MIT-green) ![License](https://img.shields.io/badge/License-MIT-green)
![Security](https://img.shields.io/badge/Security-AES--256--GCM-red) ![Security](https://img.shields.io/badge/Security-AES--256--GCM-red)
![Version](https://img.shields.io/badge/Version-3.0.2-purple)
## Features ## Features
@@ -17,50 +18,40 @@ A secure steganography system for hiding encrypted messages in images using hybr
- 🌐 **Multiple interfaces**: CLI, Web UI, REST API - 🌐 **Multiple interfaces**: CLI, Web UI, REST API
- 📁 **File embedding** - Hide any file type (PDF, ZIP, documents) - 📁 **File embedding** - Hide any file type (PDF, ZIP, documents)
- 📱 **QR code support** - Encode/decode RSA keys via QR codes - 📱 **QR code support** - Encode/decode RSA keys via QR codes
- 🆕 **DCT steganography** - JPEG-resilient embedding for social media (v3.0+)
## What's New in v3.0.2
| 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 |
### 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 |
## WebUI Preview ## WebUI Preview
Front Page | Encode | Decode | Generate | | Front Page | Encode | Decode | Generate |
:-------------------------:|:-------------------------:|:------------------------:|:--------:| |:----------:|:------:|:------:|:--------:|
![Screenshot](https://github.com/adlee-was-taken/stegasoo/blob/main/data/WebUI.webp) | ![Screenshot](https://github.com/adlee-was-taken/stegasoo/blob/main/data/WebUI_Encode.webp) | ![Screenshot](https://github.com/adlee-was-taken/stegasoo/blob/main/data/WebUI_Decode.webp) | ![Screenshot](https://github.com/adlee-was-taken/stegasoo/blob/main/data/WebUI_Generate.webp) | ![Screenshot](https://github.com/adlee-was-taken/stegasoo/blob/main/data/WebUI.webp) | ![Screenshot](https://github.com/adlee-was-taken/stegasoo/blob/main/data/WebUI_Encode.webp) | ![Screenshot](https://github.com/adlee-was-taken/stegasoo/blob/main/data/WebUI_Decode.webp) | ![Screenshot](https://github.com/adlee-was-taken/stegasoo/blob/main/data/WebUI_Generate.webp) |
## Quick Start
## Installation
### From Source
```bash ```bash
# Clone the repository # Install with all features
git clone https://github.com/adlee-was-taken/stegasoo.git
cd stegasoo
# Install core library
pip install -e .
# Install with CLI
pip install -e ".[cli]"
# Install with Web UI
pip install -e ".[web]"
# Install with REST API
pip install -e ".[api]"
# Install everything
pip install -e ".[all]" pip install -e ".[all]"
```
### CLI Usage # Generate credentials (memorize these!)
```bash
# Generate credentials
stegasoo generate --pin --words 3 stegasoo generate --pin --words 3
# With RSA key # Encode a message (LSB mode - default)
stegasoo generate --rsa --rsa-bits 4096 -o mykey.pem -p "secretpassword"
# Encode
stegasoo encode \ stegasoo encode \
--ref photo.jpg \ --ref photo.jpg \
--carrier meme.png \ --carrier meme.png \
@@ -68,88 +59,171 @@ stegasoo encode \
--pin 123456 \ --pin 123456 \
--message "Secret message" --message "Secret message"
# Decode # Encode for social media (DCT mode)
stegasoo encode \
--ref photo.jpg \
--carrier meme.png \
--phrase "apple forest thunder" \
--pin 123456 \
--message "Secret message" \
--mode dct \
--format jpeg
# Decode (auto-detects mode)
stegasoo decode \ stegasoo decode \
--ref photo.jpg \ --ref photo.jpg \
--stego stego.png \ --stego stego.png \
--phrase "apple forest thunder" \ --phrase "apple forest thunder" \
--pin 123456 --pin 123456
# Pipe-friendly
echo "secret" | stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 > stego.png
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 -q
``` ```
### Web UI For detailed installation instructions, see **[INSTALL.md](INSTALL.md)**.
```bash ---
# Development
cd frontends/web
python app.py
# Production
gunicorn --bind 0.0.0.0:5000 app:app
```
Visit http://localhost:5000
### REST API
```bash
# Development
cd frontends/api
python main.py
# Production
uvicorn main:app --host 0.0.0.0 --port 8000
```
API docs at http://localhost:8000/docs
#### Example API Calls
```bash
# Generate credentials
curl -X POST http://localhost:8000/generate \
-H "Content-Type: application/json" \
-d '{"use_pin": true, "use_rsa": false}'
# Encode (multipart)
curl -X POST http://localhost:8000/encode/multipart \
-F "message=Secret" \
-F "day_phrase=apple forest thunder" \
-F "pin=123456" \
-F "reference_photo=@photo.jpg" \
-F "carrier=@meme.png" \
--output stego.png
# Decode (multipart)
curl -X POST http://localhost:8000/decode/multipart \
-F "day_phrase=apple forest thunder" \
-F "pin=123456" \
-F "reference_photo=@photo.jpg" \
-F "stego_image=@stego.png"
```
## Security Model ## Security Model
Stegasoo uses multiple authentication factors combined with strong cryptography:
```
┌─────────────────────────────────────────────────────────────────┐
│ AUTHENTICATION LAYERS │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Reference Photo ──┐ │
│ (~80-256 bits) │ │
│ ├──► Argon2id KDF ──► AES-256-GCM Key │
│ Day Phrase ───────┤ (256MB RAM) │
│ (~33-132 bits) │ │
│ │ │
│ Static PIN ───────┤ │
│ (~20-30 bits) │ │
│ │ │
│ RSA Key ──────────┘ │
│ (~128 bits) (optional, adds another factor) │
│ │
└─────────────────────────────────────────────────────────────────┘
```
### Entropy Summary
| Component | Entropy | Purpose | | Component | Entropy | Purpose |
|-----------|---------|---------| |-----------|---------|---------|
| Reference Photo | ~80-256 bits | Something you have | | Reference Photo | ~80-256 bits | Something you have |
| Day Phrase (3-12 words) | ~33-100+ bits | Something you know (rotates daily) | | Day Phrase (3-12 words) | ~33-132 bits | Something you know (rotates daily) |
| PIN (6-9 digits) | ~20+ bits | Something you know (static) | | PIN (6-9 digits) | ~20-30 bits | Something you know (static) |
| RSA Key (2048-bit) | ~128 bits | Something you have | | RSA Key (2048-4096 bit) | ~112-128 bits | Something you have (optional) |
| **Combined** | **~133-400+ bits** | **Beyond brute force** | | **Combined** | **133-400+ bits** | **Beyond brute force** |
### Attack Resistance ### Attack Resistance
| Attack | Protection | | Attack | Protection |
|--------|------------| |--------|------------|
| Brute force | 2^133+ combinations | | Brute force | 2^133+ combinations minimum |
| Rainbow tables | Random salt per message | | Rainbow tables | Random 16-byte salt per message |
| Steganalysis | Random pixel selection | | Steganalysis | Pseudo-random pixel/coefficient selection |
| GPU cracking | Argon2id requires 256MB RAM per attempt | | GPU cracking | Argon2id requires 256MB RAM per attempt |
| Side-channel | Constant-time operations in crypto | | Side-channel | Constant-time operations in cryptography library |
| JPEG recompression | DCT mode embeds in frequency domain (v3.0+) |
### 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 |
---
## Interfaces
### Command-Line Interface (CLI)
Full-featured CLI with piping support:
```bash
# Generate with RSA key
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
# Encode for social media (DCT + JPEG)
stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 \
-m "Message" --mode dct --format jpeg
# Decode to stdout (quiet mode)
stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q
# Check image capacity (shows both LSB and DCT)
stegasoo info carrier.png
```
📖 Full documentation: **[CLI.md](CLI.md)**
### Web UI
Browser-based interface with drag-and-drop uploads:
```bash
# Start the server
cd frontends/web
python app.py
# Visit http://localhost:5000
```
Features:
- Drag-and-drop image uploads
- Real-time entropy calculator
- Native mobile sharing (Web Share API)
- DCT mode with advanced options panel
- Automatic day-of-week detection
📖 Full documentation: **[WEB_UI.md](WEB_UI.md)**
### REST API
FastAPI-powered REST API with OpenAPI documentation:
```bash
# Start the server
cd frontends/api
uvicorn main:app --host 0.0.0.0 --port 8000
# Docs at http://localhost:8000/docs
```
Example API calls:
```bash
# Generate credentials
curl -X POST http://localhost:8000/generate \
-H "Content-Type: application/json" \
-d '{"use_pin": true, "words_per_phrase": 3}'
# Encode with DCT mode
curl -X POST http://localhost:8000/encode/multipart \
-F "message=Secret" \
-F "day_phrase=apple forest thunder" \
-F "pin=123456" \
-F "embedding_mode=dct" \
-F "output_format=jpeg" \
-F "reference_photo=@photo.jpg" \
-F "carrier=@meme.png" \
--output stego.jpg
# Decode (auto-detects mode)
curl -X POST http://localhost:8000/decode/multipart \
-F "day_phrase=apple forest thunder" \
-F "pin=123456" \
-F "reference_photo=@photo.jpg" \
-F "stego_image=@stego.jpg"
```
📖 Full documentation: **[API.md](API.md)**
---
## Project Structure ## Project Structure
@@ -159,11 +233,13 @@ stegasoo/
│ ├── __init__.py # Public API │ ├── __init__.py # Public API
│ ├── constants.py # Configuration │ ├── constants.py # Configuration
│ ├── crypto.py # Encryption/decryption │ ├── crypto.py # Encryption/decryption
│ ├── steganography.py # Image embedding │ ├── steganography.py # LSB image embedding
│ ├── dct_steganography.py # DCT embedding (v3.0+)
│ ├── keygen.py # Credential generation │ ├── keygen.py # Credential generation
│ ├── validation.py # Input validation │ ├── validation.py # Input validation
│ ├── models.py # Data classes │ ├── models.py # Data classes
│ ├── exceptions.py # Custom exceptions │ ├── exceptions.py # Custom exceptions
│ ├── qr_utils.py # QR code utilities
│ └── utils.py # Utilities │ └── utils.py # Utilities
├── frontends/ ├── frontends/
@@ -176,18 +252,19 @@ stegasoo/
├── pyproject.toml # Package configuration ├── pyproject.toml # Package configuration
├── Dockerfile # Multi-stage Docker build ├── Dockerfile # Multi-stage Docker build
── docker-compose.yml # Container orchestration ── docker-compose.yml # Container orchestration
├── README.md # This file
├── INSTALL.md # Installation guide
├── CLI.md # CLI documentation
├── API.md # API documentation
└── WEB_UI.md # Web UI documentation
``` ```
---
## Configuration ## Configuration
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `FLASK_ENV` | production | Flask environment |
| `PYTHONPATH` | - | Include src/ for development |
### Limits ### Limits
| Limit | Value | | Limit | Value |
@@ -199,6 +276,15 @@ stegasoo/
| Phrase length | 3-12 words | | Phrase length | 3-12 words |
| RSA key sizes | 2048, 3072, 4096 bits | | RSA key sizes | 2048, 3072, 4096 bits |
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `FLASK_ENV` | production | Flask environment |
| `PYTHONPATH` | - | Include `src/` for development |
---
## Development ## Development
```bash ```bash
@@ -214,13 +300,42 @@ ruff check src/ frontends/
# Type checking # Type checking
mypy src/ 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())"
``` ```
---
## Version History
| 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 |
| **2.2.x** | QR code support, file embedding |
| **2.0.x** | Web UI, REST API, RSA keys |
| **1.0.x** | Initial release, CLI only |
---
## License ## License
MIT License - Use responsibly. MIT License - Use responsibly.
---
## ⚠️ Disclaimer ## ⚠️ Disclaimer
This tool is for educational and legitimate privacy purposes only. Users are responsible for complying with applicable laws in their jurisdiction. This tool is for educational and legitimate privacy purposes only. Users are responsible for complying with applicable laws in their jurisdiction.
---
## See Also
- **[INSTALL.md](INSTALL.md)** - Detailed installation instructions
- **[CLI.md](CLI.md)** - Command-line interface reference
- **[API.md](API.md)** - REST API documentation
- **[WEB_UI.md](WEB_UI.md)** - Web interface guide

View File

@@ -17,9 +17,9 @@ services:
deploy: deploy:
resources: resources:
limits: limits:
memory: 512M # Argon2 needs 256MB per operation memory: 768M # Increased for scipy + Argon2
reservations: reservations:
memory: 256M memory: 384M
# ============================================================================ # ============================================================================
# REST API (FastAPI) # REST API (FastAPI)
@@ -35,9 +35,9 @@ services:
deploy: deploy:
resources: resources:
limits: limits:
memory: 512M memory: 768M # Increased for scipy + Argon2
reservations: reservations:
memory: 256M memory: 384M
# ============================================================================ # ============================================================================
# Nginx Reverse Proxy (optional, for production) # Nginx Reverse Proxy (optional, for production)

View File

@@ -16,6 +16,7 @@ Complete REST API reference for Stegasoo steganography operations.
- [POST /decode](#post-decode-json) - [POST /decode](#post-decode-json)
- [POST /decode/multipart](#post-decodemultipart) - [POST /decode/multipart](#post-decodemultipart)
- [POST /image/info](#post-imageinfo) - [POST /image/info](#post-imageinfo)
- [Embedding Modes](#embedding-modes)
- [Data Models](#data-models) - [Data Models](#data-models)
- [Error Handling](#error-handling) - [Error Handling](#error-handling)
- [Code Examples](#code-examples) - [Code Examples](#code-examples)
@@ -29,12 +30,19 @@ Complete REST API reference for Stegasoo steganography operations.
The Stegasoo REST API provides programmatic access to all steganography operations: The Stegasoo REST API provides programmatic access to all steganography operations:
- **Generate** credentials (phrases, PINs, RSA keys) - **Generate** credentials (phrases, PINs, RSA keys)
- **Encode** messages into images - **Encode** messages into images (LSB or DCT mode)
- **Decode** messages from images - **Decode** messages from images (auto-detects mode)
- **Analyze** image capacity - **Analyze** image capacity
The API supports both JSON (base64-encoded images) and multipart form data (direct file uploads). The API supports both JSON (base64-encoded images) and multipart form data (direct file uploads).
### What's New in v3.0.2
- **DCT Steganography Mode** - JPEG-resilient embedding
- **Output Format Selection** - PNG or JPEG output
- **Color Mode Selection** - Color or grayscale processing
- **jpegio Integration** - Proper JPEG coefficient manipulation
--- ---
## Installation ## Installation
@@ -45,6 +53,8 @@ The API supports both JSON (base64-encoded images) and multipart form data (dire
pip install stegasoo[api] pip install stegasoo[api]
``` ```
This automatically installs DCT dependencies (scipy, jpegio) for full functionality.
### From Source ### From Source
```bash ```bash
@@ -107,8 +117,10 @@ Host: localhost:8000
```json ```json
{ {
"version": "2.0.1", "version": "3.0.2",
"has_argon2": true, "has_argon2": true,
"has_dct": true,
"has_jpegio": true,
"day_names": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] "day_names": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
} }
``` ```
@@ -119,6 +131,8 @@ Host: localhost:8000
|-------|------|-------------| |-------|------|-------------|
| `version` | string | Stegasoo library version | | `version` | string | Stegasoo library version |
| `has_argon2` | boolean | Whether Argon2id is available | | `has_argon2` | boolean | Whether Argon2id is available |
| `has_dct` | boolean | Whether DCT mode is available (scipy) |
| `has_jpegio` | boolean | Whether native JPEG DCT is available |
| `day_names` | array | Day names for phrase mapping | | `day_names` | array | Day names for phrase mapping |
#### cURL Example #### cURL Example
@@ -245,22 +259,28 @@ Content-Type: application/json
| `rsa_password` | string | | | Password for RSA key | | `rsa_password` | string | | | Password for RSA key |
| `date_str` | string | | | Date override (YYYY-MM-DD) | | `date_str` | string | | | Date override (YYYY-MM-DD) |
| `embedding_mode` | string | | `"lsb"` | `"lsb"` or `"dct"` | | `embedding_mode` | string | | `"lsb"` | `"lsb"` or `"dct"` |
\* At least one of `pin` or `rsa_key_base64` required. | `output_format` | string | | `"png"` | `"png"` or `"jpeg"` (DCT only) |
| `color_mode` | string | | `"color"` | `"color"` or `"grayscale"` (DCT only) |
\* At least one of `pin` or `rsa_key_base64` required.
#### Response #### Response
```json ```json
{ {
"stego_image_base64": "iVBORw0KGgo...", "stego_image_base64": "iVBORw0KGgo...",
"filename": "a1b2c3d4_20251227.png", "filename": "a1b2c3d4_20251227.png",
"capacity_used_percent": 12.4, "capacity_used_percent": 12.4,
"date_used": "2025-12-27", "date_used": "2025-12-27",
"day_of_week": "Saturday" "day_of_week": "Saturday",
} "embedding_mode": "lsb",
``` "output_format": "png",
"color_mode": null
#### Response Fields }
```
#### Response Fields
| Field | Type | Description | | Field | Type | Description |
|-------|------|-------------| |-------|------|-------------|
| `stego_image_base64` | string | Base64-encoded stego image | | `stego_image_base64` | string | Base64-encoded stego image |
@@ -272,7 +292,10 @@ Content-Type: application/json
| `output_format` | string | Output format: `"png"` or `"jpeg"` | | `output_format` | string | Output format: `"png"` or `"jpeg"` |
| `color_mode` | string\|null | Color mode (DCT only): `"color"` or `"grayscale"` | | `color_mode` | string\|null | Color mode (DCT only): `"color"` or `"grayscale"` |
# Prepare base64-encoded images #### cURL Example (LSB Mode - Default)
```bash
# Prepare base64-encoded images
REF_B64=$(base64 -w0 reference.jpg) REF_B64=$(base64 -w0 reference.jpg)
CARRIER_B64=$(base64 -w0 carrier.png) CARRIER_B64=$(base64 -w0 carrier.png)
@@ -280,13 +303,16 @@ Content-Type: application/json
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{ -d "{
\"message\": \"Secret message\", \"message\": \"Secret message\",
\"reference_photo_base64\": \"$REF_B64\", \"reference_photo_base64\": \"$REF_B64\",
\"carrier_image_base64\": \"$CARRIER_B64\", \"carrier_image_base64\": \"$CARRIER_B64\",
\"day_phrase\": \"apple forest thunder\", \"day_phrase\": \"apple forest thunder\",
\"pin\": \"123456\" \"pin\": \"123456\"
}" | jq -r '.stego_image_base64' | base64 -d > stego.png }" | jq -r '.stego_image_base64' | base64 -d > stego.png
```
#### cURL Example (DCT Mode with JPEG Output)
```bash
curl -X POST http://localhost:8000/encode \ curl -X POST http://localhost:8000/encode \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{ -d "{
@@ -304,6 +330,23 @@ curl -X POST http://localhost:8000/encode \
--- ---
### POST /encode/multipart ### POST /encode/multipart
Encode a message using direct file uploads. Returns the stego image directly.
#### Request
```http
POST /encode/multipart HTTP/1.1
Host: localhost:8000
Content-Type: multipart/form-data; boundary=----FormBoundary
------FormBoundary
Content-Disposition: form-data; name="message"
Secret message here
------FormBoundary
Content-Disposition: form-data; name="day_phrase"
apple forest thunder apple forest thunder
------FormBoundary ------FormBoundary
Content-Disposition: form-data; name="pin" Content-Disposition: form-data; name="pin"
@@ -330,6 +373,18 @@ Content-Disposition: form-data; name="pin"
Content-Disposition: form-data; name="carrier"; filename="carrier.png" Content-Disposition: form-data; name="carrier"; filename="carrier.png"
Content-Type: image/png Content-Type: image/png
<binary image data>
------FormBoundary--
```
#### Form Fields
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `message` | string | ✓ | | Message to encode |
| `reference_photo` | file | ✓ | | Reference photo file |
| `carrier` | file | ✓ | | Carrier image file |
| `day_phrase` | string | ✓ | | Today's passphrase |
| `pin` | string | * | | Static PIN | | `pin` | string | * | | Static PIN |
| `rsa_key` | file | * | | RSA key file (.pem) | | `rsa_key` | file | * | | RSA key file (.pem) |
| `rsa_password` | string | | | Password for RSA key | | `rsa_password` | string | | | Password for RSA key |
@@ -344,83 +399,72 @@ Content-Type: image/png
Returns the image directly with headers: Returns the image directly with headers:
- `Content-Disposition: attachment; filename=<generated_filename>.png` ```http
- `X-Stegasoo-Date: 2025-12-27` (date used for encoding) HTTP/1.1 200 OK
- `X-Stegasoo-Day: Saturday` (day of week for passphrase rotation) Content-Type: image/png
- `X-Stegasoo-Capacity-Percent: 12.4` (capacity used) Content-Disposition: attachment; filename="a1b2c3d4_20251227.png"
X-Stegasoo-Date: 2025-12-27
#### cURL Examples X-Stegasoo-Day: Saturday
X-Stegasoo-Capacity-Used: 12.4
**With PIN:** X-Stegasoo-Embedding-Mode: lsb
```bash X-Stegasoo-Output-Format: png
curl -X POST http://localhost:8000/encode/multipart \
<binary image data>
```
#### Response Headers #### Response Headers
| Header | Description | | Header | Description |
|--------|-------------| |--------|-------------|
| `Content-Type` | `image/png` or `image/jpeg` | | `Content-Type` | `image/png` or `image/jpeg` |
--output stego.png | `Content-Disposition` | Suggested filename |
```
**With RSA key:**
```bash
curl -X POST http://localhost:8000/encode/multipart \
| `X-Stegasoo-Date` | Encoding date | | `X-Stegasoo-Date` | Encoding date |
-F "day_phrase=apple forest thunder" \ | `X-Stegasoo-Day` | Day of week |
| `X-Stegasoo-Capacity-Used` | Capacity percentage |
| `X-Stegasoo-Embedding-Mode` | `lsb` or `dct` |
| `X-Stegasoo-Output-Format` | `png` or `jpeg` |
| `X-Stegasoo-Color-Mode` | `color` or `grayscale` (DCT only) |
#### cURL Example (DCT + JPEG)
```bash
curl -X POST http://localhost:8000/encode/multipart \ curl -X POST http://localhost:8000/encode/multipart \
-F "rsa_password=keypassword" \ -F "message=Secret message for social media" \
-F "reference_photo=@reference.jpg" \
-F "carrier=@carrier.png" \
--output stego.png
```
**With both PIN and RSA:**
```bash
curl -X POST http://localhost:8000/encode/multipart \
-F "day_phrase=apple forest thunder" \ -F "day_phrase=apple forest thunder" \
-F "pin=123456" \ -F "pin=123456" \
-F "pin=123456" \ -F "embedding_mode=dct" \
-F "rsa_key=@mykey.pem" \ -F "output_format=jpeg" \
-F "rsa_password=keypassword" \ -F "color_mode=color" \
-F "reference_photo=@reference.jpg" \ -F "reference_photo=@reference.jpg" \
-F "carrier=@carrier.png" \ -F "carrier=@carrier.png" \
--output stego.png --output stego.jpg
``` ```
**With custom date:** ---
```bash
curl -X POST http://localhost:8000/encode/multipart \ ### POST /decode (JSON)
Decode a message using base64-encoded images. Auto-detects embedding mode.
#### Request #### Request
-F "day_phrase=monday phrase here" \
```http ```http
-F "reference_photo=@reference.jpg" \ POST /decode HTTP/1.1
Host: localhost:8000 Host: localhost:8000
Content-Type: application/json Content-Type: application/json
```
```
#### Request Body #### Request Body
### POST /decode (JSON) | Field | Type | Required | Description |
Decode a message using base64-encoded images.
#### Request
```http
POST /decode HTTP/1.1
Host: localhost:8000
Content-Type: application/json
```
|-------|------|----------|-------------| |-------|------|----------|-------------|
| `stego_image_base64` | string | ✓ | Base64-encoded stego image | | `stego_image_base64` | string | ✓ | Base64-encoded stego image |
| `reference_photo_base64` | string | ✓ | Base64-encoded reference photo | | `reference_photo_base64` | string | ✓ | Base64-encoded reference photo |
| `day_phrase` | string | ✓ | Passphrase for encoding day | | `day_phrase` | string | ✓ | Passphrase for encoding day |
| `pin` | string | * | Static PIN | | `pin` | string | * | Static PIN |
| `rsa_key_base64` | string | * | Base64-encoded RSA key | | `rsa_key_base64` | string | * | Base64-encoded RSA key |
| `day_phrase` | string | | Passphrase for encoding day | | `rsa_password` | string | | Password for RSA key |
\* Must match security factors used during encoding. \* Must match security factors used during encoding.
@@ -450,20 +494,27 @@ Content-Type: application/json
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{ -d "{
\"stego_image_base64\": \"$STEGO_B64\", \"stego_image_base64\": \"$STEGO_B64\",
``` \"reference_photo_base64\": \"$REF_B64\",
\"day_phrase\": \"apple forest thunder\", \"day_phrase\": \"apple forest thunder\",
\"pin\": \"123456\" \"pin\": \"123456\"
}" }"
``` ```
Decode a message using direct file uploads. ---
### POST /decode/multipart ### POST /decode/multipart
Decode using direct file uploads. Auto-detects embedding mode. Decode using direct file uploads. Auto-detects embedding mode.
#### Form Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `stego_image` | file | ✓ | Stego image file |
| `reference_photo` | file | ✓ | Reference photo file |
| `day_phrase` | string | ✓ | Passphrase for encoding day | | `day_phrase` | string | ✓ | Passphrase for encoding day |
| `pin` | string | * | Static PIN | | `pin` | string | * | Static PIN |
| `rsa_key` | file | * | RSA key file (.pem) | | `rsa_key` | file | * | RSA key file (.pem) |
Content-Type: multipart/form-data
| `rsa_password` | string | | Password for RSA key | | `rsa_password` | string | | Password for RSA key |
#### Response #### Response
@@ -481,15 +532,7 @@ curl -X POST http://localhost:8000/decode \
curl -X POST http://localhost:8000/decode/multipart \ curl -X POST http://localhost:8000/decode/multipart \
-F "day_phrase=apple forest thunder" \ -F "day_phrase=apple forest thunder" \
-F "pin=123456" \ -F "pin=123456" \
"message": "Secret message here" -F "reference_photo=@reference.jpg" \
}
```
#### cURL Examples
**With PIN:**
```bash
curl -X POST http://localhost:8000/decode/multipart \
-F "stego_image=@stego.png" -F "stego_image=@stego.png"
``` ```
@@ -499,20 +542,20 @@ Content-Type: multipart/form-data
Get image information and capacity for both LSB and DCT modes. Get image information and capacity for both LSB and DCT modes.
-F "day_phrase=apple forest thunder" \ #### Request (JSON)
```http ```http
POST /image/info HTTP/1.1 POST /image/info HTTP/1.1
Host: localhost:8000 Host: localhost:8000
Content-Type: application/json Content-Type: application/json
--- ```
#### Request (Multipart) #### Request (Multipart)
```bash ```bash
Get information about an image's capacity. curl -X POST http://localhost:8000/image/info \
-F "image=@carrier.png" -F "image=@carrier.png"
#### Request
``` ```
#### Response #### Response
@@ -521,35 +564,30 @@ curl -X POST http://localhost:8000/decode/multipart \
{ {
"width": 1920, "width": 1920,
"height": 1080, "height": 1080,
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `image` | file | ✓ | Image file to analyze |
#### Response
```json
{
"width": 1920,
"pixels": 2073600, "pixels": 2073600,
"format": "PNG", "format": "PNG",
"mode": "RGB", "mode": "RGB",
"capacity": { "capacity": {
} "lsb": {
"bytes": 776970, "bytes": 776970,
"kb": 758
}, },
"dct": { "dct": {
"bytes": 64800, "bytes": 64800,
"kb": 63, "kb": 63,
| `width` | integer | Image width in pixels | "note": "Approximate - actual capacity depends on image content"
}
}
}
```
#### Response Fields #### Response Fields
| `capacity_bytes` | integer | Maximum message capacity (bytes) |
| Field | Type | Description | | Field | Type | Description |
|-------|------|-------------|
#### cURL Example | `width` | integer | Image width in pixels |
| `height` | integer | Image height in pixels |
| `pixels` | integer | Total pixel count |
| `format` | string | Image format (PNG, JPEG, etc.) | | `format` | string | Image format (PNG, JPEG, etc.) |
| `mode` | string | Color mode (RGB, L, etc.) | | `mode` | string | Color mode (RGB, L, etc.) |
| `capacity.lsb.bytes` | integer | LSB capacity in bytes | | `capacity.lsb.bytes` | integer | LSB capacity in bytes |
@@ -558,8 +596,19 @@ Content-Type: multipart/form-data
| `capacity.dct.kb` | integer | Estimated DCT capacity in KB | | `capacity.dct.kb` | integer | Estimated DCT capacity in KB |
| `capacity.dct.note` | string | Capacity estimation note | | `capacity.dct.note` | string | Capacity estimation note |
---
### GenerateRequest
## Embedding Modes
### LSB Mode (Default)
**Least Significant Bit** embedding modifies pixel values directly.
| Aspect | Details |
|--------|---------|
| **Parameter** | `"embedding_mode": "lsb"` |
| **Capacity** | ~3 bits/pixel (~770 KB for 1920×1080) |
| **Output** | PNG only (lossless required) |
| **Resilience** | ❌ Destroyed by JPEG compression | | **Resilience** | ❌ Destroyed by JPEG compression |
| **Best For** | Maximum capacity, controlled channels | | **Best For** | Maximum capacity, controlled channels |
@@ -570,15 +619,58 @@ Content-Type: multipart/form-data
| Aspect | Details | | Aspect | Details |
|--------|---------| |--------|---------|
| **Parameter** | `"embedding_mode": "dct"` | | **Parameter** | `"embedding_mode": "dct"` |
| **Capacity** | ~0.25 bits/pixel (~65 KB for 1920×1080) |
### GenerateResponse | **Output** | PNG or JPEG |
| **Resilience** | ✅ Survives JPEG compression |
| **Best For** | Social media, messaging apps |
> ⚠️ **Experimental**: DCT mode may have edge cases. Test with your workflow.
### DCT Options ### DCT Options
```json
| Option | Values | Default | Description | | Option | Values | Default | Description |
"phrases": {"Monday": "...", "Tuesday": "...", ...}, |--------|--------|---------|-------------|
"pin": "123456", | `output_format` | `"png"`, `"jpeg"` | `"png"` | Output image format |
"rsa_key_pem": "-----BEGIN PRIVATE KEY-----...", | `color_mode` | `"color"`, `"grayscale"` | `"color"` | Color processing mode |
"entropy": {"phrase": 33, "pin": 19, "rsa": 0, "total": 52}
### Capacity Comparison
| Mode | 1920×1080 Capacity |
|------|-------------------|
| LSB (PNG) | ~770 KB |
| DCT (PNG) | ~65 KB |
| DCT (JPEG) | ~30-50 KB |
---
## Data Models
### GenerateRequest
```json
{
"use_pin": true,
"use_rsa": false,
"pin_length": 6,
"rsa_bits": 2048,
"words_per_phrase": 3
}
```
### GenerateResponse
```json
{
"phrases": {"Monday": "...", "Tuesday": "...", ...},
"pin": "123456",
"rsa_key_pem": "-----BEGIN PRIVATE KEY-----...",
"entropy": {"phrase": 33, "pin": 19, "rsa": 0, "total": 52}
}
```
### EncodeRequest
```json
{ {
"message": "string", "message": "string",
"reference_photo_base64": "string", "reference_photo_base64": "string",
@@ -618,7 +710,10 @@ curl -X POST http://localhost:8000/image/info \
"day_phrase": "string", "day_phrase": "string",
"pin": "string", "pin": "string",
"rsa_key_base64": "string", "rsa_key_base64": "string",
"rsa_password": "string" "rsa_password": "string"
}
```
### DecodeResponse ### DecodeResponse
```json ```json
@@ -630,7 +725,10 @@ curl -X POST http://localhost:8000/image/info \
### ImageInfoResponse ### ImageInfoResponse
### ImageInfoResponse ```json
{
"width": 1920,
"height": 1080,
"pixels": 2073600, "pixels": 2073600,
"format": "PNG", "format": "PNG",
"mode": "RGB", "mode": "RGB",
@@ -651,7 +749,8 @@ curl -X POST http://localhost:8000/image/info \
``` ```
--- ---
---
## Error Handling
### HTTP Status Codes ### HTTP Status Codes
@@ -662,8 +761,12 @@ curl -X POST http://localhost:8000/image/info \
| 401 | Unauthorized | Decryption failed (wrong credentials) | | 401 | Unauthorized | Decryption failed (wrong credentials) |
| 500 | Internal Error | Unexpected server error | | 500 | Internal Error | Unexpected server error |
| 500 | Internal Error | Unexpected server error | ### Error Response Format
```json
{
"detail": "Error message describing the problem"
}
``` ```
### Common Errors ### Common Errors
@@ -705,8 +808,11 @@ curl -X POST http://localhost:8000/image/info \
# Encode using multipart (LSB mode - default) # Encode using multipart (LSB mode - default)
with open("reference.jpg", "rb") as ref, open("carrier.png", "rb") as carrier: with open("reference.jpg", "rb") as ref, open("carrier.png", "rb") as carrier:
response = requests.post(f"{BASE_URL}/encode/multipart", files={ response = requests.post(f"{BASE_URL}/encode/multipart", files={
"reference_photo": ref,
"carrier": carrier,
}, data={
"message": "Secret message", "message": "Secret message",
with open("reference.jpg", "rb") as ref, open("carrier.png", "rb") as carrier: "day_phrase": "apple forest thunder",
"pin": "123456" "pin": "123456"
}) })
@@ -730,7 +836,7 @@ creds = response.json()
with open("stego_social.jpg", "wb") as f: with open("stego_social.jpg", "wb") as f:
f.write(response.content) f.write(response.content)
``` # Decode using multipart (auto-detects mode)
with open("reference.jpg", "rb") as ref, open("stego.png", "rb") as stego: with open("reference.jpg", "rb") as ref, open("stego.png", "rb") as stego:
response = requests.post(f"{BASE_URL}/decode/multipart", files={ response = requests.post(f"{BASE_URL}/decode/multipart", files={
"reference_photo": ref, "reference_photo": ref,
@@ -744,7 +850,24 @@ with open("reference.jpg", "rb") as ref, open("carrier.png", "rb") as carrier:
print(f"Decoded: {result['message']}") print(f"Decoded: {result['message']}")
print(f"Mode detected: {result['embedding_mode_detected']}") print(f"Mode detected: {result['embedding_mode_detected']}")
``` ```
form.append('day_phrase', 'apple forest thunder');
### JavaScript/Node.js
```javascript
const FormData = require('form-data');
const fs = require('fs');
const axios = require('axios');
const BASE_URL = 'http://localhost:8000';
async function encodeDCT() {
const form = new FormData();
form.append('message', 'Secret message for social media');
form.append('day_phrase', 'apple forest thunder');
form.append('pin', '123456');
form.append('embedding_mode', 'dct');
form.append('output_format', 'jpeg');
form.append('color_mode', 'color');
form.append('reference_photo', fs.createReadStream('reference.jpg')); form.append('reference_photo', fs.createReadStream('reference.jpg'));
form.append('carrier', fs.createReadStream('carrier.png')); form.append('carrier', fs.createReadStream('carrier.png'));
@@ -754,7 +877,9 @@ with open("reference.jpg", "rb") as ref, open("stego.png", "rb") as stego:
}); });
fs.writeFileSync('stego.jpg', response.data); fs.writeFileSync('stego.jpg', response.data);
fs.writeFileSync('stego.png', response.data); console.log('Encoded with DCT mode');
console.log('Embedding mode:', response.headers['x-stegasoo-embedding-mode']);
}
async function decode() { async function decode() {
const form = new FormData(); const form = new FormData();
@@ -766,11 +891,14 @@ const axios = require('axios');
const response = await axios.post(`${BASE_URL}/decode/multipart`, form, { const response = await axios.post(`${BASE_URL}/decode/multipart`, form, {
headers: form.getHeaders() headers: form.getHeaders()
}); });
headers: form.getHeaders()
console.log('Decoded:', response.data.message); console.log('Decoded:', response.data.message);
console.log('Mode detected:', response.data.embedding_mode_detected);
} }
encodeDCT().then(decode);
```
### Go ### Go
```go ```go
@@ -779,8 +907,9 @@ async function encode() {
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
import ( "io"
"mime/multipart"
"net/http" "net/http"
"os" "os"
) )
@@ -788,16 +917,17 @@ async function decode() {
func main() { func main() {
// Encode with DCT mode // Encode with DCT mode
body := &bytes.Buffer{} body := &bytes.Buffer{}
) writer := multipart.NewWriter(body)
writer.WriteField("message", "Secret message") writer.WriteField("message", "Secret message")
writer.WriteField("day_phrase", "apple forest thunder") writer.WriteField("day_phrase", "apple forest thunder")
writer.WriteField("pin", "123456") writer.WriteField("pin", "123456")
writer.WriteField("embedding_mode", "dct") writer.WriteField("embedding_mode", "dct")
writer.WriteField("output_format", "jpeg") writer.WriteField("output_format", "jpeg")
writer.WriteField("color_mode", "color")
ref, _ := os.Open("reference.jpg") ref, _ := os.Open("reference.jpg")
writer.WriteField("pin", "123456") refPart, _ := writer.CreateFormFile("reference_photo", "reference.jpg")
io.Copy(refPart, ref) io.Copy(refPart, ref)
ref.Close() ref.Close()
@@ -816,13 +946,16 @@ import (
// Check embedding mode from header // Check embedding mode from header
fmt.Println("Embedding mode:", resp.Header.Get("X-Stegasoo-Embedding-Mode")) fmt.Println("Embedding mode:", resp.Header.Get("X-Stegasoo-Embedding-Mode"))
stego, _ := os.Create("stego.jpg") stego, _ := os.Create("stego.jpg")
io.Copy(stego, resp.Body) io.Copy(stego, resp.Body)
stego.Close() stego.Close()
resp.Body.Close() resp.Body.Close()
fmt.Println("Encoded successfully with DCT mode") fmt.Println("Encoded successfully with DCT mode")
}
```
### Shell Script (Bash) ### Shell Script (Bash)
```bash ```bash
@@ -842,12 +975,15 @@ func main() {
-F "day_phrase=$PHRASE" \ -F "day_phrase=$PHRASE" \
-F "pin=$PIN" \ -F "pin=$PIN" \
-F "reference_photo=@$REF_PHOTO" \ -F "reference_photo=@$REF_PHOTO" \
-F "day_phrase=$PHRASE" \ -F "carrier=@$CARRIER" \
--output stego_lsb.png
echo "Encoded to stego_lsb.png"
# Encode with DCT for social media # Encode with DCT for social media
echo "Encoding with DCT mode..." echo "Encoding with DCT mode..."
curl -s -X POST "$BASE_URL/encode/multipart" \ curl -s -X POST "$BASE_URL/encode/multipart" \
-F "message=$MESSAGE" \
-F "day_phrase=$PHRASE" \ -F "day_phrase=$PHRASE" \
-F "pin=$PIN" \ -F "pin=$PIN" \
-F "embedding_mode=dct" \ -F "embedding_mode=dct" \
@@ -863,27 +999,43 @@ PHRASE="apple forest thunder"
echo "Decoding..." echo "Decoding..."
RESULT=$(curl -s -X POST "$BASE_URL/decode/multipart" \ RESULT=$(curl -s -X POST "$BASE_URL/decode/multipart" \
-F "day_phrase=$PHRASE" \ -F "day_phrase=$PHRASE" \
## Rate Limiting -F "pin=$PIN" \
-F "reference_photo=@$REF_PHOTO" \
-F "stego_image=@stego_dct.jpg") -F "stego_image=@stego_dct.jpg")
echo "Decoded message: $(echo $RESULT | jq -r '.message')" echo "Decoded message: $(echo $RESULT | jq -r '.message')"
echo "Mode detected: $(echo $RESULT | jq -r '.embedding_mode_detected')" echo "Mode detected: $(echo $RESULT | jq -r '.embedding_mode_detected')"
``` ```
```nginx ---
## Rate Limiting
limit_req zone=stegasoo burst=20 nodelay; The API does not implement rate limiting by default. For production:
1. **Reverse Proxy**: Use nginx or Caddy rate limiting
2. **Application Level**: Add FastAPI middleware
Example nginx rate limiting:
```nginx
limit_req_zone $binary_remote_addr zone=stegasoo:10m rate=10r/s;
location /api/ {
limit_req zone=stegasoo burst=20 nodelay;
proxy_pass http://localhost:8000/;
}
```
---
} ## Security Considerations
### In Transit ### In Transit
- Use HTTPS in production
- Configure TLS at reverse proxy level - Configure TLS at reverse proxy level
### Memory Usage
- Argon2id requires 256MB RAM per operation - Argon2id requires 256MB RAM per operation
- DCT mode adds ~100MB for scipy operations - DCT mode adds ~100MB for scipy operations
@@ -917,9 +1069,15 @@ location /api/ {
|------|--------------| |------|--------------|
| LSB | Maximum capacity but fragile | | LSB | Maximum capacity but fragile |
| DCT | Lower capacity but survives recompression | | DCT | Lower capacity but survives recompression |
Both modes use identical encryption (AES-256-GCM with Argon2id). Both modes use identical encryption (AES-256-GCM with Argon2id).
--- ---
## Interactive Documentation
When the API is running, visit:
- **Swagger UI**: http://localhost:8000/docs - **Swagger UI**: http://localhost:8000/docs
- **ReDoc**: http://localhost:8000/redoc - **ReDoc**: http://localhost:8000/redoc
@@ -927,6 +1085,8 @@ The API validates:
## See Also ## See Also
- [CLI Documentation](CLI.md) - Command-line interface
- [Web UI Documentation](WEB_UI.md) - Browser interface
- [README](README.md) - Project overview - [README](README.md) - Project overview
### Credential Handling ### Credential Handling
@@ -934,6 +1094,15 @@ The API validates:
- No persistent storage of secrets - No persistent storage of secrets
- Memory cleared after operations - Memory cleared after operations
### Embedding Mode Security
| Mode | Consideration |
|------|--------------|
| LSB | Maximum capacity but fragile |
| DCT | Lower capacity but survives recompression |
Both modes use identical encryption (AES-256-GCM with Argon2id).
--- ---
## Interactive Documentation ## Interactive Documentation

View File

@@ -11,6 +11,7 @@ Complete command-line interface reference for Stegasoo steganography operations.
- [encode](#encode-command) - [encode](#encode-command)
- [decode](#decode-command) - [decode](#decode-command)
- [info](#info-command) - [info](#info-command)
- [Embedding Modes](#embedding-modes)
- [Security Factors](#security-factors) - [Security Factors](#security-factors)
- [Workflow Examples](#workflow-examples) - [Workflow Examples](#workflow-examples)
- [Piping & Scripting](#piping--scripting) - [Piping & Scripting](#piping--scripting)
@@ -27,6 +28,9 @@ Complete command-line interface reference for Stegasoo steganography operations.
# CLI only # CLI only
pip install stegasoo[cli] pip install stegasoo[cli]
# CLI with DCT support
pip install stegasoo[cli,dct]
# With all extras # With all extras
pip install stegasoo[all] pip install stegasoo[all]
``` ```
@@ -36,7 +40,7 @@ pip install stegasoo[all]
```bash ```bash
git clone https://github.com/example/stegasoo.git git clone https://github.com/example/stegasoo.git
cd stegasoo cd stegasoo
pip install -e ".[cli]" pip install -e ".[cli,dct]"
``` ```
### Verify Installation ### Verify Installation
@@ -44,6 +48,9 @@ pip install -e ".[cli]"
```bash ```bash
stegasoo --version stegasoo --version
stegasoo --help stegasoo --help
# Check DCT support
python -c "from stegasoo.dct_steganography import has_jpegio_support; print('jpegio:', has_jpegio_support())"
``` ```
--- ---
@@ -54,7 +61,7 @@ stegasoo --help
# 1. Generate credentials (do this once, memorize results) # 1. Generate credentials (do this once, memorize results)
stegasoo generate --pin --words 3 stegasoo generate --pin --words 3
# 2. Encode a message # 2. Encode a message (LSB mode - default)
stegasoo encode \ stegasoo encode \
--ref secret_photo.jpg \ --ref secret_photo.jpg \
--carrier meme.png \ --carrier meme.png \
@@ -62,7 +69,17 @@ stegasoo encode \
--pin 123456 \ --pin 123456 \
--message "Meet at midnight" --message "Meet at midnight"
# 3. Decode a message # 3. Encode for social media (DCT mode)
stegasoo encode \
--ref secret_photo.jpg \
--carrier meme.png \
--phrase "apple forest thunder" \
--pin 123456 \
--message "Meet at midnight" \
--mode dct \
--format jpeg
# 4. Decode a message (auto-detects mode)
stegasoo decode \ stegasoo decode \
--ref secret_photo.jpg \ --ref secret_photo.jpg \
--stego stego_abc123_20251227.png \ --stego stego_abc123_20251227.png \
@@ -106,9 +123,9 @@ stegasoo generate
Output: Output:
``` ```
════════════════════════════════════════════════════════════ ═══════════════════════════════════════════════════════════════
STEGASOO CREDENTIALS STEGASOO CREDENTIALS
════════════════════════════════════════════════════════════ ═══════════════════════════════════════════════════════════════
⚠️ MEMORIZE THESE AND CLOSE THIS WINDOW ⚠️ MEMORIZE THESE AND CLOSE THIS WINDOW
Do not screenshot or save to file! Do not screenshot or save to file!
@@ -171,19 +188,22 @@ stegasoo encode [OPTIONS]
#### Options #### Options
| Option | Short | Type | Required | Description | | Option | Short | Type | Required | Default | Description |
|--------|-------|------|----------|-------------| |--------|-------|------|----------|---------|-------------|
| `--ref` | `-r` | path | ✓ | Reference photo (shared secret) | | `--ref` | `-r` | path | ✓ | | Reference photo (shared secret) |
| `--carrier` | `-c` | path | ✓ | Carrier image to hide message in | | `--carrier` | `-c` | path | ✓ | | Carrier image to hide message in |
| `--phrase` | `-p` | string | ✓ | Today's passphrase | | `--phrase` | `-p` | string | ✓ | | Today's passphrase |
| `--message` | `-m` | string | | Message to encode | | `--message` | `-m` | string | | | Message to encode |
| `--message-file` | `-f` | path | | Read message from file | | `--message-file` | `-f` | path | | | Read message from file |
| `--pin` | | string | * | Static PIN (6-9 digits) | | `--pin` | | string | * | | Static PIN (6-9 digits) |
| `--key` | `-k` | path | * | RSA key file | | `--key` | `-k` | path | * | | RSA key file |
| `--key-password` | | string | | Password for RSA key | | `--key-password` | | string | | | Password for RSA key |
| `--output` | `-o` | path | | Output filename | | `--output` | `-o` | path | | | Output filename |
| `--date` | | YYYY-MM-DD | | Date override | | `--date` | | YYYY-MM-DD | | | Date override |
| `--quiet` | `-q` | flag | | Suppress output | | `--mode` | | choice | | `lsb` | Embedding mode: `lsb` or `dct` |
| `--format` | | choice | | `png` | Output format: `png` or `jpeg` (DCT only) |
| `--color` | | choice | | `color` | Color mode: `color` or `grayscale` (DCT only) |
| `--quiet` | `-q` | flag | | | Suppress output |
\* At least one of `--pin` or `--key` is required. \* At least one of `--pin` or `--key` is required.
@@ -206,7 +226,7 @@ stegasoo encode [OPTIONS]
#### Examples #### Examples
**Basic encoding with PIN:** **Basic encoding with PIN (LSB mode - default):**
```bash ```bash
stegasoo encode \ stegasoo encode \
--ref photos/vacation.jpg \ --ref photos/vacation.jpg \
@@ -221,10 +241,60 @@ Output:
✓ Encoded successfully! ✓ Encoded successfully!
Output: a1b2c3d4_20251227.png Output: a1b2c3d4_20251227.png
Size: 245,832 bytes Size: 245,832 bytes
Mode: LSB
Capacity used: 12.4% Capacity used: 12.4%
Date: 2025-12-27 Date: 2025-12-27
``` ```
**DCT mode for social media (JPEG output):**
```bash
stegasoo encode \
--ref photos/vacation.jpg \
--carrier memes/funny_cat.png \
--phrase "correct horse battery" \
--pin 847293 \
--message "The package arrives Tuesday" \
--mode dct \
--format jpeg
```
Output:
```
✓ Encoded successfully!
Output: a1b2c3d4_20251227.jpg
Size: 89,432 bytes
Mode: DCT (color, jpeg)
Capacity used: 45.2%
Date: 2025-12-27
⚠️ DCT mode is experimental
```
**DCT mode with PNG output (maximum DCT capacity):**
```bash
stegasoo encode \
-r ref.jpg \
-c carrier.png \
-p "phrase words here" \
--pin 123456 \
-m "Longer message that needs more space" \
--mode dct \
--format png \
--color color
```
**DCT grayscale mode:**
```bash
stegasoo encode \
-r ref.jpg \
-c bw_photo.png \
-p "phrase" \
--pin 123456 \
-m "Message" \
--mode dct \
--color grayscale
```
**With RSA key:** **With RSA key:**
```bash ```bash
stegasoo encode \ stegasoo encode \
@@ -291,7 +361,7 @@ stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 -m "msg" -q -
### Decode Command ### Decode Command
Decode a secret message from a stego image. Decode a secret message from a stego image. **Automatically detects LSB vs DCT mode.**
#### Synopsis #### Synopsis
@@ -328,6 +398,24 @@ stegasoo decode \
Output: Output:
``` ```
✓ Decoded successfully! ✓ Decoded successfully!
Mode detected: LSB
The package arrives Tuesday
```
**Decoding DCT image (auto-detected):**
```bash
stegasoo decode \
--ref photos/vacation.jpg \
--stego received_image.jpg \
--phrase "correct horse battery" \
--pin 847293
```
Output:
```
✓ Decoded successfully!
Mode detected: DCT
The package arrives Tuesday The package arrives Tuesday
``` ```
@@ -377,7 +465,7 @@ stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q | gpg --decr
### Info Command ### Info Command
Display information about an image's capacity and embedded date. Display information about an image's capacity for both LSB and DCT modes.
#### Synopsis #### Synopsis
@@ -405,10 +493,15 @@ Image: vacation_photo.png
Pixels: 2,073,600 Pixels: 2,073,600
Mode: RGB Mode: RGB
Format: PNG Format: PNG
Capacity: ~776,970 bytes (758 KB)
Capacity:
LSB Mode: ~776,970 bytes (758 KB)
DCT Mode: ~64,800 bytes (63 KB) [approximate]
Note: DCT capacity varies based on image content
``` ```
**Check stego image (shows encoding date):** **Check stego image (shows encoding date and mode):**
```bash ```bash
stegasoo info stego_a1b2c3d4_20251227.png stegasoo info stego_a1b2c3d4_20251227.png
``` ```
@@ -420,12 +513,88 @@ Image: stego_a1b2c3d4_20251227.png
Pixels: 2,073,600 Pixels: 2,073,600
Mode: RGB Mode: RGB
Format: PNG Format: PNG
Capacity: ~776,970 bytes (758 KB)
Stego Info:
Embed date: 2025-12-27 (Saturday) Embed date: 2025-12-27 (Saturday)
Embed mode: DCT (detected)
Capacity:
LSB Mode: ~776,970 bytes (758 KB)
DCT Mode: ~64,800 bytes (63 KB) [approximate]
``` ```
--- ---
## Embedding Modes
Stegasoo v3.0+ supports two steganography algorithms.
### LSB Mode (Default)
**Least Significant Bit** embedding modifies pixel values directly.
```bash
stegasoo encode ... --mode lsb
# or just omit --mode (LSB is default)
```
| Aspect | Details |
|--------|---------|
| **Capacity** | ~3 bits/pixel (~770 KB for 1920×1080) |
| **Output** | PNG only (lossless required) |
| **Resilience** | ❌ Destroyed by JPEG compression |
| **Best For** | Maximum capacity, controlled channels |
### DCT Mode (Experimental)
**Discrete Cosine Transform** embedding hides data in frequency coefficients.
```bash
stegasoo encode ... --mode dct --format jpeg --color color
```
| Aspect | Details |
|--------|---------|
| **Capacity** | ~0.25 bits/pixel (~65 KB for 1920×1080) |
| **Output** | PNG or JPEG |
| **Resilience** | ✅ Survives JPEG compression |
| **Best For** | Social media, messaging apps |
> ⚠️ **Experimental**: DCT mode may have edge cases. Test with your workflow.
### DCT Options
| Option | Values | Default | Description |
|--------|--------|---------|-------------|
| `--format` | `png`, `jpeg` | `png` | Output image format |
| `--color` | `color`, `grayscale` | `color` | Color processing |
### Choosing the Right Mode
```
Will the image be recompressed?
(social media, messaging apps, etc.)
┌──────┴──────┐
▼ ▼
YES NO
│ │
▼ ▼
Use DCT Use LSB
--mode dct (default)
--format jpeg
```
### Capacity Comparison
| Mode | 1920×1080 Capacity |
|------|-------------------|
| LSB (PNG) | ~770 KB |
| DCT (PNG) | ~65 KB |
| DCT (JPEG) | ~30-50 KB |
---
## Security Factors ## Security Factors
Stegasoo uses multiple authentication factors: Stegasoo uses multiple authentication factors:
@@ -468,25 +637,33 @@ stegasoo generate --rsa -o shared_key.pem -p "agreedpassword"
# Securely transfer shared_key.pem to recipient # Securely transfer shared_key.pem to recipient
``` ```
**Sender (daily):** **Sender (daily - private channel):**
```bash ```bash
# Get today's phrase from your memorized list # For email, file transfer, etc. (no recompression)
TODAY_PHRASE="monday phrase words"
# Encode message
stegasoo encode \ stegasoo encode \
-r our_shared_photo.jpg \ -r our_shared_photo.jpg \
-c random_meme.png \ -c random_meme.png \
-p "$TODAY_PHRASE" \ -p "$TODAY_PHRASE" \
--pin 847293 \ --pin 847293 \
-m "Meeting moved to 3pm" -m "Meeting moved to 3pm"
```
# Share output image via normal channels (email, chat, etc.) **Sender (daily - social media):**
```bash
# For Instagram, Twitter, WhatsApp, etc.
stegasoo encode \
-r our_shared_photo.jpg \
-c random_meme.png \
-p "$TODAY_PHRASE" \
--pin 847293 \
-m "Meeting moved to 3pm" \
--mode dct \
--format jpeg
``` ```
**Recipient (daily):** **Recipient (daily):**
```bash ```bash
# Use the phrase for the day the message was SENT # Works for both LSB and DCT (auto-detected)
stegasoo decode \ stegasoo decode \
-r our_shared_photo.jpg \ -r our_shared_photo.jpg \
-s received_image.png \ -s received_image.png \
@@ -496,7 +673,7 @@ stegasoo decode \
### Batch Processing ### Batch Processing
**Encode multiple messages:** **Encode multiple messages (LSB):**
```bash ```bash
#!/bin/bash #!/bin/bash
PHRASE="apple forest thunder" PHRASE="apple forest thunder"
@@ -517,6 +694,25 @@ for file in messages/*.txt; do
done done
``` ```
**Encode for social media (DCT):**
```bash
#!/bin/bash
for file in messages/*.txt; do
name=$(basename "$file" .txt)
stegasoo encode \
-r "$REF" \
-c "carriers/${name}.png" \
-p "$PHRASE" \
--pin "$PIN" \
-f "$file" \
--mode dct \
--format jpeg \
-o "output/${name}_social.jpg" \
-q
echo "Encoded for social: $name"
done
```
### Archive with Date Preservation ### Archive with Date Preservation
```bash ```bash
@@ -531,6 +727,31 @@ stegasoo encode \
-o archive_2025-01-15.png -o archive_2025-01-15.png
``` ```
### Testing Mode Compatibility
```bash
# Encode with DCT
stegasoo encode \
-r ref.jpg \
-c carrier.png \
-p "test phrase" \
--pin 123456 \
-m "Test message" \
--mode dct \
--format jpeg \
-o test_dct.jpg
# Simulate social media recompression
convert test_dct.jpg -quality 85 test_recompressed.jpg
# Decode (should still work!)
stegasoo decode \
-r ref.jpg \
-s test_recompressed.jpg \
-p "test phrase" \
--pin 123456
```
--- ---
## Piping & Scripting ## Piping & Scripting
@@ -585,6 +806,15 @@ if ! stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q 2>/dev/
fi fi
``` ```
### Mode Detection in Scripts
```bash
#!/bin/bash
# Get mode from verbose output
MODE=$(stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 2>&1 | grep "Mode detected" | awk '{print $3}')
echo "Image was encoded with: $MODE mode"
```
--- ---
## Error Handling ## Error Handling
@@ -596,16 +826,31 @@ fi
| "Must provide --pin or --key" | No security factor given | Add `--pin` or `--key` option | | "Must provide --pin or --key" | No security factor given | Add `--pin` or `--key` option |
| "PIN must be 6-9 digits" | Invalid PIN format | Use numeric PIN, 6-9 chars | | "PIN must be 6-9 digits" | Invalid PIN format | Use numeric PIN, 6-9 chars |
| "PIN cannot start with zero" | Leading zero in PIN | Use PIN starting with 1-9 | | "PIN cannot start with zero" | Leading zero in PIN | Use PIN starting with 1-9 |
| "Carrier image too small" | Message exceeds capacity | Use larger carrier image | | "Carrier image too small" | Message exceeds capacity | Use larger carrier or LSB mode |
| "Message too long for DCT capacity" | DCT has less space | Shorten message or use LSB |
| "Decryption failed" | Wrong credentials | Verify phrase, PIN, ref photo | | "Decryption failed" | Wrong credentials | Verify phrase, PIN, ref photo |
| "Invalid or missing Stegasoo header" | Wrong mode or corruption | Check mode, try other credentials |
| "RSA key is password-protected" | Missing key password | Add `--key-password` option | | "RSA key is password-protected" | Missing key password | Add `--key-password` option |
| "jpegio not available" | Missing library | Install: `pip install jpegio` |
| "Invalid --format for LSB mode" | JPEG with LSB | Use `--mode dct` for JPEG output |
### Troubleshooting Decryption Failures ### Troubleshooting Decryption Failures
1. **Check the encoding date:** The filename often contains the date (e.g., `_20251227`) 1. **Check the encoding date:** The filename often contains the date (e.g., `_20251227`)
2. **Use correct phrase:** The phrase must match the day the message was encoded, not today 2. **Use correct phrase:** The phrase must match the day the message was encoded, not today
3. **Verify reference photo:** Must be the exact same file, not a resized copy 3. **Verify reference photo:** Must be the exact same file, not a resized copy
4. **Check stego image:** Ensure it wasn't resized, recompressed, or converted 4. **Check stego image:**
- LSB: Ensure it wasn't resized, recompressed, or converted
- DCT: More resilient, but heavy recompression may still destroy data
5. **Check embedding mode:** The decoder auto-detects, but if issues persist, verify the original was encoded with the expected mode
### DCT-Specific Issues
| Issue | Cause | Solution |
|-------|-------|----------|
| "Invalid or missing Stegasoo header" after social media | Heavy recompression | Try higher quality original or shorter message |
| JPEG output not working | jpegio not installed | `pip install jpegio` |
| Lower capacity than expected | Normal for DCT | DCT has ~10% of LSB capacity |
--- ---
@@ -627,6 +872,33 @@ fi
--- ---
## Dependencies
### Core Dependencies
- `pillow` - Image processing
- `cryptography` - Encryption
- `argon2-cffi` - Key derivation
- `click` - CLI framework
### DCT Mode Dependencies
- `scipy` - DCT transformations
- `jpegio` - Native JPEG coefficient access (recommended)
Install DCT dependencies:
```bash
pip install scipy jpegio
```
Check availability:
```bash
python -c "import scipy; print('scipy:', scipy.__version__)"
python -c "import jpegio; print('jpegio: available')"
```
---
## See Also ## See Also
- [API Documentation](API.md) - REST API reference - [API Documentation](API.md) - REST API reference

View File

@@ -12,6 +12,9 @@ Complete guide for the Stegasoo web-based steganography interface.
- [Encode Message](#encode-message) - [Encode Message](#encode-message)
- [Decode Message](#decode-message) - [Decode Message](#decode-message)
- [About Page](#about-page) - [About Page](#about-page)
- [Embedding Modes](#embedding-modes)
- [LSB Mode (Default)](#lsb-mode-default)
- [DCT Mode (Experimental)](#dct-mode-experimental)
- [User Interface Guide](#user-interface-guide) - [User Interface Guide](#user-interface-guide)
- [Workflow Examples](#workflow-examples) - [Workflow Examples](#workflow-examples)
- [Security Features](#security-features) - [Security Features](#security-features)
@@ -42,6 +45,8 @@ Built with Flask, Bootstrap 5, and a modern dark theme.
- ✅ Password-protected RSA key downloads - ✅ Password-protected RSA key downloads
- ✅ Real-time entropy calculations - ✅ Real-time entropy calculations
- ✅ Automatic file cleanup - ✅ Automatic file cleanup
-**DCT steganography mode** (v3.0+) - JPEG-resilient embedding
-**Color mode selection** (v3.0.1+) - Preserve carrier colors
--- ---
@@ -53,6 +58,8 @@ Built with Flask, Bootstrap 5, and a modern dark theme.
pip install stegasoo[web] pip install stegasoo[web]
``` ```
This automatically installs DCT dependencies (scipy, jpegio) for full functionality.
### From Source ### From Source
```bash ```bash
@@ -210,6 +217,18 @@ Hide a secret message inside an image.
\* At least one security factor (PIN or RSA Key) required. \* At least one security factor (PIN or RSA Key) required.
#### Advanced Options (v3.0+)
Expand "Advanced Options" to access embedding mode settings:
| Option | Values | Default | Description |
|--------|--------|---------|-------------|
| Embedding Mode | LSB / DCT | LSB | Steganography algorithm |
| Output Format | PNG / JPEG | PNG | Output image format (DCT only) |
| Color Mode | Color / Grayscale | Color | Carrier color handling (DCT only) |
See [Embedding Modes](#embedding-modes) for detailed explanations.
#### Drag-and-Drop Upload #### Drag-and-Drop Upload
Both image upload zones support: Both image upload zones support:
@@ -237,9 +256,10 @@ Saturday's Phrase: [ ]
#### Encoding Process #### Encoding Process
1. Fill in all required fields 1. Fill in all required fields
2. Click "Encode Message" 2. (Optional) Expand "Advanced Options" for DCT mode
3. Wait for processing (shows spinner) 3. Click "Encode Message"
4. Redirected to result page 4. Wait for processing (shows spinner)
5. Redirected to result page
#### Result Page #### Result Page
@@ -255,6 +275,9 @@ After successful encoding:
│ Your secret message is hidden │ │ Your secret message is hidden │
│ in this image │ │ in this image │
│ │ │ │
│ Mode: DCT (Color, JPEG) │ ← v3.0+ shows mode info
│ Capacity used: 45.2% │
│ │
│ [ Download Image ] │ │ [ Download Image ] │
│ [ Share Image ] │ │ [ Share Image ] │
│ │ │ │
@@ -299,6 +322,10 @@ Extract a hidden message from a stego image.
\* Must match security factors used during encoding. \* Must match security factors used during encoding.
#### Automatic Mode Detection (v3.0+)
The decoder automatically detects whether a stego image uses LSB or DCT mode. You don't need to specify the mode manually—it just works!
#### Date Detection from Filename #### Date Detection from Filename
When you upload a stego image with a date in the filename (e.g., `stego_20251227.png`), the UI: When you upload a stego image with a date in the filename (e.g., `stego_20251227.png`), the UI:
@@ -333,13 +360,11 @@ This helps you use the correct daily phrase.
#### Troubleshooting Tips #### Troubleshooting Tips
The page includes built-in troubleshooting guidance: If decryption fails:
1. **Check the date** - Use phrase for encoding day, not today
- ✓ Use the **exact same reference photo** file 2. **Same reference photo** - Must be identical file
- ✓ Use the phrase for the **encoding day**, not today 3. **Correct PIN/RSA** - Match what was used for encoding
- ✓ Provide the **same security factors** used during encoding 4. **Image integrity** - Ensure no resizing/recompression
- ✓ Ensure the stego image hasn't been **resized or recompressed**
- ✓ If using RSA key, verify the **password is correct**
--- ---
@@ -347,62 +372,130 @@ The page includes built-in troubleshooting guidance:
**URL:** `/about` **URL:** `/about`
Learn about Stegasoo's security model and best practices. Information about the Stegasoo project, security model, and credits.
#### Sections ---
**System Status:** ## Embedding Modes
- Argon2id availability (vs PBKDF2 fallback)
- AES-256-GCM encryption status
**Security Model Table:** Stegasoo v3.0+ offers two steganography algorithms, each with different trade-offs.
| Component | Entropy | Purpose | ### LSB Mode (Default)
|-----------|---------|---------|
| Reference Photo | ~80-256 bits | Something you have |
| 3-Word Phrase | ~33 bits | Something you know (daily) |
| 6-Digit PIN | ~20 bits | Something you know (static) |
| Date | N/A | Automatic key rotation |
| **Combined** | **133+ bits** | **Beyond brute force** |
**Attack Resistance:** **Least Significant Bit** embedding modifies the least significant bits of pixel values.
What attackers can't do: | Aspect | Details |
- Brute force (2^133 combinations) |--------|---------|
- Use rainbow tables (random salt) | **Capacity** | ~3 bits/pixel (~770 KB for 1920×1080) |
- Detect hidden data (random pixels) | **Output Format** | PNG only (lossless required) |
- Use GPU farms (256MB RAM per attempt) | **Resilience** | ❌ Destroyed by JPEG compression |
| **Best For** | Maximum capacity, controlled sharing |
Real threats: **When to use LSB:**
- Social engineering - Sharing via lossless channels (email attachment, file transfer)
- Physical device access - Maximum message capacity needed
- Malware/keyloggers - Recipient won't modify the image
- Shoulder surfing
**Best Practices:** ### DCT Mode (Experimental)
Do: **Discrete Cosine Transform** embedding hides data in frequency domain coefficients.
- Memorize phrases and PIN
- Use reference photo both parties have
- Use different carrier images each time
- Share stego images through normal channels
Don't: | Aspect | Details |
- Transmit the reference photo |--------|---------|
- Reuse carrier images | **Capacity** | ~0.25 bits/pixel (~65 KB for 1920×1080 PNG, ~30-50 KB JPEG) |
- Store credentials digitally | **Output Formats** | PNG or JPEG |
- Resize/recompress stego images | **Resilience** | ✅ Survives JPEG compression |
| **Best For** | Social media, messaging apps, web sharing |
> ⚠️ **Experimental Feature**: DCT mode is marked experimental and may have edge cases. Test with your specific workflow before relying on it for critical messages.
**When to use DCT:**
- Posting to social media (which recompresses images)
- Sharing via messaging apps (WhatsApp, Telegram, etc.)
- When channel may apply JPEG compression
- Smaller messages that fit in reduced capacity
#### DCT Output Formats
| Format | Pros | Cons |
|--------|------|------|
| **PNG** | Lossless, predictable | Larger file, obvious if channel expects JPEG |
| **JPEG** | Native format, natural | Slightly lower capacity |
#### DCT Color Modes
| Mode | Description | Use Case |
|------|-------------|----------|
| **Color** | Embeds in luminance (Y), preserves chrominance | Most images, photos |
| **Grayscale** | Converts to grayscale before embedding | Black & white images |
### Capacity Comparison
For a 1920×1080 image:
| Mode | Approximate Capacity |
|------|---------------------|
| LSB (PNG) | ~770 KB |
| DCT (PNG, Color) | ~65 KB |
| DCT (JPEG) | ~30-50 KB |
### Choosing the Right Mode
```
┌─────────────────────────────────────────────────────────────┐
│ Mode Selection Guide │
├─────────────────────────────────────────────────────────────┤
│ │
│ Will the image be recompressed (social media, chat apps)? │
│ │ │
│ ┌───────────┴───────────┐ │
│ ▼ ▼ │
│ YES NO │
│ │ │ │
│ ▼ ▼ │
│ Use DCT Mode Use LSB Mode │
│ │ │ │
│ ▼ ▼ │
│ Output: JPEG (natural) Output: PNG (automatic) │
│ Color: Color (usually) Capacity: ~770 KB │
│ Capacity: ~30-50 KB │
│ │
└─────────────────────────────────────────────────────────────┘
```
--- ---
## User Interface Guide ## User Interface Guide
### Navigation ### Layout Structure
The navbar provides quick access to all pages:
``` ```
[Logo] Stegasoo Home | Encode | Decode | Generate | About ┌──────────────────────────────────────────────────────────────┐
│ 🦕 Stegasoo [Encode] [Decode] [Generate] │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Page Content │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Upload Zone │ │ Upload Zone │ │ │
│ │ │ (Reference) │ │ (Carrier) │ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ │ │ │
│ │ [Advanced Options ▼] │ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ Embedding Mode: [LSB ▼] │ │ │
│ │ │ Output Format: [PNG ▼] (DCT only) │ │ │
│ │ │ Color Mode: [Color ▼] (DCT only) │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ [ Encode Message ] │ │
│ │ │ │
│ └────────────────────────────────────────────────────┘ │
│ │
├──────────────────────────────────────────────────────────────┤
│ Footer │
└──────────────────────────────────────────────────────────────┘
``` ```
### Color Scheme ### Color Scheme
@@ -415,6 +508,7 @@ The navbar provides quick access to all pages:
| Success | Green | Positive actions | | Success | Green | Positive actions |
| Warning | Yellow | Caution messages | | Warning | Yellow | Caution messages |
| Error | Red | Error states | | Error | Red | Error states |
| Experimental | Orange badge | DCT mode indicator |
### Form Validation ### Form Validation
@@ -462,7 +556,7 @@ Types:
- The PIN - The PIN
- The reference photo file (if not already shared) - The reference photo file (if not already shared)
### Sending a Secret Message ### Sending a Secret Message (LSB - Default)
1. Go to `/encode` 1. Go to `/encode`
2. Upload your shared reference photo 2. Upload your shared reference photo
@@ -472,7 +566,22 @@ Types:
6. Enter your PIN 6. Enter your PIN
7. Click "Encode Message" 7. Click "Encode Message"
8. Download or share the resulting image 8. Download or share the resulting image
9. Send via any channel (email, social media, chat) 9. Send via any channel (email, file transfer)
### Sending via Social Media (DCT Mode)
1. Go to `/encode`
2. Upload your shared reference photo
3. Upload carrier image
4. Type your secret message
5. Enter today's phrase and PIN
6. **Expand "Advanced Options"**
7. **Select "DCT" embedding mode**
8. **Select "JPEG" output format**
9. Click "Encode Message"
10. Download and post to social media
The recipient can decode even after the platform recompresses the image!
### Receiving a Secret Message ### Receiving a Secret Message
@@ -486,6 +595,8 @@ Types:
8. Click "Decode Message" 8. Click "Decode Message"
9. Read the secret message 9. Read the secret message
> 💡 Decoding automatically detects LSB vs DCT mode—no configuration needed!
### Changing Credentials ### Changing Credentials
To rotate to new credentials: To rotate to new credentials:
@@ -527,6 +638,15 @@ To rotate to new credentials:
| Access control | Random 16-byte file ID | | Access control | Random 16-byte file ID |
| Cleanup | Automatic + manual | | Cleanup | Automatic + manual |
### Embedding Mode Security
| Mode | Security Consideration |
|------|----------------------|
| LSB | Full capacity, but fragile to modification |
| DCT | Lower capacity, but survives recompression |
Both modes use the same strong encryption (AES-256-GCM with Argon2id key derivation).
--- ---
## Configuration ## Configuration
@@ -561,8 +681,8 @@ gunicorn \
``` ```
**Worker Calculation:** **Worker Calculation:**
- Each encode/decode uses ~256MB RAM (Argon2) - Each encode/decode uses ~256MB RAM (Argon2) + ~100MB for scipy (DCT mode)
- Formula: `workers = (available_RAM - 512MB) / 256MB` - Formula: `workers = (available_RAM - 512MB) / 350MB`
**With Nginx (reverse proxy):** **With Nginx (reverse proxy):**
```nginx ```nginx
@@ -594,9 +714,9 @@ services:
deploy: deploy:
resources: resources:
limits: limits:
memory: 512M memory: 768M # Increased for scipy/DCT
reservations: reservations:
memory: 256M memory: 384M
``` ```
--- ---
@@ -617,7 +737,19 @@ services:
1. Check the date in the stego filename 1. Check the date in the stego filename
2. Use the phrase for that specific day 2. Use the phrase for that specific day
3. Verify you're using the original reference photo 3. Verify you're using the original reference photo
4. Ensure the stego image wasn't resized/recompressed 4. Ensure the stego image wasn't resized/recompressed (LSB mode)
#### "Invalid or missing Stegasoo header" (DCT Mode)
**Causes:**
- Image was heavily recompressed
- Wrong credentials
- Corrupted during transfer
**Solutions:**
1. If sharing via lossy channel, ensure DCT mode was used for encoding
2. Verify credentials match
3. Try obtaining original file
#### "Carrier image too small" #### "Carrier image too small"
@@ -626,7 +758,8 @@ services:
**Solutions:** **Solutions:**
1. Use a larger carrier image (more pixels) 1. Use a larger carrier image (more pixels)
2. Shorten the message 2. Shorten the message
3. Check capacity with `/info` command (CLI) 3. Use LSB mode for more capacity (if channel supports it)
4. Check capacity with `/info` command (CLI)
#### "You must provide at least a PIN or RSA Key" #### "You must provide at least a PIN or RSA Key"
@@ -658,6 +791,17 @@ services:
2. If key is unencrypted, leave password blank 2. If key is unencrypted, leave password blank
3. Re-download or regenerate the key 3. Re-download or regenerate the key
#### DCT mode shows "jpegio not available"
**Cause:** jpegio library not installed (required for JPEG output)
**Solution:**
```bash
pip install jpegio
# Or rebuild Docker image
docker-compose build --no-cache
```
### Browser Compatibility ### Browser Compatibility
| Browser | Status | Notes | | Browser | Status | Notes |
@@ -672,10 +816,12 @@ services:
**Slow encoding/decoding:** **Slow encoding/decoding:**
- Normal: Argon2 is intentionally slow (security feature) - Normal: Argon2 is intentionally slow (security feature)
- Expected time: 2-5 seconds per operation - DCT mode adds ~1-2 seconds for transform operations
- Expected time: 3-7 seconds per operation
**High memory usage:** **High memory usage:**
- Normal: Argon2 requires 256MB RAM - Normal: Argon2 requires 256MB RAM
- DCT mode adds scipy memory overhead (~100MB)
- Configure worker count based on available RAM - Configure worker count based on available RAM
--- ---
@@ -689,6 +835,7 @@ The UI adapts to mobile screens:
- Touch-friendly buttons (48px minimum) - Touch-friendly buttons (48px minimum)
- Readable text without zooming - Readable text without zooming
- Scrollable tables - Scrollable tables
- Collapsible "Advanced Options" for cleaner mobile view
### Mobile-Specific Features ### Mobile-Specific Features

View File

@@ -1,10 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Stegasoo REST API (v3.0) Stegasoo REST API (v3.0.1)
FastAPI-based REST API for steganography operations. FastAPI-based REST API for steganography operations.
Supports both text messages and file embedding. Supports both text messages and file embedding.
NEW in v3.0: LSB and DCT embedding modes. NEW in v3.0: LSB and DCT embedding modes.
NEW in v3.0.1: DCT color mode and JPEG output format.
""" """
import io import io
@@ -70,7 +71,12 @@ Secure steganography with hybrid authentication. Supports text messages and file
## Embedding Modes (v3.0) ## Embedding Modes (v3.0)
- **LSB mode** (default): Spatial LSB embedding, full color output, higher capacity - **LSB mode** (default): Spatial LSB embedding, full color output, higher capacity
- **DCT mode**: Frequency domain embedding, grayscale output, ~20% capacity, better stealth - **DCT mode**: Frequency domain embedding, ~20% capacity, better stealth
## DCT Options (v3.0.1)
- **dct_color_mode**: 'grayscale' (default) or 'color' (preserves original colors)
- **dct_output_format**: 'png' (lossless) or 'jpeg' (smaller, more natural)
Use the `/modes` endpoint to check availability and `/compare` to compare capacities. Use the `/modes` endpoint to check availability and `/compare` to compare capacities.
""", """,
@@ -86,6 +92,8 @@ Use the `/modes` endpoint to check availability and `/compare` to compare capaci
EmbedModeType = Literal["lsb", "dct"] EmbedModeType = Literal["lsb", "dct"]
ExtractModeType = Literal["auto", "lsb", "dct"] ExtractModeType = Literal["auto", "lsb", "dct"]
DctColorModeType = Literal["grayscale", "color"]
DctOutputFormatType = Literal["png", "jpeg"]
# ============================================================================ # ============================================================================
@@ -118,7 +126,16 @@ class EncodeRequest(BaseModel):
date_str: Optional[str] = None date_str: Optional[str] = None
embed_mode: EmbedModeType = Field( embed_mode: EmbedModeType = Field(
default="lsb", default="lsb",
description="Embedding mode: 'lsb' (default, color) or 'dct' (grayscale, requires scipy)" description="Embedding mode: 'lsb' (default, color) or 'dct' (requires scipy)"
)
# NEW in v3.0.1
dct_output_format: DctOutputFormatType = Field(
default="png",
description="DCT output format: 'png' (lossless) or 'jpeg' (smaller). Only applies to DCT mode."
)
dct_color_mode: DctColorModeType = Field(
default="grayscale",
description="DCT color mode: 'grayscale' (default) or 'color' (preserves colors). Only applies to DCT mode."
) )
@@ -136,7 +153,16 @@ class EncodeFileRequest(BaseModel):
date_str: Optional[str] = None date_str: Optional[str] = None
embed_mode: EmbedModeType = Field( embed_mode: EmbedModeType = Field(
default="lsb", default="lsb",
description="Embedding mode: 'lsb' (default, color) or 'dct' (grayscale, requires scipy)" description="Embedding mode: 'lsb' (default, color) or 'dct' (requires scipy)"
)
# NEW in v3.0.1
dct_output_format: DctOutputFormatType = Field(
default="png",
description="DCT output format: 'png' (lossless) or 'jpeg' (smaller). Only applies to DCT mode."
)
dct_color_mode: DctColorModeType = Field(
default="grayscale",
description="DCT color mode: 'grayscale' (default) or 'color' (preserves colors). Only applies to DCT mode."
) )
@@ -147,6 +173,15 @@ class EncodeResponse(BaseModel):
date_used: str date_used: str
day_of_week: str day_of_week: str
embed_mode: str = Field(description="Embedding mode used: 'lsb' or 'dct'") embed_mode: str = Field(description="Embedding mode used: 'lsb' or 'dct'")
# NEW in v3.0.1
output_format: str = Field(
default="png",
description="Output format: 'png' or 'jpeg' (for DCT mode)"
)
color_mode: str = Field(
default="color",
description="Color mode: 'color' (LSB/DCT color) or 'grayscale' (DCT grayscale)"
)
class DecodeRequest(BaseModel): class DecodeRequest(BaseModel):
@@ -211,20 +246,36 @@ class CompareModesResponse(BaseModel):
recommendation: str recommendation: str
class DctModeInfo(BaseModel):
"""Detailed DCT mode information."""
available: bool
name: str
description: str
output_formats: list[str]
color_modes: list[str]
capacity_ratio: str
requires: str
class ModesResponse(BaseModel): class ModesResponse(BaseModel):
"""Response showing available embedding modes.""" """Response showing available embedding modes."""
lsb: dict lsb: dict
dct: dict dct: DctModeInfo
class StatusResponse(BaseModel): class StatusResponse(BaseModel):
version: str version: str
has_argon2: bool has_argon2: bool
has_qrcode_read: bool has_qrcode_read: bool
has_dct: bool # NEW in v3.0 has_dct: bool
day_names: list[str] day_names: list[str]
max_payload_kb: int max_payload_kb: int
available_modes: list[str] # NEW in v3.0 available_modes: list[str]
# NEW in v3.0.1
dct_features: Optional[dict] = Field(
default=None,
description="DCT mode features (v3.0.1+)"
)
class QrExtractResponse(BaseModel): class QrExtractResponse(BaseModel):
@@ -263,8 +314,16 @@ class ErrorResponse(BaseModel):
async def root(): async def root():
"""Get API status and configuration.""" """Get API status and configuration."""
available_modes = ["lsb"] available_modes = ["lsb"]
dct_features = None
if has_dct_support(): if has_dct_support():
available_modes.append("dct") available_modes.append("dct")
dct_features = {
"output_formats": ["png", "jpeg"],
"color_modes": ["grayscale", "color"],
"default_output_format": "png",
"default_color_mode": "grayscale",
}
return StatusResponse( return StatusResponse(
version=__version__, version=__version__,
@@ -273,7 +332,8 @@ async def root():
has_dct=has_dct_support(), has_dct=has_dct_support(),
day_names=list(DAY_NAMES), day_names=list(DAY_NAMES),
max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024, max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024,
available_modes=available_modes available_modes=available_modes,
dct_features=dct_features,
) )
@@ -283,6 +343,7 @@ async def api_modes():
Get available embedding modes and their status. Get available embedding modes and their status.
NEW in v3.0: Shows LSB and DCT mode availability. NEW in v3.0: Shows LSB and DCT mode availability.
NEW in v3.0.1: Shows DCT color modes and output formats.
""" """
return ModesResponse( return ModesResponse(
lsb={ lsb={
@@ -292,14 +353,15 @@ async def api_modes():
"output_format": "PNG (color)", "output_format": "PNG (color)",
"capacity_ratio": "100%", "capacity_ratio": "100%",
}, },
dct={ dct=DctModeInfo(
"available": has_dct_support(), available=has_dct_support(),
"name": "DCT Domain", name="DCT Domain",
"description": "Embed in DCT coefficients, outputs grayscale PNG", description="Embed in DCT coefficients, frequency domain steganography",
"output_format": "PNG (grayscale)", output_formats=["png", "jpeg"],
"capacity_ratio": "~20% of LSB", color_modes=["grayscale", "color"],
"requires": "scipy", capacity_ratio="~20% of LSB",
} requires="scipy",
)
) )
@@ -328,7 +390,8 @@ async def api_compare_modes(request: CompareModesRequest):
"capacity_bytes": comparison['dct']['capacity_bytes'], "capacity_bytes": comparison['dct']['capacity_bytes'],
"capacity_kb": round(comparison['dct']['capacity_kb'], 1), "capacity_kb": round(comparison['dct']['capacity_kb'], 1),
"available": comparison['dct']['available'], "available": comparison['dct']['available'],
"output_format": comparison['dct']['output'], "output_formats": ["png", "jpeg"],
"color_modes": ["grayscale", "color"],
"ratio_vs_lsb_percent": round(comparison['dct']['ratio_vs_lsb'], 1), "ratio_vs_lsb_percent": round(comparison['dct']['ratio_vs_lsb'], 1),
}, },
recommendation="lsb" if not comparison['dct']['available'] else "dct for stealth, lsb for capacity" recommendation="lsb" if not comparison['dct']['available'] else "dct for stealth, lsb for capacity"
@@ -464,6 +527,41 @@ async def api_generate(request: GenerateRequest):
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
# ============================================================================
# HELPER FUNCTION FOR DCT PARAMETERS
# ============================================================================
def _get_dct_params(embed_mode: str, dct_output_format: str, dct_color_mode: str) -> dict:
"""
Get DCT-specific parameters if DCT mode is selected.
Returns kwargs to pass to encode().
"""
if embed_mode != "dct":
return {}
return {
"dct_output_format": dct_output_format,
"dct_color_mode": dct_color_mode,
}
def _get_output_info(embed_mode: str, dct_output_format: str, dct_color_mode: str) -> tuple:
"""
Get output format and color mode strings for response.
Returns (output_format, color_mode, mime_type).
"""
if embed_mode == "dct":
output_format = dct_output_format
color_mode = dct_color_mode
mime_type = "image/jpeg" if dct_output_format == "jpeg" else "image/png"
else:
output_format = "png"
color_mode = "color"
mime_type = "image/png"
return output_format, color_mode, mime_type
# ============================================================================ # ============================================================================
# ROUTES - ENCODE (JSON) # ROUTES - ENCODE (JSON)
# ============================================================================ # ============================================================================
@@ -476,6 +574,7 @@ async def api_encode(request: EncodeRequest):
Images must be base64-encoded. Returns base64-encoded stego image. Images must be base64-encoded. Returns base64-encoded stego image.
NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct'). NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct').
NEW in v3.0.1: Supports dct_output_format ('png' or 'jpeg') and dct_color_mode ('grayscale' or 'color').
""" """
# Validate mode # Validate mode
if request.embed_mode == "dct" and not has_dct_support(): if request.embed_mode == "dct" and not has_dct_support():
@@ -486,6 +585,13 @@ async def api_encode(request: EncodeRequest):
carrier = base64.b64decode(request.carrier_image_base64) carrier = base64.b64decode(request.carrier_image_base64)
rsa_key = base64.b64decode(request.rsa_key_base64) if request.rsa_key_base64 else None rsa_key = base64.b64decode(request.rsa_key_base64) if request.rsa_key_base64 else None
# Get DCT parameters
dct_params = _get_dct_params(
request.embed_mode,
request.dct_output_format,
request.dct_color_mode
)
result = encode( result = encode(
message=request.message, message=request.message,
reference_photo=ref_photo, reference_photo=ref_photo,
@@ -495,12 +601,19 @@ async def api_encode(request: EncodeRequest):
rsa_key_data=rsa_key, rsa_key_data=rsa_key,
rsa_password=request.rsa_password, rsa_password=request.rsa_password,
date_str=request.date_str, date_str=request.date_str,
embed_mode=request.embed_mode, # NEW in v3.0 embed_mode=request.embed_mode,
**dct_params, # NEW in v3.0.1
) )
stego_b64 = base64.b64encode(result.stego_image).decode('utf-8') stego_b64 = base64.b64encode(result.stego_image).decode('utf-8')
day_of_week = get_day_from_date(result.date_used) day_of_week = get_day_from_date(result.date_used)
output_format, color_mode, _ = _get_output_info(
request.embed_mode,
request.dct_output_format,
request.dct_color_mode
)
return EncodeResponse( return EncodeResponse(
stego_image_base64=stego_b64, stego_image_base64=stego_b64,
filename=result.filename, filename=result.filename,
@@ -508,6 +621,8 @@ async def api_encode(request: EncodeRequest):
date_used=result.date_used, date_used=result.date_used,
day_of_week=day_of_week, day_of_week=day_of_week,
embed_mode=request.embed_mode, embed_mode=request.embed_mode,
output_format=output_format,
color_mode=color_mode,
) )
except CapacityError as e: except CapacityError as e:
@@ -526,6 +641,7 @@ async def api_encode_file(request: EncodeFileRequest):
File data must be base64-encoded. File data must be base64-encoded.
NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct'). NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct').
NEW in v3.0.1: Supports dct_output_format and dct_color_mode.
""" """
# Validate mode # Validate mode
if request.embed_mode == "dct" and not has_dct_support(): if request.embed_mode == "dct" and not has_dct_support():
@@ -543,6 +659,13 @@ async def api_encode_file(request: EncodeFileRequest):
mime_type=request.mime_type mime_type=request.mime_type
) )
# Get DCT parameters
dct_params = _get_dct_params(
request.embed_mode,
request.dct_output_format,
request.dct_color_mode
)
result = encode( result = encode(
message=payload, message=payload,
reference_photo=ref_photo, reference_photo=ref_photo,
@@ -552,12 +675,19 @@ async def api_encode_file(request: EncodeFileRequest):
rsa_key_data=rsa_key, rsa_key_data=rsa_key,
rsa_password=request.rsa_password, rsa_password=request.rsa_password,
date_str=request.date_str, date_str=request.date_str,
embed_mode=request.embed_mode, # NEW in v3.0 embed_mode=request.embed_mode,
**dct_params, # NEW in v3.0.1
) )
stego_b64 = base64.b64encode(result.stego_image).decode('utf-8') stego_b64 = base64.b64encode(result.stego_image).decode('utf-8')
day_of_week = get_day_from_date(result.date_used) day_of_week = get_day_from_date(result.date_used)
output_format, color_mode, _ = _get_output_info(
request.embed_mode,
request.dct_output_format,
request.dct_color_mode
)
return EncodeResponse( return EncodeResponse(
stego_image_base64=stego_b64, stego_image_base64=stego_b64,
filename=result.filename, filename=result.filename,
@@ -565,6 +695,8 @@ async def api_encode_file(request: EncodeFileRequest):
date_used=result.date_used, date_used=result.date_used,
day_of_week=day_of_week, day_of_week=day_of_week,
embed_mode=request.embed_mode, embed_mode=request.embed_mode,
output_format=output_format,
color_mode=color_mode,
) )
except CapacityError as e: except CapacityError as e:
@@ -588,6 +720,9 @@ async def api_decode(request: DecodeRequest):
NEW in v3.0: Supports embed_mode parameter ('auto', 'lsb', or 'dct'). NEW in v3.0: Supports embed_mode parameter ('auto', 'lsb', or 'dct').
With 'auto' (default), tries LSB first then DCT. With 'auto' (default), tries LSB first then DCT.
Note: Extraction works regardless of whether the image was created with
color mode or grayscale mode - both use the same Y channel for data.
""" """
# Validate mode # Validate mode
if request.embed_mode == "dct" and not has_dct_support(): if request.embed_mode == "dct" and not has_dct_support():
@@ -605,7 +740,7 @@ async def api_decode(request: DecodeRequest):
pin=request.pin, pin=request.pin,
rsa_key_data=rsa_key, rsa_key_data=rsa_key,
rsa_password=request.rsa_password, rsa_password=request.rsa_password,
embed_mode=request.embed_mode, # NEW in v3.0 embed_mode=request.embed_mode,
) )
if result.is_file: if result.is_file:
@@ -645,16 +780,20 @@ async def api_encode_multipart(
rsa_key_qr: Optional[UploadFile] = File(None), rsa_key_qr: Optional[UploadFile] = File(None),
rsa_password: str = Form(""), rsa_password: str = Form(""),
date_str: str = Form(""), date_str: str = Form(""),
embed_mode: str = Form("lsb"), # NEW in v3.0 embed_mode: str = Form("lsb"),
# NEW in v3.0.1
dct_output_format: str = Form("png"),
dct_color_mode: str = Form("grayscale"),
): ):
""" """
Encode using multipart form data (file uploads). Encode using multipart form data (file uploads).
Provide either 'message' (text) or 'payload_file' (binary file). Provide either 'message' (text) or 'payload_file' (binary file).
RSA key can be provided as 'rsa_key' (.pem file) or 'rsa_key_qr' (QR code image). RSA key can be provided as 'rsa_key' (.pem file) or 'rsa_key_qr' (QR code image).
Returns the stego image directly as PNG with metadata headers. Returns the stego image directly with metadata headers.
NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct'). NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct').
NEW in v3.0.1: Supports dct_output_format ('png' or 'jpeg') and dct_color_mode ('grayscale' or 'color').
""" """
# Validate mode # Validate mode
if embed_mode not in ("lsb", "dct"): if embed_mode not in ("lsb", "dct"):
@@ -662,6 +801,12 @@ async def api_encode_multipart(
if embed_mode == "dct" and not has_dct_support(): if embed_mode == "dct" and not has_dct_support():
raise HTTPException(400, "DCT mode requires scipy. Install with: pip install scipy") raise HTTPException(400, "DCT mode requires scipy. Install with: pip install scipy")
# Validate DCT options
if dct_output_format not in ("png", "jpeg"):
raise HTTPException(400, "dct_output_format must be 'png' or 'jpeg'")
if dct_color_mode not in ("grayscale", "color"):
raise HTTPException(400, "dct_color_mode must be 'grayscale' or 'color'")
try: try:
ref_data = await reference_photo.read() ref_data = await reference_photo.read()
carrier_data = await carrier.read() carrier_data = await carrier.read()
@@ -701,6 +846,9 @@ async def api_encode_multipart(
else: else:
raise HTTPException(400, "Must provide either 'message' or 'payload_file'") raise HTTPException(400, "Must provide either 'message' or 'payload_file'")
# Get DCT parameters
dct_params = _get_dct_params(embed_mode, dct_output_format, dct_color_mode)
result = encode( result = encode(
message=payload, message=payload,
reference_photo=ref_data, reference_photo=ref_data,
@@ -710,20 +858,26 @@ async def api_encode_multipart(
rsa_key_data=rsa_key_data, rsa_key_data=rsa_key_data,
rsa_password=effective_password, rsa_password=effective_password,
date_str=date_str if date_str else None, date_str=date_str if date_str else None,
embed_mode=embed_mode, # NEW in v3.0 embed_mode=embed_mode,
**dct_params, # NEW in v3.0.1
) )
day_of_week = get_day_from_date(result.date_used) day_of_week = get_day_from_date(result.date_used)
output_format, color_mode, mime_type = _get_output_info(
embed_mode, dct_output_format, dct_color_mode
)
return Response( return Response(
content=result.stego_image, content=result.stego_image,
media_type="image/png", media_type=mime_type,
headers={ headers={
"Content-Disposition": f"attachment; filename={result.filename}", "Content-Disposition": f"attachment; filename={result.filename}",
"X-Stegasoo-Date": result.date_used, "X-Stegasoo-Date": result.date_used,
"X-Stegasoo-Day": day_of_week, "X-Stegasoo-Day": day_of_week,
"X-Stegasoo-Capacity-Percent": f"{result.capacity_percent:.1f}", "X-Stegasoo-Capacity-Percent": f"{result.capacity_percent:.1f}",
"X-Stegasoo-Embed-Mode": embed_mode, # NEW in v3.0 "X-Stegasoo-Embed-Mode": embed_mode,
"X-Stegasoo-Output-Format": output_format, # NEW in v3.0.1
"X-Stegasoo-Color-Mode": color_mode, # NEW in v3.0.1
} }
) )
@@ -746,7 +900,7 @@ async def api_decode_multipart(
rsa_key: Optional[UploadFile] = File(None), rsa_key: Optional[UploadFile] = File(None),
rsa_key_qr: Optional[UploadFile] = File(None), rsa_key_qr: Optional[UploadFile] = File(None),
rsa_password: str = Form(""), rsa_password: str = Form(""),
embed_mode: str = Form("auto"), # NEW in v3.0 embed_mode: str = Form("auto"),
): ):
""" """
Decode using multipart form data (file uploads). Decode using multipart form data (file uploads).
@@ -755,6 +909,8 @@ async def api_decode_multipart(
Returns JSON with payload_type indicating text or file. Returns JSON with payload_type indicating text or file.
NEW in v3.0: Supports embed_mode parameter ('auto', 'lsb', or 'dct'). NEW in v3.0: Supports embed_mode parameter ('auto', 'lsb', or 'dct').
Note: Extraction works the same regardless of color mode used during encoding.
""" """
# Validate mode # Validate mode
if embed_mode not in ("auto", "lsb", "dct"): if embed_mode not in ("auto", "lsb", "dct"):
@@ -795,7 +951,7 @@ async def api_decode_multipart(
pin=pin, pin=pin,
rsa_key_data=rsa_key_data, rsa_key_data=rsa_key_data,
rsa_password=effective_password, rsa_password=effective_password,
embed_mode=embed_mode, # NEW in v3.0 embed_mode=embed_mode,
) )
if result.is_file: if result.is_file:
@@ -866,7 +1022,7 @@ async def api_image_info(
capacity_bytes=comparison['dct']['capacity_bytes'], capacity_bytes=comparison['dct']['capacity_bytes'],
capacity_kb=round(comparison['dct']['capacity_kb'], 1), capacity_kb=round(comparison['dct']['capacity_kb'], 1),
available=comparison['dct']['available'], available=comparison['dct']['available'],
output_format=comparison['dct']['output'], output_format="PNG/JPEG (grayscale or color)", # Updated for v3.0.1
), ),
} }

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Stegasoo CLI - Command-line interface for steganography operations. Stegasoo CLI - Command-line interface for steganography operations (v3.0.1).
Usage: Usage:
stegasoo generate [OPTIONS] stegasoo generate [OPTIONS]
@@ -8,7 +8,12 @@ Usage:
stegasoo decode [OPTIONS] stegasoo decode [OPTIONS]
stegasoo verify [OPTIONS] stegasoo verify [OPTIONS]
stegasoo info [OPTIONS] stegasoo info [OPTIONS]
stegasoo compare [OPTIONS] # NEW in v3.0 stegasoo compare [OPTIONS]
stegasoo modes [OPTIONS]
New in v3.0.1:
- DCT color mode: --dct-color (grayscale or color)
- DCT output format: --dct-format (png or jpeg)
""" """
import sys import sys
@@ -73,14 +78,19 @@ def cli():
Hide encrypted messages or files in images using a combination of: Hide encrypted messages or files in images using a combination of:
\b \b
Reference photo (something you have) - Reference photo (something you have)
Daily passphrase (something you know) - Daily passphrase (something you know)
Static PIN or RSA key (additional security) - Static PIN or RSA key (additional security)
\b \b
NEW in v3.0 - Embedding Modes: Embedding Modes (v3.0):
LSB mode (default): Full color output, higher capacity - LSB mode (default): Full color output, higher capacity
DCT mode: Grayscale output, ~20% capacity, better stealth - DCT mode: Frequency domain, ~20% capacity, better stealth
\b
DCT Options (v3.0.1):
- Color mode: grayscale (default) or color (preserves colors)
- Output format: png (lossless) or jpeg (smaller, natural)
""" """
pass pass
@@ -148,29 +158,29 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
# Pretty output # Pretty output
click.echo() click.echo()
click.secho("" * 60, fg='cyan') click.secho("=" * 60, fg='cyan')
click.secho(" STEGASOO CREDENTIALS", fg='cyan', bold=True) click.secho(" STEGASOO CREDENTIALS", fg='cyan', bold=True)
click.secho("" * 60, fg='cyan') click.secho("=" * 60, fg='cyan')
click.echo() click.echo()
click.secho("⚠️ MEMORIZE THESE AND CLOSE THIS WINDOW", fg='yellow', bold=True) click.secho(" MEMORIZE THESE AND CLOSE THIS WINDOW", fg='yellow', bold=True)
click.secho(" Do not screenshot or save to file!", fg='yellow') click.secho(" Do not screenshot or save to file!", fg='yellow')
click.echo() click.echo()
if creds.pin: if creds.pin:
click.secho("─── STATIC PIN ───", fg='green') click.secho("--- STATIC PIN ---", fg='green')
click.secho(f" {creds.pin}", fg='bright_yellow', bold=True) click.secho(f" {creds.pin}", fg='bright_yellow', bold=True)
click.echo() click.echo()
click.secho("─── DAILY PHRASES ───", fg='green') click.secho("--- DAILY PHRASES ---", fg='green')
for day in DAY_NAMES: for day in DAY_NAMES:
phrase = creds.phrases[day] phrase = creds.phrases[day]
click.echo(f" {day:9} ", nl=False) click.echo(f" {day:9} | ", nl=False)
click.secho(phrase, fg='bright_white') click.secho(phrase, fg='bright_white')
click.echo() click.echo()
if creds.rsa_key_pem: if creds.rsa_key_pem:
click.secho("─── RSA KEY ───", fg='green') click.secho("--- RSA KEY ---", fg='green')
if output: if output:
# Save to file # Save to file
private_key = load_rsa_key(creds.rsa_key_pem.encode()) private_key = load_rsa_key(creds.rsa_key_pem.encode())
@@ -182,7 +192,7 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
click.echo(creds.rsa_key_pem) click.echo(creds.rsa_key_pem)
click.echo() click.echo()
click.secho("─── SECURITY ───", fg='green') click.secho("--- SECURITY ---", fg='green')
click.echo(f" Phrase entropy: {creds.phrase_entropy} bits") click.echo(f" Phrase entropy: {creds.phrase_entropy} bits")
if creds.pin: if creds.pin:
click.echo(f" PIN entropy: {creds.pin_entropy} bits") click.echo(f" PIN entropy: {creds.pin_entropy} bits")
@@ -214,9 +224,14 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
@click.option('--output', '-o', type=click.Path(), help='Output file (default: auto-generated)') @click.option('--output', '-o', type=click.Path(), help='Output file (default: auto-generated)')
@click.option('--date', 'date_str', help='Date override (YYYY-MM-DD)') @click.option('--date', 'date_str', help='Date override (YYYY-MM-DD)')
@click.option('--mode', 'embed_mode', type=click.Choice(['lsb', 'dct']), default='lsb', @click.option('--mode', 'embed_mode', type=click.Choice(['lsb', 'dct']), default='lsb',
help='Embedding mode: lsb (default, color) or dct (grayscale, requires scipy)') help='Embedding mode: lsb (default, color) or dct (requires scipy)')
@click.option('--dct-format', 'dct_output_format', type=click.Choice(['png', 'jpeg']), default='png',
help='DCT output format: png (lossless, default) or jpeg (smaller)')
@click.option('--dct-color', 'dct_color_mode', type=click.Choice(['grayscale', 'color']), default='grayscale',
help='DCT color mode: grayscale (default) or color (preserves original colors)')
@click.option('--quiet', '-q', is_flag=True, help='Suppress output except errors') @click.option('--quiet', '-q', is_flag=True, help='Suppress output except errors')
def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key, key_qr, key_password, output, date_str, embed_mode, quiet): def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key, key_qr,
key_password, output, date_str, embed_mode, dct_output_format, dct_color_mode, quiet):
""" """
Encode a secret message or file into an image. Encode a secret message or file into an image.
@@ -230,27 +245,37 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
\b \b
Embedding Modes (v3.0): Embedding Modes (v3.0):
--mode lsb Spatial LSB embedding (default) --mode lsb Spatial LSB embedding (default)
Full color output (PNG/BMP) - Full color output (PNG/BMP)
Higher capacity (~375 KB/megapixel) - Higher capacity (~375 KB/megapixel)
--mode dct DCT domain embedding (requires scipy) --mode dct DCT domain embedding (requires scipy)
• Grayscale output only - Configurable color/grayscale output
Lower capacity (~75 KB/megapixel) - Lower capacity (~75 KB/megapixel)
Better resistance to visual analysis - Better resistance to visual analysis
\b
DCT Options (v3.0.1):
--dct-format png Lossless output (default)
--dct-format jpeg Smaller file, more natural appearance
--dct-color grayscale Convert to grayscale (default, traditional)
--dct-color color Preserve original colors (experimental)
\b \b
Examples: Examples:
# Text message with PIN (LSB mode, default) # Text message with PIN (LSB mode, default)
stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder" --pin 123456 -m "secret" stegasoo encode -r photo.jpg -c meme.png -p "apple forest" --pin 123456 -m "secret"
# DCT mode for better stealth # DCT mode - grayscale PNG (traditional)
stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -m "secret" --mode dct stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -m "secret" --mode dct
# With RSA key file # DCT mode - color JPEG (v3.0.1)
stegasoo encode -r photo.jpg -c meme.png -p "words" -k mykey.pem -m "secret" stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -m "secret" \
--mode dct --dct-color color --dct-format jpeg
# Embed a binary file # DCT mode - color PNG (best quality + color preservation)
stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -e secret.pdf stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -m "secret" \
--mode dct --dct-color color --dct-format png
""" """
# Check DCT mode availability # Check DCT mode availability
if embed_mode == 'dct' and not has_dct_support(): if embed_mode == 'dct' and not has_dct_support():
@@ -258,6 +283,12 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
"DCT mode requires scipy. Install with: pip install scipy" "DCT mode requires scipy. Install with: pip install scipy"
) )
# Warn if DCT options used with LSB mode
if embed_mode == 'lsb':
if dct_output_format != 'png' or dct_color_mode != 'grayscale':
if not quiet:
click.secho("Note: --dct-format and --dct-color only apply to DCT mode", fg='yellow', dim=True)
# Determine what to encode # Determine what to encode
payload = None payload = None
@@ -329,7 +360,10 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
) )
if not quiet: if not quiet:
click.echo(f"Mode: {embed_mode.upper()} ({fit_check['usage_percent']:.1f}% capacity)") mode_desc = embed_mode.upper()
if embed_mode == 'dct':
mode_desc += f" ({dct_color_mode}, {dct_output_format.upper()})"
click.echo(f"Mode: {mode_desc} ({fit_check['usage_percent']:.1f}% capacity)")
result = encode( result = encode(
message=payload, message=payload,
@@ -340,7 +374,9 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
rsa_key_data=rsa_key_data, rsa_key_data=rsa_key_data,
rsa_password=effective_key_password, rsa_password=effective_key_password,
date_str=date_str, date_str=date_str,
embed_mode=embed_mode, # NEW in v3.0 embed_mode=embed_mode,
dct_output_format=dct_output_format,
dct_color_mode=dct_color_mode,
) )
# Determine output path # Determine output path
@@ -353,13 +389,15 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
out_path.write_bytes(result.stego_image) out_path.write_bytes(result.stego_image)
if not quiet: if not quiet:
click.secho(f" Encoded successfully!", fg='green') click.secho(f"[OK] Encoded successfully!", fg='green')
click.echo(f" Output: {out_path}") click.echo(f" Output: {out_path}")
click.echo(f" Size: {len(result.stego_image):,} bytes") click.echo(f" Size: {len(result.stego_image):,} bytes")
click.echo(f" Capacity used: {result.capacity_percent:.1f}%") click.echo(f" Capacity used: {result.capacity_percent:.1f}%")
click.echo(f" Date: {result.date_used}") click.echo(f" Date: {result.date_used}")
if embed_mode == 'dct': if embed_mode == 'dct':
click.secho(f" Note: Output is grayscale (DCT mode)", dim=True) color_note = "color preserved" if dct_color_mode == 'color' else "grayscale"
format_note = dct_output_format.upper()
click.secho(f" DCT output: {format_note} ({color_note})", dim=True)
except StegasooError as e: except StegasooError as e:
raise click.ClickException(str(e)) raise click.ClickException(str(e))
@@ -394,6 +432,9 @@ def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, embed
Automatically detects whether content is text or a file. Automatically detects whether content is text or a file.
RSA key can be provided as a .pem file (--key) or QR code image (--key-qr). RSA key can be provided as a .pem file (--key) or QR code image (--key-qr).
Note: Extraction works the same regardless of whether the image was
created with color mode or grayscale mode - both use the same Y channel.
\b \b
Extraction Modes (v3.0): Extraction Modes (v3.0):
--mode auto Auto-detect (default) - tries LSB first, then DCT --mode auto Auto-detect (default) - tries LSB first, then DCT
@@ -461,7 +502,7 @@ def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, embed
pin=pin or "", pin=pin or "",
rsa_key_data=rsa_key_data, rsa_key_data=rsa_key_data,
rsa_password=effective_key_password, rsa_password=effective_key_password,
embed_mode=embed_mode, # NEW in v3.0 embed_mode=embed_mode,
) )
if result.is_file: if result.is_file:
@@ -481,7 +522,7 @@ def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, embed
out_path.write_bytes(result.file_data) out_path.write_bytes(result.file_data)
if not quiet: if not quiet:
click.secho(" Decoded file successfully!", fg='green') click.secho("[OK] Decoded file successfully!", fg='green')
click.echo(f" Saved to: {out_path}") click.echo(f" Saved to: {out_path}")
click.echo(f" Size: {len(result.file_data):,} bytes") click.echo(f" Size: {len(result.file_data):,} bytes")
if result.mime_type: if result.mime_type:
@@ -491,13 +532,13 @@ def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, embed
if output: if output:
Path(output).write_text(result.message) Path(output).write_text(result.message)
if not quiet: if not quiet:
click.secho(" Decoded successfully!", fg='green') click.secho("[OK] Decoded successfully!", fg='green')
click.echo(f" Saved to: {output}") click.echo(f" Saved to: {output}")
else: else:
if quiet: if quiet:
click.echo(result.message) click.echo(result.message)
else: else:
click.secho(" Decoded successfully!", fg='green') click.secho("[OK] Decoded successfully!", fg='green')
click.echo() click.echo()
click.echo(result.message) click.echo(result.message)
@@ -583,7 +624,7 @@ def verify(ref, stego, phrase, pin, key, key_qr, key_password, embed_mode, as_js
pin=pin or "", pin=pin or "",
rsa_key_data=rsa_key_data, rsa_key_data=rsa_key_data,
rsa_password=effective_key_password, rsa_password=effective_key_password,
embed_mode=embed_mode, # NEW in v3.0 embed_mode=embed_mode,
) )
# Calculate payload size # Calculate payload size
@@ -617,7 +658,7 @@ def verify(ref, stego, phrase, pin, key, key_qr, key_password, embed_mode, as_js
output["mime_type"] = result.mime_type output["mime_type"] = result.mime_type
click.echo(json.dumps(output, indent=2)) click.echo(json.dumps(output, indent=2))
else: else:
click.secho(" Valid stego image", fg='green', bold=True) click.secho("[OK] Valid stego image", fg='green', bold=True)
click.echo(f" Payload: {payload_type} ({payload_desc})") click.echo(f" Payload: {payload_type} ({payload_desc})")
click.echo(f" Size: {payload_size:,} bytes") click.echo(f" Size: {payload_size:,} bytes")
if date_encoded: if date_encoded:
@@ -634,7 +675,7 @@ def verify(ref, stego, phrase, pin, key, key_qr, key_password, embed_mode, as_js
click.echo(json.dumps(output, indent=2)) click.echo(json.dumps(output, indent=2))
sys.exit(1) sys.exit(1)
else: else:
click.secho(" Verification failed", fg='red', bold=True) click.secho("[FAIL] Verification failed", fg='red', bold=True)
click.echo(f" Error: {e}") click.echo(f" Error: {e}")
sys.exit(1) sys.exit(1)
except StegasooError as e: except StegasooError as e:
@@ -690,6 +731,8 @@ def info(image, as_json):
"kb": round(comparison['dct']['capacity_kb'], 1), "kb": round(comparison['dct']['capacity_kb'], 1),
"available": comparison['dct']['available'], "available": comparison['dct']['available'],
"ratio_vs_lsb": round(comparison['dct']['ratio_vs_lsb'], 1), "ratio_vs_lsb": round(comparison['dct']['ratio_vs_lsb'], 1),
"output_formats": ["png", "jpeg"],
"color_modes": ["grayscale", "color"],
}, },
}, },
} }
@@ -701,7 +744,7 @@ def info(image, as_json):
click.echo() click.echo()
click.secho(f"Image: {image}", bold=True) click.secho(f"Image: {image}", bold=True)
click.echo(f" Dimensions: {result.details['width']} × {result.details['height']}") click.echo(f" Dimensions: {result.details['width']} x {result.details['height']}")
click.echo(f" Pixels: {result.details['pixels']:,}") click.echo(f" Pixels: {result.details['pixels']:,}")
click.echo(f" Mode: {result.details['mode']}") click.echo(f" Mode: {result.details['mode']}")
click.echo(f" Format: {result.details['format']}") click.echo(f" Format: {result.details['format']}")
@@ -710,10 +753,13 @@ def info(image, as_json):
click.secho(" Capacity:", bold=True) click.secho(" Capacity:", bold=True)
click.echo(f" LSB mode: ~{comparison['lsb']['capacity_bytes']:,} bytes ({comparison['lsb']['capacity_kb']:.1f} KB)") click.echo(f" LSB mode: ~{comparison['lsb']['capacity_bytes']:,} bytes ({comparison['lsb']['capacity_kb']:.1f} KB)")
dct_status = "" if comparison['dct']['available'] else " (scipy not installed)" dct_status = "[OK]" if comparison['dct']['available'] else "[X] (scipy not installed)"
click.echo(f" DCT mode: ~{comparison['dct']['capacity_bytes']:,} bytes ({comparison['dct']['capacity_kb']:.1f} KB) {dct_status}") click.echo(f" DCT mode: ~{comparison['dct']['capacity_bytes']:,} bytes ({comparison['dct']['capacity_kb']:.1f} KB) {dct_status}")
click.echo(f" DCT ratio: {comparison['dct']['ratio_vs_lsb']:.1f}% of LSB") click.echo(f" DCT ratio: {comparison['dct']['ratio_vs_lsb']:.1f}% of LSB")
if comparison['dct']['available']:
click.secho(" DCT options: grayscale/color, png/jpeg", dim=True)
if date_str: if date_str:
click.echo() click.echo()
click.echo(f" Embed date: {date_str} ({day_name})") click.echo(f" Embed date: {date_str} ({day_name})")
@@ -725,7 +771,7 @@ def info(image, as_json):
# ============================================================================ # ============================================================================
# COMPARE COMMAND (NEW in v3.0) # COMPARE COMMAND
# ============================================================================ # ============================================================================
@cli.command() @cli.command()
@@ -767,7 +813,8 @@ def compare(image, payload_size, as_json):
"capacity_bytes": comparison['dct']['capacity_bytes'], "capacity_bytes": comparison['dct']['capacity_bytes'],
"capacity_kb": round(comparison['dct']['capacity_kb'], 1), "capacity_kb": round(comparison['dct']['capacity_kb'], 1),
"available": comparison['dct']['available'], "available": comparison['dct']['available'],
"output_format": comparison['dct']['output'], "output_formats": ["png", "jpeg"],
"color_modes": ["grayscale", "color"],
"ratio_vs_lsb_percent": round(comparison['dct']['ratio_vs_lsb'], 1), "ratio_vs_lsb_percent": round(comparison['dct']['ratio_vs_lsb'], 1),
}, },
}, },
@@ -784,60 +831,63 @@ def compare(image, payload_size, as_json):
return return
click.echo() click.echo()
click.secho(f"═══ Mode Comparison: {image} ═══", fg='cyan', bold=True) click.secho(f"=== Mode Comparison: {image} ===", fg='cyan', bold=True)
click.echo(f" Dimensions: {comparison['width']} × {comparison['height']}") click.echo(f" Dimensions: {comparison['width']} x {comparison['height']}")
click.echo() click.echo()
# LSB mode # LSB mode
click.secho(" ┌─── LSB Mode ───", fg='green') click.secho(" +--- LSB Mode ---", fg='green')
click.echo(f" Capacity: {comparison['lsb']['capacity_bytes']:,} bytes ({comparison['lsb']['capacity_kb']:.1f} KB)") click.echo(f" | Capacity: {comparison['lsb']['capacity_bytes']:,} bytes ({comparison['lsb']['capacity_kb']:.1f} KB)")
click.echo(f" Output: {comparison['lsb']['output']}") click.echo(f" | Output: {comparison['lsb']['output']}")
click.echo(f" Status: Available") click.echo(f" | Status: [OK] Available")
click.echo(" ") click.echo(" |")
# DCT mode # DCT mode
click.secho(" ├─── DCT Mode ───", fg='blue') click.secho(" +--- DCT Mode ---", fg='blue')
click.echo(f" Capacity: {comparison['dct']['capacity_bytes']:,} bytes ({comparison['dct']['capacity_kb']:.1f} KB)") click.echo(f" | Capacity: {comparison['dct']['capacity_bytes']:,} bytes ({comparison['dct']['capacity_kb']:.1f} KB)")
click.echo(f" │ Output: {comparison['dct']['output']}") click.echo(f" | Ratio: {comparison['dct']['ratio_vs_lsb']:.1f}% of LSB capacity")
click.echo(f" │ Ratio: {comparison['dct']['ratio_vs_lsb']:.1f}% of LSB capacity")
if comparison['dct']['available']: if comparison['dct']['available']:
click.echo(f" Status: Available") click.echo(f" | Status: [OK] Available")
click.echo(f" | Formats: PNG (lossless), JPEG (smaller)")
click.echo(f" | Colors: Grayscale (default), Color (v3.0.1)")
else: else:
click.secho(f" Status: Requires scipy (pip install scipy)", fg='yellow') click.secho(f" | Status: [X] Requires scipy (pip install scipy)", fg='yellow')
click.echo(" ") click.echo(" |")
# Payload check # Payload check
if payload_size: if payload_size:
click.secho(" ├─── Payload Check ───", fg='magenta') click.secho(" +--- Payload Check ---", fg='magenta')
click.echo(f" Size: {payload_size:,} bytes") click.echo(f" | Size: {payload_size:,} bytes")
fits_lsb = payload_size <= comparison['lsb']['capacity_bytes'] fits_lsb = payload_size <= comparison['lsb']['capacity_bytes']
fits_dct = payload_size <= comparison['dct']['capacity_bytes'] fits_dct = payload_size <= comparison['dct']['capacity_bytes']
lsb_icon = "" if fits_lsb else "" lsb_icon = "[OK]" if fits_lsb else "[X]"
dct_icon = "" if fits_dct else "" dct_icon = "[OK]" if fits_dct else "[X]"
lsb_color = 'green' if fits_lsb else 'red' lsb_color = 'green' if fits_lsb else 'red'
dct_color = 'green' if fits_dct else 'red' dct_color = 'green' if fits_dct else 'red'
click.echo(f" LSB mode: ", nl=False) click.echo(f" | LSB mode: ", nl=False)
click.secho(f"{lsb_icon} {'Fits' if fits_lsb else 'Too large'}", fg=lsb_color) click.secho(f"{lsb_icon} {'Fits' if fits_lsb else 'Too large'}", fg=lsb_color)
click.echo(f" DCT mode: ", nl=False) click.echo(f" | DCT mode: ", nl=False)
click.secho(f"{dct_icon} {'Fits' if fits_dct else 'Too large'}", fg=dct_color) click.secho(f"{dct_icon} {'Fits' if fits_dct else 'Too large'}", fg=dct_color)
click.echo(" ") click.echo(" |")
# Recommendation # Recommendation
click.secho(" └─── Recommendation ───", fg='yellow') click.secho(" +--- Recommendation ---", fg='yellow')
if not comparison['dct']['available']: if not comparison['dct']['available']:
click.echo(" Use LSB mode (DCT unavailable)") click.echo(" Use LSB mode (DCT unavailable)")
elif payload_size: elif payload_size:
if fits_dct: if fits_dct:
click.echo(" DCT mode for better stealth (payload fits both modes)") click.echo(" DCT mode for better stealth (payload fits both modes)")
click.echo(" Use --dct-color color to preserve original colors")
elif fits_lsb: elif fits_lsb:
click.echo(" LSB mode (payload too large for DCT)") click.echo(" LSB mode (payload too large for DCT)")
else: else:
click.secho(" Payload too large for both modes!", fg='red') click.secho(" [X] Payload too large for both modes!", fg='red')
else: else:
click.echo(" LSB for larger payloads, DCT for better stealth") click.echo(" LSB for larger payloads, DCT for better stealth")
click.echo(" DCT supports color output with --dct-color color")
click.echo() click.echo()
@@ -881,7 +931,7 @@ def strip_metadata_cmd(image, output, output_format, quiet):
out_path.write_bytes(clean_data) out_path.write_bytes(clean_data)
if not quiet: if not quiet:
click.secho(" Metadata stripped", fg='green') click.secho("[OK] Metadata stripped", fg='green')
click.echo(f" Input: {image} ({original_size:,} bytes)") click.echo(f" Input: {image} ({original_size:,} bytes)")
click.echo(f" Output: {out_path} ({len(clean_data):,} bytes)") click.echo(f" Output: {out_path} ({len(clean_data):,} bytes)")
@@ -890,7 +940,7 @@ def strip_metadata_cmd(image, output, output_format, quiet):
# ============================================================================ # ============================================================================
# MODES COMMAND (NEW in v3.0) # MODES COMMAND
# ============================================================================ # ============================================================================
@cli.command() @cli.command()
@@ -901,12 +951,12 @@ def modes():
Displays which modes are available and their characteristics. Displays which modes are available and their characteristics.
""" """
click.echo() click.echo()
click.secho("═══ Stegasoo Embedding Modes ═══", fg='cyan', bold=True) click.secho("=== Stegasoo Embedding Modes ===", fg='cyan', bold=True)
click.echo() click.echo()
# LSB Mode # LSB Mode
click.secho(" LSB Mode (Spatial LSB)", fg='green', bold=True) click.secho(" LSB Mode (Spatial LSB)", fg='green', bold=True)
click.echo(" Status: Always available") click.echo(" Status: [OK] Always available")
click.echo(" Output: PNG/BMP (full color)") click.echo(" Output: PNG/BMP (full color)")
click.echo(" Capacity: ~375 KB per megapixel") click.echo(" Capacity: ~375 KB per megapixel")
click.echo(" Use case: Larger payloads, color preservation") click.echo(" Use case: Larger payloads, color preservation")
@@ -916,18 +966,36 @@ def modes():
# DCT Mode # DCT Mode
click.secho(" DCT Mode (Frequency Domain)", fg='blue', bold=True) click.secho(" DCT Mode (Frequency Domain)", fg='blue', bold=True)
if has_dct_support(): if has_dct_support():
click.echo(" Status: Available") click.echo(" Status: [OK] Available")
else: else:
click.secho(" Status: Requires scipy", fg='yellow') click.secho(" Status: [X] Requires scipy", fg='yellow')
click.echo(" Install: pip install scipy") click.echo(" Install: pip install scipy")
click.echo(" Output: PNG (grayscale only)")
click.echo(" Capacity: ~75 KB per megapixel (~20% of LSB)") click.echo(" Capacity: ~75 KB per megapixel (~20% of LSB)")
click.echo(" Use case: Better stealth, smaller messages") click.echo(" Use case: Better stealth, frequency domain hiding")
click.echo(" CLI flag: --mode dct") click.echo(" CLI flag: --mode dct")
click.echo() click.echo()
click.secho(" Tip:", dim=True) # DCT Options (v3.0.1)
click.echo(" Use 'stegasoo compare <image>' to see capacity for both modes") click.secho(" DCT Options (v3.0.1)", fg='magenta', bold=True)
click.echo(" Output format:")
click.echo(" --dct-format png Lossless, larger file (default)")
click.echo(" --dct-format jpeg Lossy, smaller, more natural")
click.echo()
click.echo(" Color mode:")
click.echo(" --dct-color grayscale Traditional DCT (default)")
click.echo(" --dct-color color Preserves original colors")
click.echo()
# Examples
click.secho(" Examples:", dim=True)
click.echo(" # Traditional DCT (grayscale PNG)")
click.echo(" stegasoo encode ... --mode dct")
click.echo()
click.echo(" # Color-preserving DCT with JPEG output")
click.echo(" stegasoo encode ... --mode dct --dct-color color --dct-format jpeg")
click.echo()
click.echo(" # Compare modes for an image")
click.echo(" stegasoo compare carrier.png")
click.echo() click.echo()

View File

@@ -5,7 +5,7 @@ Stegasoo Web Frontend (v3.0.1)
Flask-based web UI for steganography operations. Flask-based web UI for steganography operations.
Supports both text messages and file embedding. Supports both text messages and file embedding.
NEW in v3.0: LSB and DCT embedding modes with advanced options. NEW in v3.0: LSB and DCT embedding modes with advanced options.
NEW in v3.0.1: DCT output format selection (PNG or JPEG). NEW in v3.0.1: DCT output format selection (PNG or JPEG) and color mode (grayscale or color).
""" """
import io import io
@@ -532,6 +532,11 @@ def encode_page():
if dct_output_format not in ('png', 'jpeg'): if dct_output_format not in ('png', 'jpeg'):
dct_output_format = 'png' dct_output_format = 'png'
# NEW in v3.0.1 - DCT color mode (default to 'color')
dct_color_mode = request.form.get('dct_color_mode', 'color')
if dct_color_mode not in ('grayscale', 'color'):
dct_color_mode = 'color'
# Check DCT availability # Check DCT availability
if embed_mode == 'dct' and not has_dct_support(): if embed_mode == 'dct' and not has_dct_support():
flash('DCT mode requires scipy. Install with: pip install scipy', 'error') flash('DCT mode requires scipy. Install with: pip install scipy', 'error')
@@ -624,7 +629,7 @@ def encode_page():
else: else:
date_str = datetime.now().strftime('%Y-%m-%d') date_str = datetime.now().strftime('%Y-%m-%d')
# Encode with selected mode and output format # Encode with selected mode, output format, and color mode
encode_result = encode( encode_result = encode(
message=payload, message=payload,
reference_photo=ref_data, reference_photo=ref_data,
@@ -634,8 +639,9 @@ def encode_page():
rsa_key_data=rsa_key_data, rsa_key_data=rsa_key_data,
rsa_password=key_password, rsa_password=key_password,
date_str=date_str, date_str=date_str,
embed_mode=embed_mode, # NEW in v3.0 embed_mode=embed_mode,
dct_output_format=dct_output_format if embed_mode == 'dct' else None, # NEW in v3.0.1 dct_output_format=dct_output_format if embed_mode == 'dct' else None,
dct_color_mode=dct_color_mode if embed_mode == 'dct' else None,
) )
# Determine actual output format for filename and storage # Determine actual output format for filename and storage
@@ -660,6 +666,7 @@ def encode_page():
'timestamp': time.time(), 'timestamp': time.time(),
'embed_mode': embed_mode, 'embed_mode': embed_mode,
'output_format': dct_output_format if embed_mode == 'dct' else 'png', 'output_format': dct_output_format if embed_mode == 'dct' else 'png',
'color_mode': dct_color_mode if embed_mode == 'dct' else None,
'mime_type': output_mime, 'mime_type': output_mime,
} }
@@ -699,7 +706,8 @@ def encode_result(file_id):
filename=file_info['filename'], filename=file_info['filename'],
thumbnail_url=url_for('encode_thumbnail', thumb_id=thumbnail_id) if thumbnail_id else None, thumbnail_url=url_for('encode_thumbnail', thumb_id=thumbnail_id) if thumbnail_id else None,
embed_mode=file_info.get('embed_mode', 'lsb'), embed_mode=file_info.get('embed_mode', 'lsb'),
output_format=file_info.get('output_format', 'png'), # NEW in v3.0.1 output_format=file_info.get('output_format', 'png'),
color_mode=file_info.get('color_mode'), # NEW in v3.0.1
) )
@@ -856,7 +864,7 @@ def decode_page():
rsa_key_data=rsa_key_data, rsa_key_data=rsa_key_data,
rsa_password=key_password, rsa_password=key_password,
date_str=stego_date if stego_date else None, date_str=stego_date if stego_date else None,
embed_mode=embed_mode, # NEW in v3.0 embed_mode=embed_mode,
) )
if decode_result.is_file: if decode_result.is_file:

View File

@@ -1,766 +0,0 @@
#!/usr/bin/env python3
"""
Stegasoo Web Frontend
Flask-based web UI for steganography operations.
Supports both text messages and file embedding.
"""
import io
import sys
import time
import secrets
import mimetypes
from pathlib import Path
from datetime import datetime
from PIL import Image
from flask import (
Flask, render_template, request, send_file,
jsonify, flash, redirect, url_for
)
# 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,
export_rsa_key_pem, load_rsa_key,
validate_pin, validate_message, validate_image,
validate_rsa_key, validate_security_factors,
validate_file_payload,
get_today_day, generate_filename,
DAY_NAMES, __version__,
StegasooError, DecryptionError, CapacityError,
has_argon2,
FilePayload,
MAX_FILE_PAYLOAD_SIZE,
)
from stegasoo.constants import (
MAX_MESSAGE_SIZE, MIN_PIN_LENGTH, MAX_PIN_LENGTH,
VALID_RSA_SIZES, MAX_FILE_SIZE,
)
# QR Code support
try:
import qrcode
from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_M
HAS_QRCODE = True
except ImportError:
HAS_QRCODE = False
# QR Code reading
try:
from pyzbar.pyzbar import decode as pyzbar_decode
HAS_QRCODE_READ = True
except ImportError:
HAS_QRCODE_READ = False
import zlib
import base64
# Import QR utilities
from stegasoo.qr_utils import (
compress_data, decompress_data, auto_decompress,
is_compressed, can_fit_in_qr, needs_compression,
generate_qr_code, read_qr_code, extract_key_from_qr,
has_qr_write, has_qr_read,
QR_MAX_BINARY, COMPRESSION_PREFIX
)
# ============================================================================
# FLASK APP CONFIGURATION
# ============================================================================
app = Flask(__name__)
app.secret_key = secrets.token_hex(32)
app.config['MAX_CONTENT_LENGTH'] = MAX_FILE_SIZE # 20MB max upload
# Temporary file storage for sharing (file_id -> {data, timestamp, filename})
TEMP_FILES: dict[str, dict] = {}
THUMBNAIL_FILES: dict[str, bytes] = {}
TEMP_FILE_EXPIRY = 300 # 5 minutes
THUMBNAIL_SIZE = (250, 250) # Maximum dimensions for thumbnail
# ============================================================================
# CONFIGURATION
# ============================================================================
# Override stegasoo limits for larger files
# Note: You might need to modify the stegasoo library itself
# to actually increase these limits in its internal calculations
# Flask upload limit (30MB)
MAX_UPLOAD_SIZE = 30 * 1024 * 1024
# Try to import and override stegasoo constants if possible
try:
# Check current limits
print(f"Current MAX_FILE_SIZE from constants: {MAX_FILE_SIZE}")
print(f"Current MAX_FILE_PAYLOAD_SIZE: {MAX_FILE_PAYLOAD_SIZE}")
DESIRED_PAYLOAD_SIZE = 2 * 1024 * 1024 # 2MB
# Note: You might need to patch the stegasoo module
# if MAX_FILE_PAYLOAD_SIZE is used internally
import stegasoo
if hasattr(stegasoo, 'MAX_FILE_PAYLOAD_SIZE'):
print(f"Overriding MAX_FILE_PAYLOAD_SIZE to {DESIRED_PAYLOAD_SIZE}")
stegasoo.MAX_FILE_PAYLOAD_SIZE = DESIRED_PAYLOAD_SIZE
except Exception as e:
print(f"Could not override stegasoo limits: {e}")
def generate_thumbnail(image_data: bytes, size: tuple = THUMBNAIL_SIZE) -> bytes:
"""Generate thumbnail from image data."""
try:
with Image.open(io.BytesIO(image_data)) as img:
# Convert to RGB if necessary
if img.mode in ('RGBA', 'LA', 'P'):
# Create white background for transparent images
background = Image.new('RGB', img.size, (255, 255, 255))
if img.mode == 'P':
img = img.convert('RGBA')
background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
img = background
elif img.mode != 'RGB':
img = img.convert('RGB')
# Create thumbnail
img.thumbnail(size, Image.Resampling.LANCZOS)
# Save to bytes
buffer = io.BytesIO()
img.save(buffer, format='JPEG', quality=85, optimize=True)
return buffer.getvalue()
except Exception as e:
# Log error but don't crash
print(f"Thumbnail generation error: {e}")
return None
def cleanup_temp_files():
"""Remove expired temporary files."""
now = time.time()
expired = [fid for fid, info in TEMP_FILES.items() if now - info['timestamp'] > TEMP_FILE_EXPIRY]
for fid in expired:
TEMP_FILES.pop(fid, None)
# Also clean up corresponding thumbnail
thumb_id = f"{fid}_thumb"
THUMBNAIL_FILES.pop(thumb_id, None)
def allowed_image(filename: str) -> bool:
"""Check if file has allowed image extension."""
if not filename or '.' not in filename:
return False
ext = filename.rsplit('.', 1)[1].lower()
return ext in {'png', 'jpg', 'jpeg', 'bmp', 'gif'}
def format_size(size_bytes: int) -> str:
"""Format file size for display."""
if size_bytes < 1024:
return f"{size_bytes} B"
elif size_bytes < 1024 * 1024:
return f"{size_bytes / 1024:.1f} KB"
else:
return f"{size_bytes / (1024 * 1024):.1f} MB"
# ============================================================================
# ROUTES
# ============================================================================
@app.route('/')
def index():
return render_template('index.html')
@app.route('/generate', methods=['GET', 'POST'])
def generate():
if request.method == 'POST':
words_per_phrase = int(request.form.get('words_per_phrase', 3))
use_pin = request.form.get('use_pin') == 'on'
use_rsa = request.form.get('use_rsa') == 'on'
if not use_pin and not use_rsa:
flash('You must select at least one security factor (PIN or RSA Key)', 'error')
return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE)
pin_length = int(request.form.get('pin_length', 6))
rsa_bits = int(request.form.get('rsa_bits', 2048))
# Clamp values
words_per_phrase = max(3, min(12, words_per_phrase))
pin_length = max(MIN_PIN_LENGTH, min(MAX_PIN_LENGTH, pin_length))
if rsa_bits not in VALID_RSA_SIZES:
rsa_bits = 2048
try:
creds = generate_credentials(
use_pin=use_pin,
use_rsa=use_rsa,
pin_length=pin_length,
rsa_bits=rsa_bits,
words_per_phrase=words_per_phrase
)
# Store RSA key temporarily for QR generation
qr_token = None
qr_needs_compression = False
qr_too_large = False
if creds.rsa_key_pem and HAS_QRCODE:
# Check if key fits in QR code
if can_fit_in_qr(creds.rsa_key_pem, compress=True):
qr_needs_compression = True
else:
qr_too_large = True
if not qr_too_large:
qr_token = secrets.token_urlsafe(16)
cleanup_temp_files()
TEMP_FILES[qr_token] = {
'data': creds.rsa_key_pem.encode(),
'filename': 'rsa_key.pem',
'timestamp': time.time(),
'type': 'rsa_key',
'compress': qr_needs_compression
}
return render_template('generate.html',
phrases=creds.phrases,
pin=creds.pin,
days=DAY_NAMES,
generated=True,
words_per_phrase=words_per_phrase,
pin_length=pin_length if use_pin else None,
use_pin=use_pin,
use_rsa=use_rsa,
rsa_bits=rsa_bits,
rsa_key_pem=creds.rsa_key_pem,
phrase_entropy=creds.phrase_entropy,
pin_entropy=creds.pin_entropy,
rsa_entropy=creds.rsa_entropy,
total_entropy=creds.total_entropy,
has_qrcode=HAS_QRCODE,
qr_token=qr_token,
qr_needs_compression=qr_needs_compression,
qr_too_large=qr_too_large
)
except Exception as e:
flash(f'Error generating credentials: {e}', 'error')
return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE)
return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE)
@app.route('/generate/qr/<token>')
def generate_qr(token):
"""Generate QR code for RSA key."""
if not HAS_QRCODE:
return "QR code support not available", 501
if token not in TEMP_FILES:
return "Token expired or invalid", 404
file_info = TEMP_FILES[token]
if file_info.get('type') != 'rsa_key':
return "Invalid token type", 400
try:
key_pem = file_info['data'].decode('utf-8')
compress = file_info.get('compress', False)
qr_png = generate_qr_code(key_pem, compress=compress)
return send_file(
io.BytesIO(qr_png),
mimetype='image/png',
as_attachment=False
)
except Exception as e:
return f"Error generating QR code: {e}", 500
@app.route('/generate/qr-download/<token>')
def generate_qr_download(token):
"""Download QR code as PNG file."""
if not HAS_QRCODE:
return "QR code support not available", 501
if token not in TEMP_FILES:
return "Token expired or invalid", 404
file_info = TEMP_FILES[token]
if file_info.get('type') != 'rsa_key':
return "Invalid token type", 400
try:
key_pem = file_info['data'].decode('utf-8')
compress = file_info.get('compress', False)
qr_png = generate_qr_code(key_pem, compress=compress)
return send_file(
io.BytesIO(qr_png),
mimetype='image/png',
as_attachment=True,
download_name='stegasoo_rsa_key_qr.png'
)
except Exception as e:
return f"Error generating QR code: {e}", 500
@app.route('/generate/download-key', methods=['POST'])
def download_key():
"""Download RSA key as password-protected PEM file."""
key_pem = request.form.get('key_pem', '')
password = request.form.get('key_password', '')
if not key_pem:
flash('No key to download', 'error')
return redirect(url_for('generate'))
if not password or len(password) < 8:
flash('Password must be at least 8 characters', 'error')
return redirect(url_for('generate'))
try:
private_key = load_rsa_key(key_pem.encode('utf-8'))
encrypted_pem = export_rsa_key_pem(private_key, password=password)
key_id = secrets.token_hex(4)
filename = f'stegasoo_key_{private_key.key_size}_{key_id}.pem'
return send_file(
io.BytesIO(encrypted_pem),
mimetype='application/x-pem-file',
as_attachment=True,
download_name=filename
)
except Exception as e:
flash(f'Error creating key file: {e}', 'error')
return redirect(url_for('generate'))
@app.route('/extract-key-from-qr', methods=['POST'])
def extract_key_from_qr_route():
"""
Extract RSA key from uploaded QR code image.
Returns JSON with the extracted key or error.
"""
if not HAS_QRCODE_READ:
return jsonify({
'success': False,
'error': 'QR code reading not available. Install pyzbar and libzbar.'
}), 501
qr_image = request.files.get('qr_image')
if not qr_image:
return jsonify({
'success': False,
'error': 'No QR image provided'
}), 400
try:
image_data = qr_image.read()
key_pem = extract_key_from_qr(image_data)
if key_pem:
return jsonify({
'success': True,
'key_pem': key_pem
})
else:
return jsonify({
'success': False,
'error': 'No valid RSA key found in QR code'
}), 400
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.route('/encode', methods=['GET', 'POST'])
def encode_page():
day_of_week = get_today_day()
max_payload_kb = MAX_FILE_PAYLOAD_SIZE // 1024
if request.method == 'POST':
try:
# Get files
ref_photo = request.files.get('reference_photo')
carrier = request.files.get('carrier')
rsa_key_file = request.files.get('rsa_key')
payload_file = request.files.get('payload_file')
if not ref_photo or not carrier:
flash('Both reference photo and carrier image are required', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
if not allowed_image(ref_photo.filename) or not allowed_image(carrier.filename):
flash('Invalid file type. Use PNG, JPG, or BMP', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Get form data
message = request.form.get('message', '')
day_phrase = request.form.get('day_phrase', '')
pin = request.form.get('pin', '').strip()
rsa_password = request.form.get('rsa_password', '')
payload_type = request.form.get('payload_type', 'text')
# Determine payload
if payload_type == 'file' and payload_file and payload_file.filename:
# File payload
file_data = payload_file.read()
result = validate_file_payload(file_data, payload_file.filename)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
mime_type, _ = mimetypes.guess_type(payload_file.filename)
payload = FilePayload(
data=file_data,
filename=payload_file.filename,
mime_type=mime_type
)
else:
# Text message
result = validate_message(message)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
payload = message
if not day_phrase:
flash('Day phrase is required', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Read files
ref_data = ref_photo.read()
carrier_data = carrier.read()
# Handle RSA key - can come from .pem file or QR code image
rsa_key_data = None
rsa_key_qr = request.files.get('rsa_key_qr')
rsa_key_from_qr = False # Track source for password handling
if rsa_key_file and rsa_key_file.filename:
# RSA key from .pem file
rsa_key_data = rsa_key_file.read()
elif rsa_key_qr and rsa_key_qr.filename and HAS_QRCODE_READ:
# RSA key from QR code image
qr_image_data = rsa_key_qr.read()
key_pem = extract_key_from_qr(qr_image_data)
if key_pem:
rsa_key_data = key_pem.encode('utf-8')
rsa_key_from_qr = True # QR keys are never password-protected
else:
flash('Could not extract RSA key from QR code image. Make sure the image contains a valid QR code.', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Validate security factors
result = validate_security_factors(pin, rsa_key_data)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Validate PIN if provided
if pin:
result = validate_pin(pin)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Determine key password - QR code keys are never password-protected
key_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None)
# Validate RSA key if provided
if rsa_key_data:
result = validate_rsa_key(rsa_key_data, key_password)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Validate carrier image
result = validate_image(carrier_data, "Carrier image")
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Get date
client_date = request.form.get('client_date', '').strip()
if client_date and len(client_date) == 10 and client_date[4] == '-' and client_date[7] == '-':
date_str = client_date
else:
date_str = datetime.now().strftime('%Y-%m-%d')
# Encode
encode_result = encode(
message=payload,
reference_photo=ref_data,
carrier_image=carrier_data,
day_phrase=day_phrase,
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=key_password,
date_str=date_str
)
# Store temporarily
file_id = secrets.token_urlsafe(16)
cleanup_temp_files()
TEMP_FILES[file_id] = {
'data': encode_result.stego_image,
'filename': encode_result.filename,
'timestamp': time.time()
}
return redirect(url_for('encode_result', file_id=file_id))
except CapacityError as e:
flash(str(e), 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
except StegasooError as e:
flash(str(e), 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
except Exception as e:
flash(f'Error: {e}', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
@app.route('/encode/result/<file_id>')
def encode_result(file_id):
if file_id not in TEMP_FILES:
flash('File expired or not found. Please encode again.', 'error')
return redirect(url_for('encode_page'))
file_info = TEMP_FILES[file_id]
# Generate thumbnail
thumbnail_data = generate_thumbnail(file_info['data'])
thumbnail_id = None
if thumbnail_data:
thumbnail_id = f"{file_id}_thumb"
THUMBNAIL_FILES[thumbnail_id] = thumbnail_data
return render_template('encode_result.html',
file_id=file_id,
filename=file_info['filename'],
thumbnail_url=url_for('encode_thumbnail', thumb_id=thumbnail_id) if thumbnail_id else None
)
@app.route('/encode/thumbnail/<thumb_id>')
def encode_thumbnail(thumb_id):
"""Serve thumbnail image."""
if thumb_id not in THUMBNAIL_FILES:
return "Thumbnail not found", 404
return send_file(
io.BytesIO(THUMBNAIL_FILES[thumb_id]),
mimetype='image/jpeg',
as_attachment=False
)
@app.route('/encode/download/<file_id>')
def encode_download(file_id):
if file_id not in TEMP_FILES:
flash('File expired or not found.', 'error')
return redirect(url_for('encode_page'))
file_info = TEMP_FILES[file_id]
return send_file(
io.BytesIO(file_info['data']),
mimetype='image/png',
as_attachment=True,
download_name=file_info['filename']
)
@app.route('/encode/file/<file_id>')
def encode_file_route(file_id):
"""Serve file for Web Share API."""
if file_id not in TEMP_FILES:
return "Not found", 404
file_info = TEMP_FILES[file_id]
return send_file(
io.BytesIO(file_info['data']),
mimetype='image/png',
as_attachment=False,
download_name=file_info['filename']
)
@app.route('/encode/cleanup/<file_id>', methods=['POST'])
def encode_cleanup(file_id):
"""Manually cleanup a file after sharing."""
TEMP_FILES.pop(file_id, None)
# Also cleanup thumbnail if exists
thumb_id = f"{file_id}_thumb"
THUMBNAIL_FILES.pop(thumb_id, None)
return jsonify({'status': 'ok'})
@app.route('/decode', methods=['GET', 'POST'])
def decode_page():
if request.method == 'POST':
try:
# Get files
ref_photo = request.files.get('reference_photo')
stego_image = request.files.get('stego_image')
rsa_key_file = request.files.get('rsa_key')
if not ref_photo or not stego_image:
flash('Both reference photo and stego image are required', 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Get form data
day_phrase = request.form.get('day_phrase', '')
pin = request.form.get('pin', '').strip()
rsa_password = request.form.get('rsa_password', '')
if not day_phrase:
flash('Day phrase is required', 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Read files
ref_data = ref_photo.read()
stego_data = stego_image.read()
# Handle RSA key - can come from .pem file or QR code image
rsa_key_data = None
rsa_key_qr = request.files.get('rsa_key_qr')
rsa_key_from_qr = False # Track source for password handling
if rsa_key_file and rsa_key_file.filename:
# RSA key from .pem file
rsa_key_data = rsa_key_file.read()
elif rsa_key_qr and rsa_key_qr.filename and HAS_QRCODE_READ:
# RSA key from QR code image
qr_image_data = rsa_key_qr.read()
key_pem = extract_key_from_qr(qr_image_data)
if key_pem:
rsa_key_data = key_pem.encode('utf-8')
rsa_key_from_qr = True # QR keys are never password-protected
else:
flash('Could not extract RSA key from QR code image. Make sure the image contains a valid QR code.', 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Validate security factors
result = validate_security_factors(pin, rsa_key_data)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Validate PIN if provided
if pin:
result = validate_pin(pin)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Determine key password - QR code keys are never password-protected
key_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None)
# Validate RSA key if provided
if rsa_key_data:
result = validate_rsa_key(rsa_key_data, key_password)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Decode
decode_result = decode(
stego_image=stego_data,
reference_photo=ref_data,
day_phrase=day_phrase,
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=key_password
)
if decode_result.is_file:
# File content - store temporarily for download
file_id = secrets.token_urlsafe(16)
cleanup_temp_files()
filename = decode_result.filename or 'decoded_file'
TEMP_FILES[file_id] = {
'data': decode_result.file_data,
'filename': filename,
'mime_type': decode_result.mime_type,
'timestamp': time.time()
}
return render_template('decode.html',
decoded_file=True,
file_id=file_id,
filename=filename,
file_size=format_size(len(decode_result.file_data)),
mime_type=decode_result.mime_type
)
else:
# Text content
return render_template('decode.html', decoded_message=decode_result.message)
except DecryptionError:
flash('Decryption failed. Check your phrase, PIN, RSA key, and reference photo.', 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
except StegasooError as e:
flash(str(e), 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
except Exception as e:
flash(f'Error: {e}', 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
@app.route('/decode/download/<file_id>')
def decode_download(file_id):
"""Download decoded file."""
if file_id not in TEMP_FILES:
flash('File expired or not found.', 'error')
return redirect(url_for('decode_page'))
file_info = TEMP_FILES[file_id]
mime_type = file_info.get('mime_type', 'application/octet-stream')
return send_file(
io.BytesIO(file_info['data']),
mimetype=mime_type,
as_attachment=True,
download_name=file_info['filename']
)
@app.route('/about')
def about():
return render_template('about.html',
has_argon2=has_argon2(),
has_qrcode_read=HAS_QRCODE_READ,
max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024
)
# ============================================================================
# MAIN
# ============================================================================
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)

View File

@@ -1,781 +0,0 @@
#!/usr/bin/env python3
"""
Stegasoo Web Frontend
Flask-based web UI for steganography operations.
Supports both text messages and file embedding.
"""
import io
import sys
import time
import secrets
import mimetypes
from pathlib import Path
from datetime import datetime
from PIL import Image
from flask import (
Flask, render_template, request, send_file,
jsonify, flash, redirect, url_for
)
# 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,
export_rsa_key_pem, load_rsa_key,
validate_pin, validate_message, validate_image,
validate_rsa_key, validate_security_factors,
validate_file_payload,
get_today_day, generate_filename,
DAY_NAMES, __version__,
StegasooError, DecryptionError, CapacityError,
has_argon2,
FilePayload,
MAX_FILE_PAYLOAD_SIZE,
)
from stegasoo.constants import (
MAX_MESSAGE_SIZE, MIN_PIN_LENGTH, MAX_PIN_LENGTH,
VALID_RSA_SIZES, MAX_FILE_SIZE,
)
# QR Code support
try:
import qrcode
from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_M
HAS_QRCODE = True
except ImportError:
HAS_QRCODE = False
# QR Code reading
try:
from pyzbar.pyzbar import decode as pyzbar_decode
HAS_QRCODE_READ = True
except ImportError:
HAS_QRCODE_READ = False
import zlib
import base64
# Import QR utilities
from stegasoo.qr_utils import (
compress_data, decompress_data, auto_decompress,
is_compressed, can_fit_in_qr, needs_compression,
generate_qr_code, read_qr_code, extract_key_from_qr,
has_qr_write, has_qr_read,
QR_MAX_BINARY, COMPRESSION_PREFIX
)
# ============================================================================
# FLASK APP CONFIGURATION
# ============================================================================
app = Flask(__name__)
app.secret_key = secrets.token_hex(32)
app.config['MAX_CONTENT_LENGTH'] = MAX_FILE_SIZE # 20MB max upload
# Temporary file storage for sharing (file_id -> {data, timestamp, filename})
TEMP_FILES: dict[str, dict] = {}
THUMBNAIL_FILES: dict[str, bytes] = {}
TEMP_FILE_EXPIRY = 300 # 5 minutes
THUMBNAIL_SIZE = (250, 250) # Maximum dimensions for thumbnail
# ============================================================================
# CONFIGURATION
# ============================================================================
# Override stegasoo limits for larger files
# Note: You might need to modify the stegasoo library itself
# to actually increase these limits in its internal calculations
# Flask upload limit (30MB)
MAX_UPLOAD_SIZE = 30 * 1024 * 1024
# Try to import and override stegasoo constants if possible
try:
# Check current limits
print(f"Current MAX_FILE_SIZE from constants: {MAX_FILE_SIZE}")
print(f"Current MAX_FILE_PAYLOAD_SIZE: {MAX_FILE_PAYLOAD_SIZE}")
DESIRED_PAYLOAD_SIZE = 2 * 1024 * 1024 # 2MB
# Note: You might need to patch the stegasoo module
# if MAX_FILE_PAYLOAD_SIZE is used internally
import stegasoo
if hasattr(stegasoo, 'MAX_FILE_PAYLOAD_SIZE'):
print(f"Overriding MAX_FILE_PAYLOAD_SIZE to {DESIRED_PAYLOAD_SIZE}")
stegasoo.MAX_FILE_PAYLOAD_SIZE = DESIRED_PAYLOAD_SIZE
except Exception as e:
print(f"Could not override stegasoo limits: {e}")
def generate_thumbnail(image_data: bytes, size: tuple = THUMBNAIL_SIZE) -> bytes:
"""Generate thumbnail from image data."""
try:
with Image.open(io.BytesIO(image_data)) as img:
# Convert to RGB if necessary
if img.mode in ('RGBA', 'LA', 'P'):
# Create white background for transparent images
background = Image.new('RGB', img.size, (255, 255, 255))
if img.mode == 'P':
img = img.convert('RGBA')
background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
img = background
elif img.mode != 'RGB':
img = img.convert('RGB')
# Create thumbnail
img.thumbnail(size, Image.Resampling.LANCZOS)
# Save to bytes
buffer = io.BytesIO()
img.save(buffer, format='JPEG', quality=85, optimize=True)
return buffer.getvalue()
except Exception as e:
# Log error but don't crash
print(f"Thumbnail generation error: {e}")
return None
def cleanup_temp_files():
"""Remove expired temporary files."""
now = time.time()
expired = [fid for fid, info in TEMP_FILES.items() if now - info['timestamp'] > TEMP_FILE_EXPIRY]
for fid in expired:
TEMP_FILES.pop(fid, None)
# Also clean up corresponding thumbnail
thumb_id = f"{fid}_thumb"
THUMBNAIL_FILES.pop(thumb_id, None)
def allowed_image(filename: str) -> bool:
"""Check if file has allowed image extension."""
if not filename or '.' not in filename:
return False
ext = filename.rsplit('.', 1)[1].lower()
return ext in {'png', 'jpg', 'jpeg', 'bmp', 'gif'}
def format_size(size_bytes: int) -> str:
"""Format file size for display."""
if size_bytes < 1024:
return f"{size_bytes} B"
elif size_bytes < 1024 * 1024:
return f"{size_bytes / 1024:.1f} KB"
else:
return f"{size_bytes / (1024 * 1024):.1f} MB"
# ============================================================================
# ROUTES
# ============================================================================
@app.route('/')
def index():
return render_template('index.html')
@app.route('/generate', methods=['GET', 'POST'])
def generate():
if request.method == 'POST':
words_per_phrase = int(request.form.get('words_per_phrase', 3))
use_pin = request.form.get('use_pin') == 'on'
use_rsa = request.form.get('use_rsa') == 'on'
if not use_pin and not use_rsa:
flash('You must select at least one security factor (PIN or RSA Key)', 'error')
return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE)
pin_length = int(request.form.get('pin_length', 6))
rsa_bits = int(request.form.get('rsa_bits', 2048))
# Clamp values
words_per_phrase = max(3, min(12, words_per_phrase))
pin_length = max(MIN_PIN_LENGTH, min(MAX_PIN_LENGTH, pin_length))
if rsa_bits not in VALID_RSA_SIZES:
rsa_bits = 2048
try:
creds = generate_credentials(
use_pin=use_pin,
use_rsa=use_rsa,
pin_length=pin_length,
rsa_bits=rsa_bits,
words_per_phrase=words_per_phrase
)
# Store RSA key temporarily for QR generation
qr_token = None
qr_needs_compression = False
qr_too_large = False
if creds.rsa_key_pem and HAS_QRCODE:
# Check if key fits in QR code
if can_fit_in_qr(creds.rsa_key_pem, compress=True):
qr_needs_compression = True
else:
qr_too_large = True
if not qr_too_large:
qr_token = secrets.token_urlsafe(16)
cleanup_temp_files()
TEMP_FILES[qr_token] = {
'data': creds.rsa_key_pem.encode(),
'filename': 'rsa_key.pem',
'timestamp': time.time(),
'type': 'rsa_key',
'compress': qr_needs_compression
}
return render_template('generate.html',
phrases=creds.phrases,
pin=creds.pin,
days=DAY_NAMES,
generated=True,
words_per_phrase=words_per_phrase,
pin_length=pin_length if use_pin else None,
use_pin=use_pin,
use_rsa=use_rsa,
rsa_bits=rsa_bits,
rsa_key_pem=creds.rsa_key_pem,
phrase_entropy=creds.phrase_entropy,
pin_entropy=creds.pin_entropy,
rsa_entropy=creds.rsa_entropy,
total_entropy=creds.total_entropy,
has_qrcode=HAS_QRCODE,
qr_token=qr_token,
qr_needs_compression=qr_needs_compression,
qr_too_large=qr_too_large
)
except Exception as e:
flash(f'Error generating credentials: {e}', 'error')
return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE)
return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE)
@app.route('/generate/qr/<token>')
def generate_qr(token):
"""Generate QR code for RSA key."""
if not HAS_QRCODE:
return "QR code support not available", 501
if token not in TEMP_FILES:
return "Token expired or invalid", 404
file_info = TEMP_FILES[token]
if file_info.get('type') != 'rsa_key':
return "Invalid token type", 400
try:
key_pem = file_info['data'].decode('utf-8')
compress = file_info.get('compress', False)
qr_png = generate_qr_code(key_pem, compress=compress)
return send_file(
io.BytesIO(qr_png),
mimetype='image/png',
as_attachment=False
)
except Exception as e:
return f"Error generating QR code: {e}", 500
@app.route('/generate/qr-download/<token>')
def generate_qr_download(token):
"""Download QR code as PNG file."""
if not HAS_QRCODE:
return "QR code support not available", 501
if token not in TEMP_FILES:
return "Token expired or invalid", 404
file_info = TEMP_FILES[token]
if file_info.get('type') != 'rsa_key':
return "Invalid token type", 400
try:
key_pem = file_info['data'].decode('utf-8')
compress = file_info.get('compress', False)
qr_png = generate_qr_code(key_pem, compress=compress)
return send_file(
io.BytesIO(qr_png),
mimetype='image/png',
as_attachment=True,
download_name='stegasoo_rsa_key_qr.png'
)
except Exception as e:
return f"Error generating QR code: {e}", 500
@app.route('/generate/download-key', methods=['POST'])
def download_key():
"""Download RSA key as password-protected PEM file."""
key_pem = request.form.get('key_pem', '')
password = request.form.get('key_password', '')
if not key_pem:
flash('No key to download', 'error')
return redirect(url_for('generate'))
if not password or len(password) < 8:
flash('Password must be at least 8 characters', 'error')
return redirect(url_for('generate'))
try:
private_key = load_rsa_key(key_pem.encode('utf-8'))
encrypted_pem = export_rsa_key_pem(private_key, password=password)
key_id = secrets.token_hex(4)
filename = f'stegasoo_key_{private_key.key_size}_{key_id}.pem'
return send_file(
io.BytesIO(encrypted_pem),
mimetype='application/x-pem-file',
as_attachment=True,
download_name=filename
)
except Exception as e:
flash(f'Error creating key file: {e}', 'error')
return redirect(url_for('generate'))
@app.route('/extract-key-from-qr', methods=['POST'])
def extract_key_from_qr_route():
"""
Extract RSA key from uploaded QR code image.
Returns JSON with the extracted key or error.
"""
if not HAS_QRCODE_READ:
return jsonify({
'success': False,
'error': 'QR code reading not available. Install pyzbar and libzbar.'
}), 501
qr_image = request.files.get('qr_image')
if not qr_image:
return jsonify({
'success': False,
'error': 'No QR image provided'
}), 400
try:
image_data = qr_image.read()
key_pem = extract_key_from_qr(image_data)
if key_pem:
return jsonify({
'success': True,
'key_pem': key_pem
})
else:
return jsonify({
'success': False,
'error': 'No valid RSA key found in QR code'
}), 400
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.route('/encode', methods=['GET', 'POST'])
def encode_page():
day_of_week = get_today_day()
max_payload_kb = MAX_FILE_PAYLOAD_SIZE // 1024
if request.method == 'POST':
try:
# Get files
ref_photo = request.files.get('reference_photo')
carrier = request.files.get('carrier')
rsa_key_file = request.files.get('rsa_key')
payload_file = request.files.get('payload_file')
if not ref_photo or not carrier:
flash('Both reference photo and carrier image are required', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
if not allowed_image(ref_photo.filename) or not allowed_image(carrier.filename):
flash('Invalid file type. Use PNG, JPG, or BMP', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Get form data
message = request.form.get('message', '')
day_phrase = request.form.get('day_phrase', '')
pin = request.form.get('pin', '').strip()
rsa_password = request.form.get('rsa_password', '')
payload_type = request.form.get('payload_type', 'text')
# Determine payload
if payload_type == 'file' and payload_file and payload_file.filename:
# File payload
file_data = payload_file.read()
result = validate_file_payload(file_data, payload_file.filename)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
mime_type, _ = mimetypes.guess_type(payload_file.filename)
payload = FilePayload(
data=file_data,
filename=payload_file.filename,
mime_type=mime_type
)
else:
# Text message
result = validate_message(message)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
payload = message
if not day_phrase:
flash('Day phrase is required', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Read files
ref_data = ref_photo.read()
carrier_data = carrier.read()
# Handle RSA key - can come from .pem file or QR code image
rsa_key_data = None
rsa_key_qr = request.files.get('rsa_key_qr')
rsa_key_from_qr = False # Track source for password handling
if rsa_key_file and rsa_key_file.filename:
# RSA key from .pem file
rsa_key_data = rsa_key_file.read()
elif rsa_key_qr and rsa_key_qr.filename and HAS_QRCODE_READ:
# RSA key from QR code image
qr_image_data = rsa_key_qr.read()
key_pem = extract_key_from_qr(qr_image_data)
if key_pem:
rsa_key_data = key_pem.encode('utf-8')
rsa_key_from_qr = True # QR keys are never password-protected
else:
flash('Could not extract RSA key from QR code image. Make sure the image contains a valid QR code.', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Validate security factors
result = validate_security_factors(pin, rsa_key_data)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Validate PIN if provided
if pin:
result = validate_pin(pin)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Determine key password - QR code keys are never password-protected
key_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None)
# Validate RSA key if provided
if rsa_key_data:
result = validate_rsa_key(rsa_key_data, key_password)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Validate carrier image
result = validate_image(carrier_data, "Carrier image")
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Get date
client_date = request.form.get('client_date', '').strip()
if client_date and len(client_date) == 10 and client_date[4] == '-' and client_date[7] == '-':
date_str = client_date
else:
date_str = datetime.now().strftime('%Y-%m-%d')
# Encode
encode_result = encode(
message=payload,
reference_photo=ref_data,
carrier_image=carrier_data,
day_phrase=day_phrase,
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=key_password,
date_str=date_str
)
# Store temporarily
file_id = secrets.token_urlsafe(16)
cleanup_temp_files()
TEMP_FILES[file_id] = {
'data': encode_result.stego_image,
'filename': encode_result.filename,
'timestamp': time.time()
}
return redirect(url_for('encode_result', file_id=file_id))
except CapacityError as e:
flash(str(e), 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
except StegasooError as e:
flash(str(e), 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
except Exception as e:
flash(f'Error: {e}', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
@app.route('/encode/result/<file_id>')
def encode_result(file_id):
if file_id not in TEMP_FILES:
flash('File expired or not found. Please encode again.', 'error')
return redirect(url_for('encode_page'))
file_info = TEMP_FILES[file_id]
# Generate thumbnail
thumbnail_data = generate_thumbnail(file_info['data'])
thumbnail_id = None
if thumbnail_data:
thumbnail_id = f"{file_id}_thumb"
THUMBNAIL_FILES[thumbnail_id] = thumbnail_data
return render_template('encode_result.html',
file_id=file_id,
filename=file_info['filename'],
thumbnail_url=url_for('encode_thumbnail', thumb_id=thumbnail_id) if thumbnail_id else None
)
@app.route('/encode/thumbnail/<thumb_id>')
def encode_thumbnail(thumb_id):
"""Serve thumbnail image."""
if thumb_id not in THUMBNAIL_FILES:
return "Thumbnail not found", 404
return send_file(
io.BytesIO(THUMBNAIL_FILES[thumb_id]),
mimetype='image/jpeg',
as_attachment=False
)
@app.route('/encode/download/<file_id>')
def encode_download(file_id):
if file_id not in TEMP_FILES:
flash('File expired or not found.', 'error')
return redirect(url_for('encode_page'))
file_info = TEMP_FILES[file_id]
return send_file(
io.BytesIO(file_info['data']),
mimetype='image/png',
as_attachment=True,
download_name=file_info['filename']
)
@app.route('/encode/file/<file_id>')
def encode_file_route(file_id):
"""Serve file for Web Share API."""
if file_id not in TEMP_FILES:
return "Not found", 404
file_info = TEMP_FILES[file_id]
return send_file(
io.BytesIO(file_info['data']),
mimetype='image/png',
as_attachment=False,
download_name=file_info['filename']
)
@app.route('/encode/cleanup/<file_id>', methods=['POST'])
def encode_cleanup(file_id):
"""Manually cleanup a file after sharing."""
TEMP_FILES.pop(file_id, None)
# Also cleanup thumbnail if exists
thumb_id = f"{file_id}_thumb"
THUMBNAIL_FILES.pop(thumb_id, None)
return jsonify({'status': 'ok'})
@app.route('/decode', methods=['GET', 'POST'])
def decode_page():
if request.method == 'POST':
try:
# Get files
ref_photo = request.files.get('reference_photo')
stego_image = request.files.get('stego_image')
rsa_key_file = request.files.get('rsa_key')
if not ref_photo or not stego_image:
flash('Both reference photo and stego image are required', 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Get form data
day_phrase = request.form.get('day_phrase', '')
pin = request.form.get('pin', '').strip()
rsa_password = request.form.get('rsa_password', '')
# Get encoding date from form (detected from filename in JS)
stego_date = request.form.get('stego_date', '').strip()
if not day_phrase:
flash('Day phrase is required', 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Read files
ref_data = ref_photo.read()
stego_data = stego_image.read()
# Handle RSA key - can come from .pem file or QR code image
rsa_key_data = None
rsa_key_qr = request.files.get('rsa_key_qr')
rsa_key_from_qr = False # Track source for password handling
if rsa_key_file and rsa_key_file.filename:
# RSA key from .pem file
rsa_key_data = rsa_key_file.read()
elif rsa_key_qr and rsa_key_qr.filename and HAS_QRCODE_READ:
# RSA key from QR code image
qr_image_data = rsa_key_qr.read()
key_pem = extract_key_from_qr(qr_image_data)
if key_pem:
rsa_key_data = key_pem.encode('utf-8')
rsa_key_from_qr = True # QR keys are never password-protected
else:
flash('Could not extract RSA key from QR code image. Make sure the image contains a valid QR code.', 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Validate security factors
result = validate_security_factors(pin, rsa_key_data)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Validate PIN if provided
if pin:
result = validate_pin(pin)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Determine key password - QR code keys are never password-protected
key_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None)
# Validate RSA key if provided
if rsa_key_data:
result = validate_rsa_key(rsa_key_data, key_password)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
with open('/tmp/debug_stego.png', 'wb') as f:
f.write(stego_data)
with open('/tmp/debug_ref.png', 'wb') as f:
f.write(ref_data)
with open('/tmp/debug_params.txt', 'w') as f:
f.write(f"day_phrase: {day_phrase}\n")
f.write(f"pin: {pin}\n")
f.write(f"date_str: {stego_date}\n")
f.write(f"rsa_key: {len(rsa_key_data) if rsa_key_data else None}\n")
print(f"DEBUG: Saved inputs to /tmp/debug_*")
# Decode
decode_result = decode(
stego_image=stego_data,
reference_photo=ref_data,
day_phrase=day_phrase,
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=key_password,
date_str=stego_date if stego_date else None
)
if decode_result.is_file:
# File content - store temporarily for download
file_id = secrets.token_urlsafe(16)
cleanup_temp_files()
filename = decode_result.filename or 'decoded_file'
TEMP_FILES[file_id] = {
'data': decode_result.file_data,
'filename': filename,
'mime_type': decode_result.mime_type,
'timestamp': time.time()
}
return render_template('decode.html',
decoded_file=True,
file_id=file_id,
filename=filename,
file_size=format_size(len(decode_result.file_data)),
mime_type=decode_result.mime_type
)
else:
# Text content
return render_template('decode.html', decoded_message=decode_result.message)
except DecryptionError:
flash('Decryption failed. Check your phrase, PIN, RSA key, and reference photo.', 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
except StegasooError as e:
flash(str(e), 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
except Exception as e:
flash(f'Error: {e}', 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
@app.route('/decode/download/<file_id>')
def decode_download(file_id):
"""Download decoded file."""
if file_id not in TEMP_FILES:
flash('File expired or not found.', 'error')
return redirect(url_for('decode_page'))
file_info = TEMP_FILES[file_id]
mime_type = file_info.get('mime_type', 'application/octet-stream')
return send_file(
io.BytesIO(file_info['data']),
mimetype=mime_type,
as_attachment=True,
download_name=file_info['filename']
)
@app.route('/about')
def about():
return render_template('about.html',
has_argon2=has_argon2(),
has_qrcode_read=HAS_QRCODE_READ,
max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024
)
# ============================================================================
# MAIN
# ============================================================================
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)

View File

@@ -241,13 +241,13 @@
<i class="bi bi-soundwave text-info fs-4 me-2"></i> <i class="bi bi-soundwave text-info fs-4 me-2"></i>
<strong>DCT Mode</strong> <strong>DCT Mode</strong>
{% if has_dct %} {% if has_dct %}
<span class="badge bg-info ms-auto">Stealth</span> <span class="badge bg-warning text-dark ms-auto">Experimental</span>
{% else %} {% else %}
<span class="badge bg-secondary ms-auto">Unavailable</span> <span class="badge bg-secondary ms-auto">Unavailable</span>
{% endif %} {% endif %}
</div> </div>
<ul class="small text-muted mb-0 ps-3"> <ul class="small text-muted mb-0 ps-3">
<li>Grayscale output (PNG/JPEG)</li> <li>Color or grayscale output</li>
<li>Lower capacity (~75 KB/MP)</li> <li>Lower capacity (~75 KB/MP)</li>
<li>Better detection resistance</li> <li>Better detection resistance</li>
</ul> </ul>
@@ -266,47 +266,98 @@
<div class="form-text mt-2" id="modeHint"> <div class="form-text mt-2" id="modeHint">
<i class="bi bi-lightbulb me-1"></i> <i class="bi bi-lightbulb me-1"></i>
<strong>LSB</strong> is best for most uses. <strong>LSB</strong> is best for most uses.
<strong>DCT</strong> provides better stealth but smaller capacity and grayscale output. <strong>DCT</strong> provides better stealth but lower capacity.
</div> </div>
</div> </div>
<!-- DCT Output Format (shown only when DCT selected) --> <!-- DCT Options Panel (shown only when DCT selected) -->
<div class="mb-3 d-none" id="dctOutputFormatGroup"> <div class="d-none" id="dctOptionsPanel">
<label class="form-label">
<i class="bi bi-file-image me-1"></i> DCT Output Format
</label>
<div class="row g-2"> <hr class="my-3">
<div class="col-6">
<div class="form-check card p-2 text-center" id="dctPngCard"> <div class="alert alert-warning small mb-3">
<input class="form-check-input mx-auto" type="radio" name="dct_output_format" id="dctFormatPng" value="png" checked> <i class="bi bi-flask me-1"></i>
<label class="form-check-label w-100" for="dctFormatPng"> <strong>Experimental Feature:</strong> DCT embedding is still being refined.
<i class="bi bi-file-earmark-image text-success fs-5 d-block"></i> Color mode preserves original colors but extraction uses Y channel only.
<strong>PNG</strong> </div>
<div class="small text-muted">Lossless, larger</div>
</label> <!-- DCT Color Mode (NEW in v3.0.1) -->
<div class="mb-3">
<label class="form-label">
<i class="bi bi-palette me-1"></i> DCT Color Mode
<span class="badge bg-success ms-1">v3.0.1</span>
</label>
<div class="row g-2">
<div class="col-6">
<div class="form-check card p-2 text-center border-success border-2" id="dctColorCard">
<input class="form-check-input mx-auto" type="radio" name="dct_color_mode" id="dctColorColor" value="color" checked>
<label class="form-check-label w-100" for="dctColorColor">
<i class="bi bi-palette-fill text-success fs-5 d-block"></i>
<strong>Color</strong>
<div class="small text-muted">Preserve colors</div>
</label>
</div>
</div>
<div class="col-6">
<div class="form-check card p-2 text-center" id="dctGrayscaleCard">
<input class="form-check-input mx-auto" type="radio" name="dct_color_mode" id="dctColorGrayscale" value="grayscale">
<label class="form-check-label w-100" for="dctColorGrayscale">
<i class="bi bi-circle-half text-secondary fs-5 d-block"></i>
<strong>Grayscale</strong>
<div class="small text-muted">Traditional DCT</div>
</label>
</div>
</div> </div>
</div> </div>
<div class="col-6">
<div class="form-check card p-2 text-center" id="dctJpegCard"> <div class="form-text mt-2">
<input class="form-check-input mx-auto" type="radio" name="dct_output_format" id="dctFormatJpeg" value="jpeg"> <i class="bi bi-info-circle me-1"></i>
<label class="form-check-label w-100" for="dctFormatJpeg"> <strong>Color</strong> preserves original image colors (recommended).
<i class="bi bi-file-earmark-richtext text-warning fs-5 d-block"></i> <strong>Grayscale</strong> converts to B&W (traditional DCT steganography).
<strong>JPEG</strong>
<div class="small text-muted">Smaller, natural</div>
</label>
</div>
</div> </div>
</div> </div>
<div class="form-text mt-2"> <!-- DCT Output Format -->
<i class="bi bi-info-circle me-1"></i> <div class="mb-3">
<strong>PNG</strong> is 100% reliable. <strong>JPEG</strong> produces smaller, more natural-looking files but uses lossy compression (Q=95). <label class="form-label">
<i class="bi bi-file-image me-1"></i> DCT Output Format
</label>
<div class="row g-2">
<div class="col-6">
<div class="form-check card p-2 text-center border-primary border-2" id="dctPngCard">
<input class="form-check-input mx-auto" type="radio" name="dct_output_format" id="dctFormatPng" value="png" checked>
<label class="form-check-label w-100" for="dctFormatPng">
<i class="bi bi-file-earmark-image text-primary fs-5 d-block"></i>
<strong>PNG</strong>
<div class="small text-muted">Lossless, larger</div>
</label>
</div>
</div>
<div class="col-6">
<div class="form-check card p-2 text-center" id="dctJpegCard">
<input class="form-check-input mx-auto" type="radio" name="dct_output_format" id="dctFormatJpeg" value="jpeg">
<label class="form-check-label w-100" for="dctFormatJpeg">
<i class="bi bi-file-earmark-richtext text-warning fs-5 d-block"></i>
<strong>JPEG</strong>
<div class="small text-muted">Smaller, natural</div>
</label>
</div>
</div>
</div>
<div class="form-text mt-2">
<i class="bi bi-info-circle me-1"></i>
<strong>PNG</strong> is 100% reliable. <strong>JPEG</strong> produces smaller, more natural-looking files but uses lossy compression (Q=95).
</div>
</div> </div>
</div> </div>
<!-- Capacity Comparison (populated by JS) --> <!-- Capacity Comparison (populated by JS) -->
<div class="d-none" id="modeCapacityComparison"> <div class="d-none" id="modeCapacityComparison">
<hr class="my-3">
<div class="alert alert-secondary small mb-0"> <div class="alert alert-secondary small mb-0">
<div class="row text-center"> <div class="row text-center">
<div class="col-6 border-end"> <div class="col-6 border-end">
@@ -353,7 +404,7 @@
<div class="alert alert-secondary mt-4 small"> <div class="alert alert-secondary mt-4 small">
<i class="bi bi-info-circle me-1"></i> <i class="bi bi-info-circle me-1"></i>
<strong>Limits:</strong> <strong>Limits:</strong>
Carrier image max ~24 megapixels (6000×4000). Carrier image max ~24 megapixels (6000x4000).
Files max 30MB upload. Files max 30MB upload.
Payload max {{ max_payload_kb }} KB. Payload max {{ max_payload_kb }} KB.
</div> </div>
@@ -470,8 +521,9 @@ document.getElementById('encodeForm').addEventListener('submit', function(e) {
let modeLabel = selectedMode.toUpperCase(); let modeLabel = selectedMode.toUpperCase();
if (selectedMode === 'dct') { if (selectedMode === 'dct') {
const colorMode = document.querySelector('input[name="dct_color_mode"]:checked')?.value || 'color';
const outputFormat = document.querySelector('input[name="dct_output_format"]:checked')?.value || 'png'; const outputFormat = document.querySelector('input[name="dct_output_format"]:checked')?.value || 'png';
modeLabel += ` ${outputFormat.toUpperCase()}`; modeLabel += ` (${colorMode}, ${outputFormat.toUpperCase()})`;
} }
btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>Encoding (${modeLabel})...`; btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>Encoding (${modeLabel})...`;
@@ -535,7 +587,7 @@ async function fetchCapacityComparison(file) {
function updateCapacityDisplay(data) { function updateCapacityDisplay(data) {
// Update top panel // Update top panel
carrierDimensions.textContent = `${data.width} × ${data.height}`; carrierDimensions.textContent = `${data.width} x ${data.height}`;
lsbCapacityBadge.textContent = `LSB: ${data.lsb.capacity_kb} KB`; lsbCapacityBadge.textContent = `LSB: ${data.lsb.capacity_kb} KB`;
if (data.dct.available) { if (data.dct.available) {
@@ -573,19 +625,27 @@ if (carrierInput) {
} }
// ============================================================================ // ============================================================================
// Mode card highlighting & DCT output format visibility // Mode card highlighting & DCT options visibility
// ============================================================================ // ============================================================================
const lsbModeCard = document.getElementById('lsbModeCard'); const lsbModeCard = document.getElementById('lsbModeCard');
const dctModeCard = document.getElementById('dctModeCard'); const dctModeCard = document.getElementById('dctModeCard');
const modeLsb = document.getElementById('modeLsb'); const modeLsb = document.getElementById('modeLsb');
const modeDct = document.getElementById('modeDct'); const modeDct = document.getElementById('modeDct');
const dctOutputFormatGroup = document.getElementById('dctOutputFormatGroup'); const dctOptionsPanel = document.getElementById('dctOptionsPanel');
// DCT format cards
const dctPngCard = document.getElementById('dctPngCard'); const dctPngCard = document.getElementById('dctPngCard');
const dctJpegCard = document.getElementById('dctJpegCard'); const dctJpegCard = document.getElementById('dctJpegCard');
const dctFormatPng = document.getElementById('dctFormatPng'); const dctFormatPng = document.getElementById('dctFormatPng');
const dctFormatJpeg = document.getElementById('dctFormatJpeg'); const dctFormatJpeg = document.getElementById('dctFormatJpeg');
// DCT color mode cards
const dctColorCard = document.getElementById('dctColorCard');
const dctGrayscaleCard = document.getElementById('dctGrayscaleCard');
const dctColorColor = document.getElementById('dctColorColor');
const dctColorGrayscale = document.getElementById('dctColorGrayscale');
function updateModeCardHighlight() { function updateModeCardHighlight() {
// Mode cards // Mode cards
lsbModeCard.classList.toggle('border-primary', modeLsb.checked); lsbModeCard.classList.toggle('border-primary', modeLsb.checked);
@@ -593,28 +653,40 @@ function updateModeCardHighlight() {
dctModeCard.classList.toggle('border-info', modeDct.checked); dctModeCard.classList.toggle('border-info', modeDct.checked);
dctModeCard.classList.toggle('border-2', modeDct.checked); dctModeCard.classList.toggle('border-2', modeDct.checked);
// Show/hide DCT output format selector // Show/hide DCT options panel
if (dctOutputFormatGroup) { if (dctOptionsPanel) {
dctOutputFormatGroup.classList.toggle('d-none', !modeDct.checked); dctOptionsPanel.classList.toggle('d-none', !modeDct.checked);
} }
} }
function updateDctFormatCardHighlight() { function updateDctFormatCardHighlight() {
if (dctPngCard && dctJpegCard) { if (dctPngCard && dctJpegCard) {
dctPngCard.classList.toggle('border-success', dctFormatPng.checked); dctPngCard.classList.toggle('border-primary', dctFormatPng.checked);
dctPngCard.classList.toggle('border-2', dctFormatPng.checked); dctPngCard.classList.toggle('border-2', dctFormatPng.checked);
dctJpegCard.classList.toggle('border-warning', dctFormatJpeg.checked); dctJpegCard.classList.toggle('border-warning', dctFormatJpeg.checked);
dctJpegCard.classList.toggle('border-2', dctFormatJpeg.checked); dctJpegCard.classList.toggle('border-2', dctFormatJpeg.checked);
} }
} }
function updateDctColorCardHighlight() {
if (dctColorCard && dctGrayscaleCard) {
dctColorCard.classList.toggle('border-success', dctColorColor.checked);
dctColorCard.classList.toggle('border-2', dctColorColor.checked);
dctGrayscaleCard.classList.toggle('border-secondary', dctColorGrayscale.checked);
dctGrayscaleCard.classList.toggle('border-2', dctColorGrayscale.checked);
}
}
modeLsb.addEventListener('change', updateModeCardHighlight); modeLsb.addEventListener('change', updateModeCardHighlight);
modeDct.addEventListener('change', updateModeCardHighlight); modeDct.addEventListener('change', updateModeCardHighlight);
dctFormatPng?.addEventListener('change', updateDctFormatCardHighlight); dctFormatPng?.addEventListener('change', updateDctFormatCardHighlight);
dctFormatJpeg?.addEventListener('change', updateDctFormatCardHighlight); dctFormatJpeg?.addEventListener('change', updateDctFormatCardHighlight);
dctColorColor?.addEventListener('change', updateDctColorCardHighlight);
dctColorGrayscale?.addEventListener('change', updateDctColorCardHighlight);
updateModeCardHighlight(); // Initial state updateModeCardHighlight(); // Initial state
updateDctFormatCardHighlight(); // Initial state updateDctFormatCardHighlight(); // Initial state
updateDctColorCardHighlight(); // Initial state
// Advanced options chevron rotation // Advanced options chevron rotation
document.getElementById('advancedOptions').addEventListener('show.bs.collapse', function() { document.getElementById('advancedOptions').addEventListener('show.bs.collapse', function() {

View File

@@ -34,31 +34,60 @@
<code class="fs-5">{{ filename }}</code> <code class="fs-5">{{ filename }}</code>
</div> </div>
<!-- Mode and format badges (v3.0) --> <!-- Mode and format badges (v3.0 / v3.0.1) -->
<div class="mb-4"> <div class="mb-4">
{% if embed_mode == 'dct' %} {% if embed_mode == 'dct' %}
<span class="badge bg-info fs-6"> <span class="badge bg-info fs-6">
<i class="bi bi-soundwave me-1"></i>DCT Mode <i class="bi bi-soundwave me-1"></i>DCT Mode
</span> </span>
<!-- Color mode badge (v3.0.1) -->
{% if color_mode == 'color' %}
<span class="badge bg-success fs-6 ms-1">
<i class="bi bi-palette-fill me-1"></i>Color
</span>
{% else %}
<span class="badge bg-secondary fs-6 ms-1">
<i class="bi bi-circle-half me-1"></i>Grayscale
</span>
{% endif %}
<!-- Output format badge -->
{% if output_format == 'jpeg' %} {% if output_format == 'jpeg' %}
<span class="badge bg-warning text-dark fs-6 ms-1"> <span class="badge bg-warning text-dark fs-6 ms-1">
<i class="bi bi-file-earmark-richtext me-1"></i>JPEG <i class="bi bi-file-earmark-richtext me-1"></i>JPEG
</span> </span>
<div class="small text-muted mt-1">Grayscale JPEG, frequency domain embedding (Q=95)</div> <div class="small text-muted mt-2">
{% if color_mode == 'color' %}
Color JPEG, frequency domain embedding (Q=95)
{% else %}
Grayscale JPEG, frequency domain embedding (Q=95)
{% endif %}
</div>
{% else %} {% else %}
<span class="badge bg-success fs-6 ms-1"> <span class="badge bg-primary fs-6 ms-1">
<i class="bi bi-file-earmark-image me-1"></i>PNG <i class="bi bi-file-earmark-image me-1"></i>PNG
</span> </span>
<div class="small text-muted mt-1">Grayscale PNG, frequency domain embedding (lossless)</div> <div class="small text-muted mt-2">
{% if color_mode == 'color' %}
Color PNG, frequency domain embedding (lossless)
{% else %}
Grayscale PNG, frequency domain embedding (lossless)
{% endif %}
</div>
{% endif %} {% endif %}
{% else %} {% else %}
<span class="badge bg-primary fs-6"> <span class="badge bg-primary fs-6">
<i class="bi bi-grid-3x3-gap me-1"></i>LSB Mode <i class="bi bi-grid-3x3-gap me-1"></i>LSB Mode
</span> </span>
<span class="badge bg-success fs-6 ms-1"> <span class="badge bg-success fs-6 ms-1">
<i class="bi bi-palette-fill me-1"></i>Full Color
</span>
<span class="badge bg-primary fs-6 ms-1">
<i class="bi bi-file-earmark-image me-1"></i>PNG <i class="bi bi-file-earmark-image me-1"></i>PNG
</span> </span>
<div class="small text-muted mt-1">Full color PNG, spatial LSB embedding</div> <div class="small text-muted mt-2">Full color PNG, spatial LSB embedding</div>
{% endif %} {% endif %}
</div> </div>
@@ -88,6 +117,9 @@
{% endif %} {% endif %}
{% if embed_mode == 'dct' %} {% if embed_mode == 'dct' %}
<li>Recipient needs <strong>DCT mode</strong> or <strong>Auto</strong> detection to decode</li> <li>Recipient needs <strong>DCT mode</strong> or <strong>Auto</strong> detection to decode</li>
{% if color_mode == 'color' %}
<li><span class="badge bg-success">v3.0.1</span> Color preserved - extraction works on both color and grayscale</li>
{% endif %}
{% endif %} {% endif %}
</ul> </ul>
</div> </div>

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "stegasoo" name = "stegasoo"
version = "2.2.1" version = "3.0.2"
description = "Secure steganography with hybrid photo + passphrase + PIN authentication" description = "Secure steganography with hybrid photo + passphrase + PIN authentication"
readme = "README.md" readme = "README.md"
license = "MIT" license = "MIT"
@@ -43,6 +43,11 @@ dependencies = [
] ]
[project.optional-dependencies] [project.optional-dependencies]
# DCT steganography support (v3.0+)
dct = [
"scipy>=1.10.0",
"jpegio>=0.2.0",
]
cli = [ cli = [
"click>=8.0.0", "click>=8.0.0",
"qrcode>=7.30" "qrcode>=7.30"
@@ -55,6 +60,9 @@ web = [
"gunicorn>=21.0.0", "gunicorn>=21.0.0",
"qrcode>=7.3.0", "qrcode>=7.3.0",
"pyzbar>=0.1.9", "pyzbar>=0.1.9",
# Include DCT support for web UI
"scipy>=1.10.0",
"jpegio>=0.2.0",
] ]
api = [ api = [
"fastapi>=0.100.0", "fastapi>=0.100.0",
@@ -62,9 +70,12 @@ api = [
"python-multipart>=0.0.6", "python-multipart>=0.0.6",
"qrcode>=7.30", "qrcode>=7.30",
"pyzbar>=0.1.9", "pyzbar>=0.1.9",
# Include DCT support for API
"scipy>=1.10.0",
"jpegio>=0.2.0",
] ]
all = [ all = [
"stegasoo[cli,web,api]", "stegasoo[cli,web,api,dct,compression]",
] ]
dev = [ dev = [
"stegasoo[all]", "stegasoo[all]",

144
src/stegasoo/Dockerfile Normal file
View File

@@ -0,0 +1,144 @@
# Stegasoo Docker Image
# Multi-stage build for smaller image size
# 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
# 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: libjpeg-dev is required for jpegio compilation
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libc-dev \
libffi-dev \
libzbar0 \
libjpeg-dev \
&& rm -rf /var/lib/apt/lists/*
# ============================================================================
# Builder stage - install Python packages
# ============================================================================
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]"
# ============================================================================
# Production stage - Web UI
# ============================================================================
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 src/ src/
COPY data/ data/
COPY frontends/web/ frontends/web/
# Create upload directory
RUN mkdir -p /tmp/stego_uploads
# Create non-root user
RUN useradd -m -u 1000 stego && chown -R stego:stego /app /tmp/stego_uploads
USER stego
# Set Python path
ENV PYTHONPATH=/app/src
# Expose port
EXPOSE 5000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/')" || exit 1
# Run with gunicorn
WORKDIR /app/frontends/web
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "--threads", "4", "--timeout", "60", "app:app"]
# ============================================================================
# API stage - REST API
# ============================================================================
FROM base as api
WORKDIR /app
# Install API extras (includes DCT dependencies)
COPY pyproject.toml README.md ./
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
RUN useradd -m -u 1000 stego && chown -R stego:stego /app
USER stego
# Set Python path
ENV PYTHONPATH=/app/src
# Expose port
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/')" || exit 1
# Run with uvicorn
WORKDIR /app/frontends/api
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# ============================================================================
# CLI stage - Command line tool
# ============================================================================
FROM base as cli
WORKDIR /app
# Install CLI extras
COPY pyproject.toml README.md ./
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
RUN useradd -m -u 1000 stego && chown -R stego:stego /app
USER stego
# Set Python path
ENV PYTHONPATH=/app/src
# Default to help
WORKDIR /app/frontends/cli
ENTRYPOINT ["python", "main.py"]
CMD ["--help"]

View File

@@ -315,6 +315,7 @@ def encode(
output_format = None, # Optional[str] output_format = None, # Optional[str]
embed_mode: str = EMBED_MODE_LSB, embed_mode: str = EMBED_MODE_LSB,
dct_output_format: str = "png", # NEW in v3.0.1: 'png' or 'jpeg' dct_output_format: str = "png", # NEW in v3.0.1: 'png' or 'jpeg'
dct_color_mode: str = "grayscale", # NEW in v3.0.1: 'grayscale' or 'color'
) -> EncodeResult: ) -> EncodeResult:
""" """
Encode a secret message or file into an image. Encode a secret message or file into an image.
@@ -334,6 +335,7 @@ def encode(
output_format: Force output format ('PNG', 'BMP') - LSB mode only output_format: Force output format ('PNG', 'BMP') - LSB mode only
embed_mode: Embedding mode - 'lsb' (default) or 'dct' (v3.0+) embed_mode: Embedding mode - 'lsb' (default) or 'dct' (v3.0+)
dct_output_format: For DCT mode - 'png' (lossless) or 'jpeg' (smaller) dct_output_format: For DCT mode - 'png' (lossless) or 'jpeg' (smaller)
dct_color_mode: For DCT mode - 'grayscale' (default) or 'color' (preserves colors)
Returns: Returns:
EncodeResult with stego image and metadata EncodeResult with stego image and metadata
@@ -349,16 +351,18 @@ def encode(
# Default LSB mode # Default LSB mode
>>> result = encode(message="Secret", ...) >>> result = encode(message="Secret", ...)
# DCT mode with PNG output (lossless) # DCT mode with grayscale PNG output (default)
>>> result = encode(message="Secret", ..., embed_mode='dct') >>> result = encode(message="Secret", ..., embed_mode='dct')
# DCT mode with JPEG output (smaller, natural) # DCT mode with color JPEG output
>>> result = encode(message="Secret", ..., embed_mode='dct', dct_output_format='jpeg') >>> result = encode(message="Secret", ..., embed_mode='dct',
... dct_output_format='jpeg', dct_color_mode='color')
""" """
# Debug logging # Debug logging
debug.print(f"encode called: message type={type(message).__name__}, " debug.print(f"encode called: message type={type(message).__name__}, "
f"day_phrase='{day_phrase[:20]}...', pin_length={len(pin)}, " f"day_phrase='{day_phrase[:20]}...', pin_length={len(pin)}, "
f"embed_mode={embed_mode}, dct_output_format={dct_output_format}") f"embed_mode={embed_mode}, dct_output_format={dct_output_format}, "
f"dct_color_mode={dct_color_mode}")
# Validate embed_mode # Validate embed_mode
if embed_mode not in (EMBED_MODE_LSB, EMBED_MODE_DCT): if embed_mode not in (EMBED_MODE_LSB, EMBED_MODE_DCT):
@@ -375,6 +379,11 @@ def encode(
debug.print(f"Invalid dct_output_format '{dct_output_format}', defaulting to 'png'") debug.print(f"Invalid dct_output_format '{dct_output_format}', defaulting to 'png'")
dct_output_format = 'png' dct_output_format = 'png'
# Validate dct_color_mode (v3.0.1)
if dct_color_mode not in ('grayscale', 'color'):
debug.print(f"Invalid dct_color_mode '{dct_color_mode}', defaulting to 'grayscale'")
dct_color_mode = 'grayscale'
# Validate inputs # Validate inputs
require_valid_payload(message) require_valid_payload(message)
require_valid_image(carrier_image, "Carrier image") require_valid_image(carrier_image, "Carrier image")
@@ -407,7 +416,7 @@ def encode(
debug.data(pixel_key, "Pixel key") debug.data(pixel_key, "Pixel key")
# Embed in image (returns extension too) # Embed in image (returns extension too)
# CRITICAL: Pass dct_output_format to embed_in_image # CRITICAL: Pass dct_output_format and dct_color_mode to embed_in_image
stego_data, stats, extension = embed_in_image( stego_data, stats, extension = embed_in_image(
encrypted, encrypted,
carrier_image, carrier_image,
@@ -415,6 +424,7 @@ def encode(
output_format=output_format, output_format=output_format,
embed_mode=embed_mode, embed_mode=embed_mode,
dct_output_format=dct_output_format, # NEW in v3.0.1 dct_output_format=dct_output_format, # NEW in v3.0.1
dct_color_mode=dct_color_mode, # NEW in v3.0.1
) )
# Generate filename with correct extension # Generate filename with correct extension
@@ -468,6 +478,7 @@ def encode_file(
filename_override: Optional[str] = None, filename_override: Optional[str] = None,
embed_mode: str = EMBED_MODE_LSB, embed_mode: str = EMBED_MODE_LSB,
dct_output_format: str = "png", # NEW in v3.0.1 dct_output_format: str = "png", # NEW in v3.0.1
dct_color_mode: str = "grayscale", # NEW in v3.0.1
) -> EncodeResult: ) -> EncodeResult:
""" """
Encode a file into an image. Encode a file into an image.
@@ -487,12 +498,13 @@ def encode_file(
filename_override: Override the stored filename filename_override: Override the stored filename
embed_mode: 'lsb' (default) or 'dct' (v3.0+) embed_mode: 'lsb' (default) or 'dct' (v3.0+)
dct_output_format: For DCT mode - 'png' or 'jpeg' (v3.0.1+) dct_output_format: For DCT mode - 'png' or 'jpeg' (v3.0.1+)
dct_color_mode: For DCT mode - 'grayscale' or 'color' (v3.0.1+)
Returns: Returns:
EncodeResult with stego image and metadata EncodeResult with stego image and metadata
""" """
debug.print(f"encode_file called: filepath={filepath}, embed_mode={embed_mode}, " debug.print(f"encode_file called: filepath={filepath}, embed_mode={embed_mode}, "
f"dct_output_format={dct_output_format}") f"dct_output_format={dct_output_format}, dct_color_mode={dct_color_mode}")
payload = FilePayload.from_file(str(filepath), filename_override) payload = FilePayload.from_file(str(filepath), filename_override)
return encode( return encode(
@@ -507,6 +519,7 @@ def encode_file(
output_format=output_format, output_format=output_format,
embed_mode=embed_mode, embed_mode=embed_mode,
dct_output_format=dct_output_format, # NEW in v3.0.1 dct_output_format=dct_output_format, # NEW in v3.0.1
dct_color_mode=dct_color_mode, # NEW in v3.0.1
) )
@@ -528,6 +541,7 @@ def encode_bytes(
mime_type: Optional[str] = None, mime_type: Optional[str] = None,
embed_mode: str = EMBED_MODE_LSB, embed_mode: str = EMBED_MODE_LSB,
dct_output_format: str = "png", # NEW in v3.0.1 dct_output_format: str = "png", # NEW in v3.0.1
dct_color_mode: str = "grayscale", # NEW in v3.0.1
) -> EncodeResult: ) -> EncodeResult:
""" """
Encode raw bytes with a filename into an image. Encode raw bytes with a filename into an image.
@@ -548,12 +562,14 @@ def encode_bytes(
mime_type: MIME type of the data mime_type: MIME type of the data
embed_mode: 'lsb' (default) or 'dct' (v3.0+) embed_mode: 'lsb' (default) or 'dct' (v3.0+)
dct_output_format: For DCT mode - 'png' or 'jpeg' (v3.0.1+) dct_output_format: For DCT mode - 'png' or 'jpeg' (v3.0.1+)
dct_color_mode: For DCT mode - 'grayscale' or 'color' (v3.0.1+)
Returns: Returns:
EncodeResult with stego image and metadata EncodeResult with stego image and metadata
""" """
debug.print(f"encode_bytes called: filename={filename}, data_size={len(data)}, " debug.print(f"encode_bytes called: filename={filename}, data_size={len(data)}, "
f"embed_mode={embed_mode}, dct_output_format={dct_output_format}") f"embed_mode={embed_mode}, dct_output_format={dct_output_format}, "
f"dct_color_mode={dct_color_mode}")
payload = FilePayload(data=data, filename=filename, mime_type=mime_type) payload = FilePayload(data=data, filename=filename, mime_type=mime_type)
return encode( return encode(
@@ -568,6 +584,7 @@ def encode_bytes(
output_format=output_format, output_format=output_format,
embed_mode=embed_mode, embed_mode=embed_mode,
dct_output_format=dct_output_format, # NEW in v3.0.1 dct_output_format=dct_output_format, # NEW in v3.0.1
dct_color_mode=dct_color_mode, # NEW in v3.0.1
) )

View File

@@ -1,29 +1,32 @@
""" """
DCT Domain Steganography Module (v3.0.1) DCT Domain Steganography Module (v3.0.2)
Embeds data in DCT coefficients of grayscale images. Embeds data in DCT coefficients with two approaches:
Supports PNG (lossless) or JPEG (natural, smaller) output. 1. PNG output: Scipy-based DCT transform (grayscale or color)
2. JPEG output: jpegio-based coefficient manipulation (if available)
This provides an alternative to LSB embedding with different trade-offs: The JPEG approach is the "correct" way to do JPEG steganography because
- More resistant to visual inspection it directly modifies the already-quantized coefficients without re-encoding.
- Survives some image processing
- Lower capacity (~20% of LSB)
- Works in frequency domain
Requires: scipy (for DCT transforms) New in v3.0.2:
- jpegio integration for proper JPEG coefficient embedding
- Falls back to warning if jpegio not available for JPEG output
- Maintains backward compatibility with v3.0.1
Requires: scipy (for PNG mode), optionally jpegio (for JPEG mode)
""" """
import io import io
import struct import struct
import hashlib import hashlib
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional, Literal from typing import Optional, Literal, Tuple
from enum import Enum from enum import Enum
import numpy as np import numpy as np
from PIL import Image from PIL import Image
# Check for scipy availability # Check for scipy availability (for PNG/DCT mode)
try: try:
from scipy.fftpack import dct, idct from scipy.fftpack import dct, idct
HAS_SCIPY = True HAS_SCIPY = True
@@ -32,6 +35,14 @@ except ImportError:
dct = None dct = None
idct = None idct = None
# Check for jpegio availability (for proper JPEG mode)
try:
import jpegio as jio
HAS_JPEGIO = True
except ImportError:
HAS_JPEGIO = False
jio = None
# ============================================================================ # ============================================================================
# CONSTANTS # CONSTANTS
@@ -41,8 +52,6 @@ except ImportError:
BLOCK_SIZE = 8 BLOCK_SIZE = 8
# Coefficients to use for embedding (mid-frequency, zig-zag order positions) # Coefficients to use for embedding (mid-frequency, zig-zag order positions)
# Avoiding DC (0,0) and high-frequency edges
# These positions are relatively stable across JPEG compression
EMBED_POSITIONS = [ EMBED_POSITIONS = [
(0, 1), (1, 0), (2, 0), (1, 1), (0, 2), (0, 3), (1, 2), (2, 1), (3, 0), (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, 0), (3, 1), (2, 2), (1, 3), (0, 4), (0, 5), (1, 4), (2, 3), (3, 2),
@@ -51,25 +60,29 @@ EMBED_POSITIONS = [
] ]
# Use subset of mid-frequency coefficients for better robustness # Use subset of mid-frequency coefficients for better robustness
# Positions 4-20 in zig-zag order (skip very low and very high frequencies)
DEFAULT_EMBED_POSITIONS = EMBED_POSITIONS[4:20] # 16 coefficients per block DEFAULT_EMBED_POSITIONS = EMBED_POSITIONS[4:20] # 16 coefficients per block
# Quantization step for embedding (larger = more robust, more visible) # Quantization step for QIM embedding (larger = more robust, more visible)
QUANT_STEP = 25 QUANT_STEP = 25
# Magic bytes for DCT stego identification # Magic bytes for DCT stego identification
DCT_MAGIC = b'DCTS' DCT_MAGIC = b'DCTS'
# Header: magic(4) + version(1) + flags(1) + length(4) = 10 bytes # Header size: magic(4) + version(1) + flags(1) + length(4) = 10 bytes
HEADER_SIZE = 10 HEADER_SIZE = 10
# Output format options # Output format options
OUTPUT_FORMAT_PNG = 'png' OUTPUT_FORMAT_PNG = 'png'
OUTPUT_FORMAT_JPEG = 'jpeg' OUTPUT_FORMAT_JPEG = 'jpeg'
# JPEG quality for output (high to preserve coefficients) # JPEG output quality (only for fallback mode, not jpegio)
JPEG_OUTPUT_QUALITY = 95 JPEG_OUTPUT_QUALITY = 95
# jpegio constants for JPEG coefficient embedding
JPEGIO_MAGIC = b'JPGS'
JPEGIO_MIN_COEF_MAGNITUDE = 2
JPEGIO_EMBED_CHANNEL = 0 # Y channel
# ============================================================================ # ============================================================================
# DATA CLASSES # DATA CLASSES
@@ -91,7 +104,9 @@ class DCTEmbedStats:
usage_percent: float usage_percent: float
image_width: int image_width: int
image_height: int image_height: int
output_format: str # 'png' or 'jpeg' output_format: str
jpeg_native: bool = False # True if used jpegio for proper JPEG embedding
color_mode: str = 'grayscale' # 'color' or 'grayscale' (v3.0.1+)
@dataclass @dataclass
@@ -105,11 +120,11 @@ class DCTCapacityInfo:
bits_per_block: int bits_per_block: int
total_capacity_bits: int total_capacity_bits: int
total_capacity_bytes: int total_capacity_bytes: int
usable_capacity_bytes: int # After header overhead usable_capacity_bytes: int
# ============================================================================ # ============================================================================
# HELPER FUNCTIONS # AVAILABILITY CHECKS
# ============================================================================ # ============================================================================
def _check_scipy(): def _check_scipy():
@@ -121,6 +136,20 @@ def _check_scipy():
) )
def has_dct_support() -> bool:
"""Check if DCT steganography is available (scipy installed)."""
return HAS_SCIPY
def has_jpegio_support() -> bool:
"""Check if jpegio is available for proper JPEG coefficient embedding."""
return HAS_JPEGIO
# ============================================================================
# SCIPY DCT HELPERS (for PNG output)
# ============================================================================
def _dct2(block: np.ndarray) -> np.ndarray: def _dct2(block: np.ndarray) -> np.ndarray:
"""Apply 2D DCT to a block.""" """Apply 2D DCT to a block."""
return dct(dct(block.T, norm='ortho').T, norm='ortho') return dct(dct(block.T, norm='ortho').T, norm='ortho')
@@ -138,7 +167,7 @@ def _to_grayscale(image_data: bytes) -> np.ndarray:
return np.array(gray, dtype=np.float64) return np.array(gray, dtype=np.float64)
def _pad_to_blocks(image: np.ndarray) -> tuple[np.ndarray, tuple[int, int]]: def _pad_to_blocks(image: np.ndarray) -> Tuple[np.ndarray, Tuple[int, int]]:
"""Pad image dimensions to be divisible by block size.""" """Pad image dimensions to be divisible by block size."""
h, w = image.shape h, w = image.shape
new_h = ((h + BLOCK_SIZE - 1) // BLOCK_SIZE) * BLOCK_SIZE new_h = ((h + BLOCK_SIZE - 1) // BLOCK_SIZE) * BLOCK_SIZE
@@ -150,7 +179,6 @@ def _pad_to_blocks(image: np.ndarray) -> tuple[np.ndarray, tuple[int, int]]:
padded = np.zeros((new_h, new_w), dtype=image.dtype) padded = np.zeros((new_h, new_w), dtype=image.dtype)
padded[:h, :w] = image padded[:h, :w] = image
# Mirror padding for smoother edges
if new_h > h: if new_h > h:
padded[h:, :w] = image[h-(new_h-h):h, :w][::-1, :] padded[h:, :w] = image[h-(new_h-h):h, :w][::-1, :]
if new_w > w: if new_w > w:
@@ -161,82 +189,125 @@ def _pad_to_blocks(image: np.ndarray) -> tuple[np.ndarray, tuple[int, int]]:
return padded, (h, w) return padded, (h, w)
def _unpad_image(image: np.ndarray, original_size: tuple[int, int]) -> np.ndarray: def _unpad_image(image: np.ndarray, original_size: Tuple[int, int]) -> np.ndarray:
"""Remove padding from image.""" """Remove padding from image."""
h, w = original_size h, w = original_size
return image[:h, :w] return image[:h, :w]
def _embed_bit_in_coeff(coeff: float, bit: int, quant_step: int = QUANT_STEP) -> float: def _embed_bit_in_coeff(coef: float, bit: int, quant_step: int = QUANT_STEP) -> float:
"""Embed a single bit into a DCT coefficient using QIM.""" """Embed a single bit into a DCT coefficient using QIM."""
# Quantization Index Modulation quantized = round(coef / quant_step)
quantized = round(coeff / quant_step)
if (quantized % 2) != bit: if (quantized % 2) != bit:
# Adjust to embed the bit
if quantized % 2 == 0 and bit == 1: if quantized % 2 == 0 and bit == 1:
quantized += 1 if coeff >= quantized * quant_step else -1 quantized += 1 if coef >= quantized * quant_step else -1
elif quantized % 2 == 1 and bit == 0: elif quantized % 2 == 1 and bit == 0:
quantized += 1 if coeff >= quantized * quant_step else -1 quantized += 1 if coef >= quantized * quant_step else -1
return quantized * quant_step return quantized * quant_step
def _extract_bit_from_coeff(coeff: float, quant_step: int = QUANT_STEP) -> int: def _extract_bit_from_coeff(coef: float, quant_step: int = QUANT_STEP) -> int:
"""Extract a single bit from a DCT coefficient.""" """Extract a single bit from a DCT coefficient."""
quantized = round(coeff / quant_step) quantized = round(coef / quant_step)
return quantized % 2 return quantized % 2
def _generate_block_order(num_blocks: int, seed: bytes) -> list[int]: def _generate_block_order(num_blocks: int, seed: bytes) -> list:
"""Generate pseudo-random block order from seed.""" """Generate pseudo-random block order from seed."""
# Create deterministic RNG from seed
hash_bytes = hashlib.sha256(seed).digest() hash_bytes = hashlib.sha256(seed).digest()
rng = np.random.RandomState(int.from_bytes(hash_bytes[:4], 'big')) rng = np.random.RandomState(int.from_bytes(hash_bytes[:4], 'big'))
order = list(range(num_blocks)) order = list(range(num_blocks))
rng.shuffle(order) rng.shuffle(order)
return order return order
def _save_stego_image( def _save_stego_image(image: np.ndarray, output_format: str = OUTPUT_FORMAT_PNG) -> bytes:
image: np.ndarray, """Save stego image in specified format (grayscale)."""
output_format: str = OUTPUT_FORMAT_PNG
) -> bytes:
"""Save stego image in specified format."""
# Clip to valid range and convert to uint8
clipped = np.clip(image, 0, 255).astype(np.uint8) clipped = np.clip(image, 0, 255).astype(np.uint8)
img = Image.fromarray(clipped, mode='L') img = Image.fromarray(clipped, mode='L')
buffer = io.BytesIO() buffer = io.BytesIO()
if output_format == OUTPUT_FORMAT_JPEG: if output_format == OUTPUT_FORMAT_JPEG:
# High-quality JPEG with no chroma subsampling img.save(buffer, format='JPEG', quality=JPEG_OUTPUT_QUALITY,
img.save( subsampling=0, optimize=True)
buffer,
format='JPEG',
quality=JPEG_OUTPUT_QUALITY,
subsampling=0, # 4:4:4 - no subsampling
optimize=True
)
else: else:
# PNG (lossless, default)
img.save(buffer, format='PNG', optimize=True) img.save(buffer, format='PNG', optimize=True)
return buffer.getvalue() return buffer.getvalue()
def _save_color_image(rgb_array: np.ndarray, output_format: str = OUTPUT_FORMAT_PNG) -> bytes:
"""Save color RGB image in specified format."""
clipped = np.clip(rgb_array, 0, 255).astype(np.uint8)
img = Image.fromarray(clipped, mode='RGB')
buffer = io.BytesIO()
if output_format == OUTPUT_FORMAT_JPEG:
img.save(buffer, format='JPEG', quality=JPEG_OUTPUT_QUALITY,
subsampling=0, optimize=True)
else:
img.save(buffer, format='PNG', optimize=True)
return buffer.getvalue()
def _rgb_to_ycbcr(rgb: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""
Convert RGB array to YCbCr components.
Uses ITU-R BT.601 conversion (standard for JPEG).
Args:
rgb: RGB image array (H, W, 3), float64
Returns:
Tuple of (Y, Cb, Cr) arrays
"""
R = rgb[:, :, 0]
G = rgb[:, :, 1]
B = rgb[:, :, 2]
# ITU-R BT.601 conversion
Y = 0.299 * R + 0.587 * G + 0.114 * B
Cb = 128 - 0.168736 * R - 0.331264 * G + 0.5 * B
Cr = 128 + 0.5 * R - 0.418688 * G - 0.081312 * B
return Y, Cb, Cr
def _ycbcr_to_rgb(Y: np.ndarray, Cb: np.ndarray, Cr: np.ndarray) -> np.ndarray:
"""
Convert YCbCr components back to RGB array.
Args:
Y: Luminance channel
Cb: Blue-difference chroma
Cr: Red-difference chroma
Returns:
RGB array (H, W, 3)
"""
R = Y + 1.402 * (Cr - 128)
G = Y - 0.344136 * (Cb - 128) - 0.714136 * (Cr - 128)
B = Y + 1.772 * (Cb - 128)
rgb = np.stack([R, G, B], axis=-1)
return rgb
def _create_header(data_length: int, flags: int = 0) -> bytes: def _create_header(data_length: int, flags: int = 0) -> bytes:
"""Create DCT stego header.""" """Create DCT stego header."""
# Header format: MAGIC(4) + VERSION(1) + FLAGS(1) + LENGTH(4)
version = 1 version = 1
return struct.pack('>4sBBI', DCT_MAGIC, version, flags, data_length) return struct.pack('>4sBBI', DCT_MAGIC, version, flags, data_length)
def _parse_header(header_bits: list[int]) -> tuple[int, int, int]: def _parse_header(header_bits: list) -> Tuple[int, int, int]:
"""Parse header from extracted bits. Returns (version, flags, data_length).""" """Parse header from extracted bits. Returns (version, flags, data_length)."""
if len(header_bits) < HEADER_SIZE * 8: if len(header_bits) < HEADER_SIZE * 8:
raise ValueError("Insufficient header data") raise ValueError("Insufficient header data")
# Convert bits to bytes
header_bytes = bytes([ header_bytes = bytes([
sum(header_bits[i*8:(i+1)*8][j] << (7-j) for j in range(8)) sum(header_bits[i*8:(i+1)*8][j] << (7-j) for j in range(8))
for i in range(HEADER_SIZE) for i in range(HEADER_SIZE)
@@ -245,7 +316,80 @@ def _parse_header(header_bits: list[int]) -> tuple[int, int, int]:
magic, version, flags, length = struct.unpack('>4sBBI', header_bytes) magic, version, flags, length = struct.unpack('>4sBBI', header_bytes)
if magic != DCT_MAGIC: if magic != DCT_MAGIC:
raise ValueError("Invalid DCT stego magic bytes - not a DCT stego image") raise ValueError("Invalid DCT stego magic bytes")
return version, flags, length
# ============================================================================
# JPEGIO HELPERS (for proper JPEG output)
# ============================================================================
def _jpegio_bytes_to_file(data: bytes, suffix: str = '.jpg') -> str:
"""Write bytes to temp file for jpegio."""
import tempfile
import os
fd, path = tempfile.mkstemp(suffix=suffix)
try:
os.write(fd, data)
finally:
os.close(fd)
return path
def _jpegio_file_to_bytes(path: str) -> bytes:
"""Read file to bytes and delete it."""
import os
try:
with open(path, 'rb') as f:
return f.read()
finally:
try:
os.unlink(path)
except OSError:
pass
def _jpegio_get_usable_positions(coef_array: np.ndarray) -> list:
"""Get usable coefficient positions for jpegio embedding."""
positions = []
h, w = coef_array.shape
for row in range(h):
for col in range(w):
# Skip DC coefficients
if (row % BLOCK_SIZE == 0) and (col % BLOCK_SIZE == 0):
continue
# Check magnitude
if abs(coef_array[row, col]) >= JPEGIO_MIN_COEF_MAGNITUDE:
positions.append((row, col))
return positions
def _jpegio_generate_order(num_positions: int, seed: bytes) -> list:
"""Generate pseudo-random order for jpegio embedding."""
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 _jpegio_create_header(data_length: int) -> bytes:
"""Create header for jpegio embedding."""
return struct.pack('>4sBBI', JPEGIO_MAGIC, 1, 0, data_length)
def _jpegio_parse_header(header_bytes: bytes) -> Tuple[int, int, int]:
"""Parse jpegio header."""
if len(header_bytes) < HEADER_SIZE:
raise ValueError("Insufficient header data")
magic, version, flags, length = struct.unpack('>4sBBI', header_bytes[:HEADER_SIZE])
if magic != JPEGIO_MAGIC:
raise ValueError(f"Invalid JPEG stego magic: {magic}")
return version, flags, length return version, flags, length
@@ -254,11 +398,6 @@ def _parse_header(header_bits: list[int]) -> tuple[int, int, int]:
# PUBLIC API # PUBLIC API
# ============================================================================ # ============================================================================
def has_dct_support() -> bool:
"""Check if DCT steganography is available."""
return HAS_SCIPY
def calculate_dct_capacity(image_data: bytes) -> DCTCapacityInfo: def calculate_dct_capacity(image_data: bytes) -> DCTCapacityInfo:
""" """
Calculate the DCT embedding capacity of an image. Calculate the DCT embedding capacity of an image.
@@ -274,19 +413,13 @@ def calculate_dct_capacity(image_data: bytes) -> DCTCapacityInfo:
img = Image.open(io.BytesIO(image_data)) img = Image.open(io.BytesIO(image_data))
width, height = img.size width, height = img.size
# Calculate blocks
blocks_x = width // BLOCK_SIZE blocks_x = width // BLOCK_SIZE
blocks_y = height // BLOCK_SIZE blocks_y = height // BLOCK_SIZE
total_blocks = blocks_x * blocks_y total_blocks = blocks_x * blocks_y
# Bits per block (using selected coefficient positions)
bits_per_block = len(DEFAULT_EMBED_POSITIONS) bits_per_block = len(DEFAULT_EMBED_POSITIONS)
# Total capacity
total_bits = total_blocks * bits_per_block total_bits = total_blocks * bits_per_block
total_bytes = total_bits // 8 total_bytes = total_bits // 8
# Usable capacity (minus header)
usable_bytes = max(0, total_bytes - HEADER_SIZE) usable_bytes = max(0, total_bytes - HEADER_SIZE)
return DCTCapacityInfo( return DCTCapacityInfo(
@@ -303,43 +436,23 @@ def calculate_dct_capacity(image_data: bytes) -> DCTCapacityInfo:
def will_fit_dct(data_length: int, image_data: bytes) -> bool: def will_fit_dct(data_length: int, image_data: bytes) -> bool:
""" """Check if data will fit in the image using DCT embedding."""
Check if data will fit in the image using DCT embedding.
Args:
data_length: Length of data in bytes
image_data: Carrier image bytes
Returns:
True if data fits, False otherwise
"""
capacity = calculate_dct_capacity(image_data) capacity = calculate_dct_capacity(image_data)
return data_length <= capacity.usable_capacity_bytes return data_length <= capacity.usable_capacity_bytes
def estimate_capacity_comparison(image_data: bytes) -> dict: def estimate_capacity_comparison(image_data: bytes) -> dict:
""" """Compare LSB and DCT capacity for an image."""
Compare LSB and DCT capacity for an image.
Args:
image_data: Image file bytes
Returns:
Dict with 'lsb' and 'dct' capacity info
"""
img = Image.open(io.BytesIO(image_data)) img = Image.open(io.BytesIO(image_data))
width, height = img.size width, height = img.size
pixels = width * height pixels = width * height
# LSB capacity (3 bits per pixel for RGB, simplified)
lsb_bytes = (pixels * 3) // 8 lsb_bytes = (pixels * 3) // 8
# DCT capacity
if HAS_SCIPY: if HAS_SCIPY:
dct_info = calculate_dct_capacity(image_data) dct_info = calculate_dct_capacity(image_data)
dct_bytes = dct_info.usable_capacity_bytes dct_bytes = dct_info.usable_capacity_bytes
else: else:
# Estimate without scipy
blocks = (width // 8) * (height // 8) blocks = (width // 8) * (height // 8)
dct_bytes = (blocks * 16) // 8 - HEADER_SIZE dct_bytes = (blocks * 16) // 8 - HEADER_SIZE
@@ -357,6 +470,10 @@ def estimate_capacity_comparison(image_data: bytes) -> dict:
'output': 'PNG or JPEG (grayscale)', 'output': 'PNG or JPEG (grayscale)',
'ratio_vs_lsb': (dct_bytes / lsb_bytes * 100) if lsb_bytes > 0 else 0, 'ratio_vs_lsb': (dct_bytes / lsb_bytes * 100) if lsb_bytes > 0 else 0,
'available': HAS_SCIPY, 'available': HAS_SCIPY,
},
'jpeg_native': {
'available': HAS_JPEGIO,
'note': 'Uses jpegio for proper JPEG coefficient embedding',
} }
} }
@@ -366,30 +483,60 @@ def embed_in_dct(
carrier_image: bytes, carrier_image: bytes,
seed: bytes, seed: bytes,
output_format: str = OUTPUT_FORMAT_PNG, output_format: str = OUTPUT_FORMAT_PNG,
) -> tuple[bytes, DCTEmbedStats]: color_mode: str = 'color', # v3.0.1: 'color' or 'grayscale'
) -> Tuple[bytes, DCTEmbedStats]:
""" """
Embed data into image using DCT coefficient modification. Embed data into image using DCT coefficient modification.
For PNG output: Uses scipy DCT transform
For JPEG output: Uses jpegio if available for proper coefficient embedding
Args: Args:
data: Data to embed data: Data to embed
carrier_image: Carrier image bytes carrier_image: Carrier image bytes
seed: Seed for pseudo-random block selection seed: Seed for pseudo-random selection
output_format: Output format - 'png' (default, lossless) or 'jpeg' (smaller) output_format: 'png' (default, lossless) or 'jpeg'
color_mode: 'color' (preserve colors) or 'grayscale' (v3.0.1+)
Returns: Returns:
Tuple of (stego_image_bytes, stats) Tuple of (stego_image_bytes, stats)
Raises:
ImportError: If scipy is not available
ValueError: If data is too large for carrier
""" """
_check_scipy()
# Validate output format # Validate output format
if output_format not in (OUTPUT_FORMAT_PNG, OUTPUT_FORMAT_JPEG): if output_format not in (OUTPUT_FORMAT_PNG, OUTPUT_FORMAT_JPEG):
raise ValueError(f"Invalid output format: {output_format}. Use 'png' or 'jpeg'") raise ValueError(f"Invalid output format: {output_format}")
# Calculate capacity # Validate color mode
if color_mode not in ('color', 'grayscale'):
color_mode = 'color' # Default to color
# For JPEG output, try to use jpegio for proper coefficient embedding
# Note: jpegio naturally preserves color (works in YCbCr space)
if output_format == OUTPUT_FORMAT_JPEG:
if HAS_JPEGIO:
return _embed_jpegio(data, carrier_image, seed, color_mode)
else:
# Fall back to scipy + PIL JPEG (WARNING: may not decode properly)
import warnings
warnings.warn(
"jpegio not available. JPEG output may not decode correctly. "
"Install jpegio for proper JPEG steganography support.",
RuntimeWarning
)
# Continue with scipy method but output as JPEG
# PNG output or JPEG fallback: use scipy DCT method
_check_scipy()
return _embed_scipy_dct(data, carrier_image, seed, output_format, color_mode)
def _embed_scipy_dct(
data: bytes,
carrier_image: bytes,
seed: bytes,
output_format: str,
color_mode: str = 'color',
) -> Tuple[bytes, DCTEmbedStats]:
"""Embed using scipy DCT (for PNG output), with color preservation option."""
capacity_info = calculate_dct_capacity(carrier_image) capacity_info = calculate_dct_capacity(carrier_image)
if len(data) > capacity_info.usable_capacity_bytes: if len(data) > capacity_info.usable_capacity_bytes:
@@ -398,69 +545,216 @@ def embed_in_dct(
f"(capacity: {capacity_info.usable_capacity_bytes} bytes)" f"(capacity: {capacity_info.usable_capacity_bytes} bytes)"
) )
# Prepare image # Load image
image = _to_grayscale(carrier_image) img = Image.open(io.BytesIO(carrier_image))
padded, original_size = _pad_to_blocks(image) width, height = img.size
# Create header + data if color_mode == 'color' and img.mode in ('RGB', 'RGBA'):
# Color mode: convert to YCbCr, embed in Y only, preserve Cb/Cr
if img.mode == 'RGBA':
img = img.convert('RGB')
rgb_array = np.array(img, dtype=np.float64)
Y, Cb, Cr = _rgb_to_ycbcr(rgb_array)
# Pad Y channel
Y_padded, original_size = _pad_to_blocks(Y)
# Embed in Y channel
Y_embedded = _embed_in_channel(Y_padded, data, seed, capacity_info)
# Unpad
Y_result = _unpad_image(Y_embedded, original_size)
# Convert back to RGB
result_rgb = _ycbcr_to_rgb(Y_result, Cb, Cr)
# Save as color image
stego_bytes = _save_color_image(result_rgb, output_format)
else:
# Grayscale mode: original behavior
image = _to_grayscale(carrier_image)
padded, original_size = _pad_to_blocks(image)
embedded = _embed_in_channel(padded, data, seed, capacity_info)
result = _unpad_image(embedded, original_size)
stego_bytes = _save_stego_image(result, output_format)
# Calculate stats
header = _create_header(len(data))
payload = header + data
bits = len(payload) * 8
stats = DCTEmbedStats(
blocks_used=(bits + len(DEFAULT_EMBED_POSITIONS) - 1) // len(DEFAULT_EMBED_POSITIONS),
blocks_available=capacity_info.total_blocks,
bits_embedded=bits,
capacity_bits=capacity_info.total_capacity_bits,
usage_percent=(bits / capacity_info.total_capacity_bits) * 100,
image_width=width,
image_height=height,
output_format=output_format,
jpeg_native=False,
color_mode=color_mode,
)
return stego_bytes, stats
def _embed_in_channel(
channel: np.ndarray,
data: bytes,
seed: bytes,
capacity_info: DCTCapacityInfo,
) -> np.ndarray:
"""Embed data in a single channel using DCT."""
header = _create_header(len(data)) header = _create_header(len(data))
payload = header + data payload = header + data
# Convert payload to bits
bits = [] bits = []
for byte in payload: for byte in payload:
for i in range(7, -1, -1): for i in range(7, -1, -1):
bits.append((byte >> i) & 1) bits.append((byte >> i) & 1)
# Generate block order
num_blocks = capacity_info.total_blocks num_blocks = capacity_info.total_blocks
block_order = _generate_block_order(num_blocks, seed) block_order = _generate_block_order(num_blocks, seed)
# Embed bits h, w = channel.shape
bit_idx = 0 result = channel.copy()
blocks_used = 0
h, w = padded.shape
bit_idx = 0
for block_num in block_order: for block_num in block_order:
if bit_idx >= len(bits): if bit_idx >= len(bits):
break break
# Calculate block position
by = (block_num // (w // BLOCK_SIZE)) * BLOCK_SIZE by = (block_num // (w // BLOCK_SIZE)) * BLOCK_SIZE
bx = (block_num % (w // BLOCK_SIZE)) * BLOCK_SIZE bx = (block_num % (w // BLOCK_SIZE)) * BLOCK_SIZE
# Extract and transform block block = result[by:by+BLOCK_SIZE, bx:bx+BLOCK_SIZE].copy()
block = padded[by:by+BLOCK_SIZE, bx:bx+BLOCK_SIZE].copy()
dct_block = _dct2(block) dct_block = _dct2(block)
# Embed bits in selected coefficients
for pos in DEFAULT_EMBED_POSITIONS: for pos in DEFAULT_EMBED_POSITIONS:
if bit_idx >= len(bits): if bit_idx >= len(bits):
break break
dct_block[pos] = _embed_bit_in_coeff(dct_block[pos], bits[bit_idx]) dct_block[pos] = _embed_bit_in_coeff(dct_block[pos], bits[bit_idx])
bit_idx += 1 bit_idx += 1
# Inverse transform and store
modified_block = _idct2(dct_block) modified_block = _idct2(dct_block)
padded[by:by+BLOCK_SIZE, bx:bx+BLOCK_SIZE] = modified_block result[by:by+BLOCK_SIZE, bx:bx+BLOCK_SIZE] = modified_block
blocks_used += 1
# Remove padding and save return result
result = _unpad_image(padded, original_size)
stego_bytes = _save_stego_image(result, output_format)
stats = DCTEmbedStats(
blocks_used=blocks_used,
blocks_available=capacity_info.total_blocks,
bits_embedded=len(bits),
capacity_bits=capacity_info.total_capacity_bits,
usage_percent=(len(bits) / capacity_info.total_capacity_bits) * 100,
image_width=original_size[1],
image_height=original_size[0],
output_format=output_format,
)
return stego_bytes, stats def _embed_jpegio(
data: bytes,
carrier_image: bytes,
seed: bytes,
color_mode: str = 'color',
) -> Tuple[bytes, DCTEmbedStats]:
"""
Embed using jpegio for proper JPEG coefficient modification.
Note: jpegio naturally preserves color since JPEG stores YCbCr
and we only modify Y channel coefficients.
"""
import tempfile
import os
# Check if carrier is JPEG - if not, convert it
img = Image.open(io.BytesIO(carrier_image))
width, height = img.size
if img.format != 'JPEG':
# Convert to JPEG first
buffer = io.BytesIO()
if img.mode != 'RGB':
img = img.convert('RGB')
img.save(buffer, format='JPEG', quality=95, subsampling=0)
carrier_image = buffer.getvalue()
# Write carrier to temp file
input_path = _jpegio_bytes_to_file(carrier_image, suffix='.jpg')
output_path = tempfile.mktemp(suffix='.jpg')
try:
# Read JPEG with jpegio
jpeg = jio.read(input_path)
# Get Y channel coefficients (channel 0)
# For grayscale mode, we could convert to grayscale, but jpegio
# works with the original JPEG which already has color info.
# The color_mode primarily affects the output interpretation.
coef_array = jpeg.coef_arrays[JPEGIO_EMBED_CHANNEL]
# Find usable positions
all_positions = _jpegio_get_usable_positions(coef_array)
# Generate pseudo-random order
order = _jpegio_generate_order(len(all_positions), seed)
# Create payload
header = _jpegio_create_header(len(data))
payload = header + data
# Convert to bits
bits = []
for byte in payload:
for i in range(7, -1, -1):
bits.append((byte >> i) & 1)
if len(bits) > len(all_positions):
raise ValueError(
f"Payload too large: {len(bits)} bits, "
f"only {len(all_positions)} usable coefficients"
)
# Embed using LSB
coefs_used = 0
for bit_idx, pos_idx in enumerate(order):
if bit_idx >= len(bits):
break
row, col = all_positions[pos_idx]
coef = coef_array[row, col]
# Embed bit in LSB
if (coef & 1) != bits[bit_idx]:
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
coefs_used += 1
# Write modified JPEG
jio.write(jpeg, output_path)
# Read back as bytes
with open(output_path, 'rb') as f:
stego_bytes = f.read()
stats = DCTEmbedStats(
blocks_used=coefs_used // 63, # Approximate blocks
blocks_available=len(all_positions) // 63,
bits_embedded=len(bits),
capacity_bits=len(all_positions),
usage_percent=(len(bits) / len(all_positions)) * 100 if all_positions else 0,
image_width=width,
image_height=height,
output_format=OUTPUT_FORMAT_JPEG,
jpeg_native=True,
color_mode=color_mode, # JPEG naturally preserves color
)
return stego_bytes, stats
finally:
for path in [input_path, output_path]:
try:
os.unlink(path)
except OSError:
pass
def extract_from_dct( def extract_from_dct(
@@ -470,33 +764,43 @@ def extract_from_dct(
""" """
Extract data from DCT stego image. Extract data from DCT stego image.
Automatically detects whether image uses scipy DCT or jpegio embedding.
Args: Args:
stego_image: Stego image bytes stego_image: Stego image bytes
seed: Same seed used for embedding seed: Same seed used for embedding
Returns: Returns:
Extracted data bytes Extracted data bytes
Raises:
ImportError: If scipy is not available
ValueError: If image is not a valid DCT stego image
""" """
_check_scipy() # Check image format
img = Image.open(io.BytesIO(stego_image))
# Prepare image if img.format == 'JPEG' and HAS_JPEGIO:
# Try jpegio extraction first
try:
return _extract_jpegio(stego_image, seed)
except ValueError:
# If jpegio magic not found, fall back to scipy method
pass
# PNG or fallback: use scipy DCT method
_check_scipy()
return _extract_scipy_dct(stego_image, seed)
def _extract_scipy_dct(stego_image: bytes, seed: bytes) -> bytes:
"""Extract using scipy DCT (for PNG images)."""
image = _to_grayscale(stego_image) image = _to_grayscale(stego_image)
padded, original_size = _pad_to_blocks(image) padded, original_size = _pad_to_blocks(image)
# Calculate capacity
h, w = padded.shape h, w = padded.shape
blocks_x = w // BLOCK_SIZE blocks_x = w // BLOCK_SIZE
blocks_y = h // BLOCK_SIZE blocks_y = h // BLOCK_SIZE
num_blocks = blocks_x * blocks_y num_blocks = blocks_x * blocks_y
# Generate same block order
block_order = _generate_block_order(num_blocks, seed) block_order = _generate_block_order(num_blocks, seed)
# Extract all bits (we'll stop when we have enough based on header)
all_bits = [] all_bits = []
for block_num in block_order: for block_num in block_order:
@@ -510,7 +814,6 @@ def extract_from_dct(
bit = _extract_bit_from_coeff(dct_block[pos]) bit = _extract_bit_from_coeff(dct_block[pos])
all_bits.append(bit) all_bits.append(bit)
# Check if we have enough for header
if len(all_bits) >= HEADER_SIZE * 8: if len(all_bits) >= HEADER_SIZE * 8:
try: try:
_, _, data_length = _parse_header(all_bits[:HEADER_SIZE * 8]) _, _, data_length = _parse_header(all_bits[:HEADER_SIZE * 8])
@@ -518,16 +821,12 @@ def extract_from_dct(
if len(all_bits) >= total_needed: if len(all_bits) >= total_needed:
break break
except ValueError: except ValueError:
# Not enough data yet or invalid, continue
pass pass
# Parse header
version, flags, data_length = _parse_header(all_bits) version, flags, data_length = _parse_header(all_bits)
# Extract data bits
data_bits = all_bits[HEADER_SIZE * 8:(HEADER_SIZE + data_length) * 8] data_bits = all_bits[HEADER_SIZE * 8:(HEADER_SIZE + data_length) * 8]
# Convert bits to bytes
data = bytes([ data = bytes([
sum(data_bits[i*8:(i+1)*8][j] << (7-j) for j in range(8)) sum(data_bits[i*8:(i+1)*8][j] << (7-j) for j in range(8))
for i in range(data_length) for i in range(data_length)
@@ -536,6 +835,61 @@ def extract_from_dct(
return data return data
def _extract_jpegio(stego_image: bytes, seed: bytes) -> bytes:
"""Extract using jpegio for JPEG images."""
import os
temp_path = _jpegio_bytes_to_file(stego_image, suffix='.jpg')
try:
jpeg = jio.read(temp_path)
coef_array = jpeg.coef_arrays[JPEGIO_EMBED_CHANNEL]
all_positions = _jpegio_get_usable_positions(coef_array)
order = _jpegio_generate_order(len(all_positions), seed)
# Extract header bits
header_bits = []
for pos_idx in order[:HEADER_SIZE * 8]:
row, col = all_positions[pos_idx]
coef = coef_array[row, col]
header_bits.append(coef & 1)
header_bytes = bytes([
sum(header_bits[i*8:(i+1)*8][j] << (7-j) for j in range(8))
for i in range(HEADER_SIZE)
])
version, flags, data_length = _jpegio_parse_header(header_bytes)
# Extract all needed bits
total_bits_needed = (HEADER_SIZE + data_length) * 8
all_bits = []
for bit_idx, pos_idx in enumerate(order):
if bit_idx >= total_bits_needed:
break
row, col = all_positions[pos_idx]
coef = coef_array[row, col]
all_bits.append(coef & 1)
# Extract data
data_bits = all_bits[HEADER_SIZE * 8:]
data = bytes([
sum(data_bits[i*8:(i+1)*8][j] << (7-j) for j in range(8))
for i in range(data_length)
])
return data
finally:
try:
os.unlink(temp_path)
except OSError:
pass
# ============================================================================ # ============================================================================
# CONVENIENCE FUNCTIONS # CONVENIENCE FUNCTIONS
# ============================================================================ # ============================================================================

View File

@@ -11,6 +11,7 @@ New in v3.0:
New in v3.0.1: New in v3.0.1:
- dct_output_format parameter for DCT mode ('png' or 'jpeg') - dct_output_format parameter for DCT mode ('png' or 'jpeg')
- dct_color_mode parameter for DCT mode ('grayscale' or 'color')
""" """
import io import io
@@ -59,6 +60,10 @@ ENCRYPTION_OVERHEAD = HEADER_OVERHEAD + LENGTH_PREFIX
DCT_OUTPUT_PNG = 'png' DCT_OUTPUT_PNG = 'png'
DCT_OUTPUT_JPEG = 'jpeg' DCT_OUTPUT_JPEG = 'jpeg'
# DCT color mode options (v3.0.1)
DCT_COLOR_GRAYSCALE = 'grayscale'
DCT_COLOR_COLOR = 'color'
# ============================================================================= # =============================================================================
# DCT MODULE LAZY LOADING # DCT MODULE LAZY LOADING
@@ -477,6 +482,7 @@ def embed_in_image(
output_format: Optional[str] = None, output_format: Optional[str] = None,
embed_mode: str = EMBED_MODE_LSB, embed_mode: str = EMBED_MODE_LSB,
dct_output_format: str = DCT_OUTPUT_PNG, # NEW in v3.0.1 dct_output_format: str = DCT_OUTPUT_PNG, # NEW in v3.0.1
dct_color_mode: str = 'grayscale', # NEW in v3.0.1: 'grayscale' or 'color'
) -> Tuple[bytes, Union[EmbedStats, 'DCTEmbedStats'], str]: ) -> Tuple[bytes, Union[EmbedStats, 'DCTEmbedStats'], str]:
""" """
Embed data into an image using specified mode. Embed data into an image using specified mode.
@@ -489,6 +495,7 @@ def embed_in_image(
output_format: Force output format (LSB mode only) output_format: Force output format (LSB mode only)
embed_mode: 'lsb' (default) or 'dct' embed_mode: 'lsb' (default) or 'dct'
dct_output_format: For DCT mode - 'png' (lossless) or 'jpeg' (smaller) dct_output_format: For DCT mode - 'png' (lossless) or 'jpeg' (smaller)
dct_color_mode: For DCT mode - 'grayscale' (default) or 'color' (preserves colors)
Returns: Returns:
Tuple of (stego image bytes, stats, file extension) Tuple of (stego image bytes, stats, file extension)
@@ -515,14 +522,20 @@ def embed_in_image(
debug.print(f"Invalid dct_output_format '{dct_output_format}', defaulting to PNG") debug.print(f"Invalid dct_output_format '{dct_output_format}', defaulting to PNG")
dct_output_format = DCT_OUTPUT_PNG dct_output_format = DCT_OUTPUT_PNG
# Validate DCT color mode (v3.0.1)
if dct_color_mode not in ('grayscale', 'color'):
debug.print(f"Invalid dct_color_mode '{dct_color_mode}', defaulting to grayscale")
dct_color_mode = 'grayscale'
dct_mod = _get_dct_module() dct_mod = _get_dct_module()
# Pass output_format to DCT module (v3.0.1) # Pass output_format and color_mode to DCT module (v3.0.1)
stego_bytes, dct_stats = dct_mod.embed_in_dct( stego_bytes, dct_stats = dct_mod.embed_in_dct(
data, data,
image_data, image_data,
pixel_key, pixel_key,
output_format=dct_output_format, output_format=dct_output_format,
color_mode=dct_color_mode, # NEW in v3.0.1
) )
# Determine extension based on output format # Determine extension based on output format
@@ -531,7 +544,8 @@ def embed_in_image(
else: else:
ext = 'png' ext = 'png'
debug.print(f"DCT embedding complete: {dct_output_format.upper()} output, ext={ext}") debug.print(f"DCT embedding complete: {dct_output_format.upper()} output, "
f"color_mode={dct_color_mode}, ext={ext}")
return stego_bytes, dct_stats, ext return stego_bytes, dct_stats, ext
# LSB MODE # LSB MODE