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
# 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/*
# ============================================================================
@@ -31,8 +33,10 @@ COPY pyproject.toml README.md ./
COPY src/ src/
COPY data/ data/
# Install the package with web extras
RUN pip install --no-cache-dir ".[web]"
# 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
@@ -78,11 +82,14 @@ FROM base as api
WORKDIR /app
# Install API extras
# Install API extras (includes DCT dependencies)
COPY pyproject.toml README.md ./
COPY src/ src/
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 frontends/api/ frontends/api/
@@ -116,7 +123,10 @@ WORKDIR /app
COPY pyproject.toml README.md ./
COPY src/ src/
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 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)
![License](https://img.shields.io/badge/License-MIT-green)
![Security](https://img.shields.io/badge/Security-AES--256--GCM-red)
![Version](https://img.shields.io/badge/Version-3.0.2-purple)
## Features
@@ -17,50 +18,40 @@ A secure steganography system for hiding encrypted messages in images using hybr
- 🌐 **Multiple interfaces**: CLI, Web UI, REST API
- 📁 **File embedding** - Hide any file type (PDF, ZIP, documents)
- 📱 **QR code support** - Encode/decode RSA keys via QR codes
- 🆕 **DCT steganography** - JPEG-resilient embedding for social media (v3.0+)
## 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
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)
| 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) |
## Installation
### From Source
## Quick Start
```bash
# Clone the repository
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
# Install with all features
pip install -e ".[all]"
```
### CLI Usage
```bash
# Generate credentials
# Generate credentials (memorize these!)
stegasoo generate --pin --words 3
# With RSA key
stegasoo generate --rsa --rsa-bits 4096 -o mykey.pem -p "secretpassword"
# Encode
# Encode a message (LSB mode - default)
stegasoo encode \
--ref photo.jpg \
--carrier meme.png \
@@ -68,88 +59,171 @@ stegasoo encode \
--pin 123456 \
--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 \
--ref photo.jpg \
--stego stego.png \
--phrase "apple forest thunder" \
--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
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 |
|-----------|---------|---------|
| Reference Photo | ~80-256 bits | Something you have |
| Day Phrase (3-12 words) | ~33-100+ bits | Something you know (rotates daily) |
| PIN (6-9 digits) | ~20+ bits | Something you know (static) |
| RSA Key (2048-bit) | ~128 bits | Something you have |
| **Combined** | **~133-400+ bits** | **Beyond brute force** |
| Day Phrase (3-12 words) | ~33-132 bits | Something you know (rotates daily) |
| PIN (6-9 digits) | ~20-30 bits | Something you know (static) |
| RSA Key (2048-4096 bit) | ~112-128 bits | Something you have (optional) |
| **Combined** | **133-400+ bits** | **Beyond brute force** |
### Attack Resistance
| Attack | Protection |
|--------|------------|
| Brute force | 2^133+ combinations |
| Rainbow tables | Random salt per message |
| Steganalysis | Random pixel selection |
| Brute force | 2^133+ combinations minimum |
| Rainbow tables | Random 16-byte salt per message |
| Steganalysis | Pseudo-random pixel/coefficient selection |
| 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
@@ -159,11 +233,13 @@ stegasoo/
│ ├── __init__.py # Public API
│ ├── constants.py # Configuration
│ ├── crypto.py # Encryption/decryption
│ ├── steganography.py # Image embedding
│ ├── steganography.py # LSB image embedding
│ ├── dct_steganography.py # DCT embedding (v3.0+)
│ ├── keygen.py # Credential generation
│ ├── validation.py # Input validation
│ ├── models.py # Data classes
│ ├── exceptions.py # Custom exceptions
│ ├── qr_utils.py # QR code utilities
│ └── utils.py # Utilities
├── frontends/
@@ -176,18 +252,19 @@ stegasoo/
├── pyproject.toml # Package configuration
├── 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
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `FLASK_ENV` | production | Flask environment |
| `PYTHONPATH` | - | Include src/ for development |
### Limits
| Limit | Value |
@@ -199,6 +276,15 @@ stegasoo/
| Phrase length | 3-12 words |
| RSA key sizes | 2048, 3072, 4096 bits |
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `FLASK_ENV` | production | Flask environment |
| `PYTHONPATH` | - | Include `src/` for development |
---
## Development
```bash
@@ -214,13 +300,42 @@ ruff check src/ frontends/
# Type checking
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
MIT License - Use responsibly.
---
## ⚠️ Disclaimer
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:
resources:
limits:
memory: 512M # Argon2 needs 256MB per operation
memory: 768M # Increased for scipy + Argon2
reservations:
memory: 256M
memory: 384M
# ============================================================================
# REST API (FastAPI)
@@ -35,9 +35,9 @@ services:
deploy:
resources:
limits:
memory: 512M
memory: 768M # Increased for scipy + Argon2
reservations:
memory: 256M
memory: 384M
# ============================================================================
# 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/multipart](#post-decodemultipart)
- [POST /image/info](#post-imageinfo)
- [Embedding Modes](#embedding-modes)
- [Data Models](#data-models)
- [Error Handling](#error-handling)
- [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:
- **Generate** credentials (phrases, PINs, RSA keys)
- **Encode** messages into images
- **Decode** messages from images
- **Encode** messages into images (LSB or DCT mode)
- **Decode** messages from images (auto-detects mode)
- **Analyze** image capacity
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
@@ -45,6 +53,8 @@ The API supports both JSON (base64-encoded images) and multipart form data (dire
pip install stegasoo[api]
```
This automatically installs DCT dependencies (scipy, jpegio) for full functionality.
### From Source
```bash
@@ -107,8 +117,10 @@ Host: localhost:8000
```json
{
"version": "2.0.1",
"version": "3.0.2",
"has_argon2": true,
"has_dct": true,
"has_jpegio": true,
"day_names": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
}
```
@@ -119,6 +131,8 @@ Host: localhost:8000
|-------|------|-------------|
| `version` | string | Stegasoo library version |
| `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 |
#### cURL Example
@@ -245,22 +259,28 @@ Content-Type: application/json
| `rsa_password` | string | | | Password for RSA key |
| `date_str` | string | | | Date override (YYYY-MM-DD) |
| `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
```json
{
"stego_image_base64": "iVBORw0KGgo...",
"filename": "a1b2c3d4_20251227.png",
"capacity_used_percent": 12.4,
"date_used": "2025-12-27",
"day_of_week": "Saturday"
}
```
#### Response Fields
"stego_image_base64": "iVBORw0KGgo...",
"filename": "a1b2c3d4_20251227.png",
"capacity_used_percent": 12.4,
"date_used": "2025-12-27",
"day_of_week": "Saturday",
"embedding_mode": "lsb",
"output_format": "png",
"color_mode": null
}
```
#### Response Fields
| Field | Type | Description |
|-------|------|-------------|
| `stego_image_base64` | string | Base64-encoded stego image |
@@ -272,7 +292,10 @@ Content-Type: application/json
| `output_format` | string | Output format: `"png"` or `"jpeg"` |
| `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)
CARRIER_B64=$(base64 -w0 carrier.png)
@@ -280,13 +303,16 @@ Content-Type: application/json
-H "Content-Type: application/json" \
-d "{
\"message\": \"Secret message\",
\"reference_photo_base64\": \"$REF_B64\",
\"reference_photo_base64\": \"$REF_B64\",
\"carrier_image_base64\": \"$CARRIER_B64\",
\"day_phrase\": \"apple forest thunder\",
\"pin\": \"123456\"
}" | jq -r '.stego_image_base64' | base64 -d > stego.png
```
#### cURL Example (DCT Mode with JPEG Output)
```bash
curl -X POST http://localhost:8000/encode \
-H "Content-Type: application/json" \
-d "{
@@ -304,6 +330,23 @@ curl -X POST http://localhost:8000/encode \
---
### 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
------FormBoundary
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-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 |
| `rsa_key` | file | * | | RSA key file (.pem) |
| `rsa_password` | string | | | Password for RSA key |
@@ -344,83 +399,72 @@ Content-Type: image/png
Returns the image directly with headers:
- `Content-Disposition: attachment; filename=<generated_filename>.png`
- `X-Stegasoo-Date: 2025-12-27` (date used for encoding)
- `X-Stegasoo-Day: Saturday` (day of week for passphrase rotation)
- `X-Stegasoo-Capacity-Percent: 12.4` (capacity used)
#### cURL Examples
**With PIN:**
```bash
curl -X POST http://localhost:8000/encode/multipart \
```http
HTTP/1.1 200 OK
Content-Type: image/png
Content-Disposition: attachment; filename="a1b2c3d4_20251227.png"
X-Stegasoo-Date: 2025-12-27
X-Stegasoo-Day: Saturday
X-Stegasoo-Capacity-Used: 12.4
X-Stegasoo-Embedding-Mode: lsb
X-Stegasoo-Output-Format: png
<binary image data>
```
#### Response Headers
| Header | Description |
|--------|-------------|
| `Content-Type` | `image/png` or `image/jpeg` |
--output stego.png
```
**With RSA key:**
```bash
curl -X POST http://localhost:8000/encode/multipart \
| `Content-Disposition` | Suggested filename |
| `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 \
-F "rsa_password=keypassword" \
-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 "message=Secret message for social media" \
-F "day_phrase=apple forest thunder" \
-F "pin=123456" \
-F "pin=123456" \
-F "rsa_key=@mykey.pem" \
-F "rsa_password=keypassword" \
-F "reference_photo=@reference.jpg" \
-F "carrier=@carrier.png" \
--output stego.png
```
**With custom date:**
```bash
curl -X POST http://localhost:8000/encode/multipart \
-F "embedding_mode=dct" \
-F "output_format=jpeg" \
-F "color_mode=color" \
-F "reference_photo=@reference.jpg" \
-F "carrier=@carrier.png" \
--output stego.jpg
```
---
### POST /decode (JSON)
Decode a message using base64-encoded images. Auto-detects embedding mode.
#### Request
-F "day_phrase=monday phrase here" \
```http
-F "reference_photo=@reference.jpg" \
POST /decode HTTP/1.1
Host: localhost:8000
Content-Type: application/json
```
```
#### Request Body
### POST /decode (JSON)
Decode a message using base64-encoded images.
#### Request
```http
POST /decode HTTP/1.1
Host: localhost:8000
Content-Type: application/json
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `stego_image_base64` | string | ✓ | Base64-encoded stego image |
| `reference_photo_base64` | string | ✓ | Base64-encoded reference photo |
| `day_phrase` | string | ✓ | Passphrase for encoding day |
| `pin` | string | * | Static PIN |
| `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.
@@ -450,20 +494,27 @@ Content-Type: application/json
-H "Content-Type: application/json" \
-d "{
\"stego_image_base64\": \"$STEGO_B64\",
```
\"reference_photo_base64\": \"$REF_B64\",
\"day_phrase\": \"apple forest thunder\",
\"pin\": \"123456\"
}"
```
Decode a message using direct file uploads.
---
### POST /decode/multipart
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 |
| `pin` | string | * | Static PIN |
| `rsa_key` | file | * | RSA key file (.pem) |
Content-Type: multipart/form-data
| `rsa_password` | string | | Password for RSA key |
#### Response
@@ -481,15 +532,7 @@ curl -X POST http://localhost:8000/decode \
curl -X POST http://localhost:8000/decode/multipart \
-F "day_phrase=apple forest thunder" \
-F "pin=123456" \
"message": "Secret message here"
}
```
#### cURL Examples
**With PIN:**
```bash
curl -X POST http://localhost:8000/decode/multipart \
-F "reference_photo=@reference.jpg" \
-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.
-F "day_phrase=apple forest thunder" \
#### Request (JSON)
```http
POST /image/info HTTP/1.1
Host: localhost:8000
Content-Type: application/json
---
```
#### Request (Multipart)
```bash
Get information about an image's capacity.
curl -X POST http://localhost:8000/image/info \
-F "image=@carrier.png"
#### Request
```
#### Response
@@ -521,35 +564,30 @@ curl -X POST http://localhost:8000/decode/multipart \
{
"width": 1920,
"height": 1080,
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `image` | file | ✓ | Image file to analyze |
#### Response
```json
{
"width": 1920,
"pixels": 2073600,
"format": "PNG",
"mode": "RGB",
"capacity": {
}
"lsb": {
"bytes": 776970,
"kb": 758
},
"dct": {
"bytes": 64800,
"kb": 63,
| `width` | integer | Image width in pixels |
"note": "Approximate - actual capacity depends on image content"
}
}
}
```
#### Response Fields
| `capacity_bytes` | integer | Maximum message capacity (bytes) |
| 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.) |
| `mode` | string | Color mode (RGB, L, etc.) |
| `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.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 |
| **Best For** | Maximum capacity, controlled channels |
@@ -570,15 +619,58 @@ Content-Type: multipart/form-data
| Aspect | Details |
|--------|---------|
| **Parameter** | `"embedding_mode": "dct"` |
### GenerateResponse
| **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
```json
| Option | Values | Default | Description |
"phrases": {"Monday": "...", "Tuesday": "...", ...},
"pin": "123456",
"rsa_key_pem": "-----BEGIN PRIVATE KEY-----...",
"entropy": {"phrase": 33, "pin": 19, "rsa": 0, "total": 52}
|--------|--------|---------|-------------|
| `output_format` | `"png"`, `"jpeg"` | `"png"` | Output image format |
| `color_mode` | `"color"`, `"grayscale"` | `"color"` | Color processing mode |
### 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",
"reference_photo_base64": "string",
@@ -618,7 +710,10 @@ curl -X POST http://localhost:8000/image/info \
"day_phrase": "string",
"pin": "string",
"rsa_key_base64": "string",
"rsa_password": "string"
"rsa_password": "string"
}
```
### DecodeResponse
```json
@@ -630,7 +725,10 @@ curl -X POST http://localhost:8000/image/info \
### ImageInfoResponse
### ImageInfoResponse
```json
{
"width": 1920,
"height": 1080,
"pixels": 2073600,
"format": "PNG",
"mode": "RGB",
@@ -651,7 +749,8 @@ curl -X POST http://localhost:8000/image/info \
```
---
---
## Error Handling
### HTTP Status Codes
@@ -662,8 +761,12 @@ curl -X POST http://localhost:8000/image/info \
| 401 | Unauthorized | Decryption failed (wrong credentials) |
| 500 | Internal Error | Unexpected server error |
| 500 | Internal Error | Unexpected server error |
### Error Response Format
```json
{
"detail": "Error message describing the problem"
}
```
### Common Errors
@@ -705,8 +808,11 @@ curl -X POST http://localhost:8000/image/info \
# Encode using multipart (LSB mode - default)
with open("reference.jpg", "rb") as ref, open("carrier.png", "rb") as carrier:
response = requests.post(f"{BASE_URL}/encode/multipart", files={
"reference_photo": ref,
"carrier": carrier,
}, data={
"message": "Secret message",
with open("reference.jpg", "rb") as ref, open("carrier.png", "rb") as carrier:
"day_phrase": "apple forest thunder",
"pin": "123456"
})
@@ -730,7 +836,7 @@ creds = response.json()
with open("stego_social.jpg", "wb") as f:
f.write(response.content)
```
# Decode using multipart (auto-detects mode)
with open("reference.jpg", "rb") as ref, open("stego.png", "rb") as stego:
response = requests.post(f"{BASE_URL}/decode/multipart", files={
"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"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('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.png', response.data);
console.log('Encoded with DCT mode');
console.log('Embedding mode:', response.headers['x-stegasoo-embedding-mode']);
}
async function decode() {
const form = new FormData();
@@ -766,11 +891,14 @@ const axios = require('axios');
const response = await axios.post(`${BASE_URL}/decode/multipart`, form, {
headers: form.getHeaders()
});
headers: form.getHeaders()
console.log('Decoded:', response.data.message);
console.log('Mode detected:', response.data.embedding_mode_detected);
}
encodeDCT().then(decode);
```
### Go
```go
@@ -779,8 +907,9 @@ async function encode() {
import (
"bytes"
"encoding/json"
import (
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
@@ -788,16 +917,17 @@ async function decode() {
func main() {
// Encode with DCT mode
body := &bytes.Buffer{}
)
writer := multipart.NewWriter(body)
writer.WriteField("message", "Secret message")
writer.WriteField("day_phrase", "apple forest thunder")
writer.WriteField("pin", "123456")
writer.WriteField("embedding_mode", "dct")
writer.WriteField("output_format", "jpeg")
writer.WriteField("color_mode", "color")
ref, _ := os.Open("reference.jpg")
writer.WriteField("pin", "123456")
refPart, _ := writer.CreateFormFile("reference_photo", "reference.jpg")
io.Copy(refPart, ref)
ref.Close()
@@ -816,13 +946,16 @@ import (
// Check embedding mode from header
fmt.Println("Embedding mode:", resp.Header.Get("X-Stegasoo-Embedding-Mode"))
stego, _ := os.Create("stego.jpg")
io.Copy(stego, resp.Body)
stego.Close()
resp.Body.Close()
fmt.Println("Encoded successfully with DCT mode")
}
```
### Shell Script (Bash)
```bash
@@ -842,12 +975,15 @@ func main() {
-F "day_phrase=$PHRASE" \
-F "pin=$PIN" \
-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
echo "Encoding with DCT mode..."
curl -s -X POST "$BASE_URL/encode/multipart" \
-F "message=$MESSAGE" \
-F "day_phrase=$PHRASE" \
-F "pin=$PIN" \
-F "embedding_mode=dct" \
@@ -863,27 +999,43 @@ PHRASE="apple forest thunder"
echo "Decoding..."
RESULT=$(curl -s -X POST "$BASE_URL/decode/multipart" \
-F "day_phrase=$PHRASE" \
## Rate Limiting
-F "pin=$PIN" \
-F "reference_photo=@$REF_PHOTO" \
-F "stego_image=@stego_dct.jpg")
echo "Decoded message: $(echo $RESULT | jq -r '.message')"
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
- Use HTTPS in production
- Configure TLS at reverse proxy level
### Memory Usage
- Argon2id requires 256MB RAM per operation
- DCT mode adds ~100MB for scipy operations
@@ -917,9 +1069,15 @@ location /api/ {
|------|--------------|
| LSB | Maximum capacity but fragile |
| DCT | Lower capacity but survives recompression |
Both modes use identical encryption (AES-256-GCM with Argon2id).
---
## Interactive Documentation
When the API is running, visit:
- **Swagger UI**: http://localhost:8000/docs
- **ReDoc**: http://localhost:8000/redoc
@@ -927,6 +1085,8 @@ The API validates:
## See Also
- [CLI Documentation](CLI.md) - Command-line interface
- [Web UI Documentation](WEB_UI.md) - Browser interface
- [README](README.md) - Project overview
### Credential Handling
@@ -934,6 +1094,15 @@ The API validates:
- No persistent storage of secrets
- 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

View File

@@ -11,6 +11,7 @@ Complete command-line interface reference for Stegasoo steganography operations.
- [encode](#encode-command)
- [decode](#decode-command)
- [info](#info-command)
- [Embedding Modes](#embedding-modes)
- [Security Factors](#security-factors)
- [Workflow Examples](#workflow-examples)
- [Piping & Scripting](#piping--scripting)
@@ -27,6 +28,9 @@ Complete command-line interface reference for Stegasoo steganography operations.
# CLI only
pip install stegasoo[cli]
# CLI with DCT support
pip install stegasoo[cli,dct]
# With all extras
pip install stegasoo[all]
```
@@ -36,7 +40,7 @@ pip install stegasoo[all]
```bash
git clone https://github.com/example/stegasoo.git
cd stegasoo
pip install -e ".[cli]"
pip install -e ".[cli,dct]"
```
### Verify Installation
@@ -44,6 +48,9 @@ pip install -e ".[cli]"
```bash
stegasoo --version
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)
stegasoo generate --pin --words 3
# 2. Encode a message
# 2. Encode a message (LSB mode - default)
stegasoo encode \
--ref secret_photo.jpg \
--carrier meme.png \
@@ -62,7 +69,17 @@ stegasoo encode \
--pin 123456 \
--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 \
--ref secret_photo.jpg \
--stego stego_abc123_20251227.png \
@@ -106,9 +123,9 @@ stegasoo generate
Output:
```
════════════════════════════════════════════════════════════
═══════════════════════════════════════════════════════════════
STEGASOO CREDENTIALS
════════════════════════════════════════════════════════════
═══════════════════════════════════════════════════════════════
⚠️ MEMORIZE THESE AND CLOSE THIS WINDOW
Do not screenshot or save to file!
@@ -171,19 +188,22 @@ stegasoo encode [OPTIONS]
#### Options
| Option | Short | Type | Required | Description |
|--------|-------|------|----------|-------------|
| `--ref` | `-r` | path | ✓ | Reference photo (shared secret) |
| `--carrier` | `-c` | path | ✓ | Carrier image to hide message in |
| `--phrase` | `-p` | string | ✓ | Today's passphrase |
| `--message` | `-m` | string | | Message to encode |
| `--message-file` | `-f` | path | | Read message from file |
| `--pin` | | string | * | Static PIN (6-9 digits) |
| `--key` | `-k` | path | * | RSA key file |
| `--key-password` | | string | | Password for RSA key |
| `--output` | `-o` | path | | Output filename |
| `--date` | | YYYY-MM-DD | | Date override |
| `--quiet` | `-q` | flag | | Suppress output |
| Option | Short | Type | Required | Default | Description |
|--------|-------|------|----------|---------|-------------|
| `--ref` | `-r` | path | ✓ | | Reference photo (shared secret) |
| `--carrier` | `-c` | path | ✓ | | Carrier image to hide message in |
| `--phrase` | `-p` | string | ✓ | | Today's passphrase |
| `--message` | `-m` | string | | | Message to encode |
| `--message-file` | `-f` | path | | | Read message from file |
| `--pin` | | string | * | | Static PIN (6-9 digits) |
| `--key` | `-k` | path | * | | RSA key file |
| `--key-password` | | string | | | Password for RSA key |
| `--output` | `-o` | path | | | Output filename |
| `--date` | | YYYY-MM-DD | | | Date override |
| `--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.
@@ -206,7 +226,7 @@ stegasoo encode [OPTIONS]
#### Examples
**Basic encoding with PIN:**
**Basic encoding with PIN (LSB mode - default):**
```bash
stegasoo encode \
--ref photos/vacation.jpg \
@@ -221,10 +241,60 @@ Output:
✓ Encoded successfully!
Output: a1b2c3d4_20251227.png
Size: 245,832 bytes
Mode: LSB
Capacity used: 12.4%
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:**
```bash
stegasoo encode \
@@ -291,7 +361,7 @@ stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 -m "msg" -q -
### 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
@@ -328,6 +398,24 @@ stegasoo decode \
Output:
```
✓ 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
```
@@ -377,7 +465,7 @@ stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q | gpg --decr
### 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
@@ -405,10 +493,15 @@ Image: vacation_photo.png
Pixels: 2,073,600
Mode: RGB
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
stegasoo info stego_a1b2c3d4_20251227.png
```
@@ -420,12 +513,88 @@ Image: stego_a1b2c3d4_20251227.png
Pixels: 2,073,600
Mode: RGB
Format: PNG
Capacity: ~776,970 bytes (758 KB)
Stego Info:
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
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
```
**Sender (daily):**
**Sender (daily - private channel):**
```bash
# Get today's phrase from your memorized list
TODAY_PHRASE="monday phrase words"
# Encode message
# For email, file transfer, etc. (no recompression)
stegasoo encode \
-r our_shared_photo.jpg \
-c random_meme.png \
-p "$TODAY_PHRASE" \
--pin 847293 \
-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):**
```bash
# Use the phrase for the day the message was SENT
# Works for both LSB and DCT (auto-detected)
stegasoo decode \
-r our_shared_photo.jpg \
-s received_image.png \
@@ -496,7 +673,7 @@ stegasoo decode \
### Batch Processing
**Encode multiple messages:**
**Encode multiple messages (LSB):**
```bash
#!/bin/bash
PHRASE="apple forest thunder"
@@ -517,6 +694,25 @@ for file in messages/*.txt; do
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
```bash
@@ -531,6 +727,31 @@ stegasoo encode \
-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
@@ -585,6 +806,15 @@ if ! stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q 2>/dev/
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
@@ -596,16 +826,31 @@ fi
| "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 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 |
| "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 |
| "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
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
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
- [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)
- [Decode Message](#decode-message)
- [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)
- [Workflow Examples](#workflow-examples)
- [Security Features](#security-features)
@@ -42,6 +45,8 @@ Built with Flask, Bootstrap 5, and a modern dark theme.
- ✅ Password-protected RSA key downloads
- ✅ Real-time entropy calculations
- ✅ 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]
```
This automatically installs DCT dependencies (scipy, jpegio) for full functionality.
### From Source
```bash
@@ -210,6 +217,18 @@ Hide a secret message inside an image.
\* 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
Both image upload zones support:
@@ -237,9 +256,10 @@ Saturday's Phrase: [ ]
#### Encoding Process
1. Fill in all required fields
2. Click "Encode Message"
3. Wait for processing (shows spinner)
4. Redirected to result page
2. (Optional) Expand "Advanced Options" for DCT mode
3. Click "Encode Message"
4. Wait for processing (shows spinner)
5. Redirected to result page
#### Result Page
@@ -255,6 +275,9 @@ After successful encoding:
│ Your secret message is hidden │
│ in this image │
│ │
│ Mode: DCT (Color, JPEG) │ ← v3.0+ shows mode info
│ Capacity used: 45.2% │
│ │
│ [ Download Image ] │
│ [ Share Image ] │
│ │
@@ -299,6 +322,10 @@ Extract a hidden message from a stego image.
\* 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
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
The page includes built-in troubleshooting guidance:
- ✓ Use the **exact same reference photo** file
- ✓ Use the phrase for the **encoding day**, not today
- ✓ Provide the **same security factors** used during encoding
- ✓ Ensure the stego image hasn't been **resized or recompressed**
- ✓ If using RSA key, verify the **password is correct**
If decryption fails:
1. **Check the date** - Use phrase for encoding day, not today
2. **Same reference photo** - Must be identical file
3. **Correct PIN/RSA** - Match what was used for encoding
4. **Image integrity** - Ensure no resizing/recompression
---
@@ -347,62 +372,130 @@ The page includes built-in troubleshooting guidance:
**URL:** `/about`
Learn about Stegasoo's security model and best practices.
Information about the Stegasoo project, security model, and credits.
#### Sections
---
**System Status:**
- Argon2id availability (vs PBKDF2 fallback)
- AES-256-GCM encryption status
## Embedding Modes
**Security Model Table:**
Stegasoo v3.0+ offers two steganography algorithms, each with different trade-offs.
| Component | Entropy | Purpose |
|-----------|---------|---------|
| 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** |
### LSB Mode (Default)
**Attack Resistance:**
**Least Significant Bit** embedding modifies the least significant bits of pixel values.
What attackers can't do:
- Brute force (2^133 combinations)
- Use rainbow tables (random salt)
- Detect hidden data (random pixels)
- Use GPU farms (256MB RAM per attempt)
| Aspect | Details |
|--------|---------|
| **Capacity** | ~3 bits/pixel (~770 KB for 1920×1080) |
| **Output Format** | PNG only (lossless required) |
| **Resilience** | ❌ Destroyed by JPEG compression |
| **Best For** | Maximum capacity, controlled sharing |
Real threats:
- Social engineering
- Physical device access
- Malware/keyloggers
- Shoulder surfing
**When to use LSB:**
- Sharing via lossless channels (email attachment, file transfer)
- Maximum message capacity needed
- Recipient won't modify the image
**Best Practices:**
### DCT Mode (Experimental)
Do:
- Memorize phrases and PIN
- Use reference photo both parties have
- Use different carrier images each time
- Share stego images through normal channels
**Discrete Cosine Transform** embedding hides data in frequency domain coefficients.
Don't:
- Transmit the reference photo
- Reuse carrier images
- Store credentials digitally
- Resize/recompress stego images
| Aspect | Details |
|--------|---------|
| **Capacity** | ~0.25 bits/pixel (~65 KB for 1920×1080 PNG, ~30-50 KB JPEG) |
| **Output Formats** | PNG or JPEG |
| **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
### Navigation
The navbar provides quick access to all pages:
### Layout Structure
```
[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
@@ -415,6 +508,7 @@ The navbar provides quick access to all pages:
| Success | Green | Positive actions |
| Warning | Yellow | Caution messages |
| Error | Red | Error states |
| Experimental | Orange badge | DCT mode indicator |
### Form Validation
@@ -462,7 +556,7 @@ Types:
- The PIN
- The reference photo file (if not already shared)
### Sending a Secret Message
### Sending a Secret Message (LSB - Default)
1. Go to `/encode`
2. Upload your shared reference photo
@@ -472,7 +566,22 @@ Types:
6. Enter your PIN
7. Click "Encode Message"
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
@@ -486,6 +595,8 @@ Types:
8. Click "Decode Message"
9. Read the secret message
> 💡 Decoding automatically detects LSB vs DCT mode—no configuration needed!
### Changing Credentials
To rotate to new credentials:
@@ -527,6 +638,15 @@ To rotate to new credentials:
| Access control | Random 16-byte file ID |
| 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
@@ -561,8 +681,8 @@ gunicorn \
```
**Worker Calculation:**
- Each encode/decode uses ~256MB RAM (Argon2)
- Formula: `workers = (available_RAM - 512MB) / 256MB`
- Each encode/decode uses ~256MB RAM (Argon2) + ~100MB for scipy (DCT mode)
- Formula: `workers = (available_RAM - 512MB) / 350MB`
**With Nginx (reverse proxy):**
```nginx
@@ -594,9 +714,9 @@ services:
deploy:
resources:
limits:
memory: 512M
memory: 768M # Increased for scipy/DCT
reservations:
memory: 256M
memory: 384M
```
---
@@ -617,7 +737,19 @@ services:
1. Check the date in the stego filename
2. Use the phrase for that specific day
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"
@@ -626,7 +758,8 @@ services:
**Solutions:**
1. Use a larger carrier image (more pixels)
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"
@@ -658,6 +791,17 @@ services:
2. If key is unencrypted, leave password blank
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 | Status | Notes |
@@ -672,10 +816,12 @@ services:
**Slow encoding/decoding:**
- 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:**
- Normal: Argon2 requires 256MB RAM
- DCT mode adds scipy memory overhead (~100MB)
- Configure worker count based on available RAM
---
@@ -689,6 +835,7 @@ The UI adapts to mobile screens:
- Touch-friendly buttons (48px minimum)
- Readable text without zooming
- Scrollable tables
- Collapsible "Advanced Options" for cleaner mobile view
### Mobile-Specific Features

View File

@@ -1,10 +1,11 @@
#!/usr/bin/env python3
"""
Stegasoo REST API (v3.0)
Stegasoo REST API (v3.0.1)
FastAPI-based REST API for steganography operations.
Supports both text messages and file embedding.
NEW in v3.0: LSB and DCT embedding modes.
NEW in v3.0.1: DCT color mode and JPEG output format.
"""
import io
@@ -70,7 +71,12 @@ Secure steganography with hybrid authentication. Supports text messages and file
## Embedding Modes (v3.0)
- **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.
""",
@@ -86,6 +92,8 @@ Use the `/modes` endpoint to check availability and `/compare` to compare capaci
EmbedModeType = Literal["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
embed_mode: EmbedModeType = Field(
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
embed_mode: EmbedModeType = Field(
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
day_of_week: str
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):
@@ -211,20 +246,36 @@ class CompareModesResponse(BaseModel):
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):
"""Response showing available embedding modes."""
lsb: dict
dct: dict
dct: DctModeInfo
class StatusResponse(BaseModel):
version: str
has_argon2: bool
has_qrcode_read: bool
has_dct: bool # NEW in v3.0
has_dct: bool
day_names: list[str]
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):
@@ -263,8 +314,16 @@ class ErrorResponse(BaseModel):
async def root():
"""Get API status and configuration."""
available_modes = ["lsb"]
dct_features = None
if has_dct_support():
available_modes.append("dct")
dct_features = {
"output_formats": ["png", "jpeg"],
"color_modes": ["grayscale", "color"],
"default_output_format": "png",
"default_color_mode": "grayscale",
}
return StatusResponse(
version=__version__,
@@ -273,7 +332,8 @@ async def root():
has_dct=has_dct_support(),
day_names=list(DAY_NAMES),
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.
NEW in v3.0: Shows LSB and DCT mode availability.
NEW in v3.0.1: Shows DCT color modes and output formats.
"""
return ModesResponse(
lsb={
@@ -292,14 +353,15 @@ async def api_modes():
"output_format": "PNG (color)",
"capacity_ratio": "100%",
},
dct={
"available": has_dct_support(),
"name": "DCT Domain",
"description": "Embed in DCT coefficients, outputs grayscale PNG",
"output_format": "PNG (grayscale)",
"capacity_ratio": "~20% of LSB",
"requires": "scipy",
}
dct=DctModeInfo(
available=has_dct_support(),
name="DCT Domain",
description="Embed in DCT coefficients, frequency domain steganography",
output_formats=["png", "jpeg"],
color_modes=["grayscale", "color"],
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_kb": round(comparison['dct']['capacity_kb'], 1),
"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),
},
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))
# ============================================================================
# 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)
# ============================================================================
@@ -476,6 +574,7 @@ async def api_encode(request: EncodeRequest):
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.1: Supports dct_output_format ('png' or 'jpeg') and dct_color_mode ('grayscale' or 'color').
"""
# Validate mode
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)
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(
message=request.message,
reference_photo=ref_photo,
@@ -495,12 +601,19 @@ async def api_encode(request: EncodeRequest):
rsa_key_data=rsa_key,
rsa_password=request.rsa_password,
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')
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(
stego_image_base64=stego_b64,
filename=result.filename,
@@ -508,6 +621,8 @@ async def api_encode(request: EncodeRequest):
date_used=result.date_used,
day_of_week=day_of_week,
embed_mode=request.embed_mode,
output_format=output_format,
color_mode=color_mode,
)
except CapacityError as e:
@@ -526,6 +641,7 @@ async def api_encode_file(request: EncodeFileRequest):
File data must be base64-encoded.
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
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
)
# Get DCT parameters
dct_params = _get_dct_params(
request.embed_mode,
request.dct_output_format,
request.dct_color_mode
)
result = encode(
message=payload,
reference_photo=ref_photo,
@@ -552,12 +675,19 @@ async def api_encode_file(request: EncodeFileRequest):
rsa_key_data=rsa_key,
rsa_password=request.rsa_password,
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')
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(
stego_image_base64=stego_b64,
filename=result.filename,
@@ -565,6 +695,8 @@ async def api_encode_file(request: EncodeFileRequest):
date_used=result.date_used,
day_of_week=day_of_week,
embed_mode=request.embed_mode,
output_format=output_format,
color_mode=color_mode,
)
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').
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
if request.embed_mode == "dct" and not has_dct_support():
@@ -605,7 +740,7 @@ async def api_decode(request: DecodeRequest):
pin=request.pin,
rsa_key_data=rsa_key,
rsa_password=request.rsa_password,
embed_mode=request.embed_mode, # NEW in v3.0
embed_mode=request.embed_mode,
)
if result.is_file:
@@ -645,16 +780,20 @@ async def api_encode_multipart(
rsa_key_qr: Optional[UploadFile] = File(None),
rsa_password: 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).
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).
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.1: Supports dct_output_format ('png' or 'jpeg') and dct_color_mode ('grayscale' or 'color').
"""
# Validate mode
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():
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:
ref_data = await reference_photo.read()
carrier_data = await carrier.read()
@@ -701,6 +846,9 @@ async def api_encode_multipart(
else:
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(
message=payload,
reference_photo=ref_data,
@@ -710,20 +858,26 @@ async def api_encode_multipart(
rsa_key_data=rsa_key_data,
rsa_password=effective_password,
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)
output_format, color_mode, mime_type = _get_output_info(
embed_mode, dct_output_format, dct_color_mode
)
return Response(
content=result.stego_image,
media_type="image/png",
media_type=mime_type,
headers={
"Content-Disposition": f"attachment; filename={result.filename}",
"X-Stegasoo-Date": result.date_used,
"X-Stegasoo-Day": day_of_week,
"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_qr: Optional[UploadFile] = File(None),
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).
@@ -755,6 +909,8 @@ async def api_decode_multipart(
Returns JSON with payload_type indicating text or file.
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
if embed_mode not in ("auto", "lsb", "dct"):
@@ -795,7 +951,7 @@ async def api_decode_multipart(
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=effective_password,
embed_mode=embed_mode, # NEW in v3.0
embed_mode=embed_mode,
)
if result.is_file:
@@ -866,7 +1022,7 @@ async def api_image_info(
capacity_bytes=comparison['dct']['capacity_bytes'],
capacity_kb=round(comparison['dct']['capacity_kb'], 1),
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
"""
Stegasoo CLI - Command-line interface for steganography operations.
Stegasoo CLI - Command-line interface for steganography operations (v3.0.1).
Usage:
stegasoo generate [OPTIONS]
@@ -8,7 +8,12 @@ Usage:
stegasoo decode [OPTIONS]
stegasoo verify [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
@@ -73,14 +78,19 @@ def cli():
Hide encrypted messages or files in images using a combination of:
\b
Reference photo (something you have)
Daily passphrase (something you know)
Static PIN or RSA key (additional security)
- Reference photo (something you have)
- Daily passphrase (something you know)
- Static PIN or RSA key (additional security)
\b
NEW in v3.0 - Embedding Modes:
LSB mode (default): Full color output, higher capacity
DCT mode: Grayscale output, ~20% capacity, better stealth
Embedding Modes (v3.0):
- LSB mode (default): Full color output, higher capacity
- 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
@@ -148,29 +158,29 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
# Pretty output
click.echo()
click.secho("" * 60, fg='cyan')
click.secho("=" * 60, fg='cyan')
click.secho(" STEGASOO CREDENTIALS", fg='cyan', bold=True)
click.secho("" * 60, fg='cyan')
click.secho("=" * 60, fg='cyan')
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.echo()
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.echo()
click.secho("─── DAILY PHRASES ───", fg='green')
click.secho("--- DAILY PHRASES ---", fg='green')
for day in DAY_NAMES:
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.echo()
if creds.rsa_key_pem:
click.secho("─── RSA KEY ───", fg='green')
click.secho("--- RSA KEY ---", fg='green')
if output:
# Save to file
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()
click.secho("─── SECURITY ───", fg='green')
click.secho("--- SECURITY ---", fg='green')
click.echo(f" Phrase entropy: {creds.phrase_entropy} bits")
if creds.pin:
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('--date', 'date_str', help='Date override (YYYY-MM-DD)')
@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')
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.
@@ -230,27 +245,37 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
\b
Embedding Modes (v3.0):
--mode lsb Spatial LSB embedding (default)
Full color output (PNG/BMP)
Higher capacity (~375 KB/megapixel)
- Full color output (PNG/BMP)
- Higher capacity (~375 KB/megapixel)
--mode dct DCT domain embedding (requires scipy)
• Grayscale output only
Lower capacity (~75 KB/megapixel)
Better resistance to visual analysis
- Configurable color/grayscale output
- Lower capacity (~75 KB/megapixel)
- 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
Examples:
# 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
# With RSA key file
stegasoo encode -r photo.jpg -c meme.png -p "words" -k mykey.pem -m "secret"
# DCT mode - color JPEG (v3.0.1)
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
stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -e secret.pdf
# DCT mode - color PNG (best quality + color preservation)
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
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"
)
# 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
payload = None
@@ -329,7 +360,10 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
)
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(
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_password=effective_key_password,
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
@@ -353,13 +389,15 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
out_path.write_bytes(result.stego_image)
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" Size: {len(result.stego_image):,} bytes")
click.echo(f" Capacity used: {result.capacity_percent:.1f}%")
click.echo(f" Date: {result.date_used}")
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:
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.
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
Extraction Modes (v3.0):
--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 "",
rsa_key_data=rsa_key_data,
rsa_password=effective_key_password,
embed_mode=embed_mode, # NEW in v3.0
embed_mode=embed_mode,
)
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)
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" Size: {len(result.file_data):,} bytes")
if result.mime_type:
@@ -491,13 +532,13 @@ def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, embed
if output:
Path(output).write_text(result.message)
if not quiet:
click.secho(" Decoded successfully!", fg='green')
click.secho("[OK] Decoded successfully!", fg='green')
click.echo(f" Saved to: {output}")
else:
if quiet:
click.echo(result.message)
else:
click.secho(" Decoded successfully!", fg='green')
click.secho("[OK] Decoded successfully!", fg='green')
click.echo()
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 "",
rsa_key_data=rsa_key_data,
rsa_password=effective_key_password,
embed_mode=embed_mode, # NEW in v3.0
embed_mode=embed_mode,
)
# 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
click.echo(json.dumps(output, indent=2))
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" Size: {payload_size:,} bytes")
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))
sys.exit(1)
else:
click.secho(" Verification failed", fg='red', bold=True)
click.secho("[FAIL] Verification failed", fg='red', bold=True)
click.echo(f" Error: {e}")
sys.exit(1)
except StegasooError as e:
@@ -690,6 +731,8 @@ def info(image, as_json):
"kb": round(comparison['dct']['capacity_kb'], 1),
"available": comparison['dct']['available'],
"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.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" Mode: {result.details['mode']}")
click.echo(f" Format: {result.details['format']}")
@@ -710,10 +753,13 @@ def info(image, as_json):
click.secho(" Capacity:", bold=True)
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 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:
click.echo()
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()
@@ -767,7 +813,8 @@ def compare(image, payload_size, as_json):
"capacity_bytes": comparison['dct']['capacity_bytes'],
"capacity_kb": round(comparison['dct']['capacity_kb'], 1),
"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),
},
},
@@ -784,60 +831,63 @@ def compare(image, payload_size, as_json):
return
click.echo()
click.secho(f"═══ Mode Comparison: {image} ═══", fg='cyan', bold=True)
click.echo(f" Dimensions: {comparison['width']} × {comparison['height']}")
click.secho(f"=== Mode Comparison: {image} ===", fg='cyan', bold=True)
click.echo(f" Dimensions: {comparison['width']} x {comparison['height']}")
click.echo()
# LSB mode
click.secho(" ┌─── LSB Mode ───", fg='green')
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" Status: Available")
click.echo(" ")
click.secho(" +--- LSB Mode ---", fg='green')
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" | Status: [OK] Available")
click.echo(" |")
# DCT mode
click.secho(" ├─── DCT Mode ───", fg='blue')
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.secho(" +--- DCT Mode ---", fg='blue')
click.echo(f" | Capacity: {comparison['dct']['capacity_bytes']:,} bytes ({comparison['dct']['capacity_kb']:.1f} KB)")
click.echo(f" | Ratio: {comparison['dct']['ratio_vs_lsb']:.1f}% of LSB capacity")
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:
click.secho(f" Status: Requires scipy (pip install scipy)", fg='yellow')
click.echo(" ")
click.secho(f" | Status: [X] Requires scipy (pip install scipy)", fg='yellow')
click.echo(" |")
# Payload check
if payload_size:
click.secho(" ├─── Payload Check ───", fg='magenta')
click.echo(f" Size: {payload_size:,} bytes")
click.secho(" +--- Payload Check ---", fg='magenta')
click.echo(f" | Size: {payload_size:,} bytes")
fits_lsb = payload_size <= comparison['lsb']['capacity_bytes']
fits_dct = payload_size <= comparison['dct']['capacity_bytes']
lsb_icon = "" if fits_lsb else ""
dct_icon = "" if fits_dct else ""
lsb_icon = "[OK]" if fits_lsb else "[X]"
dct_icon = "[OK]" if fits_dct else "[X]"
lsb_color = 'green' if fits_lsb 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.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.echo(" ")
click.echo(" |")
# Recommendation
click.secho(" └─── Recommendation ───", fg='yellow')
click.secho(" +--- Recommendation ---", fg='yellow')
if not comparison['dct']['available']:
click.echo(" Use LSB mode (DCT unavailable)")
elif payload_size:
if fits_dct:
click.echo(" DCT mode for better stealth (payload fits both modes)")
click.echo(" Use --dct-color color to preserve original colors")
elif fits_lsb:
click.echo(" LSB mode (payload too large for DCT)")
else:
click.secho(" Payload too large for both modes!", fg='red')
click.secho(" [X] Payload too large for both modes!", fg='red')
else:
click.echo(" LSB for larger payloads, DCT for better stealth")
click.echo(" DCT supports color output with --dct-color color")
click.echo()
@@ -881,7 +931,7 @@ def strip_metadata_cmd(image, output, output_format, quiet):
out_path.write_bytes(clean_data)
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" 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()
@@ -901,12 +951,12 @@ def modes():
Displays which modes are available and their characteristics.
"""
click.echo()
click.secho("═══ Stegasoo Embedding Modes ═══", fg='cyan', bold=True)
click.secho("=== Stegasoo Embedding Modes ===", fg='cyan', bold=True)
click.echo()
# LSB Mode
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(" Capacity: ~375 KB per megapixel")
click.echo(" Use case: Larger payloads, color preservation")
@@ -916,18 +966,36 @@ def modes():
# DCT Mode
click.secho(" DCT Mode (Frequency Domain)", fg='blue', bold=True)
if has_dct_support():
click.echo(" Status: Available")
click.echo(" Status: [OK] Available")
else:
click.secho(" Status: Requires scipy", fg='yellow')
click.secho(" Status: [X] Requires scipy", fg='yellow')
click.echo(" Install: pip install scipy")
click.echo(" Output: PNG (grayscale only)")
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()
click.secho(" Tip:", dim=True)
click.echo(" Use 'stegasoo compare <image>' to see capacity for both modes")
# DCT Options (v3.0.1)
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()

View File

@@ -5,7 +5,7 @@ Stegasoo Web Frontend (v3.0.1)
Flask-based web UI for steganography operations.
Supports both text messages and file embedding.
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
@@ -532,6 +532,11 @@ def encode_page():
if dct_output_format not in ('png', 'jpeg'):
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
if embed_mode == 'dct' and not has_dct_support():
flash('DCT mode requires scipy. Install with: pip install scipy', 'error')
@@ -624,7 +629,7 @@ def encode_page():
else:
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(
message=payload,
reference_photo=ref_data,
@@ -634,8 +639,9 @@ def encode_page():
rsa_key_data=rsa_key_data,
rsa_password=key_password,
date_str=date_str,
embed_mode=embed_mode, # NEW in v3.0
dct_output_format=dct_output_format if embed_mode == 'dct' else None, # NEW in v3.0.1
embed_mode=embed_mode,
dct_output_format=dct_output_format if embed_mode == 'dct' else None,
dct_color_mode=dct_color_mode if embed_mode == 'dct' else None,
)
# Determine actual output format for filename and storage
@@ -660,6 +666,7 @@ def encode_page():
'timestamp': time.time(),
'embed_mode': embed_mode,
'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,
}
@@ -699,7 +706,8 @@ def encode_result(file_id):
filename=file_info['filename'],
thumbnail_url=url_for('encode_thumbnail', thumb_id=thumbnail_id) if thumbnail_id else None,
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_password=key_password,
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:

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>
<strong>DCT Mode</strong>
{% if has_dct %}
<span class="badge bg-info ms-auto">Stealth</span>
<span class="badge bg-warning text-dark ms-auto">Experimental</span>
{% else %}
<span class="badge bg-secondary ms-auto">Unavailable</span>
{% endif %}
</div>
<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>Better detection resistance</li>
</ul>
@@ -266,47 +266,98 @@
<div class="form-text mt-2" id="modeHint">
<i class="bi bi-lightbulb me-1"></i>
<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>
<!-- DCT Output Format (shown only when DCT selected) -->
<div class="mb-3 d-none" id="dctOutputFormatGroup">
<label class="form-label">
<i class="bi bi-file-image me-1"></i> DCT Output Format
</label>
<!-- DCT Options Panel (shown only when DCT selected) -->
<div class="d-none" id="dctOptionsPanel">
<div class="row g-2">
<div class="col-6">
<div class="form-check card p-2 text-center" 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-success fs-5 d-block"></i>
<strong>PNG</strong>
<div class="small text-muted">Lossless, larger</div>
</label>
<hr class="my-3">
<div class="alert alert-warning small mb-3">
<i class="bi bi-flask me-1"></i>
<strong>Experimental Feature:</strong> DCT embedding is still being refined.
Color mode preserves original colors but extraction uses Y channel only.
</div>
<!-- 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 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 class="form-text mt-2">
<i class="bi bi-info-circle me-1"></i>
<strong>Color</strong> preserves original image colors (recommended).
<strong>Grayscale</strong> converts to B&W (traditional DCT steganography).
</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).
<!-- DCT Output Format -->
<div class="mb-3">
<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>
<!-- Capacity Comparison (populated by JS) -->
<div class="d-none" id="modeCapacityComparison">
<hr class="my-3">
<div class="alert alert-secondary small mb-0">
<div class="row text-center">
<div class="col-6 border-end">
@@ -353,7 +404,7 @@
<div class="alert alert-secondary mt-4 small">
<i class="bi bi-info-circle me-1"></i>
<strong>Limits:</strong>
Carrier image max ~24 megapixels (6000×4000).
Carrier image max ~24 megapixels (6000x4000).
Files max 30MB upload.
Payload max {{ max_payload_kb }} KB.
</div>
@@ -470,8 +521,9 @@ document.getElementById('encodeForm').addEventListener('submit', function(e) {
let modeLabel = selectedMode.toUpperCase();
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';
modeLabel += ` ${outputFormat.toUpperCase()}`;
modeLabel += ` (${colorMode}, ${outputFormat.toUpperCase()})`;
}
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) {
// Update top panel
carrierDimensions.textContent = `${data.width} × ${data.height}`;
carrierDimensions.textContent = `${data.width} x ${data.height}`;
lsbCapacityBadge.textContent = `LSB: ${data.lsb.capacity_kb} KB`;
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 dctModeCard = document.getElementById('dctModeCard');
const modeLsb = document.getElementById('modeLsb');
const modeDct = document.getElementById('modeDct');
const dctOutputFormatGroup = document.getElementById('dctOutputFormatGroup');
const dctOptionsPanel = document.getElementById('dctOptionsPanel');
// DCT format cards
const dctPngCard = document.getElementById('dctPngCard');
const dctJpegCard = document.getElementById('dctJpegCard');
const dctFormatPng = document.getElementById('dctFormatPng');
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() {
// Mode cards
lsbModeCard.classList.toggle('border-primary', modeLsb.checked);
@@ -593,28 +653,40 @@ function updateModeCardHighlight() {
dctModeCard.classList.toggle('border-info', modeDct.checked);
dctModeCard.classList.toggle('border-2', modeDct.checked);
// Show/hide DCT output format selector
if (dctOutputFormatGroup) {
dctOutputFormatGroup.classList.toggle('d-none', !modeDct.checked);
// Show/hide DCT options panel
if (dctOptionsPanel) {
dctOptionsPanel.classList.toggle('d-none', !modeDct.checked);
}
}
function updateDctFormatCardHighlight() {
if (dctPngCard && dctJpegCard) {
dctPngCard.classList.toggle('border-success', dctFormatPng.checked);
dctPngCard.classList.toggle('border-primary', dctFormatPng.checked);
dctPngCard.classList.toggle('border-2', dctFormatPng.checked);
dctJpegCard.classList.toggle('border-warning', 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);
modeDct.addEventListener('change', updateModeCardHighlight);
dctFormatPng?.addEventListener('change', updateDctFormatCardHighlight);
dctFormatJpeg?.addEventListener('change', updateDctFormatCardHighlight);
dctColorColor?.addEventListener('change', updateDctColorCardHighlight);
dctColorGrayscale?.addEventListener('change', updateDctColorCardHighlight);
updateModeCardHighlight(); // Initial state
updateDctFormatCardHighlight(); // Initial state
updateDctColorCardHighlight(); // Initial state
// Advanced options chevron rotation
document.getElementById('advancedOptions').addEventListener('show.bs.collapse', function() {

View File

@@ -34,31 +34,60 @@
<code class="fs-5">{{ filename }}</code>
</div>
<!-- Mode and format badges (v3.0) -->
<!-- Mode and format badges (v3.0 / v3.0.1) -->
<div class="mb-4">
{% if embed_mode == 'dct' %}
<span class="badge bg-info fs-6">
<i class="bi bi-soundwave me-1"></i>DCT Mode
</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' %}
<span class="badge bg-warning text-dark fs-6 ms-1">
<i class="bi bi-file-earmark-richtext me-1"></i>JPEG
</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 %}
<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
</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 %}
{% else %}
<span class="badge bg-primary fs-6">
<i class="bi bi-grid-3x3-gap me-1"></i>LSB Mode
</span>
<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
</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 %}
</div>
@@ -88,6 +117,9 @@
{% endif %}
{% if embed_mode == 'dct' %}
<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 %}
</ul>
</div>

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "stegasoo"
version = "2.2.1"
version = "3.0.2"
description = "Secure steganography with hybrid photo + passphrase + PIN authentication"
readme = "README.md"
license = "MIT"
@@ -43,6 +43,11 @@ dependencies = [
]
[project.optional-dependencies]
# DCT steganography support (v3.0+)
dct = [
"scipy>=1.10.0",
"jpegio>=0.2.0",
]
cli = [
"click>=8.0.0",
"qrcode>=7.30"
@@ -55,6 +60,9 @@ web = [
"gunicorn>=21.0.0",
"qrcode>=7.3.0",
"pyzbar>=0.1.9",
# Include DCT support for web UI
"scipy>=1.10.0",
"jpegio>=0.2.0",
]
api = [
"fastapi>=0.100.0",
@@ -62,9 +70,12 @@ api = [
"python-multipart>=0.0.6",
"qrcode>=7.30",
"pyzbar>=0.1.9",
# Include DCT support for API
"scipy>=1.10.0",
"jpegio>=0.2.0",
]
all = [
"stegasoo[cli,web,api]",
"stegasoo[cli,web,api,dct,compression]",
]
dev = [
"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]
embed_mode: str = EMBED_MODE_LSB,
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:
"""
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
embed_mode: Embedding mode - 'lsb' (default) or 'dct' (v3.0+)
dct_output_format: For DCT mode - 'png' (lossless) or 'jpeg' (smaller)
dct_color_mode: For DCT mode - 'grayscale' (default) or 'color' (preserves colors)
Returns:
EncodeResult with stego image and metadata
@@ -349,16 +351,18 @@ def encode(
# Default LSB mode
>>> result = encode(message="Secret", ...)
# DCT mode with PNG output (lossless)
# DCT mode with grayscale PNG output (default)
>>> result = encode(message="Secret", ..., embed_mode='dct')
# DCT mode with JPEG output (smaller, natural)
>>> result = encode(message="Secret", ..., embed_mode='dct', dct_output_format='jpeg')
# DCT mode with color JPEG output
>>> result = encode(message="Secret", ..., embed_mode='dct',
... dct_output_format='jpeg', dct_color_mode='color')
"""
# Debug logging
debug.print(f"encode called: message type={type(message).__name__}, "
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
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'")
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
require_valid_payload(message)
require_valid_image(carrier_image, "Carrier image")
@@ -407,7 +416,7 @@ def encode(
debug.data(pixel_key, "Pixel key")
# 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(
encrypted,
carrier_image,
@@ -415,6 +424,7 @@ def encode(
output_format=output_format,
embed_mode=embed_mode,
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
@@ -468,6 +478,7 @@ def encode_file(
filename_override: Optional[str] = None,
embed_mode: str = EMBED_MODE_LSB,
dct_output_format: str = "png", # NEW in v3.0.1
dct_color_mode: str = "grayscale", # NEW in v3.0.1
) -> EncodeResult:
"""
Encode a file into an image.
@@ -487,12 +498,13 @@ def encode_file(
filename_override: Override the stored filename
embed_mode: 'lsb' (default) or 'dct' (v3.0+)
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:
EncodeResult with stego image and metadata
"""
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)
return encode(
@@ -507,6 +519,7 @@ def encode_file(
output_format=output_format,
embed_mode=embed_mode,
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,
embed_mode: str = EMBED_MODE_LSB,
dct_output_format: str = "png", # NEW in v3.0.1
dct_color_mode: str = "grayscale", # NEW in v3.0.1
) -> EncodeResult:
"""
Encode raw bytes with a filename into an image.
@@ -548,12 +562,14 @@ def encode_bytes(
mime_type: MIME type of the data
embed_mode: 'lsb' (default) or 'dct' (v3.0+)
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:
EncodeResult with stego image and metadata
"""
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)
return encode(
@@ -568,6 +584,7 @@ def encode_bytes(
output_format=output_format,
embed_mode=embed_mode,
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.
Supports PNG (lossless) or JPEG (natural, smaller) output.
Embeds data in DCT coefficients with two approaches:
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:
- More resistant to visual inspection
- Survives some image processing
- Lower capacity (~20% of LSB)
- Works in frequency domain
The JPEG approach is the "correct" way to do JPEG steganography because
it directly modifies the already-quantized coefficients without re-encoding.
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 struct
import hashlib
from dataclasses import dataclass
from typing import Optional, Literal
from typing import Optional, Literal, Tuple
from enum import Enum
import numpy as np
from PIL import Image
# Check for scipy availability
# Check for scipy availability (for PNG/DCT mode)
try:
from scipy.fftpack import dct, idct
HAS_SCIPY = True
@@ -32,6 +35,14 @@ except ImportError:
dct = 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
@@ -41,8 +52,6 @@ except ImportError:
BLOCK_SIZE = 8
# 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 = [
(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),
@@ -51,25 +60,29 @@ EMBED_POSITIONS = [
]
# 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
# Quantization step for embedding (larger = more robust, more visible)
# Quantization step for QIM embedding (larger = more robust, more visible)
QUANT_STEP = 25
# Magic bytes for DCT stego identification
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
# Output format options
OUTPUT_FORMAT_PNG = 'png'
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
# jpegio constants for JPEG coefficient embedding
JPEGIO_MAGIC = b'JPGS'
JPEGIO_MIN_COEF_MAGNITUDE = 2
JPEGIO_EMBED_CHANNEL = 0 # Y channel
# ============================================================================
# DATA CLASSES
@@ -91,7 +104,9 @@ class DCTEmbedStats:
usage_percent: float
image_width: 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
@@ -105,11 +120,11 @@ class DCTCapacityInfo:
bits_per_block: int
total_capacity_bits: int
total_capacity_bytes: int
usable_capacity_bytes: int # After header overhead
usable_capacity_bytes: int
# ============================================================================
# HELPER FUNCTIONS
# AVAILABILITY CHECKS
# ============================================================================
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:
"""Apply 2D DCT to a block."""
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)
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."""
h, w = image.shape
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[:h, :w] = image
# Mirror padding for smoother edges
if new_h > h:
padded[h:, :w] = image[h-(new_h-h):h, :w][::-1, :]
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)
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."""
h, w = original_size
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."""
# Quantization Index Modulation
quantized = round(coeff / quant_step)
quantized = round(coef / quant_step)
if (quantized % 2) != bit:
# Adjust to embed the bit
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:
quantized += 1 if coeff >= quantized * quant_step else -1
quantized += 1 if coef >= quantized * quant_step else -1
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."""
quantized = round(coeff / quant_step)
quantized = round(coef / quant_step)
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."""
# Create deterministic RNG from seed
hash_bytes = hashlib.sha256(seed).digest()
rng = np.random.RandomState(int.from_bytes(hash_bytes[:4], 'big'))
order = list(range(num_blocks))
rng.shuffle(order)
return order
def _save_stego_image(
image: np.ndarray,
output_format: str = OUTPUT_FORMAT_PNG
) -> bytes:
"""Save stego image in specified format."""
# Clip to valid range and convert to uint8
def _save_stego_image(image: np.ndarray, output_format: str = OUTPUT_FORMAT_PNG) -> bytes:
"""Save stego image in specified format (grayscale)."""
clipped = np.clip(image, 0, 255).astype(np.uint8)
img = Image.fromarray(clipped, mode='L')
buffer = io.BytesIO()
if output_format == OUTPUT_FORMAT_JPEG:
# High-quality JPEG with no chroma subsampling
img.save(
buffer,
format='JPEG',
quality=JPEG_OUTPUT_QUALITY,
subsampling=0, # 4:4:4 - no subsampling
optimize=True
)
img.save(buffer, format='JPEG', quality=JPEG_OUTPUT_QUALITY,
subsampling=0, optimize=True)
else:
# PNG (lossless, default)
img.save(buffer, format='PNG', optimize=True)
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:
"""Create DCT stego header."""
# Header format: MAGIC(4) + VERSION(1) + FLAGS(1) + LENGTH(4)
version = 1
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)."""
if len(header_bits) < HEADER_SIZE * 8:
raise ValueError("Insufficient header data")
# Convert bits to bytes
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)
@@ -245,7 +316,80 @@ def _parse_header(header_bits: list[int]) -> tuple[int, int, int]:
magic, version, flags, length = struct.unpack('>4sBBI', header_bytes)
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
@@ -254,11 +398,6 @@ def _parse_header(header_bits: list[int]) -> tuple[int, int, int]:
# PUBLIC API
# ============================================================================
def has_dct_support() -> bool:
"""Check if DCT steganography is available."""
return HAS_SCIPY
def calculate_dct_capacity(image_data: bytes) -> DCTCapacityInfo:
"""
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))
width, height = img.size
# Calculate blocks
blocks_x = width // BLOCK_SIZE
blocks_y = height // BLOCK_SIZE
total_blocks = blocks_x * blocks_y
# Bits per block (using selected coefficient positions)
bits_per_block = len(DEFAULT_EMBED_POSITIONS)
# Total capacity
total_bits = total_blocks * bits_per_block
total_bytes = total_bits // 8
# Usable capacity (minus header)
usable_bytes = max(0, total_bytes - HEADER_SIZE)
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:
"""
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
"""
"""Check if data will fit in the image using DCT embedding."""
capacity = calculate_dct_capacity(image_data)
return data_length <= capacity.usable_capacity_bytes
def estimate_capacity_comparison(image_data: bytes) -> dict:
"""
Compare LSB and DCT capacity for an image.
Args:
image_data: Image file bytes
Returns:
Dict with 'lsb' and 'dct' capacity info
"""
"""Compare LSB and DCT capacity for an image."""
img = Image.open(io.BytesIO(image_data))
width, height = img.size
pixels = width * height
# LSB capacity (3 bits per pixel for RGB, simplified)
lsb_bytes = (pixels * 3) // 8
# DCT capacity
if HAS_SCIPY:
dct_info = calculate_dct_capacity(image_data)
dct_bytes = dct_info.usable_capacity_bytes
else:
# Estimate without scipy
blocks = (width // 8) * (height // 8)
dct_bytes = (blocks * 16) // 8 - HEADER_SIZE
@@ -357,6 +470,10 @@ def estimate_capacity_comparison(image_data: bytes) -> dict:
'output': 'PNG or JPEG (grayscale)',
'ratio_vs_lsb': (dct_bytes / lsb_bytes * 100) if lsb_bytes > 0 else 0,
'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,
seed: bytes,
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.
For PNG output: Uses scipy DCT transform
For JPEG output: Uses jpegio if available for proper coefficient embedding
Args:
data: Data to embed
carrier_image: Carrier image bytes
seed: Seed for pseudo-random block selection
output_format: Output format - 'png' (default, lossless) or 'jpeg' (smaller)
seed: Seed for pseudo-random selection
output_format: 'png' (default, lossless) or 'jpeg'
color_mode: 'color' (preserve colors) or 'grayscale' (v3.0.1+)
Returns:
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
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)
if len(data) > capacity_info.usable_capacity_bytes:
@@ -398,69 +545,216 @@ def embed_in_dct(
f"(capacity: {capacity_info.usable_capacity_bytes} bytes)"
)
# Prepare image
image = _to_grayscale(carrier_image)
padded, original_size = _pad_to_blocks(image)
# Load image
img = Image.open(io.BytesIO(carrier_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))
payload = header + data
# Convert payload to bits
bits = []
for byte in payload:
for i in range(7, -1, -1):
bits.append((byte >> i) & 1)
# Generate block order
num_blocks = capacity_info.total_blocks
block_order = _generate_block_order(num_blocks, seed)
# Embed bits
bit_idx = 0
blocks_used = 0
h, w = padded.shape
h, w = channel.shape
result = channel.copy()
bit_idx = 0
for block_num in block_order:
if bit_idx >= len(bits):
break
# Calculate block position
by = (block_num // (w // BLOCK_SIZE)) * BLOCK_SIZE
bx = (block_num % (w // BLOCK_SIZE)) * BLOCK_SIZE
# Extract and transform block
block = padded[by:by+BLOCK_SIZE, bx:bx+BLOCK_SIZE].copy()
block = result[by:by+BLOCK_SIZE, bx:bx+BLOCK_SIZE].copy()
dct_block = _dct2(block)
# Embed bits in selected coefficients
for pos in DEFAULT_EMBED_POSITIONS:
if bit_idx >= len(bits):
break
dct_block[pos] = _embed_bit_in_coeff(dct_block[pos], bits[bit_idx])
bit_idx += 1
# Inverse transform and store
modified_block = _idct2(dct_block)
padded[by:by+BLOCK_SIZE, bx:bx+BLOCK_SIZE] = modified_block
blocks_used += 1
result[by:by+BLOCK_SIZE, bx:bx+BLOCK_SIZE] = modified_block
# Remove padding and save
result = _unpad_image(padded, original_size)
stego_bytes = _save_stego_image(result, output_format)
return result
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.
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,
)
Note: jpegio naturally preserves color since JPEG stores YCbCr
and we only modify Y channel coefficients.
"""
import tempfile
import os
return stego_bytes, stats
# 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(
@@ -470,33 +764,43 @@ def extract_from_dct(
"""
Extract data from DCT stego image.
Automatically detects whether image uses scipy DCT or jpegio embedding.
Args:
stego_image: Stego image bytes
seed: Same seed used for embedding
Returns:
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)
padded, original_size = _pad_to_blocks(image)
# Calculate capacity
h, w = padded.shape
blocks_x = w // BLOCK_SIZE
blocks_y = h // BLOCK_SIZE
num_blocks = blocks_x * blocks_y
# Generate same block order
block_order = _generate_block_order(num_blocks, seed)
# Extract all bits (we'll stop when we have enough based on header)
all_bits = []
for block_num in block_order:
@@ -510,7 +814,6 @@ def extract_from_dct(
bit = _extract_bit_from_coeff(dct_block[pos])
all_bits.append(bit)
# Check if we have enough for header
if len(all_bits) >= HEADER_SIZE * 8:
try:
_, _, data_length = _parse_header(all_bits[:HEADER_SIZE * 8])
@@ -518,16 +821,12 @@ def extract_from_dct(
if len(all_bits) >= total_needed:
break
except ValueError:
# Not enough data yet or invalid, continue
pass
# Parse header
version, flags, data_length = _parse_header(all_bits)
# Extract data bits
data_bits = all_bits[HEADER_SIZE * 8:(HEADER_SIZE + data_length) * 8]
# Convert bits to bytes
data = bytes([
sum(data_bits[i*8:(i+1)*8][j] << (7-j) for j in range(8))
for i in range(data_length)
@@ -536,6 +835,61 @@ def extract_from_dct(
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
# ============================================================================

View File

@@ -11,6 +11,7 @@ New in v3.0:
New in v3.0.1:
- dct_output_format parameter for DCT mode ('png' or 'jpeg')
- dct_color_mode parameter for DCT mode ('grayscale' or 'color')
"""
import io
@@ -59,6 +60,10 @@ ENCRYPTION_OVERHEAD = HEADER_OVERHEAD + LENGTH_PREFIX
DCT_OUTPUT_PNG = 'png'
DCT_OUTPUT_JPEG = 'jpeg'
# DCT color mode options (v3.0.1)
DCT_COLOR_GRAYSCALE = 'grayscale'
DCT_COLOR_COLOR = 'color'
# =============================================================================
# DCT MODULE LAZY LOADING
@@ -477,6 +482,7 @@ def embed_in_image(
output_format: Optional[str] = None,
embed_mode: str = EMBED_MODE_LSB,
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]:
"""
Embed data into an image using specified mode.
@@ -489,6 +495,7 @@ def embed_in_image(
output_format: Force output format (LSB mode only)
embed_mode: 'lsb' (default) or 'dct'
dct_output_format: For DCT mode - 'png' (lossless) or 'jpeg' (smaller)
dct_color_mode: For DCT mode - 'grayscale' (default) or 'color' (preserves colors)
Returns:
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")
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()
# 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(
data,
image_data,
pixel_key,
output_format=dct_output_format,
color_mode=dct_color_mode, # NEW in v3.0.1
)
# Determine extension based on output format
@@ -531,7 +544,8 @@ def embed_in_image(
else:
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
# LSB MODE