Version 3.0.2 full expirimental DCT support, jpegio for better jpg manipulation, etc.
This commit is contained in:
20
Dockerfile
20
Dockerfile
@@ -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
712
INSTALL.md
Normal 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
323
README.md
@@ -5,6 +5,7 @@ A secure steganography system for hiding encrypted messages in images using hybr
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 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 |
|
||||
:-------------------------:|:-------------------------:|:------------------------:|:--------:|
|
||||
 |  |  | 
|
||||
| Front Page | Encode | Decode | Generate |
|
||||
|:----------:|:------:|:------:|:--------:|
|
||||
|  |  |  |  |
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
449
frontends/API.md
449
frontends/API.md
@@ -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
|
||||
|
||||
340
frontends/CLI.md
340
frontends/CLI.md
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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,22 +266,70 @@
|
||||
<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">
|
||||
<!-- DCT Options Panel (shown only when DCT selected) -->
|
||||
<div class="d-none" id="dctOptionsPanel">
|
||||
|
||||
<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="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>
|
||||
|
||||
<!-- 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" id="dctPngCard">
|
||||
<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-success fs-5 d-block"></i>
|
||||
<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>
|
||||
@@ -305,8 +353,11 @@
|
||||
</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() {
|
||||
|
||||
@@ -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 %}
|
||||
<span class="badge bg-success fs-6 ms-1">
|
||||
Grayscale JPEG, frequency domain embedding (Q=95)
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<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>
|
||||
|
||||
@@ -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
144
src/stegasoo/Dockerfile
Normal 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"]
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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,70 +545,217 @@ def embed_in_dct(
|
||||
f"(capacity: {capacity_info.usable_capacity_bytes} bytes)"
|
||||
)
|
||||
|
||||
# Prepare image
|
||||
# Load image
|
||||
img = Image.open(io.BytesIO(carrier_image))
|
||||
width, height = img.size
|
||||
|
||||
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)
|
||||
|
||||
# Create header + data
|
||||
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.
|
||||
|
||||
Note: jpegio naturally preserves color since JPEG stores YCbCr
|
||||
and we only modify Y channel coefficients.
|
||||
"""
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
# Check if carrier is JPEG - if not, convert it
|
||||
img = Image.open(io.BytesIO(carrier_image))
|
||||
width, height = img.size
|
||||
|
||||
if img.format != 'JPEG':
|
||||
# Convert to JPEG first
|
||||
buffer = io.BytesIO()
|
||||
if img.mode != 'RGB':
|
||||
img = img.convert('RGB')
|
||||
img.save(buffer, format='JPEG', quality=95, subsampling=0)
|
||||
carrier_image = buffer.getvalue()
|
||||
|
||||
# Write carrier to temp file
|
||||
input_path = _jpegio_bytes_to_file(carrier_image, suffix='.jpg')
|
||||
output_path = tempfile.mktemp(suffix='.jpg')
|
||||
|
||||
try:
|
||||
# Read JPEG with jpegio
|
||||
jpeg = jio.read(input_path)
|
||||
|
||||
# Get Y channel coefficients (channel 0)
|
||||
# For grayscale mode, we could convert to grayscale, but jpegio
|
||||
# works with the original JPEG which already has color info.
|
||||
# The color_mode primarily affects the output interpretation.
|
||||
coef_array = jpeg.coef_arrays[JPEGIO_EMBED_CHANNEL]
|
||||
|
||||
# Find usable positions
|
||||
all_positions = _jpegio_get_usable_positions(coef_array)
|
||||
|
||||
# Generate pseudo-random order
|
||||
order = _jpegio_generate_order(len(all_positions), seed)
|
||||
|
||||
# Create payload
|
||||
header = _jpegio_create_header(len(data))
|
||||
payload = header + data
|
||||
|
||||
# Convert to bits
|
||||
bits = []
|
||||
for byte in payload:
|
||||
for i in range(7, -1, -1):
|
||||
bits.append((byte >> i) & 1)
|
||||
|
||||
if len(bits) > len(all_positions):
|
||||
raise ValueError(
|
||||
f"Payload too large: {len(bits)} bits, "
|
||||
f"only {len(all_positions)} usable coefficients"
|
||||
)
|
||||
|
||||
# Embed using LSB
|
||||
coefs_used = 0
|
||||
for bit_idx, pos_idx in enumerate(order):
|
||||
if bit_idx >= len(bits):
|
||||
break
|
||||
|
||||
row, col = all_positions[pos_idx]
|
||||
coef = coef_array[row, col]
|
||||
|
||||
# Embed bit in LSB
|
||||
if (coef & 1) != bits[bit_idx]:
|
||||
if coef > 0:
|
||||
coef_array[row, col] = coef - 1 if (coef & 1) else coef + 1
|
||||
else:
|
||||
coef_array[row, col] = coef + 1 if (coef & 1) else coef - 1
|
||||
|
||||
coefs_used += 1
|
||||
|
||||
# Write modified JPEG
|
||||
jio.write(jpeg, output_path)
|
||||
|
||||
# Read back as bytes
|
||||
with open(output_path, 'rb') as f:
|
||||
stego_bytes = f.read()
|
||||
|
||||
stats = DCTEmbedStats(
|
||||
blocks_used=blocks_used,
|
||||
blocks_available=capacity_info.total_blocks,
|
||||
blocks_used=coefs_used // 63, # Approximate blocks
|
||||
blocks_available=len(all_positions) // 63,
|
||||
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,
|
||||
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(
|
||||
stego_image: bytes,
|
||||
@@ -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
|
||||
# ============================================================================
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user