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
|
ENV PIP_ROOT_USER_ACTION=ignore
|
||||||
|
|
||||||
# Install system dependencies
|
# Install system dependencies
|
||||||
|
# NOTE: libjpeg-dev is required for jpegio compilation
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
gcc \
|
gcc \
|
||||||
libc-dev \
|
libc-dev \
|
||||||
libffi-dev \
|
libffi-dev \
|
||||||
libzbar0 \
|
libzbar0 \
|
||||||
|
libjpeg-dev \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -31,8 +33,10 @@ COPY pyproject.toml README.md ./
|
|||||||
COPY src/ src/
|
COPY src/ src/
|
||||||
COPY data/ data/
|
COPY data/ data/
|
||||||
|
|
||||||
# Install the package with web extras
|
# Install build dependencies for jpegio, then install the package
|
||||||
RUN pip install --no-cache-dir ".[web]"
|
# jpegio requires Cython and numpy to compile
|
||||||
|
RUN pip install --no-cache-dir cython numpy && \
|
||||||
|
pip install --no-cache-dir ".[web]"
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Production stage - Web UI
|
# Production stage - Web UI
|
||||||
@@ -78,11 +82,14 @@ FROM base as api
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install API extras
|
# Install API extras (includes DCT dependencies)
|
||||||
COPY pyproject.toml README.md ./
|
COPY pyproject.toml README.md ./
|
||||||
COPY src/ src/
|
COPY src/ src/
|
||||||
COPY data/ data/
|
COPY data/ data/
|
||||||
RUN pip install --no-cache-dir ".[api]"
|
|
||||||
|
# Install build dependencies for jpegio, then install the package
|
||||||
|
RUN pip install --no-cache-dir cython numpy && \
|
||||||
|
pip install --no-cache-dir ".[api]"
|
||||||
|
|
||||||
# Copy API files
|
# Copy API files
|
||||||
COPY frontends/api/ frontends/api/
|
COPY frontends/api/ frontends/api/
|
||||||
@@ -116,7 +123,10 @@ WORKDIR /app
|
|||||||
COPY pyproject.toml README.md ./
|
COPY pyproject.toml README.md ./
|
||||||
COPY src/ src/
|
COPY src/ src/
|
||||||
COPY data/ data/
|
COPY data/ data/
|
||||||
RUN pip install --no-cache-dir ".[cli]"
|
|
||||||
|
# Install build dependencies for jpegio (if dct extras needed), then install
|
||||||
|
RUN pip install --no-cache-dir cython numpy && \
|
||||||
|
pip install --no-cache-dir ".[cli,dct]"
|
||||||
|
|
||||||
# Copy CLI files
|
# Copy CLI files
|
||||||
COPY frontends/cli/ frontends/cli/
|
COPY frontends/cli/ frontends/cli/
|
||||||
|
|||||||
712
INSTALL.md
Normal file
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
|
## Features
|
||||||
|
|
||||||
@@ -17,50 +18,40 @@ A secure steganography system for hiding encrypted messages in images using hybr
|
|||||||
- 🌐 **Multiple interfaces**: CLI, Web UI, REST API
|
- 🌐 **Multiple interfaces**: CLI, Web UI, REST API
|
||||||
- 📁 **File embedding** - Hide any file type (PDF, ZIP, documents)
|
- 📁 **File embedding** - Hide any file type (PDF, ZIP, documents)
|
||||||
- 📱 **QR code support** - Encode/decode RSA keys via QR codes
|
- 📱 **QR code support** - Encode/decode RSA keys via QR codes
|
||||||
|
- 🆕 **DCT steganography** - JPEG-resilient embedding for social media (v3.0+)
|
||||||
|
|
||||||
|
## What's New in v3.0.2
|
||||||
|
|
||||||
|
| Feature | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| **DCT Mode** | Frequency-domain embedding survives JPEG recompression |
|
||||||
|
| **JPEG Output** | Native JPEG output using jpegio library |
|
||||||
|
| **Color Preservation** | DCT color mode preserves carrier image colors |
|
||||||
|
| **Auto-Detection** | Decoder automatically detects LSB vs DCT mode |
|
||||||
|
|
||||||
|
### Embedding Mode Comparison
|
||||||
|
|
||||||
|
| Mode | Capacity (1080p) | JPEG Resilient | Best For |
|
||||||
|
|------|------------------|----------------|----------|
|
||||||
|
| **LSB** (default) | ~770 KB | ❌ No | Email, file transfer |
|
||||||
|
| **DCT** (experimental) | ~65 KB | ✅ Yes | Social media, messaging apps |
|
||||||
|
|
||||||
## WebUI Preview
|
## WebUI Preview
|
||||||
|
|
||||||
Front Page | Encode | Decode | Generate |
|
| Front Page | Encode | Decode | Generate |
|
||||||
:-------------------------:|:-------------------------:|:------------------------:|:--------:|
|
|:----------:|:------:|:------:|:--------:|
|
||||||
 |  |  | 
|
|  |  |  |  |
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
## Installation
|
|
||||||
|
|
||||||
### From Source
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone the repository
|
# Install with all features
|
||||||
git clone https://github.com/adlee-was-taken/stegasoo.git
|
|
||||||
cd stegasoo
|
|
||||||
|
|
||||||
# Install core library
|
|
||||||
pip install -e .
|
|
||||||
|
|
||||||
# Install with CLI
|
|
||||||
pip install -e ".[cli]"
|
|
||||||
|
|
||||||
# Install with Web UI
|
|
||||||
pip install -e ".[web]"
|
|
||||||
|
|
||||||
# Install with REST API
|
|
||||||
pip install -e ".[api]"
|
|
||||||
|
|
||||||
# Install everything
|
|
||||||
pip install -e ".[all]"
|
pip install -e ".[all]"
|
||||||
```
|
|
||||||
|
|
||||||
### CLI Usage
|
# Generate credentials (memorize these!)
|
||||||
|
|
||||||
```bash
|
|
||||||
# Generate credentials
|
|
||||||
stegasoo generate --pin --words 3
|
stegasoo generate --pin --words 3
|
||||||
|
|
||||||
# With RSA key
|
# Encode a message (LSB mode - default)
|
||||||
stegasoo generate --rsa --rsa-bits 4096 -o mykey.pem -p "secretpassword"
|
|
||||||
|
|
||||||
# Encode
|
|
||||||
stegasoo encode \
|
stegasoo encode \
|
||||||
--ref photo.jpg \
|
--ref photo.jpg \
|
||||||
--carrier meme.png \
|
--carrier meme.png \
|
||||||
@@ -68,88 +59,171 @@ stegasoo encode \
|
|||||||
--pin 123456 \
|
--pin 123456 \
|
||||||
--message "Secret message"
|
--message "Secret message"
|
||||||
|
|
||||||
# Decode
|
# Encode for social media (DCT mode)
|
||||||
|
stegasoo encode \
|
||||||
|
--ref photo.jpg \
|
||||||
|
--carrier meme.png \
|
||||||
|
--phrase "apple forest thunder" \
|
||||||
|
--pin 123456 \
|
||||||
|
--message "Secret message" \
|
||||||
|
--mode dct \
|
||||||
|
--format jpeg
|
||||||
|
|
||||||
|
# Decode (auto-detects mode)
|
||||||
stegasoo decode \
|
stegasoo decode \
|
||||||
--ref photo.jpg \
|
--ref photo.jpg \
|
||||||
--stego stego.png \
|
--stego stego.png \
|
||||||
--phrase "apple forest thunder" \
|
--phrase "apple forest thunder" \
|
||||||
--pin 123456
|
--pin 123456
|
||||||
|
|
||||||
# Pipe-friendly
|
|
||||||
echo "secret" | stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 > stego.png
|
|
||||||
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 -q
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Web UI
|
For detailed installation instructions, see **[INSTALL.md](INSTALL.md)**.
|
||||||
|
|
||||||
```bash
|
---
|
||||||
# Development
|
|
||||||
cd frontends/web
|
|
||||||
python app.py
|
|
||||||
|
|
||||||
# Production
|
|
||||||
gunicorn --bind 0.0.0.0:5000 app:app
|
|
||||||
```
|
|
||||||
|
|
||||||
Visit http://localhost:5000
|
|
||||||
|
|
||||||
### REST API
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Development
|
|
||||||
cd frontends/api
|
|
||||||
python main.py
|
|
||||||
|
|
||||||
# Production
|
|
||||||
uvicorn main:app --host 0.0.0.0 --port 8000
|
|
||||||
```
|
|
||||||
|
|
||||||
API docs at http://localhost:8000/docs
|
|
||||||
|
|
||||||
#### Example API Calls
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Generate credentials
|
|
||||||
curl -X POST http://localhost:8000/generate \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"use_pin": true, "use_rsa": false}'
|
|
||||||
|
|
||||||
# Encode (multipart)
|
|
||||||
curl -X POST http://localhost:8000/encode/multipart \
|
|
||||||
-F "message=Secret" \
|
|
||||||
-F "day_phrase=apple forest thunder" \
|
|
||||||
-F "pin=123456" \
|
|
||||||
-F "reference_photo=@photo.jpg" \
|
|
||||||
-F "carrier=@meme.png" \
|
|
||||||
--output stego.png
|
|
||||||
|
|
||||||
# Decode (multipart)
|
|
||||||
curl -X POST http://localhost:8000/decode/multipart \
|
|
||||||
-F "day_phrase=apple forest thunder" \
|
|
||||||
-F "pin=123456" \
|
|
||||||
-F "reference_photo=@photo.jpg" \
|
|
||||||
-F "stego_image=@stego.png"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security Model
|
## Security Model
|
||||||
|
|
||||||
|
Stegasoo uses multiple authentication factors combined with strong cryptography:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ AUTHENTICATION LAYERS │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Reference Photo ──┐ │
|
||||||
|
│ (~80-256 bits) │ │
|
||||||
|
│ ├──► Argon2id KDF ──► AES-256-GCM Key │
|
||||||
|
│ Day Phrase ───────┤ (256MB RAM) │
|
||||||
|
│ (~33-132 bits) │ │
|
||||||
|
│ │ │
|
||||||
|
│ Static PIN ───────┤ │
|
||||||
|
│ (~20-30 bits) │ │
|
||||||
|
│ │ │
|
||||||
|
│ RSA Key ──────────┘ │
|
||||||
|
│ (~128 bits) (optional, adds another factor) │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Entropy Summary
|
||||||
|
|
||||||
| Component | Entropy | Purpose |
|
| Component | Entropy | Purpose |
|
||||||
|-----------|---------|---------|
|
|-----------|---------|---------|
|
||||||
| Reference Photo | ~80-256 bits | Something you have |
|
| Reference Photo | ~80-256 bits | Something you have |
|
||||||
| Day Phrase (3-12 words) | ~33-100+ bits | Something you know (rotates daily) |
|
| Day Phrase (3-12 words) | ~33-132 bits | Something you know (rotates daily) |
|
||||||
| PIN (6-9 digits) | ~20+ bits | Something you know (static) |
|
| PIN (6-9 digits) | ~20-30 bits | Something you know (static) |
|
||||||
| RSA Key (2048-bit) | ~128 bits | Something you have |
|
| RSA Key (2048-4096 bit) | ~112-128 bits | Something you have (optional) |
|
||||||
| **Combined** | **~133-400+ bits** | **Beyond brute force** |
|
| **Combined** | **133-400+ bits** | **Beyond brute force** |
|
||||||
|
|
||||||
### Attack Resistance
|
### Attack Resistance
|
||||||
|
|
||||||
| Attack | Protection |
|
| Attack | Protection |
|
||||||
|--------|------------|
|
|--------|------------|
|
||||||
| Brute force | 2^133+ combinations |
|
| Brute force | 2^133+ combinations minimum |
|
||||||
| Rainbow tables | Random salt per message |
|
| Rainbow tables | Random 16-byte salt per message |
|
||||||
| Steganalysis | Random pixel selection |
|
| Steganalysis | Pseudo-random pixel/coefficient selection |
|
||||||
| GPU cracking | Argon2id requires 256MB RAM per attempt |
|
| GPU cracking | Argon2id requires 256MB RAM per attempt |
|
||||||
| Side-channel | Constant-time operations in crypto |
|
| Side-channel | Constant-time operations in cryptography library |
|
||||||
|
| JPEG recompression | DCT mode embeds in frequency domain (v3.0+) |
|
||||||
|
|
||||||
|
### Security Configurations
|
||||||
|
|
||||||
|
| Configuration | Entropy | Use Case |
|
||||||
|
|--------------|---------|----------|
|
||||||
|
| 3-word phrase + 6-digit PIN | ~133 bits | Casual private messaging |
|
||||||
|
| 6-word phrase + 9-digit PIN | ~176 bits | Standard security |
|
||||||
|
| 3-word phrase + RSA 2048 | ~241 bits | File-based authentication |
|
||||||
|
| 6-word phrase + PIN + RSA 4096 | ~304 bits | Maximum security |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Interfaces
|
||||||
|
|
||||||
|
### Command-Line Interface (CLI)
|
||||||
|
|
||||||
|
Full-featured CLI with piping support:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate with RSA key
|
||||||
|
stegasoo generate --rsa --rsa-bits 4096 -o mykey.pem -p "password"
|
||||||
|
|
||||||
|
# Encode from file
|
||||||
|
stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 -f secret.txt
|
||||||
|
|
||||||
|
# Encode for social media (DCT + JPEG)
|
||||||
|
stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 \
|
||||||
|
-m "Message" --mode dct --format jpeg
|
||||||
|
|
||||||
|
# Decode to stdout (quiet mode)
|
||||||
|
stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q
|
||||||
|
|
||||||
|
# Check image capacity (shows both LSB and DCT)
|
||||||
|
stegasoo info carrier.png
|
||||||
|
```
|
||||||
|
|
||||||
|
📖 Full documentation: **[CLI.md](CLI.md)**
|
||||||
|
|
||||||
|
### Web UI
|
||||||
|
|
||||||
|
Browser-based interface with drag-and-drop uploads:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start the server
|
||||||
|
cd frontends/web
|
||||||
|
python app.py
|
||||||
|
# Visit http://localhost:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Drag-and-drop image uploads
|
||||||
|
- Real-time entropy calculator
|
||||||
|
- Native mobile sharing (Web Share API)
|
||||||
|
- DCT mode with advanced options panel
|
||||||
|
- Automatic day-of-week detection
|
||||||
|
|
||||||
|
📖 Full documentation: **[WEB_UI.md](WEB_UI.md)**
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
|
||||||
|
FastAPI-powered REST API with OpenAPI documentation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start the server
|
||||||
|
cd frontends/api
|
||||||
|
uvicorn main:app --host 0.0.0.0 --port 8000
|
||||||
|
# Docs at http://localhost:8000/docs
|
||||||
|
```
|
||||||
|
|
||||||
|
Example API calls:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate credentials
|
||||||
|
curl -X POST http://localhost:8000/generate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"use_pin": true, "words_per_phrase": 3}'
|
||||||
|
|
||||||
|
# Encode with DCT mode
|
||||||
|
curl -X POST http://localhost:8000/encode/multipart \
|
||||||
|
-F "message=Secret" \
|
||||||
|
-F "day_phrase=apple forest thunder" \
|
||||||
|
-F "pin=123456" \
|
||||||
|
-F "embedding_mode=dct" \
|
||||||
|
-F "output_format=jpeg" \
|
||||||
|
-F "reference_photo=@photo.jpg" \
|
||||||
|
-F "carrier=@meme.png" \
|
||||||
|
--output stego.jpg
|
||||||
|
|
||||||
|
# Decode (auto-detects mode)
|
||||||
|
curl -X POST http://localhost:8000/decode/multipart \
|
||||||
|
-F "day_phrase=apple forest thunder" \
|
||||||
|
-F "pin=123456" \
|
||||||
|
-F "reference_photo=@photo.jpg" \
|
||||||
|
-F "stego_image=@stego.jpg"
|
||||||
|
```
|
||||||
|
|
||||||
|
📖 Full documentation: **[API.md](API.md)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
@@ -159,11 +233,13 @@ stegasoo/
|
|||||||
│ ├── __init__.py # Public API
|
│ ├── __init__.py # Public API
|
||||||
│ ├── constants.py # Configuration
|
│ ├── constants.py # Configuration
|
||||||
│ ├── crypto.py # Encryption/decryption
|
│ ├── crypto.py # Encryption/decryption
|
||||||
│ ├── steganography.py # Image embedding
|
│ ├── steganography.py # LSB image embedding
|
||||||
|
│ ├── dct_steganography.py # DCT embedding (v3.0+)
|
||||||
│ ├── keygen.py # Credential generation
|
│ ├── keygen.py # Credential generation
|
||||||
│ ├── validation.py # Input validation
|
│ ├── validation.py # Input validation
|
||||||
│ ├── models.py # Data classes
|
│ ├── models.py # Data classes
|
||||||
│ ├── exceptions.py # Custom exceptions
|
│ ├── exceptions.py # Custom exceptions
|
||||||
|
│ ├── qr_utils.py # QR code utilities
|
||||||
│ └── utils.py # Utilities
|
│ └── utils.py # Utilities
|
||||||
│
|
│
|
||||||
├── frontends/
|
├── frontends/
|
||||||
@@ -176,18 +252,19 @@ stegasoo/
|
|||||||
│
|
│
|
||||||
├── pyproject.toml # Package configuration
|
├── pyproject.toml # Package configuration
|
||||||
├── Dockerfile # Multi-stage Docker build
|
├── Dockerfile # Multi-stage Docker build
|
||||||
└── docker-compose.yml # Container orchestration
|
├── docker-compose.yml # Container orchestration
|
||||||
|
│
|
||||||
|
├── README.md # This file
|
||||||
|
├── INSTALL.md # Installation guide
|
||||||
|
├── CLI.md # CLI documentation
|
||||||
|
├── API.md # API documentation
|
||||||
|
└── WEB_UI.md # Web UI documentation
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
| Variable | Default | Description |
|
|
||||||
|----------|---------|-------------|
|
|
||||||
| `FLASK_ENV` | production | Flask environment |
|
|
||||||
| `PYTHONPATH` | - | Include src/ for development |
|
|
||||||
|
|
||||||
### Limits
|
### Limits
|
||||||
|
|
||||||
| Limit | Value |
|
| Limit | Value |
|
||||||
@@ -199,6 +276,15 @@ stegasoo/
|
|||||||
| Phrase length | 3-12 words |
|
| Phrase length | 3-12 words |
|
||||||
| RSA key sizes | 2048, 3072, 4096 bits |
|
| RSA key sizes | 2048, 3072, 4096 bits |
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `FLASK_ENV` | production | Flask environment |
|
||||||
|
| `PYTHONPATH` | - | Include `src/` for development |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -214,13 +300,42 @@ ruff check src/ frontends/
|
|||||||
|
|
||||||
# Type checking
|
# Type checking
|
||||||
mypy src/
|
mypy src/
|
||||||
|
|
||||||
|
# Check DCT support
|
||||||
|
python -c "from stegasoo import has_dct_support; print(has_dct_support())"
|
||||||
|
python -c "from stegasoo.dct_steganography import has_jpegio_support; print(has_jpegio_support())"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Changes |
|
||||||
|
|---------|---------|
|
||||||
|
| **3.0.2** | Fixed JPEG output with jpegio integration |
|
||||||
|
| **3.0.1** | Added DCT color mode, JPEG output (broken) |
|
||||||
|
| **3.0.0** | Added DCT steganography mode |
|
||||||
|
| **2.2.x** | QR code support, file embedding |
|
||||||
|
| **2.0.x** | Web UI, REST API, RSA keys |
|
||||||
|
| **1.0.x** | Initial release, CLI only |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT License - Use responsibly.
|
MIT License - Use responsibly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ⚠️ Disclaimer
|
## ⚠️ Disclaimer
|
||||||
|
|
||||||
This tool is for educational and legitimate privacy purposes only. Users are responsible for complying with applicable laws in their jurisdiction.
|
This tool is for educational and legitimate privacy purposes only. Users are responsible for complying with applicable laws in their jurisdiction.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- **[INSTALL.md](INSTALL.md)** - Detailed installation instructions
|
||||||
|
- **[CLI.md](CLI.md)** - Command-line interface reference
|
||||||
|
- **[API.md](API.md)** - REST API documentation
|
||||||
|
- **[WEB_UI.md](WEB_UI.md)** - Web interface guide
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ services:
|
|||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: 512M # Argon2 needs 256MB per operation
|
memory: 768M # Increased for scipy + Argon2
|
||||||
reservations:
|
reservations:
|
||||||
memory: 256M
|
memory: 384M
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# REST API (FastAPI)
|
# REST API (FastAPI)
|
||||||
@@ -35,9 +35,9 @@ services:
|
|||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: 512M
|
memory: 768M # Increased for scipy + Argon2
|
||||||
reservations:
|
reservations:
|
||||||
memory: 256M
|
memory: 384M
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Nginx Reverse Proxy (optional, for production)
|
# Nginx Reverse Proxy (optional, for production)
|
||||||
|
|||||||
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](#post-decode-json)
|
||||||
- [POST /decode/multipart](#post-decodemultipart)
|
- [POST /decode/multipart](#post-decodemultipart)
|
||||||
- [POST /image/info](#post-imageinfo)
|
- [POST /image/info](#post-imageinfo)
|
||||||
|
- [Embedding Modes](#embedding-modes)
|
||||||
- [Data Models](#data-models)
|
- [Data Models](#data-models)
|
||||||
- [Error Handling](#error-handling)
|
- [Error Handling](#error-handling)
|
||||||
- [Code Examples](#code-examples)
|
- [Code Examples](#code-examples)
|
||||||
@@ -29,12 +30,19 @@ Complete REST API reference for Stegasoo steganography operations.
|
|||||||
The Stegasoo REST API provides programmatic access to all steganography operations:
|
The Stegasoo REST API provides programmatic access to all steganography operations:
|
||||||
|
|
||||||
- **Generate** credentials (phrases, PINs, RSA keys)
|
- **Generate** credentials (phrases, PINs, RSA keys)
|
||||||
- **Encode** messages into images
|
- **Encode** messages into images (LSB or DCT mode)
|
||||||
- **Decode** messages from images
|
- **Decode** messages from images (auto-detects mode)
|
||||||
- **Analyze** image capacity
|
- **Analyze** image capacity
|
||||||
|
|
||||||
The API supports both JSON (base64-encoded images) and multipart form data (direct file uploads).
|
The API supports both JSON (base64-encoded images) and multipart form data (direct file uploads).
|
||||||
|
|
||||||
|
### What's New in v3.0.2
|
||||||
|
|
||||||
|
- **DCT Steganography Mode** - JPEG-resilient embedding
|
||||||
|
- **Output Format Selection** - PNG or JPEG output
|
||||||
|
- **Color Mode Selection** - Color or grayscale processing
|
||||||
|
- **jpegio Integration** - Proper JPEG coefficient manipulation
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -45,6 +53,8 @@ The API supports both JSON (base64-encoded images) and multipart form data (dire
|
|||||||
pip install stegasoo[api]
|
pip install stegasoo[api]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This automatically installs DCT dependencies (scipy, jpegio) for full functionality.
|
||||||
|
|
||||||
### From Source
|
### From Source
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -107,8 +117,10 @@ Host: localhost:8000
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": "2.0.1",
|
"version": "3.0.2",
|
||||||
"has_argon2": true,
|
"has_argon2": true,
|
||||||
|
"has_dct": true,
|
||||||
|
"has_jpegio": true,
|
||||||
"day_names": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
"day_names": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -119,6 +131,8 @@ Host: localhost:8000
|
|||||||
|-------|------|-------------|
|
|-------|------|-------------|
|
||||||
| `version` | string | Stegasoo library version |
|
| `version` | string | Stegasoo library version |
|
||||||
| `has_argon2` | boolean | Whether Argon2id is available |
|
| `has_argon2` | boolean | Whether Argon2id is available |
|
||||||
|
| `has_dct` | boolean | Whether DCT mode is available (scipy) |
|
||||||
|
| `has_jpegio` | boolean | Whether native JPEG DCT is available |
|
||||||
| `day_names` | array | Day names for phrase mapping |
|
| `day_names` | array | Day names for phrase mapping |
|
||||||
|
|
||||||
#### cURL Example
|
#### cURL Example
|
||||||
@@ -245,22 +259,28 @@ Content-Type: application/json
|
|||||||
| `rsa_password` | string | | | Password for RSA key |
|
| `rsa_password` | string | | | Password for RSA key |
|
||||||
| `date_str` | string | | | Date override (YYYY-MM-DD) |
|
| `date_str` | string | | | Date override (YYYY-MM-DD) |
|
||||||
| `embedding_mode` | string | | `"lsb"` | `"lsb"` or `"dct"` |
|
| `embedding_mode` | string | | `"lsb"` | `"lsb"` or `"dct"` |
|
||||||
\* At least one of `pin` or `rsa_key_base64` required.
|
| `output_format` | string | | `"png"` | `"png"` or `"jpeg"` (DCT only) |
|
||||||
|
| `color_mode` | string | | `"color"` | `"color"` or `"grayscale"` (DCT only) |
|
||||||
|
|
||||||
|
\* At least one of `pin` or `rsa_key_base64` required.
|
||||||
|
|
||||||
#### Response
|
#### Response
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"stego_image_base64": "iVBORw0KGgo...",
|
"stego_image_base64": "iVBORw0KGgo...",
|
||||||
"filename": "a1b2c3d4_20251227.png",
|
"filename": "a1b2c3d4_20251227.png",
|
||||||
"capacity_used_percent": 12.4,
|
"capacity_used_percent": 12.4,
|
||||||
"date_used": "2025-12-27",
|
"date_used": "2025-12-27",
|
||||||
"day_of_week": "Saturday"
|
"day_of_week": "Saturday",
|
||||||
}
|
"embedding_mode": "lsb",
|
||||||
```
|
"output_format": "png",
|
||||||
|
"color_mode": null
|
||||||
#### Response Fields
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response Fields
|
||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
|-------|------|-------------|
|
|-------|------|-------------|
|
||||||
| `stego_image_base64` | string | Base64-encoded stego image |
|
| `stego_image_base64` | string | Base64-encoded stego image |
|
||||||
@@ -272,7 +292,10 @@ Content-Type: application/json
|
|||||||
| `output_format` | string | Output format: `"png"` or `"jpeg"` |
|
| `output_format` | string | Output format: `"png"` or `"jpeg"` |
|
||||||
| `color_mode` | string\|null | Color mode (DCT only): `"color"` or `"grayscale"` |
|
| `color_mode` | string\|null | Color mode (DCT only): `"color"` or `"grayscale"` |
|
||||||
|
|
||||||
# Prepare base64-encoded images
|
#### cURL Example (LSB Mode - Default)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prepare base64-encoded images
|
||||||
REF_B64=$(base64 -w0 reference.jpg)
|
REF_B64=$(base64 -w0 reference.jpg)
|
||||||
CARRIER_B64=$(base64 -w0 carrier.png)
|
CARRIER_B64=$(base64 -w0 carrier.png)
|
||||||
|
|
||||||
@@ -280,13 +303,16 @@ Content-Type: application/json
|
|||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{
|
-d "{
|
||||||
\"message\": \"Secret message\",
|
\"message\": \"Secret message\",
|
||||||
\"reference_photo_base64\": \"$REF_B64\",
|
\"reference_photo_base64\": \"$REF_B64\",
|
||||||
\"carrier_image_base64\": \"$CARRIER_B64\",
|
\"carrier_image_base64\": \"$CARRIER_B64\",
|
||||||
\"day_phrase\": \"apple forest thunder\",
|
\"day_phrase\": \"apple forest thunder\",
|
||||||
\"pin\": \"123456\"
|
\"pin\": \"123456\"
|
||||||
}" | jq -r '.stego_image_base64' | base64 -d > stego.png
|
}" | jq -r '.stego_image_base64' | base64 -d > stego.png
|
||||||
|
```
|
||||||
|
|
||||||
|
#### cURL Example (DCT Mode with JPEG Output)
|
||||||
|
|
||||||
|
```bash
|
||||||
curl -X POST http://localhost:8000/encode \
|
curl -X POST http://localhost:8000/encode \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{
|
-d "{
|
||||||
@@ -304,6 +330,23 @@ curl -X POST http://localhost:8000/encode \
|
|||||||
---
|
---
|
||||||
|
|
||||||
### POST /encode/multipart
|
### POST /encode/multipart
|
||||||
|
|
||||||
|
Encode a message using direct file uploads. Returns the stego image directly.
|
||||||
|
|
||||||
|
#### Request
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /encode/multipart HTTP/1.1
|
||||||
|
Host: localhost:8000
|
||||||
|
Content-Type: multipart/form-data; boundary=----FormBoundary
|
||||||
|
|
||||||
|
------FormBoundary
|
||||||
|
Content-Disposition: form-data; name="message"
|
||||||
|
|
||||||
|
Secret message here
|
||||||
|
------FormBoundary
|
||||||
|
Content-Disposition: form-data; name="day_phrase"
|
||||||
|
|
||||||
apple forest thunder
|
apple forest thunder
|
||||||
------FormBoundary
|
------FormBoundary
|
||||||
Content-Disposition: form-data; name="pin"
|
Content-Disposition: form-data; name="pin"
|
||||||
@@ -330,6 +373,18 @@ Content-Disposition: form-data; name="pin"
|
|||||||
Content-Disposition: form-data; name="carrier"; filename="carrier.png"
|
Content-Disposition: form-data; name="carrier"; filename="carrier.png"
|
||||||
Content-Type: image/png
|
Content-Type: image/png
|
||||||
|
|
||||||
|
<binary image data>
|
||||||
|
------FormBoundary--
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Form Fields
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|-------|------|----------|---------|-------------|
|
||||||
|
| `message` | string | ✓ | | Message to encode |
|
||||||
|
| `reference_photo` | file | ✓ | | Reference photo file |
|
||||||
|
| `carrier` | file | ✓ | | Carrier image file |
|
||||||
|
| `day_phrase` | string | ✓ | | Today's passphrase |
|
||||||
| `pin` | string | * | | Static PIN |
|
| `pin` | string | * | | Static PIN |
|
||||||
| `rsa_key` | file | * | | RSA key file (.pem) |
|
| `rsa_key` | file | * | | RSA key file (.pem) |
|
||||||
| `rsa_password` | string | | | Password for RSA key |
|
| `rsa_password` | string | | | Password for RSA key |
|
||||||
@@ -344,83 +399,72 @@ Content-Type: image/png
|
|||||||
|
|
||||||
Returns the image directly with headers:
|
Returns the image directly with headers:
|
||||||
|
|
||||||
- `Content-Disposition: attachment; filename=<generated_filename>.png`
|
```http
|
||||||
- `X-Stegasoo-Date: 2025-12-27` (date used for encoding)
|
HTTP/1.1 200 OK
|
||||||
- `X-Stegasoo-Day: Saturday` (day of week for passphrase rotation)
|
Content-Type: image/png
|
||||||
- `X-Stegasoo-Capacity-Percent: 12.4` (capacity used)
|
Content-Disposition: attachment; filename="a1b2c3d4_20251227.png"
|
||||||
|
X-Stegasoo-Date: 2025-12-27
|
||||||
#### cURL Examples
|
X-Stegasoo-Day: Saturday
|
||||||
|
X-Stegasoo-Capacity-Used: 12.4
|
||||||
**With PIN:**
|
X-Stegasoo-Embedding-Mode: lsb
|
||||||
```bash
|
X-Stegasoo-Output-Format: png
|
||||||
curl -X POST http://localhost:8000/encode/multipart \
|
|
||||||
|
<binary image data>
|
||||||
|
```
|
||||||
|
|
||||||
#### Response Headers
|
#### Response Headers
|
||||||
|
|
||||||
| Header | Description |
|
| Header | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `Content-Type` | `image/png` or `image/jpeg` |
|
| `Content-Type` | `image/png` or `image/jpeg` |
|
||||||
--output stego.png
|
| `Content-Disposition` | Suggested filename |
|
||||||
```
|
|
||||||
|
|
||||||
**With RSA key:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8000/encode/multipart \
|
|
||||||
| `X-Stegasoo-Date` | Encoding date |
|
| `X-Stegasoo-Date` | Encoding date |
|
||||||
-F "day_phrase=apple forest thunder" \
|
| `X-Stegasoo-Day` | Day of week |
|
||||||
|
| `X-Stegasoo-Capacity-Used` | Capacity percentage |
|
||||||
|
| `X-Stegasoo-Embedding-Mode` | `lsb` or `dct` |
|
||||||
|
| `X-Stegasoo-Output-Format` | `png` or `jpeg` |
|
||||||
|
| `X-Stegasoo-Color-Mode` | `color` or `grayscale` (DCT only) |
|
||||||
|
|
||||||
|
#### cURL Example (DCT + JPEG)
|
||||||
|
|
||||||
|
```bash
|
||||||
curl -X POST http://localhost:8000/encode/multipart \
|
curl -X POST http://localhost:8000/encode/multipart \
|
||||||
-F "rsa_password=keypassword" \
|
-F "message=Secret message for social media" \
|
||||||
-F "reference_photo=@reference.jpg" \
|
|
||||||
-F "carrier=@carrier.png" \
|
|
||||||
--output stego.png
|
|
||||||
```
|
|
||||||
|
|
||||||
**With both PIN and RSA:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8000/encode/multipart \
|
|
||||||
-F "day_phrase=apple forest thunder" \
|
-F "day_phrase=apple forest thunder" \
|
||||||
-F "pin=123456" \
|
-F "pin=123456" \
|
||||||
-F "pin=123456" \
|
-F "embedding_mode=dct" \
|
||||||
-F "rsa_key=@mykey.pem" \
|
-F "output_format=jpeg" \
|
||||||
-F "rsa_password=keypassword" \
|
-F "color_mode=color" \
|
||||||
-F "reference_photo=@reference.jpg" \
|
-F "reference_photo=@reference.jpg" \
|
||||||
-F "carrier=@carrier.png" \
|
-F "carrier=@carrier.png" \
|
||||||
--output stego.png
|
--output stego.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
**With custom date:**
|
---
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8000/encode/multipart \
|
### POST /decode (JSON)
|
||||||
|
|
||||||
|
Decode a message using base64-encoded images. Auto-detects embedding mode.
|
||||||
|
|
||||||
#### Request
|
#### Request
|
||||||
-F "day_phrase=monday phrase here" \
|
|
||||||
|
|
||||||
```http
|
```http
|
||||||
-F "reference_photo=@reference.jpg" \
|
POST /decode HTTP/1.1
|
||||||
Host: localhost:8000
|
Host: localhost:8000
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
```
|
|
||||||
|
```
|
||||||
|
|
||||||
#### Request Body
|
#### Request Body
|
||||||
|
|
||||||
### POST /decode (JSON)
|
| Field | Type | Required | Description |
|
||||||
|
|
||||||
Decode a message using base64-encoded images.
|
|
||||||
|
|
||||||
#### Request
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /decode HTTP/1.1
|
|
||||||
Host: localhost:8000
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|-------|------|----------|-------------|
|
|-------|------|----------|-------------|
|
||||||
| `stego_image_base64` | string | ✓ | Base64-encoded stego image |
|
| `stego_image_base64` | string | ✓ | Base64-encoded stego image |
|
||||||
| `reference_photo_base64` | string | ✓ | Base64-encoded reference photo |
|
| `reference_photo_base64` | string | ✓ | Base64-encoded reference photo |
|
||||||
| `day_phrase` | string | ✓ | Passphrase for encoding day |
|
| `day_phrase` | string | ✓ | Passphrase for encoding day |
|
||||||
| `pin` | string | * | Static PIN |
|
| `pin` | string | * | Static PIN |
|
||||||
| `rsa_key_base64` | string | * | Base64-encoded RSA key |
|
| `rsa_key_base64` | string | * | Base64-encoded RSA key |
|
||||||
| `day_phrase` | string | ✓ | Passphrase for encoding day |
|
| `rsa_password` | string | | Password for RSA key |
|
||||||
|
|
||||||
\* Must match security factors used during encoding.
|
\* Must match security factors used during encoding.
|
||||||
|
|
||||||
@@ -450,20 +494,27 @@ Content-Type: application/json
|
|||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{
|
-d "{
|
||||||
\"stego_image_base64\": \"$STEGO_B64\",
|
\"stego_image_base64\": \"$STEGO_B64\",
|
||||||
```
|
\"reference_photo_base64\": \"$REF_B64\",
|
||||||
\"day_phrase\": \"apple forest thunder\",
|
\"day_phrase\": \"apple forest thunder\",
|
||||||
\"pin\": \"123456\"
|
\"pin\": \"123456\"
|
||||||
}"
|
}"
|
||||||
```
|
```
|
||||||
|
|
||||||
Decode a message using direct file uploads.
|
---
|
||||||
|
|
||||||
### POST /decode/multipart
|
### POST /decode/multipart
|
||||||
|
|
||||||
Decode using direct file uploads. Auto-detects embedding mode.
|
Decode using direct file uploads. Auto-detects embedding mode.
|
||||||
|
|
||||||
|
#### Form Fields
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `stego_image` | file | ✓ | Stego image file |
|
||||||
|
| `reference_photo` | file | ✓ | Reference photo file |
|
||||||
| `day_phrase` | string | ✓ | Passphrase for encoding day |
|
| `day_phrase` | string | ✓ | Passphrase for encoding day |
|
||||||
| `pin` | string | * | Static PIN |
|
| `pin` | string | * | Static PIN |
|
||||||
| `rsa_key` | file | * | RSA key file (.pem) |
|
| `rsa_key` | file | * | RSA key file (.pem) |
|
||||||
Content-Type: multipart/form-data
|
|
||||||
| `rsa_password` | string | | Password for RSA key |
|
| `rsa_password` | string | | Password for RSA key |
|
||||||
|
|
||||||
#### Response
|
#### Response
|
||||||
@@ -481,15 +532,7 @@ curl -X POST http://localhost:8000/decode \
|
|||||||
curl -X POST http://localhost:8000/decode/multipart \
|
curl -X POST http://localhost:8000/decode/multipart \
|
||||||
-F "day_phrase=apple forest thunder" \
|
-F "day_phrase=apple forest thunder" \
|
||||||
-F "pin=123456" \
|
-F "pin=123456" \
|
||||||
"message": "Secret message here"
|
-F "reference_photo=@reference.jpg" \
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### cURL Examples
|
|
||||||
|
|
||||||
**With PIN:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8000/decode/multipart \
|
|
||||||
-F "stego_image=@stego.png"
|
-F "stego_image=@stego.png"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -499,20 +542,20 @@ Content-Type: multipart/form-data
|
|||||||
|
|
||||||
Get image information and capacity for both LSB and DCT modes.
|
Get image information and capacity for both LSB and DCT modes.
|
||||||
|
|
||||||
-F "day_phrase=apple forest thunder" \
|
#### Request (JSON)
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /image/info HTTP/1.1
|
POST /image/info HTTP/1.1
|
||||||
Host: localhost:8000
|
Host: localhost:8000
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
---
|
```
|
||||||
|
|
||||||
#### Request (Multipart)
|
#### Request (Multipart)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
Get information about an image's capacity.
|
curl -X POST http://localhost:8000/image/info \
|
||||||
-F "image=@carrier.png"
|
-F "image=@carrier.png"
|
||||||
#### Request
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Response
|
#### Response
|
||||||
@@ -521,35 +564,30 @@ curl -X POST http://localhost:8000/decode/multipart \
|
|||||||
{
|
{
|
||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080,
|
"height": 1080,
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
|
||||||
|-------|------|----------|-------------|
|
|
||||||
| `image` | file | ✓ | Image file to analyze |
|
|
||||||
|
|
||||||
#### Response
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"width": 1920,
|
|
||||||
"pixels": 2073600,
|
"pixels": 2073600,
|
||||||
"format": "PNG",
|
"format": "PNG",
|
||||||
"mode": "RGB",
|
"mode": "RGB",
|
||||||
"capacity": {
|
"capacity": {
|
||||||
}
|
"lsb": {
|
||||||
"bytes": 776970,
|
"bytes": 776970,
|
||||||
|
"kb": 758
|
||||||
},
|
},
|
||||||
"dct": {
|
"dct": {
|
||||||
"bytes": 64800,
|
"bytes": 64800,
|
||||||
"kb": 63,
|
"kb": 63,
|
||||||
| `width` | integer | Image width in pixels |
|
"note": "Approximate - actual capacity depends on image content"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
#### Response Fields
|
#### Response Fields
|
||||||
| `capacity_bytes` | integer | Maximum message capacity (bytes) |
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
#### cURL Example
|
| `width` | integer | Image width in pixels |
|
||||||
|
| `height` | integer | Image height in pixels |
|
||||||
|
| `pixels` | integer | Total pixel count |
|
||||||
| `format` | string | Image format (PNG, JPEG, etc.) |
|
| `format` | string | Image format (PNG, JPEG, etc.) |
|
||||||
| `mode` | string | Color mode (RGB, L, etc.) |
|
| `mode` | string | Color mode (RGB, L, etc.) |
|
||||||
| `capacity.lsb.bytes` | integer | LSB capacity in bytes |
|
| `capacity.lsb.bytes` | integer | LSB capacity in bytes |
|
||||||
@@ -558,8 +596,19 @@ Content-Type: multipart/form-data
|
|||||||
| `capacity.dct.kb` | integer | Estimated DCT capacity in KB |
|
| `capacity.dct.kb` | integer | Estimated DCT capacity in KB |
|
||||||
| `capacity.dct.note` | string | Capacity estimation note |
|
| `capacity.dct.note` | string | Capacity estimation note |
|
||||||
|
|
||||||
|
---
|
||||||
### GenerateRequest
|
|
||||||
|
## Embedding Modes
|
||||||
|
|
||||||
|
### LSB Mode (Default)
|
||||||
|
|
||||||
|
**Least Significant Bit** embedding modifies pixel values directly.
|
||||||
|
|
||||||
|
| Aspect | Details |
|
||||||
|
|--------|---------|
|
||||||
|
| **Parameter** | `"embedding_mode": "lsb"` |
|
||||||
|
| **Capacity** | ~3 bits/pixel (~770 KB for 1920×1080) |
|
||||||
|
| **Output** | PNG only (lossless required) |
|
||||||
| **Resilience** | ❌ Destroyed by JPEG compression |
|
| **Resilience** | ❌ Destroyed by JPEG compression |
|
||||||
| **Best For** | Maximum capacity, controlled channels |
|
| **Best For** | Maximum capacity, controlled channels |
|
||||||
|
|
||||||
@@ -570,15 +619,58 @@ Content-Type: multipart/form-data
|
|||||||
| Aspect | Details |
|
| Aspect | Details |
|
||||||
|--------|---------|
|
|--------|---------|
|
||||||
| **Parameter** | `"embedding_mode": "dct"` |
|
| **Parameter** | `"embedding_mode": "dct"` |
|
||||||
|
| **Capacity** | ~0.25 bits/pixel (~65 KB for 1920×1080) |
|
||||||
### GenerateResponse
|
| **Output** | PNG or JPEG |
|
||||||
|
| **Resilience** | ✅ Survives JPEG compression |
|
||||||
|
| **Best For** | Social media, messaging apps |
|
||||||
|
|
||||||
|
> ⚠️ **Experimental**: DCT mode may have edge cases. Test with your workflow.
|
||||||
|
|
||||||
### DCT Options
|
### DCT Options
|
||||||
```json
|
|
||||||
| Option | Values | Default | Description |
|
| Option | Values | Default | Description |
|
||||||
"phrases": {"Monday": "...", "Tuesday": "...", ...},
|
|--------|--------|---------|-------------|
|
||||||
"pin": "123456",
|
| `output_format` | `"png"`, `"jpeg"` | `"png"` | Output image format |
|
||||||
"rsa_key_pem": "-----BEGIN PRIVATE KEY-----...",
|
| `color_mode` | `"color"`, `"grayscale"` | `"color"` | Color processing mode |
|
||||||
"entropy": {"phrase": 33, "pin": 19, "rsa": 0, "total": 52}
|
|
||||||
|
### Capacity Comparison
|
||||||
|
|
||||||
|
| Mode | 1920×1080 Capacity |
|
||||||
|
|------|-------------------|
|
||||||
|
| LSB (PNG) | ~770 KB |
|
||||||
|
| DCT (PNG) | ~65 KB |
|
||||||
|
| DCT (JPEG) | ~30-50 KB |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### GenerateRequest
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"use_pin": true,
|
||||||
|
"use_rsa": false,
|
||||||
|
"pin_length": 6,
|
||||||
|
"rsa_bits": 2048,
|
||||||
|
"words_per_phrase": 3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GenerateResponse
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"phrases": {"Monday": "...", "Tuesday": "...", ...},
|
||||||
|
"pin": "123456",
|
||||||
|
"rsa_key_pem": "-----BEGIN PRIVATE KEY-----...",
|
||||||
|
"entropy": {"phrase": 33, "pin": 19, "rsa": 0, "total": 52}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### EncodeRequest
|
||||||
|
|
||||||
|
```json
|
||||||
{
|
{
|
||||||
"message": "string",
|
"message": "string",
|
||||||
"reference_photo_base64": "string",
|
"reference_photo_base64": "string",
|
||||||
@@ -618,7 +710,10 @@ curl -X POST http://localhost:8000/image/info \
|
|||||||
"day_phrase": "string",
|
"day_phrase": "string",
|
||||||
"pin": "string",
|
"pin": "string",
|
||||||
"rsa_key_base64": "string",
|
"rsa_key_base64": "string",
|
||||||
"rsa_password": "string"
|
"rsa_password": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### DecodeResponse
|
### DecodeResponse
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -630,7 +725,10 @@ curl -X POST http://localhost:8000/image/info \
|
|||||||
|
|
||||||
### ImageInfoResponse
|
### ImageInfoResponse
|
||||||
|
|
||||||
### ImageInfoResponse
|
```json
|
||||||
|
{
|
||||||
|
"width": 1920,
|
||||||
|
"height": 1080,
|
||||||
"pixels": 2073600,
|
"pixels": 2073600,
|
||||||
"format": "PNG",
|
"format": "PNG",
|
||||||
"mode": "RGB",
|
"mode": "RGB",
|
||||||
@@ -651,7 +749,8 @@ curl -X POST http://localhost:8000/image/info \
|
|||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
---
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
### HTTP Status Codes
|
### HTTP Status Codes
|
||||||
|
|
||||||
@@ -662,8 +761,12 @@ curl -X POST http://localhost:8000/image/info \
|
|||||||
| 401 | Unauthorized | Decryption failed (wrong credentials) |
|
| 401 | Unauthorized | Decryption failed (wrong credentials) |
|
||||||
| 500 | Internal Error | Unexpected server error |
|
| 500 | Internal Error | Unexpected server error |
|
||||||
|
|
||||||
| 500 | Internal Error | Unexpected server error |
|
### Error Response Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"detail": "Error message describing the problem"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Common Errors
|
### Common Errors
|
||||||
@@ -705,8 +808,11 @@ curl -X POST http://localhost:8000/image/info \
|
|||||||
# Encode using multipart (LSB mode - default)
|
# Encode using multipart (LSB mode - default)
|
||||||
with open("reference.jpg", "rb") as ref, open("carrier.png", "rb") as carrier:
|
with open("reference.jpg", "rb") as ref, open("carrier.png", "rb") as carrier:
|
||||||
response = requests.post(f"{BASE_URL}/encode/multipart", files={
|
response = requests.post(f"{BASE_URL}/encode/multipart", files={
|
||||||
|
"reference_photo": ref,
|
||||||
|
"carrier": carrier,
|
||||||
|
}, data={
|
||||||
"message": "Secret message",
|
"message": "Secret message",
|
||||||
with open("reference.jpg", "rb") as ref, open("carrier.png", "rb") as carrier:
|
"day_phrase": "apple forest thunder",
|
||||||
"pin": "123456"
|
"pin": "123456"
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -730,7 +836,7 @@ creds = response.json()
|
|||||||
with open("stego_social.jpg", "wb") as f:
|
with open("stego_social.jpg", "wb") as f:
|
||||||
f.write(response.content)
|
f.write(response.content)
|
||||||
|
|
||||||
```
|
# Decode using multipart (auto-detects mode)
|
||||||
with open("reference.jpg", "rb") as ref, open("stego.png", "rb") as stego:
|
with open("reference.jpg", "rb") as ref, open("stego.png", "rb") as stego:
|
||||||
response = requests.post(f"{BASE_URL}/decode/multipart", files={
|
response = requests.post(f"{BASE_URL}/decode/multipart", files={
|
||||||
"reference_photo": ref,
|
"reference_photo": ref,
|
||||||
@@ -744,7 +850,24 @@ with open("reference.jpg", "rb") as ref, open("carrier.png", "rb") as carrier:
|
|||||||
print(f"Decoded: {result['message']}")
|
print(f"Decoded: {result['message']}")
|
||||||
print(f"Mode detected: {result['embedding_mode_detected']}")
|
print(f"Mode detected: {result['embedding_mode_detected']}")
|
||||||
```
|
```
|
||||||
form.append('day_phrase', 'apple forest thunder');
|
|
||||||
|
### JavaScript/Node.js
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const FormData = require('form-data');
|
||||||
|
const fs = require('fs');
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const BASE_URL = 'http://localhost:8000';
|
||||||
|
|
||||||
|
async function encodeDCT() {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('message', 'Secret message for social media');
|
||||||
|
form.append('day_phrase', 'apple forest thunder');
|
||||||
|
form.append('pin', '123456');
|
||||||
|
form.append('embedding_mode', 'dct');
|
||||||
|
form.append('output_format', 'jpeg');
|
||||||
|
form.append('color_mode', 'color');
|
||||||
form.append('reference_photo', fs.createReadStream('reference.jpg'));
|
form.append('reference_photo', fs.createReadStream('reference.jpg'));
|
||||||
form.append('carrier', fs.createReadStream('carrier.png'));
|
form.append('carrier', fs.createReadStream('carrier.png'));
|
||||||
|
|
||||||
@@ -754,7 +877,9 @@ with open("reference.jpg", "rb") as ref, open("stego.png", "rb") as stego:
|
|||||||
});
|
});
|
||||||
|
|
||||||
fs.writeFileSync('stego.jpg', response.data);
|
fs.writeFileSync('stego.jpg', response.data);
|
||||||
fs.writeFileSync('stego.png', response.data);
|
console.log('Encoded with DCT mode');
|
||||||
|
console.log('Embedding mode:', response.headers['x-stegasoo-embedding-mode']);
|
||||||
|
}
|
||||||
|
|
||||||
async function decode() {
|
async function decode() {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
@@ -766,11 +891,14 @@ const axios = require('axios');
|
|||||||
const response = await axios.post(`${BASE_URL}/decode/multipart`, form, {
|
const response = await axios.post(`${BASE_URL}/decode/multipart`, form, {
|
||||||
headers: form.getHeaders()
|
headers: form.getHeaders()
|
||||||
});
|
});
|
||||||
headers: form.getHeaders()
|
|
||||||
console.log('Decoded:', response.data.message);
|
console.log('Decoded:', response.data.message);
|
||||||
|
console.log('Mode detected:', response.data.embedding_mode_detected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
encodeDCT().then(decode);
|
||||||
|
```
|
||||||
|
|
||||||
### Go
|
### Go
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -779,8 +907,9 @@ async function encode() {
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
import (
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
@@ -788,16 +917,17 @@ async function decode() {
|
|||||||
func main() {
|
func main() {
|
||||||
// Encode with DCT mode
|
// Encode with DCT mode
|
||||||
body := &bytes.Buffer{}
|
body := &bytes.Buffer{}
|
||||||
)
|
writer := multipart.NewWriter(body)
|
||||||
|
|
||||||
writer.WriteField("message", "Secret message")
|
writer.WriteField("message", "Secret message")
|
||||||
writer.WriteField("day_phrase", "apple forest thunder")
|
writer.WriteField("day_phrase", "apple forest thunder")
|
||||||
writer.WriteField("pin", "123456")
|
writer.WriteField("pin", "123456")
|
||||||
writer.WriteField("embedding_mode", "dct")
|
writer.WriteField("embedding_mode", "dct")
|
||||||
writer.WriteField("output_format", "jpeg")
|
writer.WriteField("output_format", "jpeg")
|
||||||
|
writer.WriteField("color_mode", "color")
|
||||||
|
|
||||||
ref, _ := os.Open("reference.jpg")
|
ref, _ := os.Open("reference.jpg")
|
||||||
writer.WriteField("pin", "123456")
|
refPart, _ := writer.CreateFormFile("reference_photo", "reference.jpg")
|
||||||
io.Copy(refPart, ref)
|
io.Copy(refPart, ref)
|
||||||
ref.Close()
|
ref.Close()
|
||||||
|
|
||||||
@@ -816,13 +946,16 @@ import (
|
|||||||
|
|
||||||
// Check embedding mode from header
|
// Check embedding mode from header
|
||||||
fmt.Println("Embedding mode:", resp.Header.Get("X-Stegasoo-Embedding-Mode"))
|
fmt.Println("Embedding mode:", resp.Header.Get("X-Stegasoo-Embedding-Mode"))
|
||||||
|
|
||||||
stego, _ := os.Create("stego.jpg")
|
stego, _ := os.Create("stego.jpg")
|
||||||
io.Copy(stego, resp.Body)
|
io.Copy(stego, resp.Body)
|
||||||
stego.Close()
|
stego.Close()
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
|
|
||||||
fmt.Println("Encoded successfully with DCT mode")
|
fmt.Println("Encoded successfully with DCT mode")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Shell Script (Bash)
|
### Shell Script (Bash)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -842,12 +975,15 @@ func main() {
|
|||||||
-F "day_phrase=$PHRASE" \
|
-F "day_phrase=$PHRASE" \
|
||||||
-F "pin=$PIN" \
|
-F "pin=$PIN" \
|
||||||
-F "reference_photo=@$REF_PHOTO" \
|
-F "reference_photo=@$REF_PHOTO" \
|
||||||
-F "day_phrase=$PHRASE" \
|
-F "carrier=@$CARRIER" \
|
||||||
|
--output stego_lsb.png
|
||||||
|
|
||||||
|
echo "Encoded to stego_lsb.png"
|
||||||
|
|
||||||
# Encode with DCT for social media
|
# Encode with DCT for social media
|
||||||
echo "Encoding with DCT mode..."
|
echo "Encoding with DCT mode..."
|
||||||
curl -s -X POST "$BASE_URL/encode/multipart" \
|
curl -s -X POST "$BASE_URL/encode/multipart" \
|
||||||
|
-F "message=$MESSAGE" \
|
||||||
-F "day_phrase=$PHRASE" \
|
-F "day_phrase=$PHRASE" \
|
||||||
-F "pin=$PIN" \
|
-F "pin=$PIN" \
|
||||||
-F "embedding_mode=dct" \
|
-F "embedding_mode=dct" \
|
||||||
@@ -863,27 +999,43 @@ PHRASE="apple forest thunder"
|
|||||||
echo "Decoding..."
|
echo "Decoding..."
|
||||||
RESULT=$(curl -s -X POST "$BASE_URL/decode/multipart" \
|
RESULT=$(curl -s -X POST "$BASE_URL/decode/multipart" \
|
||||||
-F "day_phrase=$PHRASE" \
|
-F "day_phrase=$PHRASE" \
|
||||||
## Rate Limiting
|
-F "pin=$PIN" \
|
||||||
|
-F "reference_photo=@$REF_PHOTO" \
|
||||||
-F "stego_image=@stego_dct.jpg")
|
-F "stego_image=@stego_dct.jpg")
|
||||||
|
|
||||||
echo "Decoded message: $(echo $RESULT | jq -r '.message')"
|
echo "Decoded message: $(echo $RESULT | jq -r '.message')"
|
||||||
echo "Mode detected: $(echo $RESULT | jq -r '.embedding_mode_detected')"
|
echo "Mode detected: $(echo $RESULT | jq -r '.embedding_mode_detected')"
|
||||||
```
|
```
|
||||||
|
|
||||||
```nginx
|
---
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
limit_req zone=stegasoo burst=20 nodelay;
|
The API does not implement rate limiting by default. For production:
|
||||||
|
|
||||||
|
1. **Reverse Proxy**: Use nginx or Caddy rate limiting
|
||||||
|
2. **Application Level**: Add FastAPI middleware
|
||||||
|
|
||||||
|
Example nginx rate limiting:
|
||||||
|
```nginx
|
||||||
|
limit_req_zone $binary_remote_addr zone=stegasoo:10m rate=10r/s;
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
limit_req zone=stegasoo burst=20 nodelay;
|
||||||
|
proxy_pass http://localhost:8000/;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
}
|
## Security Considerations
|
||||||
|
|
||||||
### In Transit
|
### In Transit
|
||||||
|
|
||||||
|
- Use HTTPS in production
|
||||||
- Configure TLS at reverse proxy level
|
- Configure TLS at reverse proxy level
|
||||||
|
|
||||||
|
### Memory Usage
|
||||||
|
|
||||||
- Argon2id requires 256MB RAM per operation
|
- Argon2id requires 256MB RAM per operation
|
||||||
- DCT mode adds ~100MB for scipy operations
|
- DCT mode adds ~100MB for scipy operations
|
||||||
@@ -917,9 +1069,15 @@ location /api/ {
|
|||||||
|------|--------------|
|
|------|--------------|
|
||||||
| LSB | Maximum capacity but fragile |
|
| LSB | Maximum capacity but fragile |
|
||||||
| DCT | Lower capacity but survives recompression |
|
| DCT | Lower capacity but survives recompression |
|
||||||
|
|
||||||
Both modes use identical encryption (AES-256-GCM with Argon2id).
|
Both modes use identical encryption (AES-256-GCM with Argon2id).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Interactive Documentation
|
||||||
|
|
||||||
|
When the API is running, visit:
|
||||||
|
|
||||||
- **Swagger UI**: http://localhost:8000/docs
|
- **Swagger UI**: http://localhost:8000/docs
|
||||||
- **ReDoc**: http://localhost:8000/redoc
|
- **ReDoc**: http://localhost:8000/redoc
|
||||||
|
|
||||||
@@ -927,6 +1085,8 @@ The API validates:
|
|||||||
|
|
||||||
## See Also
|
## See Also
|
||||||
|
|
||||||
|
- [CLI Documentation](CLI.md) - Command-line interface
|
||||||
|
- [Web UI Documentation](WEB_UI.md) - Browser interface
|
||||||
- [README](README.md) - Project overview
|
- [README](README.md) - Project overview
|
||||||
### Credential Handling
|
### Credential Handling
|
||||||
|
|
||||||
@@ -934,6 +1094,15 @@ The API validates:
|
|||||||
- No persistent storage of secrets
|
- No persistent storage of secrets
|
||||||
- Memory cleared after operations
|
- Memory cleared after operations
|
||||||
|
|
||||||
|
### Embedding Mode Security
|
||||||
|
|
||||||
|
| Mode | Consideration |
|
||||||
|
|------|--------------|
|
||||||
|
| LSB | Maximum capacity but fragile |
|
||||||
|
| DCT | Lower capacity but survives recompression |
|
||||||
|
|
||||||
|
Both modes use identical encryption (AES-256-GCM with Argon2id).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Interactive Documentation
|
## Interactive Documentation
|
||||||
|
|||||||
340
frontends/CLI.md
340
frontends/CLI.md
@@ -11,6 +11,7 @@ Complete command-line interface reference for Stegasoo steganography operations.
|
|||||||
- [encode](#encode-command)
|
- [encode](#encode-command)
|
||||||
- [decode](#decode-command)
|
- [decode](#decode-command)
|
||||||
- [info](#info-command)
|
- [info](#info-command)
|
||||||
|
- [Embedding Modes](#embedding-modes)
|
||||||
- [Security Factors](#security-factors)
|
- [Security Factors](#security-factors)
|
||||||
- [Workflow Examples](#workflow-examples)
|
- [Workflow Examples](#workflow-examples)
|
||||||
- [Piping & Scripting](#piping--scripting)
|
- [Piping & Scripting](#piping--scripting)
|
||||||
@@ -27,6 +28,9 @@ Complete command-line interface reference for Stegasoo steganography operations.
|
|||||||
# CLI only
|
# CLI only
|
||||||
pip install stegasoo[cli]
|
pip install stegasoo[cli]
|
||||||
|
|
||||||
|
# CLI with DCT support
|
||||||
|
pip install stegasoo[cli,dct]
|
||||||
|
|
||||||
# With all extras
|
# With all extras
|
||||||
pip install stegasoo[all]
|
pip install stegasoo[all]
|
||||||
```
|
```
|
||||||
@@ -36,7 +40,7 @@ pip install stegasoo[all]
|
|||||||
```bash
|
```bash
|
||||||
git clone https://github.com/example/stegasoo.git
|
git clone https://github.com/example/stegasoo.git
|
||||||
cd stegasoo
|
cd stegasoo
|
||||||
pip install -e ".[cli]"
|
pip install -e ".[cli,dct]"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Verify Installation
|
### Verify Installation
|
||||||
@@ -44,6 +48,9 @@ pip install -e ".[cli]"
|
|||||||
```bash
|
```bash
|
||||||
stegasoo --version
|
stegasoo --version
|
||||||
stegasoo --help
|
stegasoo --help
|
||||||
|
|
||||||
|
# Check DCT support
|
||||||
|
python -c "from stegasoo.dct_steganography import has_jpegio_support; print('jpegio:', has_jpegio_support())"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -54,7 +61,7 @@ stegasoo --help
|
|||||||
# 1. Generate credentials (do this once, memorize results)
|
# 1. Generate credentials (do this once, memorize results)
|
||||||
stegasoo generate --pin --words 3
|
stegasoo generate --pin --words 3
|
||||||
|
|
||||||
# 2. Encode a message
|
# 2. Encode a message (LSB mode - default)
|
||||||
stegasoo encode \
|
stegasoo encode \
|
||||||
--ref secret_photo.jpg \
|
--ref secret_photo.jpg \
|
||||||
--carrier meme.png \
|
--carrier meme.png \
|
||||||
@@ -62,7 +69,17 @@ stegasoo encode \
|
|||||||
--pin 123456 \
|
--pin 123456 \
|
||||||
--message "Meet at midnight"
|
--message "Meet at midnight"
|
||||||
|
|
||||||
# 3. Decode a message
|
# 3. Encode for social media (DCT mode)
|
||||||
|
stegasoo encode \
|
||||||
|
--ref secret_photo.jpg \
|
||||||
|
--carrier meme.png \
|
||||||
|
--phrase "apple forest thunder" \
|
||||||
|
--pin 123456 \
|
||||||
|
--message "Meet at midnight" \
|
||||||
|
--mode dct \
|
||||||
|
--format jpeg
|
||||||
|
|
||||||
|
# 4. Decode a message (auto-detects mode)
|
||||||
stegasoo decode \
|
stegasoo decode \
|
||||||
--ref secret_photo.jpg \
|
--ref secret_photo.jpg \
|
||||||
--stego stego_abc123_20251227.png \
|
--stego stego_abc123_20251227.png \
|
||||||
@@ -106,9 +123,9 @@ stegasoo generate
|
|||||||
|
|
||||||
Output:
|
Output:
|
||||||
```
|
```
|
||||||
════════════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════════════
|
||||||
STEGASOO CREDENTIALS
|
STEGASOO CREDENTIALS
|
||||||
════════════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
⚠️ MEMORIZE THESE AND CLOSE THIS WINDOW
|
⚠️ MEMORIZE THESE AND CLOSE THIS WINDOW
|
||||||
Do not screenshot or save to file!
|
Do not screenshot or save to file!
|
||||||
@@ -171,19 +188,22 @@ stegasoo encode [OPTIONS]
|
|||||||
|
|
||||||
#### Options
|
#### Options
|
||||||
|
|
||||||
| Option | Short | Type | Required | Description |
|
| Option | Short | Type | Required | Default | Description |
|
||||||
|--------|-------|------|----------|-------------|
|
|--------|-------|------|----------|---------|-------------|
|
||||||
| `--ref` | `-r` | path | ✓ | Reference photo (shared secret) |
|
| `--ref` | `-r` | path | ✓ | | Reference photo (shared secret) |
|
||||||
| `--carrier` | `-c` | path | ✓ | Carrier image to hide message in |
|
| `--carrier` | `-c` | path | ✓ | | Carrier image to hide message in |
|
||||||
| `--phrase` | `-p` | string | ✓ | Today's passphrase |
|
| `--phrase` | `-p` | string | ✓ | | Today's passphrase |
|
||||||
| `--message` | `-m` | string | | Message to encode |
|
| `--message` | `-m` | string | | | Message to encode |
|
||||||
| `--message-file` | `-f` | path | | Read message from file |
|
| `--message-file` | `-f` | path | | | Read message from file |
|
||||||
| `--pin` | | string | * | Static PIN (6-9 digits) |
|
| `--pin` | | string | * | | Static PIN (6-9 digits) |
|
||||||
| `--key` | `-k` | path | * | RSA key file |
|
| `--key` | `-k` | path | * | | RSA key file |
|
||||||
| `--key-password` | | string | | Password for RSA key |
|
| `--key-password` | | string | | | Password for RSA key |
|
||||||
| `--output` | `-o` | path | | Output filename |
|
| `--output` | `-o` | path | | | Output filename |
|
||||||
| `--date` | | YYYY-MM-DD | | Date override |
|
| `--date` | | YYYY-MM-DD | | | Date override |
|
||||||
| `--quiet` | `-q` | flag | | Suppress output |
|
| `--mode` | | choice | | `lsb` | Embedding mode: `lsb` or `dct` |
|
||||||
|
| `--format` | | choice | | `png` | Output format: `png` or `jpeg` (DCT only) |
|
||||||
|
| `--color` | | choice | | `color` | Color mode: `color` or `grayscale` (DCT only) |
|
||||||
|
| `--quiet` | `-q` | flag | | | Suppress output |
|
||||||
|
|
||||||
\* At least one of `--pin` or `--key` is required.
|
\* At least one of `--pin` or `--key` is required.
|
||||||
|
|
||||||
@@ -206,7 +226,7 @@ stegasoo encode [OPTIONS]
|
|||||||
|
|
||||||
#### Examples
|
#### Examples
|
||||||
|
|
||||||
**Basic encoding with PIN:**
|
**Basic encoding with PIN (LSB mode - default):**
|
||||||
```bash
|
```bash
|
||||||
stegasoo encode \
|
stegasoo encode \
|
||||||
--ref photos/vacation.jpg \
|
--ref photos/vacation.jpg \
|
||||||
@@ -221,10 +241,60 @@ Output:
|
|||||||
✓ Encoded successfully!
|
✓ Encoded successfully!
|
||||||
Output: a1b2c3d4_20251227.png
|
Output: a1b2c3d4_20251227.png
|
||||||
Size: 245,832 bytes
|
Size: 245,832 bytes
|
||||||
|
Mode: LSB
|
||||||
Capacity used: 12.4%
|
Capacity used: 12.4%
|
||||||
Date: 2025-12-27
|
Date: 2025-12-27
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**DCT mode for social media (JPEG output):**
|
||||||
|
```bash
|
||||||
|
stegasoo encode \
|
||||||
|
--ref photos/vacation.jpg \
|
||||||
|
--carrier memes/funny_cat.png \
|
||||||
|
--phrase "correct horse battery" \
|
||||||
|
--pin 847293 \
|
||||||
|
--message "The package arrives Tuesday" \
|
||||||
|
--mode dct \
|
||||||
|
--format jpeg
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
✓ Encoded successfully!
|
||||||
|
Output: a1b2c3d4_20251227.jpg
|
||||||
|
Size: 89,432 bytes
|
||||||
|
Mode: DCT (color, jpeg)
|
||||||
|
Capacity used: 45.2%
|
||||||
|
Date: 2025-12-27
|
||||||
|
|
||||||
|
⚠️ DCT mode is experimental
|
||||||
|
```
|
||||||
|
|
||||||
|
**DCT mode with PNG output (maximum DCT capacity):**
|
||||||
|
```bash
|
||||||
|
stegasoo encode \
|
||||||
|
-r ref.jpg \
|
||||||
|
-c carrier.png \
|
||||||
|
-p "phrase words here" \
|
||||||
|
--pin 123456 \
|
||||||
|
-m "Longer message that needs more space" \
|
||||||
|
--mode dct \
|
||||||
|
--format png \
|
||||||
|
--color color
|
||||||
|
```
|
||||||
|
|
||||||
|
**DCT grayscale mode:**
|
||||||
|
```bash
|
||||||
|
stegasoo encode \
|
||||||
|
-r ref.jpg \
|
||||||
|
-c bw_photo.png \
|
||||||
|
-p "phrase" \
|
||||||
|
--pin 123456 \
|
||||||
|
-m "Message" \
|
||||||
|
--mode dct \
|
||||||
|
--color grayscale
|
||||||
|
```
|
||||||
|
|
||||||
**With RSA key:**
|
**With RSA key:**
|
||||||
```bash
|
```bash
|
||||||
stegasoo encode \
|
stegasoo encode \
|
||||||
@@ -291,7 +361,7 @@ stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 -m "msg" -q -
|
|||||||
|
|
||||||
### Decode Command
|
### Decode Command
|
||||||
|
|
||||||
Decode a secret message from a stego image.
|
Decode a secret message from a stego image. **Automatically detects LSB vs DCT mode.**
|
||||||
|
|
||||||
#### Synopsis
|
#### Synopsis
|
||||||
|
|
||||||
@@ -328,6 +398,24 @@ stegasoo decode \
|
|||||||
Output:
|
Output:
|
||||||
```
|
```
|
||||||
✓ Decoded successfully!
|
✓ Decoded successfully!
|
||||||
|
Mode detected: LSB
|
||||||
|
|
||||||
|
The package arrives Tuesday
|
||||||
|
```
|
||||||
|
|
||||||
|
**Decoding DCT image (auto-detected):**
|
||||||
|
```bash
|
||||||
|
stegasoo decode \
|
||||||
|
--ref photos/vacation.jpg \
|
||||||
|
--stego received_image.jpg \
|
||||||
|
--phrase "correct horse battery" \
|
||||||
|
--pin 847293
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
✓ Decoded successfully!
|
||||||
|
Mode detected: DCT
|
||||||
|
|
||||||
The package arrives Tuesday
|
The package arrives Tuesday
|
||||||
```
|
```
|
||||||
@@ -377,7 +465,7 @@ stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q | gpg --decr
|
|||||||
|
|
||||||
### Info Command
|
### Info Command
|
||||||
|
|
||||||
Display information about an image's capacity and embedded date.
|
Display information about an image's capacity for both LSB and DCT modes.
|
||||||
|
|
||||||
#### Synopsis
|
#### Synopsis
|
||||||
|
|
||||||
@@ -405,10 +493,15 @@ Image: vacation_photo.png
|
|||||||
Pixels: 2,073,600
|
Pixels: 2,073,600
|
||||||
Mode: RGB
|
Mode: RGB
|
||||||
Format: PNG
|
Format: PNG
|
||||||
Capacity: ~776,970 bytes (758 KB)
|
|
||||||
|
Capacity:
|
||||||
|
LSB Mode: ~776,970 bytes (758 KB)
|
||||||
|
DCT Mode: ~64,800 bytes (63 KB) [approximate]
|
||||||
|
|
||||||
|
Note: DCT capacity varies based on image content
|
||||||
```
|
```
|
||||||
|
|
||||||
**Check stego image (shows encoding date):**
|
**Check stego image (shows encoding date and mode):**
|
||||||
```bash
|
```bash
|
||||||
stegasoo info stego_a1b2c3d4_20251227.png
|
stegasoo info stego_a1b2c3d4_20251227.png
|
||||||
```
|
```
|
||||||
@@ -420,12 +513,88 @@ Image: stego_a1b2c3d4_20251227.png
|
|||||||
Pixels: 2,073,600
|
Pixels: 2,073,600
|
||||||
Mode: RGB
|
Mode: RGB
|
||||||
Format: PNG
|
Format: PNG
|
||||||
Capacity: ~776,970 bytes (758 KB)
|
|
||||||
|
Stego Info:
|
||||||
Embed date: 2025-12-27 (Saturday)
|
Embed date: 2025-12-27 (Saturday)
|
||||||
|
Embed mode: DCT (detected)
|
||||||
|
|
||||||
|
Capacity:
|
||||||
|
LSB Mode: ~776,970 bytes (758 KB)
|
||||||
|
DCT Mode: ~64,800 bytes (63 KB) [approximate]
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Embedding Modes
|
||||||
|
|
||||||
|
Stegasoo v3.0+ supports two steganography algorithms.
|
||||||
|
|
||||||
|
### LSB Mode (Default)
|
||||||
|
|
||||||
|
**Least Significant Bit** embedding modifies pixel values directly.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stegasoo encode ... --mode lsb
|
||||||
|
# or just omit --mode (LSB is default)
|
||||||
|
```
|
||||||
|
|
||||||
|
| Aspect | Details |
|
||||||
|
|--------|---------|
|
||||||
|
| **Capacity** | ~3 bits/pixel (~770 KB for 1920×1080) |
|
||||||
|
| **Output** | PNG only (lossless required) |
|
||||||
|
| **Resilience** | ❌ Destroyed by JPEG compression |
|
||||||
|
| **Best For** | Maximum capacity, controlled channels |
|
||||||
|
|
||||||
|
### DCT Mode (Experimental)
|
||||||
|
|
||||||
|
**Discrete Cosine Transform** embedding hides data in frequency coefficients.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stegasoo encode ... --mode dct --format jpeg --color color
|
||||||
|
```
|
||||||
|
|
||||||
|
| Aspect | Details |
|
||||||
|
|--------|---------|
|
||||||
|
| **Capacity** | ~0.25 bits/pixel (~65 KB for 1920×1080) |
|
||||||
|
| **Output** | PNG or JPEG |
|
||||||
|
| **Resilience** | ✅ Survives JPEG compression |
|
||||||
|
| **Best For** | Social media, messaging apps |
|
||||||
|
|
||||||
|
> ⚠️ **Experimental**: DCT mode may have edge cases. Test with your workflow.
|
||||||
|
|
||||||
|
### DCT Options
|
||||||
|
|
||||||
|
| Option | Values | Default | Description |
|
||||||
|
|--------|--------|---------|-------------|
|
||||||
|
| `--format` | `png`, `jpeg` | `png` | Output image format |
|
||||||
|
| `--color` | `color`, `grayscale` | `color` | Color processing |
|
||||||
|
|
||||||
|
### Choosing the Right Mode
|
||||||
|
|
||||||
|
```
|
||||||
|
Will the image be recompressed?
|
||||||
|
(social media, messaging apps, etc.)
|
||||||
|
│
|
||||||
|
┌──────┴──────┐
|
||||||
|
▼ ▼
|
||||||
|
YES NO
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
Use DCT Use LSB
|
||||||
|
--mode dct (default)
|
||||||
|
--format jpeg
|
||||||
|
```
|
||||||
|
|
||||||
|
### Capacity Comparison
|
||||||
|
|
||||||
|
| Mode | 1920×1080 Capacity |
|
||||||
|
|------|-------------------|
|
||||||
|
| LSB (PNG) | ~770 KB |
|
||||||
|
| DCT (PNG) | ~65 KB |
|
||||||
|
| DCT (JPEG) | ~30-50 KB |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Security Factors
|
## Security Factors
|
||||||
|
|
||||||
Stegasoo uses multiple authentication factors:
|
Stegasoo uses multiple authentication factors:
|
||||||
@@ -468,25 +637,33 @@ stegasoo generate --rsa -o shared_key.pem -p "agreedpassword"
|
|||||||
# Securely transfer shared_key.pem to recipient
|
# Securely transfer shared_key.pem to recipient
|
||||||
```
|
```
|
||||||
|
|
||||||
**Sender (daily):**
|
**Sender (daily - private channel):**
|
||||||
```bash
|
```bash
|
||||||
# Get today's phrase from your memorized list
|
# For email, file transfer, etc. (no recompression)
|
||||||
TODAY_PHRASE="monday phrase words"
|
|
||||||
|
|
||||||
# Encode message
|
|
||||||
stegasoo encode \
|
stegasoo encode \
|
||||||
-r our_shared_photo.jpg \
|
-r our_shared_photo.jpg \
|
||||||
-c random_meme.png \
|
-c random_meme.png \
|
||||||
-p "$TODAY_PHRASE" \
|
-p "$TODAY_PHRASE" \
|
||||||
--pin 847293 \
|
--pin 847293 \
|
||||||
-m "Meeting moved to 3pm"
|
-m "Meeting moved to 3pm"
|
||||||
|
```
|
||||||
|
|
||||||
# Share output image via normal channels (email, chat, etc.)
|
**Sender (daily - social media):**
|
||||||
|
```bash
|
||||||
|
# For Instagram, Twitter, WhatsApp, etc.
|
||||||
|
stegasoo encode \
|
||||||
|
-r our_shared_photo.jpg \
|
||||||
|
-c random_meme.png \
|
||||||
|
-p "$TODAY_PHRASE" \
|
||||||
|
--pin 847293 \
|
||||||
|
-m "Meeting moved to 3pm" \
|
||||||
|
--mode dct \
|
||||||
|
--format jpeg
|
||||||
```
|
```
|
||||||
|
|
||||||
**Recipient (daily):**
|
**Recipient (daily):**
|
||||||
```bash
|
```bash
|
||||||
# Use the phrase for the day the message was SENT
|
# Works for both LSB and DCT (auto-detected)
|
||||||
stegasoo decode \
|
stegasoo decode \
|
||||||
-r our_shared_photo.jpg \
|
-r our_shared_photo.jpg \
|
||||||
-s received_image.png \
|
-s received_image.png \
|
||||||
@@ -496,7 +673,7 @@ stegasoo decode \
|
|||||||
|
|
||||||
### Batch Processing
|
### Batch Processing
|
||||||
|
|
||||||
**Encode multiple messages:**
|
**Encode multiple messages (LSB):**
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
PHRASE="apple forest thunder"
|
PHRASE="apple forest thunder"
|
||||||
@@ -517,6 +694,25 @@ for file in messages/*.txt; do
|
|||||||
done
|
done
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Encode for social media (DCT):**
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
for file in messages/*.txt; do
|
||||||
|
name=$(basename "$file" .txt)
|
||||||
|
stegasoo encode \
|
||||||
|
-r "$REF" \
|
||||||
|
-c "carriers/${name}.png" \
|
||||||
|
-p "$PHRASE" \
|
||||||
|
--pin "$PIN" \
|
||||||
|
-f "$file" \
|
||||||
|
--mode dct \
|
||||||
|
--format jpeg \
|
||||||
|
-o "output/${name}_social.jpg" \
|
||||||
|
-q
|
||||||
|
echo "Encoded for social: $name"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
### Archive with Date Preservation
|
### Archive with Date Preservation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -531,6 +727,31 @@ stegasoo encode \
|
|||||||
-o archive_2025-01-15.png
|
-o archive_2025-01-15.png
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Testing Mode Compatibility
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Encode with DCT
|
||||||
|
stegasoo encode \
|
||||||
|
-r ref.jpg \
|
||||||
|
-c carrier.png \
|
||||||
|
-p "test phrase" \
|
||||||
|
--pin 123456 \
|
||||||
|
-m "Test message" \
|
||||||
|
--mode dct \
|
||||||
|
--format jpeg \
|
||||||
|
-o test_dct.jpg
|
||||||
|
|
||||||
|
# Simulate social media recompression
|
||||||
|
convert test_dct.jpg -quality 85 test_recompressed.jpg
|
||||||
|
|
||||||
|
# Decode (should still work!)
|
||||||
|
stegasoo decode \
|
||||||
|
-r ref.jpg \
|
||||||
|
-s test_recompressed.jpg \
|
||||||
|
-p "test phrase" \
|
||||||
|
--pin 123456
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Piping & Scripting
|
## Piping & Scripting
|
||||||
@@ -585,6 +806,15 @@ if ! stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q 2>/dev/
|
|||||||
fi
|
fi
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Mode Detection in Scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# Get mode from verbose output
|
||||||
|
MODE=$(stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 2>&1 | grep "Mode detected" | awk '{print $3}')
|
||||||
|
echo "Image was encoded with: $MODE mode"
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
@@ -596,16 +826,31 @@ fi
|
|||||||
| "Must provide --pin or --key" | No security factor given | Add `--pin` or `--key` option |
|
| "Must provide --pin or --key" | No security factor given | Add `--pin` or `--key` option |
|
||||||
| "PIN must be 6-9 digits" | Invalid PIN format | Use numeric PIN, 6-9 chars |
|
| "PIN must be 6-9 digits" | Invalid PIN format | Use numeric PIN, 6-9 chars |
|
||||||
| "PIN cannot start with zero" | Leading zero in PIN | Use PIN starting with 1-9 |
|
| "PIN cannot start with zero" | Leading zero in PIN | Use PIN starting with 1-9 |
|
||||||
| "Carrier image too small" | Message exceeds capacity | Use larger carrier image |
|
| "Carrier image too small" | Message exceeds capacity | Use larger carrier or LSB mode |
|
||||||
|
| "Message too long for DCT capacity" | DCT has less space | Shorten message or use LSB |
|
||||||
| "Decryption failed" | Wrong credentials | Verify phrase, PIN, ref photo |
|
| "Decryption failed" | Wrong credentials | Verify phrase, PIN, ref photo |
|
||||||
|
| "Invalid or missing Stegasoo header" | Wrong mode or corruption | Check mode, try other credentials |
|
||||||
| "RSA key is password-protected" | Missing key password | Add `--key-password` option |
|
| "RSA key is password-protected" | Missing key password | Add `--key-password` option |
|
||||||
|
| "jpegio not available" | Missing library | Install: `pip install jpegio` |
|
||||||
|
| "Invalid --format for LSB mode" | JPEG with LSB | Use `--mode dct` for JPEG output |
|
||||||
|
|
||||||
### Troubleshooting Decryption Failures
|
### Troubleshooting Decryption Failures
|
||||||
|
|
||||||
1. **Check the encoding date:** The filename often contains the date (e.g., `_20251227`)
|
1. **Check the encoding date:** The filename often contains the date (e.g., `_20251227`)
|
||||||
2. **Use correct phrase:** The phrase must match the day the message was encoded, not today
|
2. **Use correct phrase:** The phrase must match the day the message was encoded, not today
|
||||||
3. **Verify reference photo:** Must be the exact same file, not a resized copy
|
3. **Verify reference photo:** Must be the exact same file, not a resized copy
|
||||||
4. **Check stego image:** Ensure it wasn't resized, recompressed, or converted
|
4. **Check stego image:**
|
||||||
|
- LSB: Ensure it wasn't resized, recompressed, or converted
|
||||||
|
- DCT: More resilient, but heavy recompression may still destroy data
|
||||||
|
5. **Check embedding mode:** The decoder auto-detects, but if issues persist, verify the original was encoded with the expected mode
|
||||||
|
|
||||||
|
### DCT-Specific Issues
|
||||||
|
|
||||||
|
| Issue | Cause | Solution |
|
||||||
|
|-------|-------|----------|
|
||||||
|
| "Invalid or missing Stegasoo header" after social media | Heavy recompression | Try higher quality original or shorter message |
|
||||||
|
| JPEG output not working | jpegio not installed | `pip install jpegio` |
|
||||||
|
| Lower capacity than expected | Normal for DCT | DCT has ~10% of LSB capacity |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -627,6 +872,33 @@ fi
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Core Dependencies
|
||||||
|
|
||||||
|
- `pillow` - Image processing
|
||||||
|
- `cryptography` - Encryption
|
||||||
|
- `argon2-cffi` - Key derivation
|
||||||
|
- `click` - CLI framework
|
||||||
|
|
||||||
|
### DCT Mode Dependencies
|
||||||
|
|
||||||
|
- `scipy` - DCT transformations
|
||||||
|
- `jpegio` - Native JPEG coefficient access (recommended)
|
||||||
|
|
||||||
|
Install DCT dependencies:
|
||||||
|
```bash
|
||||||
|
pip install scipy jpegio
|
||||||
|
```
|
||||||
|
|
||||||
|
Check availability:
|
||||||
|
```bash
|
||||||
|
python -c "import scipy; print('scipy:', scipy.__version__)"
|
||||||
|
python -c "import jpegio; print('jpegio: available')"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## See Also
|
## See Also
|
||||||
|
|
||||||
- [API Documentation](API.md) - REST API reference
|
- [API Documentation](API.md) - REST API reference
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ Complete guide for the Stegasoo web-based steganography interface.
|
|||||||
- [Encode Message](#encode-message)
|
- [Encode Message](#encode-message)
|
||||||
- [Decode Message](#decode-message)
|
- [Decode Message](#decode-message)
|
||||||
- [About Page](#about-page)
|
- [About Page](#about-page)
|
||||||
|
- [Embedding Modes](#embedding-modes)
|
||||||
|
- [LSB Mode (Default)](#lsb-mode-default)
|
||||||
|
- [DCT Mode (Experimental)](#dct-mode-experimental)
|
||||||
- [User Interface Guide](#user-interface-guide)
|
- [User Interface Guide](#user-interface-guide)
|
||||||
- [Workflow Examples](#workflow-examples)
|
- [Workflow Examples](#workflow-examples)
|
||||||
- [Security Features](#security-features)
|
- [Security Features](#security-features)
|
||||||
@@ -42,6 +45,8 @@ Built with Flask, Bootstrap 5, and a modern dark theme.
|
|||||||
- ✅ Password-protected RSA key downloads
|
- ✅ Password-protected RSA key downloads
|
||||||
- ✅ Real-time entropy calculations
|
- ✅ Real-time entropy calculations
|
||||||
- ✅ Automatic file cleanup
|
- ✅ Automatic file cleanup
|
||||||
|
- ✅ **DCT steganography mode** (v3.0+) - JPEG-resilient embedding
|
||||||
|
- ✅ **Color mode selection** (v3.0.1+) - Preserve carrier colors
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -53,6 +58,8 @@ Built with Flask, Bootstrap 5, and a modern dark theme.
|
|||||||
pip install stegasoo[web]
|
pip install stegasoo[web]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This automatically installs DCT dependencies (scipy, jpegio) for full functionality.
|
||||||
|
|
||||||
### From Source
|
### From Source
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -210,6 +217,18 @@ Hide a secret message inside an image.
|
|||||||
|
|
||||||
\* At least one security factor (PIN or RSA Key) required.
|
\* At least one security factor (PIN or RSA Key) required.
|
||||||
|
|
||||||
|
#### Advanced Options (v3.0+)
|
||||||
|
|
||||||
|
Expand "Advanced Options" to access embedding mode settings:
|
||||||
|
|
||||||
|
| Option | Values | Default | Description |
|
||||||
|
|--------|--------|---------|-------------|
|
||||||
|
| Embedding Mode | LSB / DCT | LSB | Steganography algorithm |
|
||||||
|
| Output Format | PNG / JPEG | PNG | Output image format (DCT only) |
|
||||||
|
| Color Mode | Color / Grayscale | Color | Carrier color handling (DCT only) |
|
||||||
|
|
||||||
|
See [Embedding Modes](#embedding-modes) for detailed explanations.
|
||||||
|
|
||||||
#### Drag-and-Drop Upload
|
#### Drag-and-Drop Upload
|
||||||
|
|
||||||
Both image upload zones support:
|
Both image upload zones support:
|
||||||
@@ -237,9 +256,10 @@ Saturday's Phrase: [ ]
|
|||||||
#### Encoding Process
|
#### Encoding Process
|
||||||
|
|
||||||
1. Fill in all required fields
|
1. Fill in all required fields
|
||||||
2. Click "Encode Message"
|
2. (Optional) Expand "Advanced Options" for DCT mode
|
||||||
3. Wait for processing (shows spinner)
|
3. Click "Encode Message"
|
||||||
4. Redirected to result page
|
4. Wait for processing (shows spinner)
|
||||||
|
5. Redirected to result page
|
||||||
|
|
||||||
#### Result Page
|
#### Result Page
|
||||||
|
|
||||||
@@ -255,6 +275,9 @@ After successful encoding:
|
|||||||
│ Your secret message is hidden │
|
│ Your secret message is hidden │
|
||||||
│ in this image │
|
│ in this image │
|
||||||
│ │
|
│ │
|
||||||
|
│ Mode: DCT (Color, JPEG) │ ← v3.0+ shows mode info
|
||||||
|
│ Capacity used: 45.2% │
|
||||||
|
│ │
|
||||||
│ [ Download Image ] │
|
│ [ Download Image ] │
|
||||||
│ [ Share Image ] │
|
│ [ Share Image ] │
|
||||||
│ │
|
│ │
|
||||||
@@ -299,6 +322,10 @@ Extract a hidden message from a stego image.
|
|||||||
|
|
||||||
\* Must match security factors used during encoding.
|
\* Must match security factors used during encoding.
|
||||||
|
|
||||||
|
#### Automatic Mode Detection (v3.0+)
|
||||||
|
|
||||||
|
The decoder automatically detects whether a stego image uses LSB or DCT mode. You don't need to specify the mode manually—it just works!
|
||||||
|
|
||||||
#### Date Detection from Filename
|
#### Date Detection from Filename
|
||||||
|
|
||||||
When you upload a stego image with a date in the filename (e.g., `stego_20251227.png`), the UI:
|
When you upload a stego image with a date in the filename (e.g., `stego_20251227.png`), the UI:
|
||||||
@@ -333,13 +360,11 @@ This helps you use the correct daily phrase.
|
|||||||
|
|
||||||
#### Troubleshooting Tips
|
#### Troubleshooting Tips
|
||||||
|
|
||||||
The page includes built-in troubleshooting guidance:
|
If decryption fails:
|
||||||
|
1. **Check the date** - Use phrase for encoding day, not today
|
||||||
- ✓ Use the **exact same reference photo** file
|
2. **Same reference photo** - Must be identical file
|
||||||
- ✓ Use the phrase for the **encoding day**, not today
|
3. **Correct PIN/RSA** - Match what was used for encoding
|
||||||
- ✓ Provide the **same security factors** used during encoding
|
4. **Image integrity** - Ensure no resizing/recompression
|
||||||
- ✓ Ensure the stego image hasn't been **resized or recompressed**
|
|
||||||
- ✓ If using RSA key, verify the **password is correct**
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -347,62 +372,130 @@ The page includes built-in troubleshooting guidance:
|
|||||||
|
|
||||||
**URL:** `/about`
|
**URL:** `/about`
|
||||||
|
|
||||||
Learn about Stegasoo's security model and best practices.
|
Information about the Stegasoo project, security model, and credits.
|
||||||
|
|
||||||
#### Sections
|
---
|
||||||
|
|
||||||
**System Status:**
|
## Embedding Modes
|
||||||
- Argon2id availability (vs PBKDF2 fallback)
|
|
||||||
- AES-256-GCM encryption status
|
|
||||||
|
|
||||||
**Security Model Table:**
|
Stegasoo v3.0+ offers two steganography algorithms, each with different trade-offs.
|
||||||
|
|
||||||
| Component | Entropy | Purpose |
|
### LSB Mode (Default)
|
||||||
|-----------|---------|---------|
|
|
||||||
| Reference Photo | ~80-256 bits | Something you have |
|
|
||||||
| 3-Word Phrase | ~33 bits | Something you know (daily) |
|
|
||||||
| 6-Digit PIN | ~20 bits | Something you know (static) |
|
|
||||||
| Date | N/A | Automatic key rotation |
|
|
||||||
| **Combined** | **133+ bits** | **Beyond brute force** |
|
|
||||||
|
|
||||||
**Attack Resistance:**
|
**Least Significant Bit** embedding modifies the least significant bits of pixel values.
|
||||||
|
|
||||||
What attackers can't do:
|
| Aspect | Details |
|
||||||
- Brute force (2^133 combinations)
|
|--------|---------|
|
||||||
- Use rainbow tables (random salt)
|
| **Capacity** | ~3 bits/pixel (~770 KB for 1920×1080) |
|
||||||
- Detect hidden data (random pixels)
|
| **Output Format** | PNG only (lossless required) |
|
||||||
- Use GPU farms (256MB RAM per attempt)
|
| **Resilience** | ❌ Destroyed by JPEG compression |
|
||||||
|
| **Best For** | Maximum capacity, controlled sharing |
|
||||||
|
|
||||||
Real threats:
|
**When to use LSB:**
|
||||||
- Social engineering
|
- Sharing via lossless channels (email attachment, file transfer)
|
||||||
- Physical device access
|
- Maximum message capacity needed
|
||||||
- Malware/keyloggers
|
- Recipient won't modify the image
|
||||||
- Shoulder surfing
|
|
||||||
|
|
||||||
**Best Practices:**
|
### DCT Mode (Experimental)
|
||||||
|
|
||||||
Do:
|
**Discrete Cosine Transform** embedding hides data in frequency domain coefficients.
|
||||||
- Memorize phrases and PIN
|
|
||||||
- Use reference photo both parties have
|
|
||||||
- Use different carrier images each time
|
|
||||||
- Share stego images through normal channels
|
|
||||||
|
|
||||||
Don't:
|
| Aspect | Details |
|
||||||
- Transmit the reference photo
|
|--------|---------|
|
||||||
- Reuse carrier images
|
| **Capacity** | ~0.25 bits/pixel (~65 KB for 1920×1080 PNG, ~30-50 KB JPEG) |
|
||||||
- Store credentials digitally
|
| **Output Formats** | PNG or JPEG |
|
||||||
- Resize/recompress stego images
|
| **Resilience** | ✅ Survives JPEG compression |
|
||||||
|
| **Best For** | Social media, messaging apps, web sharing |
|
||||||
|
|
||||||
|
> ⚠️ **Experimental Feature**: DCT mode is marked experimental and may have edge cases. Test with your specific workflow before relying on it for critical messages.
|
||||||
|
|
||||||
|
**When to use DCT:**
|
||||||
|
- Posting to social media (which recompresses images)
|
||||||
|
- Sharing via messaging apps (WhatsApp, Telegram, etc.)
|
||||||
|
- When channel may apply JPEG compression
|
||||||
|
- Smaller messages that fit in reduced capacity
|
||||||
|
|
||||||
|
#### DCT Output Formats
|
||||||
|
|
||||||
|
| Format | Pros | Cons |
|
||||||
|
|--------|------|------|
|
||||||
|
| **PNG** | Lossless, predictable | Larger file, obvious if channel expects JPEG |
|
||||||
|
| **JPEG** | Native format, natural | Slightly lower capacity |
|
||||||
|
|
||||||
|
#### DCT Color Modes
|
||||||
|
|
||||||
|
| Mode | Description | Use Case |
|
||||||
|
|------|-------------|----------|
|
||||||
|
| **Color** | Embeds in luminance (Y), preserves chrominance | Most images, photos |
|
||||||
|
| **Grayscale** | Converts to grayscale before embedding | Black & white images |
|
||||||
|
|
||||||
|
### Capacity Comparison
|
||||||
|
|
||||||
|
For a 1920×1080 image:
|
||||||
|
|
||||||
|
| Mode | Approximate Capacity |
|
||||||
|
|------|---------------------|
|
||||||
|
| LSB (PNG) | ~770 KB |
|
||||||
|
| DCT (PNG, Color) | ~65 KB |
|
||||||
|
| DCT (JPEG) | ~30-50 KB |
|
||||||
|
|
||||||
|
### Choosing the Right Mode
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Mode Selection Guide │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Will the image be recompressed (social media, chat apps)? │
|
||||||
|
│ │ │
|
||||||
|
│ ┌───────────┴───────────┐ │
|
||||||
|
│ ▼ ▼ │
|
||||||
|
│ YES NO │
|
||||||
|
│ │ │ │
|
||||||
|
│ ▼ ▼ │
|
||||||
|
│ Use DCT Mode Use LSB Mode │
|
||||||
|
│ │ │ │
|
||||||
|
│ ▼ ▼ │
|
||||||
|
│ Output: JPEG (natural) Output: PNG (automatic) │
|
||||||
|
│ Color: Color (usually) Capacity: ~770 KB │
|
||||||
|
│ Capacity: ~30-50 KB │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## User Interface Guide
|
## User Interface Guide
|
||||||
|
|
||||||
### Navigation
|
### Layout Structure
|
||||||
|
|
||||||
The navbar provides quick access to all pages:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
[Logo] Stegasoo Home | Encode | Decode | Generate | About
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ 🦕 Stegasoo [Encode] [Decode] [Generate] │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Page Content │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌──────────────┐ ┌──────────────┐ │ │
|
||||||
|
│ │ │ Upload Zone │ │ Upload Zone │ │ │
|
||||||
|
│ │ │ (Reference) │ │ (Carrier) │ │ │
|
||||||
|
│ │ └──────────────┘ └──────────────┘ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ [Advanced Options ▼] │ │
|
||||||
|
│ │ ┌────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ Embedding Mode: [LSB ▼] │ │ │
|
||||||
|
│ │ │ Output Format: [PNG ▼] (DCT only) │ │ │
|
||||||
|
│ │ │ Color Mode: [Color ▼] (DCT only) │ │ │
|
||||||
|
│ │ └────────────────────────────────────────────┘ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ [ Encode Message ] │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ Footer │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### Color Scheme
|
### Color Scheme
|
||||||
@@ -415,6 +508,7 @@ The navbar provides quick access to all pages:
|
|||||||
| Success | Green | Positive actions |
|
| Success | Green | Positive actions |
|
||||||
| Warning | Yellow | Caution messages |
|
| Warning | Yellow | Caution messages |
|
||||||
| Error | Red | Error states |
|
| Error | Red | Error states |
|
||||||
|
| Experimental | Orange badge | DCT mode indicator |
|
||||||
|
|
||||||
### Form Validation
|
### Form Validation
|
||||||
|
|
||||||
@@ -462,7 +556,7 @@ Types:
|
|||||||
- The PIN
|
- The PIN
|
||||||
- The reference photo file (if not already shared)
|
- The reference photo file (if not already shared)
|
||||||
|
|
||||||
### Sending a Secret Message
|
### Sending a Secret Message (LSB - Default)
|
||||||
|
|
||||||
1. Go to `/encode`
|
1. Go to `/encode`
|
||||||
2. Upload your shared reference photo
|
2. Upload your shared reference photo
|
||||||
@@ -472,7 +566,22 @@ Types:
|
|||||||
6. Enter your PIN
|
6. Enter your PIN
|
||||||
7. Click "Encode Message"
|
7. Click "Encode Message"
|
||||||
8. Download or share the resulting image
|
8. Download or share the resulting image
|
||||||
9. Send via any channel (email, social media, chat)
|
9. Send via any channel (email, file transfer)
|
||||||
|
|
||||||
|
### Sending via Social Media (DCT Mode)
|
||||||
|
|
||||||
|
1. Go to `/encode`
|
||||||
|
2. Upload your shared reference photo
|
||||||
|
3. Upload carrier image
|
||||||
|
4. Type your secret message
|
||||||
|
5. Enter today's phrase and PIN
|
||||||
|
6. **Expand "Advanced Options"**
|
||||||
|
7. **Select "DCT" embedding mode**
|
||||||
|
8. **Select "JPEG" output format**
|
||||||
|
9. Click "Encode Message"
|
||||||
|
10. Download and post to social media
|
||||||
|
|
||||||
|
The recipient can decode even after the platform recompresses the image!
|
||||||
|
|
||||||
### Receiving a Secret Message
|
### Receiving a Secret Message
|
||||||
|
|
||||||
@@ -486,6 +595,8 @@ Types:
|
|||||||
8. Click "Decode Message"
|
8. Click "Decode Message"
|
||||||
9. Read the secret message
|
9. Read the secret message
|
||||||
|
|
||||||
|
> 💡 Decoding automatically detects LSB vs DCT mode—no configuration needed!
|
||||||
|
|
||||||
### Changing Credentials
|
### Changing Credentials
|
||||||
|
|
||||||
To rotate to new credentials:
|
To rotate to new credentials:
|
||||||
@@ -527,6 +638,15 @@ To rotate to new credentials:
|
|||||||
| Access control | Random 16-byte file ID |
|
| Access control | Random 16-byte file ID |
|
||||||
| Cleanup | Automatic + manual |
|
| Cleanup | Automatic + manual |
|
||||||
|
|
||||||
|
### Embedding Mode Security
|
||||||
|
|
||||||
|
| Mode | Security Consideration |
|
||||||
|
|------|----------------------|
|
||||||
|
| LSB | Full capacity, but fragile to modification |
|
||||||
|
| DCT | Lower capacity, but survives recompression |
|
||||||
|
|
||||||
|
Both modes use the same strong encryption (AES-256-GCM with Argon2id key derivation).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
@@ -561,8 +681,8 @@ gunicorn \
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Worker Calculation:**
|
**Worker Calculation:**
|
||||||
- Each encode/decode uses ~256MB RAM (Argon2)
|
- Each encode/decode uses ~256MB RAM (Argon2) + ~100MB for scipy (DCT mode)
|
||||||
- Formula: `workers = (available_RAM - 512MB) / 256MB`
|
- Formula: `workers = (available_RAM - 512MB) / 350MB`
|
||||||
|
|
||||||
**With Nginx (reverse proxy):**
|
**With Nginx (reverse proxy):**
|
||||||
```nginx
|
```nginx
|
||||||
@@ -594,9 +714,9 @@ services:
|
|||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: 512M
|
memory: 768M # Increased for scipy/DCT
|
||||||
reservations:
|
reservations:
|
||||||
memory: 256M
|
memory: 384M
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -617,7 +737,19 @@ services:
|
|||||||
1. Check the date in the stego filename
|
1. Check the date in the stego filename
|
||||||
2. Use the phrase for that specific day
|
2. Use the phrase for that specific day
|
||||||
3. Verify you're using the original reference photo
|
3. Verify you're using the original reference photo
|
||||||
4. Ensure the stego image wasn't resized/recompressed
|
4. Ensure the stego image wasn't resized/recompressed (LSB mode)
|
||||||
|
|
||||||
|
#### "Invalid or missing Stegasoo header" (DCT Mode)
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
- Image was heavily recompressed
|
||||||
|
- Wrong credentials
|
||||||
|
- Corrupted during transfer
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
1. If sharing via lossy channel, ensure DCT mode was used for encoding
|
||||||
|
2. Verify credentials match
|
||||||
|
3. Try obtaining original file
|
||||||
|
|
||||||
#### "Carrier image too small"
|
#### "Carrier image too small"
|
||||||
|
|
||||||
@@ -626,7 +758,8 @@ services:
|
|||||||
**Solutions:**
|
**Solutions:**
|
||||||
1. Use a larger carrier image (more pixels)
|
1. Use a larger carrier image (more pixels)
|
||||||
2. Shorten the message
|
2. Shorten the message
|
||||||
3. Check capacity with `/info` command (CLI)
|
3. Use LSB mode for more capacity (if channel supports it)
|
||||||
|
4. Check capacity with `/info` command (CLI)
|
||||||
|
|
||||||
#### "You must provide at least a PIN or RSA Key"
|
#### "You must provide at least a PIN or RSA Key"
|
||||||
|
|
||||||
@@ -658,6 +791,17 @@ services:
|
|||||||
2. If key is unencrypted, leave password blank
|
2. If key is unencrypted, leave password blank
|
||||||
3. Re-download or regenerate the key
|
3. Re-download or regenerate the key
|
||||||
|
|
||||||
|
#### DCT mode shows "jpegio not available"
|
||||||
|
|
||||||
|
**Cause:** jpegio library not installed (required for JPEG output)
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```bash
|
||||||
|
pip install jpegio
|
||||||
|
# Or rebuild Docker image
|
||||||
|
docker-compose build --no-cache
|
||||||
|
```
|
||||||
|
|
||||||
### Browser Compatibility
|
### Browser Compatibility
|
||||||
|
|
||||||
| Browser | Status | Notes |
|
| Browser | Status | Notes |
|
||||||
@@ -672,10 +816,12 @@ services:
|
|||||||
|
|
||||||
**Slow encoding/decoding:**
|
**Slow encoding/decoding:**
|
||||||
- Normal: Argon2 is intentionally slow (security feature)
|
- Normal: Argon2 is intentionally slow (security feature)
|
||||||
- Expected time: 2-5 seconds per operation
|
- DCT mode adds ~1-2 seconds for transform operations
|
||||||
|
- Expected time: 3-7 seconds per operation
|
||||||
|
|
||||||
**High memory usage:**
|
**High memory usage:**
|
||||||
- Normal: Argon2 requires 256MB RAM
|
- Normal: Argon2 requires 256MB RAM
|
||||||
|
- DCT mode adds scipy memory overhead (~100MB)
|
||||||
- Configure worker count based on available RAM
|
- Configure worker count based on available RAM
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -689,6 +835,7 @@ The UI adapts to mobile screens:
|
|||||||
- Touch-friendly buttons (48px minimum)
|
- Touch-friendly buttons (48px minimum)
|
||||||
- Readable text without zooming
|
- Readable text without zooming
|
||||||
- Scrollable tables
|
- Scrollable tables
|
||||||
|
- Collapsible "Advanced Options" for cleaner mobile view
|
||||||
|
|
||||||
### Mobile-Specific Features
|
### Mobile-Specific Features
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Stegasoo REST API (v3.0)
|
Stegasoo REST API (v3.0.1)
|
||||||
|
|
||||||
FastAPI-based REST API for steganography operations.
|
FastAPI-based REST API for steganography operations.
|
||||||
Supports both text messages and file embedding.
|
Supports both text messages and file embedding.
|
||||||
NEW in v3.0: LSB and DCT embedding modes.
|
NEW in v3.0: LSB and DCT embedding modes.
|
||||||
|
NEW in v3.0.1: DCT color mode and JPEG output format.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
@@ -70,7 +71,12 @@ Secure steganography with hybrid authentication. Supports text messages and file
|
|||||||
## Embedding Modes (v3.0)
|
## Embedding Modes (v3.0)
|
||||||
|
|
||||||
- **LSB mode** (default): Spatial LSB embedding, full color output, higher capacity
|
- **LSB mode** (default): Spatial LSB embedding, full color output, higher capacity
|
||||||
- **DCT mode**: Frequency domain embedding, grayscale output, ~20% capacity, better stealth
|
- **DCT mode**: Frequency domain embedding, ~20% capacity, better stealth
|
||||||
|
|
||||||
|
## DCT Options (v3.0.1)
|
||||||
|
|
||||||
|
- **dct_color_mode**: 'grayscale' (default) or 'color' (preserves original colors)
|
||||||
|
- **dct_output_format**: 'png' (lossless) or 'jpeg' (smaller, more natural)
|
||||||
|
|
||||||
Use the `/modes` endpoint to check availability and `/compare` to compare capacities.
|
Use the `/modes` endpoint to check availability and `/compare` to compare capacities.
|
||||||
""",
|
""",
|
||||||
@@ -86,6 +92,8 @@ Use the `/modes` endpoint to check availability and `/compare` to compare capaci
|
|||||||
|
|
||||||
EmbedModeType = Literal["lsb", "dct"]
|
EmbedModeType = Literal["lsb", "dct"]
|
||||||
ExtractModeType = Literal["auto", "lsb", "dct"]
|
ExtractModeType = Literal["auto", "lsb", "dct"]
|
||||||
|
DctColorModeType = Literal["grayscale", "color"]
|
||||||
|
DctOutputFormatType = Literal["png", "jpeg"]
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -118,7 +126,16 @@ class EncodeRequest(BaseModel):
|
|||||||
date_str: Optional[str] = None
|
date_str: Optional[str] = None
|
||||||
embed_mode: EmbedModeType = Field(
|
embed_mode: EmbedModeType = Field(
|
||||||
default="lsb",
|
default="lsb",
|
||||||
description="Embedding mode: 'lsb' (default, color) or 'dct' (grayscale, requires scipy)"
|
description="Embedding mode: 'lsb' (default, color) or 'dct' (requires scipy)"
|
||||||
|
)
|
||||||
|
# NEW in v3.0.1
|
||||||
|
dct_output_format: DctOutputFormatType = Field(
|
||||||
|
default="png",
|
||||||
|
description="DCT output format: 'png' (lossless) or 'jpeg' (smaller). Only applies to DCT mode."
|
||||||
|
)
|
||||||
|
dct_color_mode: DctColorModeType = Field(
|
||||||
|
default="grayscale",
|
||||||
|
description="DCT color mode: 'grayscale' (default) or 'color' (preserves colors). Only applies to DCT mode."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -136,7 +153,16 @@ class EncodeFileRequest(BaseModel):
|
|||||||
date_str: Optional[str] = None
|
date_str: Optional[str] = None
|
||||||
embed_mode: EmbedModeType = Field(
|
embed_mode: EmbedModeType = Field(
|
||||||
default="lsb",
|
default="lsb",
|
||||||
description="Embedding mode: 'lsb' (default, color) or 'dct' (grayscale, requires scipy)"
|
description="Embedding mode: 'lsb' (default, color) or 'dct' (requires scipy)"
|
||||||
|
)
|
||||||
|
# NEW in v3.0.1
|
||||||
|
dct_output_format: DctOutputFormatType = Field(
|
||||||
|
default="png",
|
||||||
|
description="DCT output format: 'png' (lossless) or 'jpeg' (smaller). Only applies to DCT mode."
|
||||||
|
)
|
||||||
|
dct_color_mode: DctColorModeType = Field(
|
||||||
|
default="grayscale",
|
||||||
|
description="DCT color mode: 'grayscale' (default) or 'color' (preserves colors). Only applies to DCT mode."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -147,6 +173,15 @@ class EncodeResponse(BaseModel):
|
|||||||
date_used: str
|
date_used: str
|
||||||
day_of_week: str
|
day_of_week: str
|
||||||
embed_mode: str = Field(description="Embedding mode used: 'lsb' or 'dct'")
|
embed_mode: str = Field(description="Embedding mode used: 'lsb' or 'dct'")
|
||||||
|
# NEW in v3.0.1
|
||||||
|
output_format: str = Field(
|
||||||
|
default="png",
|
||||||
|
description="Output format: 'png' or 'jpeg' (for DCT mode)"
|
||||||
|
)
|
||||||
|
color_mode: str = Field(
|
||||||
|
default="color",
|
||||||
|
description="Color mode: 'color' (LSB/DCT color) or 'grayscale' (DCT grayscale)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DecodeRequest(BaseModel):
|
class DecodeRequest(BaseModel):
|
||||||
@@ -211,20 +246,36 @@ class CompareModesResponse(BaseModel):
|
|||||||
recommendation: str
|
recommendation: str
|
||||||
|
|
||||||
|
|
||||||
|
class DctModeInfo(BaseModel):
|
||||||
|
"""Detailed DCT mode information."""
|
||||||
|
available: bool
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
output_formats: list[str]
|
||||||
|
color_modes: list[str]
|
||||||
|
capacity_ratio: str
|
||||||
|
requires: str
|
||||||
|
|
||||||
|
|
||||||
class ModesResponse(BaseModel):
|
class ModesResponse(BaseModel):
|
||||||
"""Response showing available embedding modes."""
|
"""Response showing available embedding modes."""
|
||||||
lsb: dict
|
lsb: dict
|
||||||
dct: dict
|
dct: DctModeInfo
|
||||||
|
|
||||||
|
|
||||||
class StatusResponse(BaseModel):
|
class StatusResponse(BaseModel):
|
||||||
version: str
|
version: str
|
||||||
has_argon2: bool
|
has_argon2: bool
|
||||||
has_qrcode_read: bool
|
has_qrcode_read: bool
|
||||||
has_dct: bool # NEW in v3.0
|
has_dct: bool
|
||||||
day_names: list[str]
|
day_names: list[str]
|
||||||
max_payload_kb: int
|
max_payload_kb: int
|
||||||
available_modes: list[str] # NEW in v3.0
|
available_modes: list[str]
|
||||||
|
# NEW in v3.0.1
|
||||||
|
dct_features: Optional[dict] = Field(
|
||||||
|
default=None,
|
||||||
|
description="DCT mode features (v3.0.1+)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class QrExtractResponse(BaseModel):
|
class QrExtractResponse(BaseModel):
|
||||||
@@ -263,8 +314,16 @@ class ErrorResponse(BaseModel):
|
|||||||
async def root():
|
async def root():
|
||||||
"""Get API status and configuration."""
|
"""Get API status and configuration."""
|
||||||
available_modes = ["lsb"]
|
available_modes = ["lsb"]
|
||||||
|
dct_features = None
|
||||||
|
|
||||||
if has_dct_support():
|
if has_dct_support():
|
||||||
available_modes.append("dct")
|
available_modes.append("dct")
|
||||||
|
dct_features = {
|
||||||
|
"output_formats": ["png", "jpeg"],
|
||||||
|
"color_modes": ["grayscale", "color"],
|
||||||
|
"default_output_format": "png",
|
||||||
|
"default_color_mode": "grayscale",
|
||||||
|
}
|
||||||
|
|
||||||
return StatusResponse(
|
return StatusResponse(
|
||||||
version=__version__,
|
version=__version__,
|
||||||
@@ -273,7 +332,8 @@ async def root():
|
|||||||
has_dct=has_dct_support(),
|
has_dct=has_dct_support(),
|
||||||
day_names=list(DAY_NAMES),
|
day_names=list(DAY_NAMES),
|
||||||
max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024,
|
max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024,
|
||||||
available_modes=available_modes
|
available_modes=available_modes,
|
||||||
|
dct_features=dct_features,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -283,6 +343,7 @@ async def api_modes():
|
|||||||
Get available embedding modes and their status.
|
Get available embedding modes and their status.
|
||||||
|
|
||||||
NEW in v3.0: Shows LSB and DCT mode availability.
|
NEW in v3.0: Shows LSB and DCT mode availability.
|
||||||
|
NEW in v3.0.1: Shows DCT color modes and output formats.
|
||||||
"""
|
"""
|
||||||
return ModesResponse(
|
return ModesResponse(
|
||||||
lsb={
|
lsb={
|
||||||
@@ -292,14 +353,15 @@ async def api_modes():
|
|||||||
"output_format": "PNG (color)",
|
"output_format": "PNG (color)",
|
||||||
"capacity_ratio": "100%",
|
"capacity_ratio": "100%",
|
||||||
},
|
},
|
||||||
dct={
|
dct=DctModeInfo(
|
||||||
"available": has_dct_support(),
|
available=has_dct_support(),
|
||||||
"name": "DCT Domain",
|
name="DCT Domain",
|
||||||
"description": "Embed in DCT coefficients, outputs grayscale PNG",
|
description="Embed in DCT coefficients, frequency domain steganography",
|
||||||
"output_format": "PNG (grayscale)",
|
output_formats=["png", "jpeg"],
|
||||||
"capacity_ratio": "~20% of LSB",
|
color_modes=["grayscale", "color"],
|
||||||
"requires": "scipy",
|
capacity_ratio="~20% of LSB",
|
||||||
}
|
requires="scipy",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -328,7 +390,8 @@ async def api_compare_modes(request: CompareModesRequest):
|
|||||||
"capacity_bytes": comparison['dct']['capacity_bytes'],
|
"capacity_bytes": comparison['dct']['capacity_bytes'],
|
||||||
"capacity_kb": round(comparison['dct']['capacity_kb'], 1),
|
"capacity_kb": round(comparison['dct']['capacity_kb'], 1),
|
||||||
"available": comparison['dct']['available'],
|
"available": comparison['dct']['available'],
|
||||||
"output_format": comparison['dct']['output'],
|
"output_formats": ["png", "jpeg"],
|
||||||
|
"color_modes": ["grayscale", "color"],
|
||||||
"ratio_vs_lsb_percent": round(comparison['dct']['ratio_vs_lsb'], 1),
|
"ratio_vs_lsb_percent": round(comparison['dct']['ratio_vs_lsb'], 1),
|
||||||
},
|
},
|
||||||
recommendation="lsb" if not comparison['dct']['available'] else "dct for stealth, lsb for capacity"
|
recommendation="lsb" if not comparison['dct']['available'] else "dct for stealth, lsb for capacity"
|
||||||
@@ -464,6 +527,41 @@ async def api_generate(request: GenerateRequest):
|
|||||||
raise HTTPException(500, str(e))
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# HELPER FUNCTION FOR DCT PARAMETERS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def _get_dct_params(embed_mode: str, dct_output_format: str, dct_color_mode: str) -> dict:
|
||||||
|
"""
|
||||||
|
Get DCT-specific parameters if DCT mode is selected.
|
||||||
|
Returns kwargs to pass to encode().
|
||||||
|
"""
|
||||||
|
if embed_mode != "dct":
|
||||||
|
return {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"dct_output_format": dct_output_format,
|
||||||
|
"dct_color_mode": dct_color_mode,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_output_info(embed_mode: str, dct_output_format: str, dct_color_mode: str) -> tuple:
|
||||||
|
"""
|
||||||
|
Get output format and color mode strings for response.
|
||||||
|
Returns (output_format, color_mode, mime_type).
|
||||||
|
"""
|
||||||
|
if embed_mode == "dct":
|
||||||
|
output_format = dct_output_format
|
||||||
|
color_mode = dct_color_mode
|
||||||
|
mime_type = "image/jpeg" if dct_output_format == "jpeg" else "image/png"
|
||||||
|
else:
|
||||||
|
output_format = "png"
|
||||||
|
color_mode = "color"
|
||||||
|
mime_type = "image/png"
|
||||||
|
|
||||||
|
return output_format, color_mode, mime_type
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# ROUTES - ENCODE (JSON)
|
# ROUTES - ENCODE (JSON)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -476,6 +574,7 @@ async def api_encode(request: EncodeRequest):
|
|||||||
Images must be base64-encoded. Returns base64-encoded stego image.
|
Images must be base64-encoded. Returns base64-encoded stego image.
|
||||||
|
|
||||||
NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct').
|
NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct').
|
||||||
|
NEW in v3.0.1: Supports dct_output_format ('png' or 'jpeg') and dct_color_mode ('grayscale' or 'color').
|
||||||
"""
|
"""
|
||||||
# Validate mode
|
# Validate mode
|
||||||
if request.embed_mode == "dct" and not has_dct_support():
|
if request.embed_mode == "dct" and not has_dct_support():
|
||||||
@@ -486,6 +585,13 @@ async def api_encode(request: EncodeRequest):
|
|||||||
carrier = base64.b64decode(request.carrier_image_base64)
|
carrier = base64.b64decode(request.carrier_image_base64)
|
||||||
rsa_key = base64.b64decode(request.rsa_key_base64) if request.rsa_key_base64 else None
|
rsa_key = base64.b64decode(request.rsa_key_base64) if request.rsa_key_base64 else None
|
||||||
|
|
||||||
|
# Get DCT parameters
|
||||||
|
dct_params = _get_dct_params(
|
||||||
|
request.embed_mode,
|
||||||
|
request.dct_output_format,
|
||||||
|
request.dct_color_mode
|
||||||
|
)
|
||||||
|
|
||||||
result = encode(
|
result = encode(
|
||||||
message=request.message,
|
message=request.message,
|
||||||
reference_photo=ref_photo,
|
reference_photo=ref_photo,
|
||||||
@@ -495,12 +601,19 @@ async def api_encode(request: EncodeRequest):
|
|||||||
rsa_key_data=rsa_key,
|
rsa_key_data=rsa_key,
|
||||||
rsa_password=request.rsa_password,
|
rsa_password=request.rsa_password,
|
||||||
date_str=request.date_str,
|
date_str=request.date_str,
|
||||||
embed_mode=request.embed_mode, # NEW in v3.0
|
embed_mode=request.embed_mode,
|
||||||
|
**dct_params, # NEW in v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
stego_b64 = base64.b64encode(result.stego_image).decode('utf-8')
|
stego_b64 = base64.b64encode(result.stego_image).decode('utf-8')
|
||||||
day_of_week = get_day_from_date(result.date_used)
|
day_of_week = get_day_from_date(result.date_used)
|
||||||
|
|
||||||
|
output_format, color_mode, _ = _get_output_info(
|
||||||
|
request.embed_mode,
|
||||||
|
request.dct_output_format,
|
||||||
|
request.dct_color_mode
|
||||||
|
)
|
||||||
|
|
||||||
return EncodeResponse(
|
return EncodeResponse(
|
||||||
stego_image_base64=stego_b64,
|
stego_image_base64=stego_b64,
|
||||||
filename=result.filename,
|
filename=result.filename,
|
||||||
@@ -508,6 +621,8 @@ async def api_encode(request: EncodeRequest):
|
|||||||
date_used=result.date_used,
|
date_used=result.date_used,
|
||||||
day_of_week=day_of_week,
|
day_of_week=day_of_week,
|
||||||
embed_mode=request.embed_mode,
|
embed_mode=request.embed_mode,
|
||||||
|
output_format=output_format,
|
||||||
|
color_mode=color_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
except CapacityError as e:
|
except CapacityError as e:
|
||||||
@@ -526,6 +641,7 @@ async def api_encode_file(request: EncodeFileRequest):
|
|||||||
File data must be base64-encoded.
|
File data must be base64-encoded.
|
||||||
|
|
||||||
NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct').
|
NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct').
|
||||||
|
NEW in v3.0.1: Supports dct_output_format and dct_color_mode.
|
||||||
"""
|
"""
|
||||||
# Validate mode
|
# Validate mode
|
||||||
if request.embed_mode == "dct" and not has_dct_support():
|
if request.embed_mode == "dct" and not has_dct_support():
|
||||||
@@ -543,6 +659,13 @@ async def api_encode_file(request: EncodeFileRequest):
|
|||||||
mime_type=request.mime_type
|
mime_type=request.mime_type
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Get DCT parameters
|
||||||
|
dct_params = _get_dct_params(
|
||||||
|
request.embed_mode,
|
||||||
|
request.dct_output_format,
|
||||||
|
request.dct_color_mode
|
||||||
|
)
|
||||||
|
|
||||||
result = encode(
|
result = encode(
|
||||||
message=payload,
|
message=payload,
|
||||||
reference_photo=ref_photo,
|
reference_photo=ref_photo,
|
||||||
@@ -552,12 +675,19 @@ async def api_encode_file(request: EncodeFileRequest):
|
|||||||
rsa_key_data=rsa_key,
|
rsa_key_data=rsa_key,
|
||||||
rsa_password=request.rsa_password,
|
rsa_password=request.rsa_password,
|
||||||
date_str=request.date_str,
|
date_str=request.date_str,
|
||||||
embed_mode=request.embed_mode, # NEW in v3.0
|
embed_mode=request.embed_mode,
|
||||||
|
**dct_params, # NEW in v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
stego_b64 = base64.b64encode(result.stego_image).decode('utf-8')
|
stego_b64 = base64.b64encode(result.stego_image).decode('utf-8')
|
||||||
day_of_week = get_day_from_date(result.date_used)
|
day_of_week = get_day_from_date(result.date_used)
|
||||||
|
|
||||||
|
output_format, color_mode, _ = _get_output_info(
|
||||||
|
request.embed_mode,
|
||||||
|
request.dct_output_format,
|
||||||
|
request.dct_color_mode
|
||||||
|
)
|
||||||
|
|
||||||
return EncodeResponse(
|
return EncodeResponse(
|
||||||
stego_image_base64=stego_b64,
|
stego_image_base64=stego_b64,
|
||||||
filename=result.filename,
|
filename=result.filename,
|
||||||
@@ -565,6 +695,8 @@ async def api_encode_file(request: EncodeFileRequest):
|
|||||||
date_used=result.date_used,
|
date_used=result.date_used,
|
||||||
day_of_week=day_of_week,
|
day_of_week=day_of_week,
|
||||||
embed_mode=request.embed_mode,
|
embed_mode=request.embed_mode,
|
||||||
|
output_format=output_format,
|
||||||
|
color_mode=color_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
except CapacityError as e:
|
except CapacityError as e:
|
||||||
@@ -588,6 +720,9 @@ async def api_decode(request: DecodeRequest):
|
|||||||
|
|
||||||
NEW in v3.0: Supports embed_mode parameter ('auto', 'lsb', or 'dct').
|
NEW in v3.0: Supports embed_mode parameter ('auto', 'lsb', or 'dct').
|
||||||
With 'auto' (default), tries LSB first then DCT.
|
With 'auto' (default), tries LSB first then DCT.
|
||||||
|
|
||||||
|
Note: Extraction works regardless of whether the image was created with
|
||||||
|
color mode or grayscale mode - both use the same Y channel for data.
|
||||||
"""
|
"""
|
||||||
# Validate mode
|
# Validate mode
|
||||||
if request.embed_mode == "dct" and not has_dct_support():
|
if request.embed_mode == "dct" and not has_dct_support():
|
||||||
@@ -605,7 +740,7 @@ async def api_decode(request: DecodeRequest):
|
|||||||
pin=request.pin,
|
pin=request.pin,
|
||||||
rsa_key_data=rsa_key,
|
rsa_key_data=rsa_key,
|
||||||
rsa_password=request.rsa_password,
|
rsa_password=request.rsa_password,
|
||||||
embed_mode=request.embed_mode, # NEW in v3.0
|
embed_mode=request.embed_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.is_file:
|
if result.is_file:
|
||||||
@@ -645,16 +780,20 @@ async def api_encode_multipart(
|
|||||||
rsa_key_qr: Optional[UploadFile] = File(None),
|
rsa_key_qr: Optional[UploadFile] = File(None),
|
||||||
rsa_password: str = Form(""),
|
rsa_password: str = Form(""),
|
||||||
date_str: str = Form(""),
|
date_str: str = Form(""),
|
||||||
embed_mode: str = Form("lsb"), # NEW in v3.0
|
embed_mode: str = Form("lsb"),
|
||||||
|
# NEW in v3.0.1
|
||||||
|
dct_output_format: str = Form("png"),
|
||||||
|
dct_color_mode: str = Form("grayscale"),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Encode using multipart form data (file uploads).
|
Encode using multipart form data (file uploads).
|
||||||
|
|
||||||
Provide either 'message' (text) or 'payload_file' (binary file).
|
Provide either 'message' (text) or 'payload_file' (binary file).
|
||||||
RSA key can be provided as 'rsa_key' (.pem file) or 'rsa_key_qr' (QR code image).
|
RSA key can be provided as 'rsa_key' (.pem file) or 'rsa_key_qr' (QR code image).
|
||||||
Returns the stego image directly as PNG with metadata headers.
|
Returns the stego image directly with metadata headers.
|
||||||
|
|
||||||
NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct').
|
NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct').
|
||||||
|
NEW in v3.0.1: Supports dct_output_format ('png' or 'jpeg') and dct_color_mode ('grayscale' or 'color').
|
||||||
"""
|
"""
|
||||||
# Validate mode
|
# Validate mode
|
||||||
if embed_mode not in ("lsb", "dct"):
|
if embed_mode not in ("lsb", "dct"):
|
||||||
@@ -662,6 +801,12 @@ async def api_encode_multipart(
|
|||||||
if embed_mode == "dct" and not has_dct_support():
|
if embed_mode == "dct" and not has_dct_support():
|
||||||
raise HTTPException(400, "DCT mode requires scipy. Install with: pip install scipy")
|
raise HTTPException(400, "DCT mode requires scipy. Install with: pip install scipy")
|
||||||
|
|
||||||
|
# Validate DCT options
|
||||||
|
if dct_output_format not in ("png", "jpeg"):
|
||||||
|
raise HTTPException(400, "dct_output_format must be 'png' or 'jpeg'")
|
||||||
|
if dct_color_mode not in ("grayscale", "color"):
|
||||||
|
raise HTTPException(400, "dct_color_mode must be 'grayscale' or 'color'")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ref_data = await reference_photo.read()
|
ref_data = await reference_photo.read()
|
||||||
carrier_data = await carrier.read()
|
carrier_data = await carrier.read()
|
||||||
@@ -701,6 +846,9 @@ async def api_encode_multipart(
|
|||||||
else:
|
else:
|
||||||
raise HTTPException(400, "Must provide either 'message' or 'payload_file'")
|
raise HTTPException(400, "Must provide either 'message' or 'payload_file'")
|
||||||
|
|
||||||
|
# Get DCT parameters
|
||||||
|
dct_params = _get_dct_params(embed_mode, dct_output_format, dct_color_mode)
|
||||||
|
|
||||||
result = encode(
|
result = encode(
|
||||||
message=payload,
|
message=payload,
|
||||||
reference_photo=ref_data,
|
reference_photo=ref_data,
|
||||||
@@ -710,20 +858,26 @@ async def api_encode_multipart(
|
|||||||
rsa_key_data=rsa_key_data,
|
rsa_key_data=rsa_key_data,
|
||||||
rsa_password=effective_password,
|
rsa_password=effective_password,
|
||||||
date_str=date_str if date_str else None,
|
date_str=date_str if date_str else None,
|
||||||
embed_mode=embed_mode, # NEW in v3.0
|
embed_mode=embed_mode,
|
||||||
|
**dct_params, # NEW in v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
day_of_week = get_day_from_date(result.date_used)
|
day_of_week = get_day_from_date(result.date_used)
|
||||||
|
output_format, color_mode, mime_type = _get_output_info(
|
||||||
|
embed_mode, dct_output_format, dct_color_mode
|
||||||
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
content=result.stego_image,
|
content=result.stego_image,
|
||||||
media_type="image/png",
|
media_type=mime_type,
|
||||||
headers={
|
headers={
|
||||||
"Content-Disposition": f"attachment; filename={result.filename}",
|
"Content-Disposition": f"attachment; filename={result.filename}",
|
||||||
"X-Stegasoo-Date": result.date_used,
|
"X-Stegasoo-Date": result.date_used,
|
||||||
"X-Stegasoo-Day": day_of_week,
|
"X-Stegasoo-Day": day_of_week,
|
||||||
"X-Stegasoo-Capacity-Percent": f"{result.capacity_percent:.1f}",
|
"X-Stegasoo-Capacity-Percent": f"{result.capacity_percent:.1f}",
|
||||||
"X-Stegasoo-Embed-Mode": embed_mode, # NEW in v3.0
|
"X-Stegasoo-Embed-Mode": embed_mode,
|
||||||
|
"X-Stegasoo-Output-Format": output_format, # NEW in v3.0.1
|
||||||
|
"X-Stegasoo-Color-Mode": color_mode, # NEW in v3.0.1
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -746,7 +900,7 @@ async def api_decode_multipart(
|
|||||||
rsa_key: Optional[UploadFile] = File(None),
|
rsa_key: Optional[UploadFile] = File(None),
|
||||||
rsa_key_qr: Optional[UploadFile] = File(None),
|
rsa_key_qr: Optional[UploadFile] = File(None),
|
||||||
rsa_password: str = Form(""),
|
rsa_password: str = Form(""),
|
||||||
embed_mode: str = Form("auto"), # NEW in v3.0
|
embed_mode: str = Form("auto"),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Decode using multipart form data (file uploads).
|
Decode using multipart form data (file uploads).
|
||||||
@@ -755,6 +909,8 @@ async def api_decode_multipart(
|
|||||||
Returns JSON with payload_type indicating text or file.
|
Returns JSON with payload_type indicating text or file.
|
||||||
|
|
||||||
NEW in v3.0: Supports embed_mode parameter ('auto', 'lsb', or 'dct').
|
NEW in v3.0: Supports embed_mode parameter ('auto', 'lsb', or 'dct').
|
||||||
|
|
||||||
|
Note: Extraction works the same regardless of color mode used during encoding.
|
||||||
"""
|
"""
|
||||||
# Validate mode
|
# Validate mode
|
||||||
if embed_mode not in ("auto", "lsb", "dct"):
|
if embed_mode not in ("auto", "lsb", "dct"):
|
||||||
@@ -795,7 +951,7 @@ async def api_decode_multipart(
|
|||||||
pin=pin,
|
pin=pin,
|
||||||
rsa_key_data=rsa_key_data,
|
rsa_key_data=rsa_key_data,
|
||||||
rsa_password=effective_password,
|
rsa_password=effective_password,
|
||||||
embed_mode=embed_mode, # NEW in v3.0
|
embed_mode=embed_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.is_file:
|
if result.is_file:
|
||||||
@@ -866,7 +1022,7 @@ async def api_image_info(
|
|||||||
capacity_bytes=comparison['dct']['capacity_bytes'],
|
capacity_bytes=comparison['dct']['capacity_bytes'],
|
||||||
capacity_kb=round(comparison['dct']['capacity_kb'], 1),
|
capacity_kb=round(comparison['dct']['capacity_kb'], 1),
|
||||||
available=comparison['dct']['available'],
|
available=comparison['dct']['available'],
|
||||||
output_format=comparison['dct']['output'],
|
output_format="PNG/JPEG (grayscale or color)", # Updated for v3.0.1
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Stegasoo CLI - Command-line interface for steganography operations.
|
Stegasoo CLI - Command-line interface for steganography operations (v3.0.1).
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
stegasoo generate [OPTIONS]
|
stegasoo generate [OPTIONS]
|
||||||
@@ -8,7 +8,12 @@ Usage:
|
|||||||
stegasoo decode [OPTIONS]
|
stegasoo decode [OPTIONS]
|
||||||
stegasoo verify [OPTIONS]
|
stegasoo verify [OPTIONS]
|
||||||
stegasoo info [OPTIONS]
|
stegasoo info [OPTIONS]
|
||||||
stegasoo compare [OPTIONS] # NEW in v3.0
|
stegasoo compare [OPTIONS]
|
||||||
|
stegasoo modes [OPTIONS]
|
||||||
|
|
||||||
|
New in v3.0.1:
|
||||||
|
- DCT color mode: --dct-color (grayscale or color)
|
||||||
|
- DCT output format: --dct-format (png or jpeg)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@@ -73,14 +78,19 @@ def cli():
|
|||||||
Hide encrypted messages or files in images using a combination of:
|
Hide encrypted messages or files in images using a combination of:
|
||||||
|
|
||||||
\b
|
\b
|
||||||
• Reference photo (something you have)
|
- Reference photo (something you have)
|
||||||
• Daily passphrase (something you know)
|
- Daily passphrase (something you know)
|
||||||
• Static PIN or RSA key (additional security)
|
- Static PIN or RSA key (additional security)
|
||||||
|
|
||||||
\b
|
\b
|
||||||
NEW in v3.0 - Embedding Modes:
|
Embedding Modes (v3.0):
|
||||||
• LSB mode (default): Full color output, higher capacity
|
- LSB mode (default): Full color output, higher capacity
|
||||||
• DCT mode: Grayscale output, ~20% capacity, better stealth
|
- DCT mode: Frequency domain, ~20% capacity, better stealth
|
||||||
|
|
||||||
|
\b
|
||||||
|
DCT Options (v3.0.1):
|
||||||
|
- Color mode: grayscale (default) or color (preserves colors)
|
||||||
|
- Output format: png (lossless) or jpeg (smaller, natural)
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -148,29 +158,29 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
|
|||||||
|
|
||||||
# Pretty output
|
# Pretty output
|
||||||
click.echo()
|
click.echo()
|
||||||
click.secho("═" * 60, fg='cyan')
|
click.secho("=" * 60, fg='cyan')
|
||||||
click.secho(" STEGASOO CREDENTIALS", fg='cyan', bold=True)
|
click.secho(" STEGASOO CREDENTIALS", fg='cyan', bold=True)
|
||||||
click.secho("═" * 60, fg='cyan')
|
click.secho("=" * 60, fg='cyan')
|
||||||
click.echo()
|
click.echo()
|
||||||
|
|
||||||
click.secho("⚠️ MEMORIZE THESE AND CLOSE THIS WINDOW", fg='yellow', bold=True)
|
click.secho(" MEMORIZE THESE AND CLOSE THIS WINDOW", fg='yellow', bold=True)
|
||||||
click.secho(" Do not screenshot or save to file!", fg='yellow')
|
click.secho(" Do not screenshot or save to file!", fg='yellow')
|
||||||
click.echo()
|
click.echo()
|
||||||
|
|
||||||
if creds.pin:
|
if creds.pin:
|
||||||
click.secho("─── STATIC PIN ───", fg='green')
|
click.secho("--- STATIC PIN ---", fg='green')
|
||||||
click.secho(f" {creds.pin}", fg='bright_yellow', bold=True)
|
click.secho(f" {creds.pin}", fg='bright_yellow', bold=True)
|
||||||
click.echo()
|
click.echo()
|
||||||
|
|
||||||
click.secho("─── DAILY PHRASES ───", fg='green')
|
click.secho("--- DAILY PHRASES ---", fg='green')
|
||||||
for day in DAY_NAMES:
|
for day in DAY_NAMES:
|
||||||
phrase = creds.phrases[day]
|
phrase = creds.phrases[day]
|
||||||
click.echo(f" {day:9} │ ", nl=False)
|
click.echo(f" {day:9} | ", nl=False)
|
||||||
click.secho(phrase, fg='bright_white')
|
click.secho(phrase, fg='bright_white')
|
||||||
click.echo()
|
click.echo()
|
||||||
|
|
||||||
if creds.rsa_key_pem:
|
if creds.rsa_key_pem:
|
||||||
click.secho("─── RSA KEY ───", fg='green')
|
click.secho("--- RSA KEY ---", fg='green')
|
||||||
if output:
|
if output:
|
||||||
# Save to file
|
# Save to file
|
||||||
private_key = load_rsa_key(creds.rsa_key_pem.encode())
|
private_key = load_rsa_key(creds.rsa_key_pem.encode())
|
||||||
@@ -182,7 +192,7 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
|
|||||||
click.echo(creds.rsa_key_pem)
|
click.echo(creds.rsa_key_pem)
|
||||||
click.echo()
|
click.echo()
|
||||||
|
|
||||||
click.secho("─── SECURITY ───", fg='green')
|
click.secho("--- SECURITY ---", fg='green')
|
||||||
click.echo(f" Phrase entropy: {creds.phrase_entropy} bits")
|
click.echo(f" Phrase entropy: {creds.phrase_entropy} bits")
|
||||||
if creds.pin:
|
if creds.pin:
|
||||||
click.echo(f" PIN entropy: {creds.pin_entropy} bits")
|
click.echo(f" PIN entropy: {creds.pin_entropy} bits")
|
||||||
@@ -214,9 +224,14 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
|
|||||||
@click.option('--output', '-o', type=click.Path(), help='Output file (default: auto-generated)')
|
@click.option('--output', '-o', type=click.Path(), help='Output file (default: auto-generated)')
|
||||||
@click.option('--date', 'date_str', help='Date override (YYYY-MM-DD)')
|
@click.option('--date', 'date_str', help='Date override (YYYY-MM-DD)')
|
||||||
@click.option('--mode', 'embed_mode', type=click.Choice(['lsb', 'dct']), default='lsb',
|
@click.option('--mode', 'embed_mode', type=click.Choice(['lsb', 'dct']), default='lsb',
|
||||||
help='Embedding mode: lsb (default, color) or dct (grayscale, requires scipy)')
|
help='Embedding mode: lsb (default, color) or dct (requires scipy)')
|
||||||
|
@click.option('--dct-format', 'dct_output_format', type=click.Choice(['png', 'jpeg']), default='png',
|
||||||
|
help='DCT output format: png (lossless, default) or jpeg (smaller)')
|
||||||
|
@click.option('--dct-color', 'dct_color_mode', type=click.Choice(['grayscale', 'color']), default='grayscale',
|
||||||
|
help='DCT color mode: grayscale (default) or color (preserves original colors)')
|
||||||
@click.option('--quiet', '-q', is_flag=True, help='Suppress output except errors')
|
@click.option('--quiet', '-q', is_flag=True, help='Suppress output except errors')
|
||||||
def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key, key_qr, key_password, output, date_str, embed_mode, quiet):
|
def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key, key_qr,
|
||||||
|
key_password, output, date_str, embed_mode, dct_output_format, dct_color_mode, quiet):
|
||||||
"""
|
"""
|
||||||
Encode a secret message or file into an image.
|
Encode a secret message or file into an image.
|
||||||
|
|
||||||
@@ -230,27 +245,37 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
|
|||||||
\b
|
\b
|
||||||
Embedding Modes (v3.0):
|
Embedding Modes (v3.0):
|
||||||
--mode lsb Spatial LSB embedding (default)
|
--mode lsb Spatial LSB embedding (default)
|
||||||
• Full color output (PNG/BMP)
|
- Full color output (PNG/BMP)
|
||||||
• Higher capacity (~375 KB/megapixel)
|
- Higher capacity (~375 KB/megapixel)
|
||||||
|
|
||||||
--mode dct DCT domain embedding (requires scipy)
|
--mode dct DCT domain embedding (requires scipy)
|
||||||
• Grayscale output only
|
- Configurable color/grayscale output
|
||||||
• Lower capacity (~75 KB/megapixel)
|
- Lower capacity (~75 KB/megapixel)
|
||||||
• Better resistance to visual analysis
|
- Better resistance to visual analysis
|
||||||
|
|
||||||
|
\b
|
||||||
|
DCT Options (v3.0.1):
|
||||||
|
--dct-format png Lossless output (default)
|
||||||
|
--dct-format jpeg Smaller file, more natural appearance
|
||||||
|
|
||||||
|
--dct-color grayscale Convert to grayscale (default, traditional)
|
||||||
|
--dct-color color Preserve original colors (experimental)
|
||||||
|
|
||||||
\b
|
\b
|
||||||
Examples:
|
Examples:
|
||||||
# Text message with PIN (LSB mode, default)
|
# Text message with PIN (LSB mode, default)
|
||||||
stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder" --pin 123456 -m "secret"
|
stegasoo encode -r photo.jpg -c meme.png -p "apple forest" --pin 123456 -m "secret"
|
||||||
|
|
||||||
# DCT mode for better stealth
|
# DCT mode - grayscale PNG (traditional)
|
||||||
stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -m "secret" --mode dct
|
stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -m "secret" --mode dct
|
||||||
|
|
||||||
# With RSA key file
|
# DCT mode - color JPEG (v3.0.1)
|
||||||
stegasoo encode -r photo.jpg -c meme.png -p "words" -k mykey.pem -m "secret"
|
stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -m "secret" \
|
||||||
|
--mode dct --dct-color color --dct-format jpeg
|
||||||
|
|
||||||
# Embed a binary file
|
# DCT mode - color PNG (best quality + color preservation)
|
||||||
stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -e secret.pdf
|
stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -m "secret" \
|
||||||
|
--mode dct --dct-color color --dct-format png
|
||||||
"""
|
"""
|
||||||
# Check DCT mode availability
|
# Check DCT mode availability
|
||||||
if embed_mode == 'dct' and not has_dct_support():
|
if embed_mode == 'dct' and not has_dct_support():
|
||||||
@@ -258,6 +283,12 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
|
|||||||
"DCT mode requires scipy. Install with: pip install scipy"
|
"DCT mode requires scipy. Install with: pip install scipy"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Warn if DCT options used with LSB mode
|
||||||
|
if embed_mode == 'lsb':
|
||||||
|
if dct_output_format != 'png' or dct_color_mode != 'grayscale':
|
||||||
|
if not quiet:
|
||||||
|
click.secho("Note: --dct-format and --dct-color only apply to DCT mode", fg='yellow', dim=True)
|
||||||
|
|
||||||
# Determine what to encode
|
# Determine what to encode
|
||||||
payload = None
|
payload = None
|
||||||
|
|
||||||
@@ -329,7 +360,10 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not quiet:
|
if not quiet:
|
||||||
click.echo(f"Mode: {embed_mode.upper()} ({fit_check['usage_percent']:.1f}% capacity)")
|
mode_desc = embed_mode.upper()
|
||||||
|
if embed_mode == 'dct':
|
||||||
|
mode_desc += f" ({dct_color_mode}, {dct_output_format.upper()})"
|
||||||
|
click.echo(f"Mode: {mode_desc} ({fit_check['usage_percent']:.1f}% capacity)")
|
||||||
|
|
||||||
result = encode(
|
result = encode(
|
||||||
message=payload,
|
message=payload,
|
||||||
@@ -340,7 +374,9 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
|
|||||||
rsa_key_data=rsa_key_data,
|
rsa_key_data=rsa_key_data,
|
||||||
rsa_password=effective_key_password,
|
rsa_password=effective_key_password,
|
||||||
date_str=date_str,
|
date_str=date_str,
|
||||||
embed_mode=embed_mode, # NEW in v3.0
|
embed_mode=embed_mode,
|
||||||
|
dct_output_format=dct_output_format,
|
||||||
|
dct_color_mode=dct_color_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Determine output path
|
# Determine output path
|
||||||
@@ -353,13 +389,15 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
|
|||||||
out_path.write_bytes(result.stego_image)
|
out_path.write_bytes(result.stego_image)
|
||||||
|
|
||||||
if not quiet:
|
if not quiet:
|
||||||
click.secho(f"✓ Encoded successfully!", fg='green')
|
click.secho(f"[OK] Encoded successfully!", fg='green')
|
||||||
click.echo(f" Output: {out_path}")
|
click.echo(f" Output: {out_path}")
|
||||||
click.echo(f" Size: {len(result.stego_image):,} bytes")
|
click.echo(f" Size: {len(result.stego_image):,} bytes")
|
||||||
click.echo(f" Capacity used: {result.capacity_percent:.1f}%")
|
click.echo(f" Capacity used: {result.capacity_percent:.1f}%")
|
||||||
click.echo(f" Date: {result.date_used}")
|
click.echo(f" Date: {result.date_used}")
|
||||||
if embed_mode == 'dct':
|
if embed_mode == 'dct':
|
||||||
click.secho(f" Note: Output is grayscale (DCT mode)", dim=True)
|
color_note = "color preserved" if dct_color_mode == 'color' else "grayscale"
|
||||||
|
format_note = dct_output_format.upper()
|
||||||
|
click.secho(f" DCT output: {format_note} ({color_note})", dim=True)
|
||||||
|
|
||||||
except StegasooError as e:
|
except StegasooError as e:
|
||||||
raise click.ClickException(str(e))
|
raise click.ClickException(str(e))
|
||||||
@@ -394,6 +432,9 @@ def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, embed
|
|||||||
Automatically detects whether content is text or a file.
|
Automatically detects whether content is text or a file.
|
||||||
RSA key can be provided as a .pem file (--key) or QR code image (--key-qr).
|
RSA key can be provided as a .pem file (--key) or QR code image (--key-qr).
|
||||||
|
|
||||||
|
Note: Extraction works the same regardless of whether the image was
|
||||||
|
created with color mode or grayscale mode - both use the same Y channel.
|
||||||
|
|
||||||
\b
|
\b
|
||||||
Extraction Modes (v3.0):
|
Extraction Modes (v3.0):
|
||||||
--mode auto Auto-detect (default) - tries LSB first, then DCT
|
--mode auto Auto-detect (default) - tries LSB first, then DCT
|
||||||
@@ -461,7 +502,7 @@ def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, embed
|
|||||||
pin=pin or "",
|
pin=pin or "",
|
||||||
rsa_key_data=rsa_key_data,
|
rsa_key_data=rsa_key_data,
|
||||||
rsa_password=effective_key_password,
|
rsa_password=effective_key_password,
|
||||||
embed_mode=embed_mode, # NEW in v3.0
|
embed_mode=embed_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.is_file:
|
if result.is_file:
|
||||||
@@ -481,7 +522,7 @@ def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, embed
|
|||||||
out_path.write_bytes(result.file_data)
|
out_path.write_bytes(result.file_data)
|
||||||
|
|
||||||
if not quiet:
|
if not quiet:
|
||||||
click.secho("✓ Decoded file successfully!", fg='green')
|
click.secho("[OK] Decoded file successfully!", fg='green')
|
||||||
click.echo(f" Saved to: {out_path}")
|
click.echo(f" Saved to: {out_path}")
|
||||||
click.echo(f" Size: {len(result.file_data):,} bytes")
|
click.echo(f" Size: {len(result.file_data):,} bytes")
|
||||||
if result.mime_type:
|
if result.mime_type:
|
||||||
@@ -491,13 +532,13 @@ def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, embed
|
|||||||
if output:
|
if output:
|
||||||
Path(output).write_text(result.message)
|
Path(output).write_text(result.message)
|
||||||
if not quiet:
|
if not quiet:
|
||||||
click.secho("✓ Decoded successfully!", fg='green')
|
click.secho("[OK] Decoded successfully!", fg='green')
|
||||||
click.echo(f" Saved to: {output}")
|
click.echo(f" Saved to: {output}")
|
||||||
else:
|
else:
|
||||||
if quiet:
|
if quiet:
|
||||||
click.echo(result.message)
|
click.echo(result.message)
|
||||||
else:
|
else:
|
||||||
click.secho("✓ Decoded successfully!", fg='green')
|
click.secho("[OK] Decoded successfully!", fg='green')
|
||||||
click.echo()
|
click.echo()
|
||||||
click.echo(result.message)
|
click.echo(result.message)
|
||||||
|
|
||||||
@@ -583,7 +624,7 @@ def verify(ref, stego, phrase, pin, key, key_qr, key_password, embed_mode, as_js
|
|||||||
pin=pin or "",
|
pin=pin or "",
|
||||||
rsa_key_data=rsa_key_data,
|
rsa_key_data=rsa_key_data,
|
||||||
rsa_password=effective_key_password,
|
rsa_password=effective_key_password,
|
||||||
embed_mode=embed_mode, # NEW in v3.0
|
embed_mode=embed_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Calculate payload size
|
# Calculate payload size
|
||||||
@@ -617,7 +658,7 @@ def verify(ref, stego, phrase, pin, key, key_qr, key_password, embed_mode, as_js
|
|||||||
output["mime_type"] = result.mime_type
|
output["mime_type"] = result.mime_type
|
||||||
click.echo(json.dumps(output, indent=2))
|
click.echo(json.dumps(output, indent=2))
|
||||||
else:
|
else:
|
||||||
click.secho("✓ Valid stego image", fg='green', bold=True)
|
click.secho("[OK] Valid stego image", fg='green', bold=True)
|
||||||
click.echo(f" Payload: {payload_type} ({payload_desc})")
|
click.echo(f" Payload: {payload_type} ({payload_desc})")
|
||||||
click.echo(f" Size: {payload_size:,} bytes")
|
click.echo(f" Size: {payload_size:,} bytes")
|
||||||
if date_encoded:
|
if date_encoded:
|
||||||
@@ -634,7 +675,7 @@ def verify(ref, stego, phrase, pin, key, key_qr, key_password, embed_mode, as_js
|
|||||||
click.echo(json.dumps(output, indent=2))
|
click.echo(json.dumps(output, indent=2))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
click.secho("✗ Verification failed", fg='red', bold=True)
|
click.secho("[FAIL] Verification failed", fg='red', bold=True)
|
||||||
click.echo(f" Error: {e}")
|
click.echo(f" Error: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except StegasooError as e:
|
except StegasooError as e:
|
||||||
@@ -690,6 +731,8 @@ def info(image, as_json):
|
|||||||
"kb": round(comparison['dct']['capacity_kb'], 1),
|
"kb": round(comparison['dct']['capacity_kb'], 1),
|
||||||
"available": comparison['dct']['available'],
|
"available": comparison['dct']['available'],
|
||||||
"ratio_vs_lsb": round(comparison['dct']['ratio_vs_lsb'], 1),
|
"ratio_vs_lsb": round(comparison['dct']['ratio_vs_lsb'], 1),
|
||||||
|
"output_formats": ["png", "jpeg"],
|
||||||
|
"color_modes": ["grayscale", "color"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -701,7 +744,7 @@ def info(image, as_json):
|
|||||||
|
|
||||||
click.echo()
|
click.echo()
|
||||||
click.secho(f"Image: {image}", bold=True)
|
click.secho(f"Image: {image}", bold=True)
|
||||||
click.echo(f" Dimensions: {result.details['width']} × {result.details['height']}")
|
click.echo(f" Dimensions: {result.details['width']} x {result.details['height']}")
|
||||||
click.echo(f" Pixels: {result.details['pixels']:,}")
|
click.echo(f" Pixels: {result.details['pixels']:,}")
|
||||||
click.echo(f" Mode: {result.details['mode']}")
|
click.echo(f" Mode: {result.details['mode']}")
|
||||||
click.echo(f" Format: {result.details['format']}")
|
click.echo(f" Format: {result.details['format']}")
|
||||||
@@ -710,10 +753,13 @@ def info(image, as_json):
|
|||||||
click.secho(" Capacity:", bold=True)
|
click.secho(" Capacity:", bold=True)
|
||||||
click.echo(f" LSB mode: ~{comparison['lsb']['capacity_bytes']:,} bytes ({comparison['lsb']['capacity_kb']:.1f} KB)")
|
click.echo(f" LSB mode: ~{comparison['lsb']['capacity_bytes']:,} bytes ({comparison['lsb']['capacity_kb']:.1f} KB)")
|
||||||
|
|
||||||
dct_status = "✓" if comparison['dct']['available'] else "✗ (scipy not installed)"
|
dct_status = "[OK]" if comparison['dct']['available'] else "[X] (scipy not installed)"
|
||||||
click.echo(f" DCT mode: ~{comparison['dct']['capacity_bytes']:,} bytes ({comparison['dct']['capacity_kb']:.1f} KB) {dct_status}")
|
click.echo(f" DCT mode: ~{comparison['dct']['capacity_bytes']:,} bytes ({comparison['dct']['capacity_kb']:.1f} KB) {dct_status}")
|
||||||
click.echo(f" DCT ratio: {comparison['dct']['ratio_vs_lsb']:.1f}% of LSB")
|
click.echo(f" DCT ratio: {comparison['dct']['ratio_vs_lsb']:.1f}% of LSB")
|
||||||
|
|
||||||
|
if comparison['dct']['available']:
|
||||||
|
click.secho(" DCT options: grayscale/color, png/jpeg", dim=True)
|
||||||
|
|
||||||
if date_str:
|
if date_str:
|
||||||
click.echo()
|
click.echo()
|
||||||
click.echo(f" Embed date: {date_str} ({day_name})")
|
click.echo(f" Embed date: {date_str} ({day_name})")
|
||||||
@@ -725,7 +771,7 @@ def info(image, as_json):
|
|||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# COMPARE COMMAND (NEW in v3.0)
|
# COMPARE COMMAND
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@@ -767,7 +813,8 @@ def compare(image, payload_size, as_json):
|
|||||||
"capacity_bytes": comparison['dct']['capacity_bytes'],
|
"capacity_bytes": comparison['dct']['capacity_bytes'],
|
||||||
"capacity_kb": round(comparison['dct']['capacity_kb'], 1),
|
"capacity_kb": round(comparison['dct']['capacity_kb'], 1),
|
||||||
"available": comparison['dct']['available'],
|
"available": comparison['dct']['available'],
|
||||||
"output_format": comparison['dct']['output'],
|
"output_formats": ["png", "jpeg"],
|
||||||
|
"color_modes": ["grayscale", "color"],
|
||||||
"ratio_vs_lsb_percent": round(comparison['dct']['ratio_vs_lsb'], 1),
|
"ratio_vs_lsb_percent": round(comparison['dct']['ratio_vs_lsb'], 1),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -784,60 +831,63 @@ def compare(image, payload_size, as_json):
|
|||||||
return
|
return
|
||||||
|
|
||||||
click.echo()
|
click.echo()
|
||||||
click.secho(f"═══ Mode Comparison: {image} ═══", fg='cyan', bold=True)
|
click.secho(f"=== Mode Comparison: {image} ===", fg='cyan', bold=True)
|
||||||
click.echo(f" Dimensions: {comparison['width']} × {comparison['height']}")
|
click.echo(f" Dimensions: {comparison['width']} x {comparison['height']}")
|
||||||
click.echo()
|
click.echo()
|
||||||
|
|
||||||
# LSB mode
|
# LSB mode
|
||||||
click.secho(" ┌─── LSB Mode ───", fg='green')
|
click.secho(" +--- LSB Mode ---", fg='green')
|
||||||
click.echo(f" │ Capacity: {comparison['lsb']['capacity_bytes']:,} bytes ({comparison['lsb']['capacity_kb']:.1f} KB)")
|
click.echo(f" | Capacity: {comparison['lsb']['capacity_bytes']:,} bytes ({comparison['lsb']['capacity_kb']:.1f} KB)")
|
||||||
click.echo(f" │ Output: {comparison['lsb']['output']}")
|
click.echo(f" | Output: {comparison['lsb']['output']}")
|
||||||
click.echo(f" │ Status: ✓ Available")
|
click.echo(f" | Status: [OK] Available")
|
||||||
click.echo(" │")
|
click.echo(" |")
|
||||||
|
|
||||||
# DCT mode
|
# DCT mode
|
||||||
click.secho(" ├─── DCT Mode ───", fg='blue')
|
click.secho(" +--- DCT Mode ---", fg='blue')
|
||||||
click.echo(f" │ Capacity: {comparison['dct']['capacity_bytes']:,} bytes ({comparison['dct']['capacity_kb']:.1f} KB)")
|
click.echo(f" | Capacity: {comparison['dct']['capacity_bytes']:,} bytes ({comparison['dct']['capacity_kb']:.1f} KB)")
|
||||||
click.echo(f" │ Output: {comparison['dct']['output']}")
|
click.echo(f" | Ratio: {comparison['dct']['ratio_vs_lsb']:.1f}% of LSB capacity")
|
||||||
click.echo(f" │ Ratio: {comparison['dct']['ratio_vs_lsb']:.1f}% of LSB capacity")
|
|
||||||
if comparison['dct']['available']:
|
if comparison['dct']['available']:
|
||||||
click.echo(f" │ Status: ✓ Available")
|
click.echo(f" | Status: [OK] Available")
|
||||||
|
click.echo(f" | Formats: PNG (lossless), JPEG (smaller)")
|
||||||
|
click.echo(f" | Colors: Grayscale (default), Color (v3.0.1)")
|
||||||
else:
|
else:
|
||||||
click.secho(f" │ Status: ✗ Requires scipy (pip install scipy)", fg='yellow')
|
click.secho(f" | Status: [X] Requires scipy (pip install scipy)", fg='yellow')
|
||||||
click.echo(" │")
|
click.echo(" |")
|
||||||
|
|
||||||
# Payload check
|
# Payload check
|
||||||
if payload_size:
|
if payload_size:
|
||||||
click.secho(" ├─── Payload Check ───", fg='magenta')
|
click.secho(" +--- Payload Check ---", fg='magenta')
|
||||||
click.echo(f" │ Size: {payload_size:,} bytes")
|
click.echo(f" | Size: {payload_size:,} bytes")
|
||||||
|
|
||||||
fits_lsb = payload_size <= comparison['lsb']['capacity_bytes']
|
fits_lsb = payload_size <= comparison['lsb']['capacity_bytes']
|
||||||
fits_dct = payload_size <= comparison['dct']['capacity_bytes']
|
fits_dct = payload_size <= comparison['dct']['capacity_bytes']
|
||||||
|
|
||||||
lsb_icon = "✓" if fits_lsb else "✗"
|
lsb_icon = "[OK]" if fits_lsb else "[X]"
|
||||||
dct_icon = "✓" if fits_dct else "✗"
|
dct_icon = "[OK]" if fits_dct else "[X]"
|
||||||
lsb_color = 'green' if fits_lsb else 'red'
|
lsb_color = 'green' if fits_lsb else 'red'
|
||||||
dct_color = 'green' if fits_dct else 'red'
|
dct_color = 'green' if fits_dct else 'red'
|
||||||
|
|
||||||
click.echo(f" │ LSB mode: ", nl=False)
|
click.echo(f" | LSB mode: ", nl=False)
|
||||||
click.secho(f"{lsb_icon} {'Fits' if fits_lsb else 'Too large'}", fg=lsb_color)
|
click.secho(f"{lsb_icon} {'Fits' if fits_lsb else 'Too large'}", fg=lsb_color)
|
||||||
click.echo(f" │ DCT mode: ", nl=False)
|
click.echo(f" | DCT mode: ", nl=False)
|
||||||
click.secho(f"{dct_icon} {'Fits' if fits_dct else 'Too large'}", fg=dct_color)
|
click.secho(f"{dct_icon} {'Fits' if fits_dct else 'Too large'}", fg=dct_color)
|
||||||
click.echo(" │")
|
click.echo(" |")
|
||||||
|
|
||||||
# Recommendation
|
# Recommendation
|
||||||
click.secho(" └─── Recommendation ───", fg='yellow')
|
click.secho(" +--- Recommendation ---", fg='yellow')
|
||||||
if not comparison['dct']['available']:
|
if not comparison['dct']['available']:
|
||||||
click.echo(" Use LSB mode (DCT unavailable)")
|
click.echo(" Use LSB mode (DCT unavailable)")
|
||||||
elif payload_size:
|
elif payload_size:
|
||||||
if fits_dct:
|
if fits_dct:
|
||||||
click.echo(" DCT mode for better stealth (payload fits both modes)")
|
click.echo(" DCT mode for better stealth (payload fits both modes)")
|
||||||
|
click.echo(" Use --dct-color color to preserve original colors")
|
||||||
elif fits_lsb:
|
elif fits_lsb:
|
||||||
click.echo(" LSB mode (payload too large for DCT)")
|
click.echo(" LSB mode (payload too large for DCT)")
|
||||||
else:
|
else:
|
||||||
click.secho(" ✗ Payload too large for both modes!", fg='red')
|
click.secho(" [X] Payload too large for both modes!", fg='red')
|
||||||
else:
|
else:
|
||||||
click.echo(" LSB for larger payloads, DCT for better stealth")
|
click.echo(" LSB for larger payloads, DCT for better stealth")
|
||||||
|
click.echo(" DCT supports color output with --dct-color color")
|
||||||
|
|
||||||
click.echo()
|
click.echo()
|
||||||
|
|
||||||
@@ -881,7 +931,7 @@ def strip_metadata_cmd(image, output, output_format, quiet):
|
|||||||
out_path.write_bytes(clean_data)
|
out_path.write_bytes(clean_data)
|
||||||
|
|
||||||
if not quiet:
|
if not quiet:
|
||||||
click.secho("✓ Metadata stripped", fg='green')
|
click.secho("[OK] Metadata stripped", fg='green')
|
||||||
click.echo(f" Input: {image} ({original_size:,} bytes)")
|
click.echo(f" Input: {image} ({original_size:,} bytes)")
|
||||||
click.echo(f" Output: {out_path} ({len(clean_data):,} bytes)")
|
click.echo(f" Output: {out_path} ({len(clean_data):,} bytes)")
|
||||||
|
|
||||||
@@ -890,7 +940,7 @@ def strip_metadata_cmd(image, output, output_format, quiet):
|
|||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# MODES COMMAND (NEW in v3.0)
|
# MODES COMMAND
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@@ -901,12 +951,12 @@ def modes():
|
|||||||
Displays which modes are available and their characteristics.
|
Displays which modes are available and their characteristics.
|
||||||
"""
|
"""
|
||||||
click.echo()
|
click.echo()
|
||||||
click.secho("═══ Stegasoo Embedding Modes ═══", fg='cyan', bold=True)
|
click.secho("=== Stegasoo Embedding Modes ===", fg='cyan', bold=True)
|
||||||
click.echo()
|
click.echo()
|
||||||
|
|
||||||
# LSB Mode
|
# LSB Mode
|
||||||
click.secho(" LSB Mode (Spatial LSB)", fg='green', bold=True)
|
click.secho(" LSB Mode (Spatial LSB)", fg='green', bold=True)
|
||||||
click.echo(" Status: ✓ Always available")
|
click.echo(" Status: [OK] Always available")
|
||||||
click.echo(" Output: PNG/BMP (full color)")
|
click.echo(" Output: PNG/BMP (full color)")
|
||||||
click.echo(" Capacity: ~375 KB per megapixel")
|
click.echo(" Capacity: ~375 KB per megapixel")
|
||||||
click.echo(" Use case: Larger payloads, color preservation")
|
click.echo(" Use case: Larger payloads, color preservation")
|
||||||
@@ -916,18 +966,36 @@ def modes():
|
|||||||
# DCT Mode
|
# DCT Mode
|
||||||
click.secho(" DCT Mode (Frequency Domain)", fg='blue', bold=True)
|
click.secho(" DCT Mode (Frequency Domain)", fg='blue', bold=True)
|
||||||
if has_dct_support():
|
if has_dct_support():
|
||||||
click.echo(" Status: ✓ Available")
|
click.echo(" Status: [OK] Available")
|
||||||
else:
|
else:
|
||||||
click.secho(" Status: ✗ Requires scipy", fg='yellow')
|
click.secho(" Status: [X] Requires scipy", fg='yellow')
|
||||||
click.echo(" Install: pip install scipy")
|
click.echo(" Install: pip install scipy")
|
||||||
click.echo(" Output: PNG (grayscale only)")
|
|
||||||
click.echo(" Capacity: ~75 KB per megapixel (~20% of LSB)")
|
click.echo(" Capacity: ~75 KB per megapixel (~20% of LSB)")
|
||||||
click.echo(" Use case: Better stealth, smaller messages")
|
click.echo(" Use case: Better stealth, frequency domain hiding")
|
||||||
click.echo(" CLI flag: --mode dct")
|
click.echo(" CLI flag: --mode dct")
|
||||||
click.echo()
|
click.echo()
|
||||||
|
|
||||||
click.secho(" Tip:", dim=True)
|
# DCT Options (v3.0.1)
|
||||||
click.echo(" Use 'stegasoo compare <image>' to see capacity for both modes")
|
click.secho(" DCT Options (v3.0.1)", fg='magenta', bold=True)
|
||||||
|
click.echo(" Output format:")
|
||||||
|
click.echo(" --dct-format png Lossless, larger file (default)")
|
||||||
|
click.echo(" --dct-format jpeg Lossy, smaller, more natural")
|
||||||
|
click.echo()
|
||||||
|
click.echo(" Color mode:")
|
||||||
|
click.echo(" --dct-color grayscale Traditional DCT (default)")
|
||||||
|
click.echo(" --dct-color color Preserves original colors")
|
||||||
|
click.echo()
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
click.secho(" Examples:", dim=True)
|
||||||
|
click.echo(" # Traditional DCT (grayscale PNG)")
|
||||||
|
click.echo(" stegasoo encode ... --mode dct")
|
||||||
|
click.echo()
|
||||||
|
click.echo(" # Color-preserving DCT with JPEG output")
|
||||||
|
click.echo(" stegasoo encode ... --mode dct --dct-color color --dct-format jpeg")
|
||||||
|
click.echo()
|
||||||
|
click.echo(" # Compare modes for an image")
|
||||||
|
click.echo(" stegasoo compare carrier.png")
|
||||||
click.echo()
|
click.echo()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Stegasoo Web Frontend (v3.0.1)
|
|||||||
Flask-based web UI for steganography operations.
|
Flask-based web UI for steganography operations.
|
||||||
Supports both text messages and file embedding.
|
Supports both text messages and file embedding.
|
||||||
NEW in v3.0: LSB and DCT embedding modes with advanced options.
|
NEW in v3.0: LSB and DCT embedding modes with advanced options.
|
||||||
NEW in v3.0.1: DCT output format selection (PNG or JPEG).
|
NEW in v3.0.1: DCT output format selection (PNG or JPEG) and color mode (grayscale or color).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
@@ -532,6 +532,11 @@ def encode_page():
|
|||||||
if dct_output_format not in ('png', 'jpeg'):
|
if dct_output_format not in ('png', 'jpeg'):
|
||||||
dct_output_format = 'png'
|
dct_output_format = 'png'
|
||||||
|
|
||||||
|
# NEW in v3.0.1 - DCT color mode (default to 'color')
|
||||||
|
dct_color_mode = request.form.get('dct_color_mode', 'color')
|
||||||
|
if dct_color_mode not in ('grayscale', 'color'):
|
||||||
|
dct_color_mode = 'color'
|
||||||
|
|
||||||
# Check DCT availability
|
# Check DCT availability
|
||||||
if embed_mode == 'dct' and not has_dct_support():
|
if embed_mode == 'dct' and not has_dct_support():
|
||||||
flash('DCT mode requires scipy. Install with: pip install scipy', 'error')
|
flash('DCT mode requires scipy. Install with: pip install scipy', 'error')
|
||||||
@@ -624,7 +629,7 @@ def encode_page():
|
|||||||
else:
|
else:
|
||||||
date_str = datetime.now().strftime('%Y-%m-%d')
|
date_str = datetime.now().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
# Encode with selected mode and output format
|
# Encode with selected mode, output format, and color mode
|
||||||
encode_result = encode(
|
encode_result = encode(
|
||||||
message=payload,
|
message=payload,
|
||||||
reference_photo=ref_data,
|
reference_photo=ref_data,
|
||||||
@@ -634,8 +639,9 @@ def encode_page():
|
|||||||
rsa_key_data=rsa_key_data,
|
rsa_key_data=rsa_key_data,
|
||||||
rsa_password=key_password,
|
rsa_password=key_password,
|
||||||
date_str=date_str,
|
date_str=date_str,
|
||||||
embed_mode=embed_mode, # NEW in v3.0
|
embed_mode=embed_mode,
|
||||||
dct_output_format=dct_output_format if embed_mode == 'dct' else None, # NEW in v3.0.1
|
dct_output_format=dct_output_format if embed_mode == 'dct' else None,
|
||||||
|
dct_color_mode=dct_color_mode if embed_mode == 'dct' else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Determine actual output format for filename and storage
|
# Determine actual output format for filename and storage
|
||||||
@@ -660,6 +666,7 @@ def encode_page():
|
|||||||
'timestamp': time.time(),
|
'timestamp': time.time(),
|
||||||
'embed_mode': embed_mode,
|
'embed_mode': embed_mode,
|
||||||
'output_format': dct_output_format if embed_mode == 'dct' else 'png',
|
'output_format': dct_output_format if embed_mode == 'dct' else 'png',
|
||||||
|
'color_mode': dct_color_mode if embed_mode == 'dct' else None,
|
||||||
'mime_type': output_mime,
|
'mime_type': output_mime,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,7 +706,8 @@ def encode_result(file_id):
|
|||||||
filename=file_info['filename'],
|
filename=file_info['filename'],
|
||||||
thumbnail_url=url_for('encode_thumbnail', thumb_id=thumbnail_id) if thumbnail_id else None,
|
thumbnail_url=url_for('encode_thumbnail', thumb_id=thumbnail_id) if thumbnail_id else None,
|
||||||
embed_mode=file_info.get('embed_mode', 'lsb'),
|
embed_mode=file_info.get('embed_mode', 'lsb'),
|
||||||
output_format=file_info.get('output_format', 'png'), # NEW in v3.0.1
|
output_format=file_info.get('output_format', 'png'),
|
||||||
|
color_mode=file_info.get('color_mode'), # NEW in v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -856,7 +864,7 @@ def decode_page():
|
|||||||
rsa_key_data=rsa_key_data,
|
rsa_key_data=rsa_key_data,
|
||||||
rsa_password=key_password,
|
rsa_password=key_password,
|
||||||
date_str=stego_date if stego_date else None,
|
date_str=stego_date if stego_date else None,
|
||||||
embed_mode=embed_mode, # NEW in v3.0
|
embed_mode=embed_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
if decode_result.is_file:
|
if decode_result.is_file:
|
||||||
|
|||||||
@@ -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>
|
<i class="bi bi-soundwave text-info fs-4 me-2"></i>
|
||||||
<strong>DCT Mode</strong>
|
<strong>DCT Mode</strong>
|
||||||
{% if has_dct %}
|
{% if has_dct %}
|
||||||
<span class="badge bg-info ms-auto">Stealth</span>
|
<span class="badge bg-warning text-dark ms-auto">Experimental</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-secondary ms-auto">Unavailable</span>
|
<span class="badge bg-secondary ms-auto">Unavailable</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<ul class="small text-muted mb-0 ps-3">
|
<ul class="small text-muted mb-0 ps-3">
|
||||||
<li>Grayscale output (PNG/JPEG)</li>
|
<li>Color or grayscale output</li>
|
||||||
<li>Lower capacity (~75 KB/MP)</li>
|
<li>Lower capacity (~75 KB/MP)</li>
|
||||||
<li>Better detection resistance</li>
|
<li>Better detection resistance</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -266,47 +266,98 @@
|
|||||||
<div class="form-text mt-2" id="modeHint">
|
<div class="form-text mt-2" id="modeHint">
|
||||||
<i class="bi bi-lightbulb me-1"></i>
|
<i class="bi bi-lightbulb me-1"></i>
|
||||||
<strong>LSB</strong> is best for most uses.
|
<strong>LSB</strong> is best for most uses.
|
||||||
<strong>DCT</strong> provides better stealth but smaller capacity and grayscale output.
|
<strong>DCT</strong> provides better stealth but lower capacity.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- DCT Output Format (shown only when DCT selected) -->
|
<!-- DCT Options Panel (shown only when DCT selected) -->
|
||||||
<div class="mb-3 d-none" id="dctOutputFormatGroup">
|
<div class="d-none" id="dctOptionsPanel">
|
||||||
<label class="form-label">
|
|
||||||
<i class="bi bi-file-image me-1"></i> DCT Output Format
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="row g-2">
|
<hr class="my-3">
|
||||||
<div class="col-6">
|
|
||||||
<div class="form-check card p-2 text-center" id="dctPngCard">
|
<div class="alert alert-warning small mb-3">
|
||||||
<input class="form-check-input mx-auto" type="radio" name="dct_output_format" id="dctFormatPng" value="png" checked>
|
<i class="bi bi-flask me-1"></i>
|
||||||
<label class="form-check-label w-100" for="dctFormatPng">
|
<strong>Experimental Feature:</strong> DCT embedding is still being refined.
|
||||||
<i class="bi bi-file-earmark-image text-success fs-5 d-block"></i>
|
Color mode preserves original colors but extraction uses Y channel only.
|
||||||
<strong>PNG</strong>
|
</div>
|
||||||
<div class="small text-muted">Lossless, larger</div>
|
|
||||||
</label>
|
<!-- DCT Color Mode (NEW in v3.0.1) -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">
|
||||||
|
<i class="bi bi-palette me-1"></i> DCT Color Mode
|
||||||
|
<span class="badge bg-success ms-1">v3.0.1</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="form-check card p-2 text-center border-success border-2" id="dctColorCard">
|
||||||
|
<input class="form-check-input mx-auto" type="radio" name="dct_color_mode" id="dctColorColor" value="color" checked>
|
||||||
|
<label class="form-check-label w-100" for="dctColorColor">
|
||||||
|
<i class="bi bi-palette-fill text-success fs-5 d-block"></i>
|
||||||
|
<strong>Color</strong>
|
||||||
|
<div class="small text-muted">Preserve colors</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="form-check card p-2 text-center" id="dctGrayscaleCard">
|
||||||
|
<input class="form-check-input mx-auto" type="radio" name="dct_color_mode" id="dctColorGrayscale" value="grayscale">
|
||||||
|
<label class="form-check-label w-100" for="dctColorGrayscale">
|
||||||
|
<i class="bi bi-circle-half text-secondary fs-5 d-block"></i>
|
||||||
|
<strong>Grayscale</strong>
|
||||||
|
<div class="small text-muted">Traditional DCT</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
|
||||||
<div class="form-check card p-2 text-center" id="dctJpegCard">
|
<div class="form-text mt-2">
|
||||||
<input class="form-check-input mx-auto" type="radio" name="dct_output_format" id="dctFormatJpeg" value="jpeg">
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
<label class="form-check-label w-100" for="dctFormatJpeg">
|
<strong>Color</strong> preserves original image colors (recommended).
|
||||||
<i class="bi bi-file-earmark-richtext text-warning fs-5 d-block"></i>
|
<strong>Grayscale</strong> converts to B&W (traditional DCT steganography).
|
||||||
<strong>JPEG</strong>
|
|
||||||
<div class="small text-muted">Smaller, natural</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-text mt-2">
|
<!-- DCT Output Format -->
|
||||||
<i class="bi bi-info-circle me-1"></i>
|
<div class="mb-3">
|
||||||
<strong>PNG</strong> is 100% reliable. <strong>JPEG</strong> produces smaller, more natural-looking files but uses lossy compression (Q=95).
|
<label class="form-label">
|
||||||
|
<i class="bi bi-file-image me-1"></i> DCT Output Format
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="form-check card p-2 text-center border-primary border-2" id="dctPngCard">
|
||||||
|
<input class="form-check-input mx-auto" type="radio" name="dct_output_format" id="dctFormatPng" value="png" checked>
|
||||||
|
<label class="form-check-label w-100" for="dctFormatPng">
|
||||||
|
<i class="bi bi-file-earmark-image text-primary fs-5 d-block"></i>
|
||||||
|
<strong>PNG</strong>
|
||||||
|
<div class="small text-muted">Lossless, larger</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="form-check card p-2 text-center" id="dctJpegCard">
|
||||||
|
<input class="form-check-input mx-auto" type="radio" name="dct_output_format" id="dctFormatJpeg" value="jpeg">
|
||||||
|
<label class="form-check-label w-100" for="dctFormatJpeg">
|
||||||
|
<i class="bi bi-file-earmark-richtext text-warning fs-5 d-block"></i>
|
||||||
|
<strong>JPEG</strong>
|
||||||
|
<div class="small text-muted">Smaller, natural</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-text mt-2">
|
||||||
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
|
<strong>PNG</strong> is 100% reliable. <strong>JPEG</strong> produces smaller, more natural-looking files but uses lossy compression (Q=95).
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Capacity Comparison (populated by JS) -->
|
<!-- Capacity Comparison (populated by JS) -->
|
||||||
<div class="d-none" id="modeCapacityComparison">
|
<div class="d-none" id="modeCapacityComparison">
|
||||||
|
<hr class="my-3">
|
||||||
<div class="alert alert-secondary small mb-0">
|
<div class="alert alert-secondary small mb-0">
|
||||||
<div class="row text-center">
|
<div class="row text-center">
|
||||||
<div class="col-6 border-end">
|
<div class="col-6 border-end">
|
||||||
@@ -353,7 +404,7 @@
|
|||||||
<div class="alert alert-secondary mt-4 small">
|
<div class="alert alert-secondary mt-4 small">
|
||||||
<i class="bi bi-info-circle me-1"></i>
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
<strong>Limits:</strong>
|
<strong>Limits:</strong>
|
||||||
Carrier image max ~24 megapixels (6000×4000).
|
Carrier image max ~24 megapixels (6000x4000).
|
||||||
Files max 30MB upload.
|
Files max 30MB upload.
|
||||||
Payload max {{ max_payload_kb }} KB.
|
Payload max {{ max_payload_kb }} KB.
|
||||||
</div>
|
</div>
|
||||||
@@ -470,8 +521,9 @@ document.getElementById('encodeForm').addEventListener('submit', function(e) {
|
|||||||
let modeLabel = selectedMode.toUpperCase();
|
let modeLabel = selectedMode.toUpperCase();
|
||||||
|
|
||||||
if (selectedMode === 'dct') {
|
if (selectedMode === 'dct') {
|
||||||
|
const colorMode = document.querySelector('input[name="dct_color_mode"]:checked')?.value || 'color';
|
||||||
const outputFormat = document.querySelector('input[name="dct_output_format"]:checked')?.value || 'png';
|
const outputFormat = document.querySelector('input[name="dct_output_format"]:checked')?.value || 'png';
|
||||||
modeLabel += ` → ${outputFormat.toUpperCase()}`;
|
modeLabel += ` (${colorMode}, ${outputFormat.toUpperCase()})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>Encoding (${modeLabel})...`;
|
btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>Encoding (${modeLabel})...`;
|
||||||
@@ -535,7 +587,7 @@ async function fetchCapacityComparison(file) {
|
|||||||
|
|
||||||
function updateCapacityDisplay(data) {
|
function updateCapacityDisplay(data) {
|
||||||
// Update top panel
|
// Update top panel
|
||||||
carrierDimensions.textContent = `${data.width} × ${data.height}`;
|
carrierDimensions.textContent = `${data.width} x ${data.height}`;
|
||||||
lsbCapacityBadge.textContent = `LSB: ${data.lsb.capacity_kb} KB`;
|
lsbCapacityBadge.textContent = `LSB: ${data.lsb.capacity_kb} KB`;
|
||||||
|
|
||||||
if (data.dct.available) {
|
if (data.dct.available) {
|
||||||
@@ -573,19 +625,27 @@ if (carrierInput) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Mode card highlighting & DCT output format visibility
|
// Mode card highlighting & DCT options visibility
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const lsbModeCard = document.getElementById('lsbModeCard');
|
const lsbModeCard = document.getElementById('lsbModeCard');
|
||||||
const dctModeCard = document.getElementById('dctModeCard');
|
const dctModeCard = document.getElementById('dctModeCard');
|
||||||
const modeLsb = document.getElementById('modeLsb');
|
const modeLsb = document.getElementById('modeLsb');
|
||||||
const modeDct = document.getElementById('modeDct');
|
const modeDct = document.getElementById('modeDct');
|
||||||
const dctOutputFormatGroup = document.getElementById('dctOutputFormatGroup');
|
const dctOptionsPanel = document.getElementById('dctOptionsPanel');
|
||||||
|
|
||||||
|
// DCT format cards
|
||||||
const dctPngCard = document.getElementById('dctPngCard');
|
const dctPngCard = document.getElementById('dctPngCard');
|
||||||
const dctJpegCard = document.getElementById('dctJpegCard');
|
const dctJpegCard = document.getElementById('dctJpegCard');
|
||||||
const dctFormatPng = document.getElementById('dctFormatPng');
|
const dctFormatPng = document.getElementById('dctFormatPng');
|
||||||
const dctFormatJpeg = document.getElementById('dctFormatJpeg');
|
const dctFormatJpeg = document.getElementById('dctFormatJpeg');
|
||||||
|
|
||||||
|
// DCT color mode cards
|
||||||
|
const dctColorCard = document.getElementById('dctColorCard');
|
||||||
|
const dctGrayscaleCard = document.getElementById('dctGrayscaleCard');
|
||||||
|
const dctColorColor = document.getElementById('dctColorColor');
|
||||||
|
const dctColorGrayscale = document.getElementById('dctColorGrayscale');
|
||||||
|
|
||||||
function updateModeCardHighlight() {
|
function updateModeCardHighlight() {
|
||||||
// Mode cards
|
// Mode cards
|
||||||
lsbModeCard.classList.toggle('border-primary', modeLsb.checked);
|
lsbModeCard.classList.toggle('border-primary', modeLsb.checked);
|
||||||
@@ -593,28 +653,40 @@ function updateModeCardHighlight() {
|
|||||||
dctModeCard.classList.toggle('border-info', modeDct.checked);
|
dctModeCard.classList.toggle('border-info', modeDct.checked);
|
||||||
dctModeCard.classList.toggle('border-2', modeDct.checked);
|
dctModeCard.classList.toggle('border-2', modeDct.checked);
|
||||||
|
|
||||||
// Show/hide DCT output format selector
|
// Show/hide DCT options panel
|
||||||
if (dctOutputFormatGroup) {
|
if (dctOptionsPanel) {
|
||||||
dctOutputFormatGroup.classList.toggle('d-none', !modeDct.checked);
|
dctOptionsPanel.classList.toggle('d-none', !modeDct.checked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDctFormatCardHighlight() {
|
function updateDctFormatCardHighlight() {
|
||||||
if (dctPngCard && dctJpegCard) {
|
if (dctPngCard && dctJpegCard) {
|
||||||
dctPngCard.classList.toggle('border-success', dctFormatPng.checked);
|
dctPngCard.classList.toggle('border-primary', dctFormatPng.checked);
|
||||||
dctPngCard.classList.toggle('border-2', dctFormatPng.checked);
|
dctPngCard.classList.toggle('border-2', dctFormatPng.checked);
|
||||||
dctJpegCard.classList.toggle('border-warning', dctFormatJpeg.checked);
|
dctJpegCard.classList.toggle('border-warning', dctFormatJpeg.checked);
|
||||||
dctJpegCard.classList.toggle('border-2', dctFormatJpeg.checked);
|
dctJpegCard.classList.toggle('border-2', dctFormatJpeg.checked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateDctColorCardHighlight() {
|
||||||
|
if (dctColorCard && dctGrayscaleCard) {
|
||||||
|
dctColorCard.classList.toggle('border-success', dctColorColor.checked);
|
||||||
|
dctColorCard.classList.toggle('border-2', dctColorColor.checked);
|
||||||
|
dctGrayscaleCard.classList.toggle('border-secondary', dctColorGrayscale.checked);
|
||||||
|
dctGrayscaleCard.classList.toggle('border-2', dctColorGrayscale.checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
modeLsb.addEventListener('change', updateModeCardHighlight);
|
modeLsb.addEventListener('change', updateModeCardHighlight);
|
||||||
modeDct.addEventListener('change', updateModeCardHighlight);
|
modeDct.addEventListener('change', updateModeCardHighlight);
|
||||||
dctFormatPng?.addEventListener('change', updateDctFormatCardHighlight);
|
dctFormatPng?.addEventListener('change', updateDctFormatCardHighlight);
|
||||||
dctFormatJpeg?.addEventListener('change', updateDctFormatCardHighlight);
|
dctFormatJpeg?.addEventListener('change', updateDctFormatCardHighlight);
|
||||||
|
dctColorColor?.addEventListener('change', updateDctColorCardHighlight);
|
||||||
|
dctColorGrayscale?.addEventListener('change', updateDctColorCardHighlight);
|
||||||
|
|
||||||
updateModeCardHighlight(); // Initial state
|
updateModeCardHighlight(); // Initial state
|
||||||
updateDctFormatCardHighlight(); // Initial state
|
updateDctFormatCardHighlight(); // Initial state
|
||||||
|
updateDctColorCardHighlight(); // Initial state
|
||||||
|
|
||||||
// Advanced options chevron rotation
|
// Advanced options chevron rotation
|
||||||
document.getElementById('advancedOptions').addEventListener('show.bs.collapse', function() {
|
document.getElementById('advancedOptions').addEventListener('show.bs.collapse', function() {
|
||||||
|
|||||||
@@ -34,31 +34,60 @@
|
|||||||
<code class="fs-5">{{ filename }}</code>
|
<code class="fs-5">{{ filename }}</code>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mode and format badges (v3.0) -->
|
<!-- Mode and format badges (v3.0 / v3.0.1) -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
{% if embed_mode == 'dct' %}
|
{% if embed_mode == 'dct' %}
|
||||||
<span class="badge bg-info fs-6">
|
<span class="badge bg-info fs-6">
|
||||||
<i class="bi bi-soundwave me-1"></i>DCT Mode
|
<i class="bi bi-soundwave me-1"></i>DCT Mode
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<!-- Color mode badge (v3.0.1) -->
|
||||||
|
{% if color_mode == 'color' %}
|
||||||
|
<span class="badge bg-success fs-6 ms-1">
|
||||||
|
<i class="bi bi-palette-fill me-1"></i>Color
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary fs-6 ms-1">
|
||||||
|
<i class="bi bi-circle-half me-1"></i>Grayscale
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Output format badge -->
|
||||||
{% if output_format == 'jpeg' %}
|
{% if output_format == 'jpeg' %}
|
||||||
<span class="badge bg-warning text-dark fs-6 ms-1">
|
<span class="badge bg-warning text-dark fs-6 ms-1">
|
||||||
<i class="bi bi-file-earmark-richtext me-1"></i>JPEG
|
<i class="bi bi-file-earmark-richtext me-1"></i>JPEG
|
||||||
</span>
|
</span>
|
||||||
<div class="small text-muted mt-1">Grayscale JPEG, frequency domain embedding (Q=95)</div>
|
<div class="small text-muted mt-2">
|
||||||
|
{% if color_mode == 'color' %}
|
||||||
|
Color JPEG, frequency domain embedding (Q=95)
|
||||||
|
{% else %}
|
||||||
|
Grayscale JPEG, frequency domain embedding (Q=95)
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-success fs-6 ms-1">
|
<span class="badge bg-primary fs-6 ms-1">
|
||||||
<i class="bi bi-file-earmark-image me-1"></i>PNG
|
<i class="bi bi-file-earmark-image me-1"></i>PNG
|
||||||
</span>
|
</span>
|
||||||
<div class="small text-muted mt-1">Grayscale PNG, frequency domain embedding (lossless)</div>
|
<div class="small text-muted mt-2">
|
||||||
|
{% if color_mode == 'color' %}
|
||||||
|
Color PNG, frequency domain embedding (lossless)
|
||||||
|
{% else %}
|
||||||
|
Grayscale PNG, frequency domain embedding (lossless)
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-primary fs-6">
|
<span class="badge bg-primary fs-6">
|
||||||
<i class="bi bi-grid-3x3-gap me-1"></i>LSB Mode
|
<i class="bi bi-grid-3x3-gap me-1"></i>LSB Mode
|
||||||
</span>
|
</span>
|
||||||
<span class="badge bg-success fs-6 ms-1">
|
<span class="badge bg-success fs-6 ms-1">
|
||||||
|
<i class="bi bi-palette-fill me-1"></i>Full Color
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-primary fs-6 ms-1">
|
||||||
<i class="bi bi-file-earmark-image me-1"></i>PNG
|
<i class="bi bi-file-earmark-image me-1"></i>PNG
|
||||||
</span>
|
</span>
|
||||||
<div class="small text-muted mt-1">Full color PNG, spatial LSB embedding</div>
|
<div class="small text-muted mt-2">Full color PNG, spatial LSB embedding</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -88,6 +117,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if embed_mode == 'dct' %}
|
{% if embed_mode == 'dct' %}
|
||||||
<li>Recipient needs <strong>DCT mode</strong> or <strong>Auto</strong> detection to decode</li>
|
<li>Recipient needs <strong>DCT mode</strong> or <strong>Auto</strong> detection to decode</li>
|
||||||
|
{% if color_mode == 'color' %}
|
||||||
|
<li><span class="badge bg-success">v3.0.1</span> Color preserved - extraction works on both color and grayscale</li>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "stegasoo"
|
name = "stegasoo"
|
||||||
version = "2.2.1"
|
version = "3.0.2"
|
||||||
description = "Secure steganography with hybrid photo + passphrase + PIN authentication"
|
description = "Secure steganography with hybrid photo + passphrase + PIN authentication"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -43,6 +43,11 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
# DCT steganography support (v3.0+)
|
||||||
|
dct = [
|
||||||
|
"scipy>=1.10.0",
|
||||||
|
"jpegio>=0.2.0",
|
||||||
|
]
|
||||||
cli = [
|
cli = [
|
||||||
"click>=8.0.0",
|
"click>=8.0.0",
|
||||||
"qrcode>=7.30"
|
"qrcode>=7.30"
|
||||||
@@ -55,6 +60,9 @@ web = [
|
|||||||
"gunicorn>=21.0.0",
|
"gunicorn>=21.0.0",
|
||||||
"qrcode>=7.3.0",
|
"qrcode>=7.3.0",
|
||||||
"pyzbar>=0.1.9",
|
"pyzbar>=0.1.9",
|
||||||
|
# Include DCT support for web UI
|
||||||
|
"scipy>=1.10.0",
|
||||||
|
"jpegio>=0.2.0",
|
||||||
]
|
]
|
||||||
api = [
|
api = [
|
||||||
"fastapi>=0.100.0",
|
"fastapi>=0.100.0",
|
||||||
@@ -62,9 +70,12 @@ api = [
|
|||||||
"python-multipart>=0.0.6",
|
"python-multipart>=0.0.6",
|
||||||
"qrcode>=7.30",
|
"qrcode>=7.30",
|
||||||
"pyzbar>=0.1.9",
|
"pyzbar>=0.1.9",
|
||||||
|
# Include DCT support for API
|
||||||
|
"scipy>=1.10.0",
|
||||||
|
"jpegio>=0.2.0",
|
||||||
]
|
]
|
||||||
all = [
|
all = [
|
||||||
"stegasoo[cli,web,api]",
|
"stegasoo[cli,web,api,dct,compression]",
|
||||||
]
|
]
|
||||||
dev = [
|
dev = [
|
||||||
"stegasoo[all]",
|
"stegasoo[all]",
|
||||||
|
|||||||
144
src/stegasoo/Dockerfile
Normal file
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]
|
output_format = None, # Optional[str]
|
||||||
embed_mode: str = EMBED_MODE_LSB,
|
embed_mode: str = EMBED_MODE_LSB,
|
||||||
dct_output_format: str = "png", # NEW in v3.0.1: 'png' or 'jpeg'
|
dct_output_format: str = "png", # NEW in v3.0.1: 'png' or 'jpeg'
|
||||||
|
dct_color_mode: str = "grayscale", # NEW in v3.0.1: 'grayscale' or 'color'
|
||||||
) -> EncodeResult:
|
) -> EncodeResult:
|
||||||
"""
|
"""
|
||||||
Encode a secret message or file into an image.
|
Encode a secret message or file into an image.
|
||||||
@@ -334,6 +335,7 @@ def encode(
|
|||||||
output_format: Force output format ('PNG', 'BMP') - LSB mode only
|
output_format: Force output format ('PNG', 'BMP') - LSB mode only
|
||||||
embed_mode: Embedding mode - 'lsb' (default) or 'dct' (v3.0+)
|
embed_mode: Embedding mode - 'lsb' (default) or 'dct' (v3.0+)
|
||||||
dct_output_format: For DCT mode - 'png' (lossless) or 'jpeg' (smaller)
|
dct_output_format: For DCT mode - 'png' (lossless) or 'jpeg' (smaller)
|
||||||
|
dct_color_mode: For DCT mode - 'grayscale' (default) or 'color' (preserves colors)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
EncodeResult with stego image and metadata
|
EncodeResult with stego image and metadata
|
||||||
@@ -349,16 +351,18 @@ def encode(
|
|||||||
# Default LSB mode
|
# Default LSB mode
|
||||||
>>> result = encode(message="Secret", ...)
|
>>> result = encode(message="Secret", ...)
|
||||||
|
|
||||||
# DCT mode with PNG output (lossless)
|
# DCT mode with grayscale PNG output (default)
|
||||||
>>> result = encode(message="Secret", ..., embed_mode='dct')
|
>>> result = encode(message="Secret", ..., embed_mode='dct')
|
||||||
|
|
||||||
# DCT mode with JPEG output (smaller, natural)
|
# DCT mode with color JPEG output
|
||||||
>>> result = encode(message="Secret", ..., embed_mode='dct', dct_output_format='jpeg')
|
>>> result = encode(message="Secret", ..., embed_mode='dct',
|
||||||
|
... dct_output_format='jpeg', dct_color_mode='color')
|
||||||
"""
|
"""
|
||||||
# Debug logging
|
# Debug logging
|
||||||
debug.print(f"encode called: message type={type(message).__name__}, "
|
debug.print(f"encode called: message type={type(message).__name__}, "
|
||||||
f"day_phrase='{day_phrase[:20]}...', pin_length={len(pin)}, "
|
f"day_phrase='{day_phrase[:20]}...', pin_length={len(pin)}, "
|
||||||
f"embed_mode={embed_mode}, dct_output_format={dct_output_format}")
|
f"embed_mode={embed_mode}, dct_output_format={dct_output_format}, "
|
||||||
|
f"dct_color_mode={dct_color_mode}")
|
||||||
|
|
||||||
# Validate embed_mode
|
# Validate embed_mode
|
||||||
if embed_mode not in (EMBED_MODE_LSB, EMBED_MODE_DCT):
|
if embed_mode not in (EMBED_MODE_LSB, EMBED_MODE_DCT):
|
||||||
@@ -375,6 +379,11 @@ def encode(
|
|||||||
debug.print(f"Invalid dct_output_format '{dct_output_format}', defaulting to 'png'")
|
debug.print(f"Invalid dct_output_format '{dct_output_format}', defaulting to 'png'")
|
||||||
dct_output_format = 'png'
|
dct_output_format = 'png'
|
||||||
|
|
||||||
|
# Validate dct_color_mode (v3.0.1)
|
||||||
|
if dct_color_mode not in ('grayscale', 'color'):
|
||||||
|
debug.print(f"Invalid dct_color_mode '{dct_color_mode}', defaulting to 'grayscale'")
|
||||||
|
dct_color_mode = 'grayscale'
|
||||||
|
|
||||||
# Validate inputs
|
# Validate inputs
|
||||||
require_valid_payload(message)
|
require_valid_payload(message)
|
||||||
require_valid_image(carrier_image, "Carrier image")
|
require_valid_image(carrier_image, "Carrier image")
|
||||||
@@ -407,7 +416,7 @@ def encode(
|
|||||||
debug.data(pixel_key, "Pixel key")
|
debug.data(pixel_key, "Pixel key")
|
||||||
|
|
||||||
# Embed in image (returns extension too)
|
# Embed in image (returns extension too)
|
||||||
# CRITICAL: Pass dct_output_format to embed_in_image
|
# CRITICAL: Pass dct_output_format and dct_color_mode to embed_in_image
|
||||||
stego_data, stats, extension = embed_in_image(
|
stego_data, stats, extension = embed_in_image(
|
||||||
encrypted,
|
encrypted,
|
||||||
carrier_image,
|
carrier_image,
|
||||||
@@ -415,6 +424,7 @@ def encode(
|
|||||||
output_format=output_format,
|
output_format=output_format,
|
||||||
embed_mode=embed_mode,
|
embed_mode=embed_mode,
|
||||||
dct_output_format=dct_output_format, # NEW in v3.0.1
|
dct_output_format=dct_output_format, # NEW in v3.0.1
|
||||||
|
dct_color_mode=dct_color_mode, # NEW in v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
# Generate filename with correct extension
|
# Generate filename with correct extension
|
||||||
@@ -468,6 +478,7 @@ def encode_file(
|
|||||||
filename_override: Optional[str] = None,
|
filename_override: Optional[str] = None,
|
||||||
embed_mode: str = EMBED_MODE_LSB,
|
embed_mode: str = EMBED_MODE_LSB,
|
||||||
dct_output_format: str = "png", # NEW in v3.0.1
|
dct_output_format: str = "png", # NEW in v3.0.1
|
||||||
|
dct_color_mode: str = "grayscale", # NEW in v3.0.1
|
||||||
) -> EncodeResult:
|
) -> EncodeResult:
|
||||||
"""
|
"""
|
||||||
Encode a file into an image.
|
Encode a file into an image.
|
||||||
@@ -487,12 +498,13 @@ def encode_file(
|
|||||||
filename_override: Override the stored filename
|
filename_override: Override the stored filename
|
||||||
embed_mode: 'lsb' (default) or 'dct' (v3.0+)
|
embed_mode: 'lsb' (default) or 'dct' (v3.0+)
|
||||||
dct_output_format: For DCT mode - 'png' or 'jpeg' (v3.0.1+)
|
dct_output_format: For DCT mode - 'png' or 'jpeg' (v3.0.1+)
|
||||||
|
dct_color_mode: For DCT mode - 'grayscale' or 'color' (v3.0.1+)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
EncodeResult with stego image and metadata
|
EncodeResult with stego image and metadata
|
||||||
"""
|
"""
|
||||||
debug.print(f"encode_file called: filepath={filepath}, embed_mode={embed_mode}, "
|
debug.print(f"encode_file called: filepath={filepath}, embed_mode={embed_mode}, "
|
||||||
f"dct_output_format={dct_output_format}")
|
f"dct_output_format={dct_output_format}, dct_color_mode={dct_color_mode}")
|
||||||
payload = FilePayload.from_file(str(filepath), filename_override)
|
payload = FilePayload.from_file(str(filepath), filename_override)
|
||||||
|
|
||||||
return encode(
|
return encode(
|
||||||
@@ -507,6 +519,7 @@ def encode_file(
|
|||||||
output_format=output_format,
|
output_format=output_format,
|
||||||
embed_mode=embed_mode,
|
embed_mode=embed_mode,
|
||||||
dct_output_format=dct_output_format, # NEW in v3.0.1
|
dct_output_format=dct_output_format, # NEW in v3.0.1
|
||||||
|
dct_color_mode=dct_color_mode, # NEW in v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -528,6 +541,7 @@ def encode_bytes(
|
|||||||
mime_type: Optional[str] = None,
|
mime_type: Optional[str] = None,
|
||||||
embed_mode: str = EMBED_MODE_LSB,
|
embed_mode: str = EMBED_MODE_LSB,
|
||||||
dct_output_format: str = "png", # NEW in v3.0.1
|
dct_output_format: str = "png", # NEW in v3.0.1
|
||||||
|
dct_color_mode: str = "grayscale", # NEW in v3.0.1
|
||||||
) -> EncodeResult:
|
) -> EncodeResult:
|
||||||
"""
|
"""
|
||||||
Encode raw bytes with a filename into an image.
|
Encode raw bytes with a filename into an image.
|
||||||
@@ -548,12 +562,14 @@ def encode_bytes(
|
|||||||
mime_type: MIME type of the data
|
mime_type: MIME type of the data
|
||||||
embed_mode: 'lsb' (default) or 'dct' (v3.0+)
|
embed_mode: 'lsb' (default) or 'dct' (v3.0+)
|
||||||
dct_output_format: For DCT mode - 'png' or 'jpeg' (v3.0.1+)
|
dct_output_format: For DCT mode - 'png' or 'jpeg' (v3.0.1+)
|
||||||
|
dct_color_mode: For DCT mode - 'grayscale' or 'color' (v3.0.1+)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
EncodeResult with stego image and metadata
|
EncodeResult with stego image and metadata
|
||||||
"""
|
"""
|
||||||
debug.print(f"encode_bytes called: filename={filename}, data_size={len(data)}, "
|
debug.print(f"encode_bytes called: filename={filename}, data_size={len(data)}, "
|
||||||
f"embed_mode={embed_mode}, dct_output_format={dct_output_format}")
|
f"embed_mode={embed_mode}, dct_output_format={dct_output_format}, "
|
||||||
|
f"dct_color_mode={dct_color_mode}")
|
||||||
payload = FilePayload(data=data, filename=filename, mime_type=mime_type)
|
payload = FilePayload(data=data, filename=filename, mime_type=mime_type)
|
||||||
|
|
||||||
return encode(
|
return encode(
|
||||||
@@ -568,6 +584,7 @@ def encode_bytes(
|
|||||||
output_format=output_format,
|
output_format=output_format,
|
||||||
embed_mode=embed_mode,
|
embed_mode=embed_mode,
|
||||||
dct_output_format=dct_output_format, # NEW in v3.0.1
|
dct_output_format=dct_output_format, # NEW in v3.0.1
|
||||||
|
dct_color_mode=dct_color_mode, # NEW in v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,32 @@
|
|||||||
"""
|
"""
|
||||||
DCT Domain Steganography Module (v3.0.1)
|
DCT Domain Steganography Module (v3.0.2)
|
||||||
|
|
||||||
Embeds data in DCT coefficients of grayscale images.
|
Embeds data in DCT coefficients with two approaches:
|
||||||
Supports PNG (lossless) or JPEG (natural, smaller) output.
|
1. PNG output: Scipy-based DCT transform (grayscale or color)
|
||||||
|
2. JPEG output: jpegio-based coefficient manipulation (if available)
|
||||||
|
|
||||||
This provides an alternative to LSB embedding with different trade-offs:
|
The JPEG approach is the "correct" way to do JPEG steganography because
|
||||||
- More resistant to visual inspection
|
it directly modifies the already-quantized coefficients without re-encoding.
|
||||||
- Survives some image processing
|
|
||||||
- Lower capacity (~20% of LSB)
|
|
||||||
- Works in frequency domain
|
|
||||||
|
|
||||||
Requires: scipy (for DCT transforms)
|
New in v3.0.2:
|
||||||
|
- jpegio integration for proper JPEG coefficient embedding
|
||||||
|
- Falls back to warning if jpegio not available for JPEG output
|
||||||
|
- Maintains backward compatibility with v3.0.1
|
||||||
|
|
||||||
|
Requires: scipy (for PNG mode), optionally jpegio (for JPEG mode)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import struct
|
import struct
|
||||||
import hashlib
|
import hashlib
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional, Literal
|
from typing import Optional, Literal, Tuple
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
# Check for scipy availability
|
# Check for scipy availability (for PNG/DCT mode)
|
||||||
try:
|
try:
|
||||||
from scipy.fftpack import dct, idct
|
from scipy.fftpack import dct, idct
|
||||||
HAS_SCIPY = True
|
HAS_SCIPY = True
|
||||||
@@ -32,6 +35,14 @@ except ImportError:
|
|||||||
dct = None
|
dct = None
|
||||||
idct = None
|
idct = None
|
||||||
|
|
||||||
|
# Check for jpegio availability (for proper JPEG mode)
|
||||||
|
try:
|
||||||
|
import jpegio as jio
|
||||||
|
HAS_JPEGIO = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_JPEGIO = False
|
||||||
|
jio = None
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# CONSTANTS
|
# CONSTANTS
|
||||||
@@ -41,8 +52,6 @@ except ImportError:
|
|||||||
BLOCK_SIZE = 8
|
BLOCK_SIZE = 8
|
||||||
|
|
||||||
# Coefficients to use for embedding (mid-frequency, zig-zag order positions)
|
# Coefficients to use for embedding (mid-frequency, zig-zag order positions)
|
||||||
# Avoiding DC (0,0) and high-frequency edges
|
|
||||||
# These positions are relatively stable across JPEG compression
|
|
||||||
EMBED_POSITIONS = [
|
EMBED_POSITIONS = [
|
||||||
(0, 1), (1, 0), (2, 0), (1, 1), (0, 2), (0, 3), (1, 2), (2, 1), (3, 0),
|
(0, 1), (1, 0), (2, 0), (1, 1), (0, 2), (0, 3), (1, 2), (2, 1), (3, 0),
|
||||||
(4, 0), (3, 1), (2, 2), (1, 3), (0, 4), (0, 5), (1, 4), (2, 3), (3, 2),
|
(4, 0), (3, 1), (2, 2), (1, 3), (0, 4), (0, 5), (1, 4), (2, 3), (3, 2),
|
||||||
@@ -51,25 +60,29 @@ EMBED_POSITIONS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Use subset of mid-frequency coefficients for better robustness
|
# Use subset of mid-frequency coefficients for better robustness
|
||||||
# Positions 4-20 in zig-zag order (skip very low and very high frequencies)
|
|
||||||
DEFAULT_EMBED_POSITIONS = EMBED_POSITIONS[4:20] # 16 coefficients per block
|
DEFAULT_EMBED_POSITIONS = EMBED_POSITIONS[4:20] # 16 coefficients per block
|
||||||
|
|
||||||
# Quantization step for embedding (larger = more robust, more visible)
|
# Quantization step for QIM embedding (larger = more robust, more visible)
|
||||||
QUANT_STEP = 25
|
QUANT_STEP = 25
|
||||||
|
|
||||||
# Magic bytes for DCT stego identification
|
# Magic bytes for DCT stego identification
|
||||||
DCT_MAGIC = b'DCTS'
|
DCT_MAGIC = b'DCTS'
|
||||||
|
|
||||||
# Header: magic(4) + version(1) + flags(1) + length(4) = 10 bytes
|
# Header size: magic(4) + version(1) + flags(1) + length(4) = 10 bytes
|
||||||
HEADER_SIZE = 10
|
HEADER_SIZE = 10
|
||||||
|
|
||||||
# Output format options
|
# Output format options
|
||||||
OUTPUT_FORMAT_PNG = 'png'
|
OUTPUT_FORMAT_PNG = 'png'
|
||||||
OUTPUT_FORMAT_JPEG = 'jpeg'
|
OUTPUT_FORMAT_JPEG = 'jpeg'
|
||||||
|
|
||||||
# JPEG quality for output (high to preserve coefficients)
|
# JPEG output quality (only for fallback mode, not jpegio)
|
||||||
JPEG_OUTPUT_QUALITY = 95
|
JPEG_OUTPUT_QUALITY = 95
|
||||||
|
|
||||||
|
# jpegio constants for JPEG coefficient embedding
|
||||||
|
JPEGIO_MAGIC = b'JPGS'
|
||||||
|
JPEGIO_MIN_COEF_MAGNITUDE = 2
|
||||||
|
JPEGIO_EMBED_CHANNEL = 0 # Y channel
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# DATA CLASSES
|
# DATA CLASSES
|
||||||
@@ -91,7 +104,9 @@ class DCTEmbedStats:
|
|||||||
usage_percent: float
|
usage_percent: float
|
||||||
image_width: int
|
image_width: int
|
||||||
image_height: int
|
image_height: int
|
||||||
output_format: str # 'png' or 'jpeg'
|
output_format: str
|
||||||
|
jpeg_native: bool = False # True if used jpegio for proper JPEG embedding
|
||||||
|
color_mode: str = 'grayscale' # 'color' or 'grayscale' (v3.0.1+)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -105,11 +120,11 @@ class DCTCapacityInfo:
|
|||||||
bits_per_block: int
|
bits_per_block: int
|
||||||
total_capacity_bits: int
|
total_capacity_bits: int
|
||||||
total_capacity_bytes: int
|
total_capacity_bytes: int
|
||||||
usable_capacity_bytes: int # After header overhead
|
usable_capacity_bytes: int
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# HELPER FUNCTIONS
|
# AVAILABILITY CHECKS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
def _check_scipy():
|
def _check_scipy():
|
||||||
@@ -121,6 +136,20 @@ def _check_scipy():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def has_dct_support() -> bool:
|
||||||
|
"""Check if DCT steganography is available (scipy installed)."""
|
||||||
|
return HAS_SCIPY
|
||||||
|
|
||||||
|
|
||||||
|
def has_jpegio_support() -> bool:
|
||||||
|
"""Check if jpegio is available for proper JPEG coefficient embedding."""
|
||||||
|
return HAS_JPEGIO
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# SCIPY DCT HELPERS (for PNG output)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
def _dct2(block: np.ndarray) -> np.ndarray:
|
def _dct2(block: np.ndarray) -> np.ndarray:
|
||||||
"""Apply 2D DCT to a block."""
|
"""Apply 2D DCT to a block."""
|
||||||
return dct(dct(block.T, norm='ortho').T, norm='ortho')
|
return dct(dct(block.T, norm='ortho').T, norm='ortho')
|
||||||
@@ -138,7 +167,7 @@ def _to_grayscale(image_data: bytes) -> np.ndarray:
|
|||||||
return np.array(gray, dtype=np.float64)
|
return np.array(gray, dtype=np.float64)
|
||||||
|
|
||||||
|
|
||||||
def _pad_to_blocks(image: np.ndarray) -> tuple[np.ndarray, tuple[int, int]]:
|
def _pad_to_blocks(image: np.ndarray) -> Tuple[np.ndarray, Tuple[int, int]]:
|
||||||
"""Pad image dimensions to be divisible by block size."""
|
"""Pad image dimensions to be divisible by block size."""
|
||||||
h, w = image.shape
|
h, w = image.shape
|
||||||
new_h = ((h + BLOCK_SIZE - 1) // BLOCK_SIZE) * BLOCK_SIZE
|
new_h = ((h + BLOCK_SIZE - 1) // BLOCK_SIZE) * BLOCK_SIZE
|
||||||
@@ -150,7 +179,6 @@ def _pad_to_blocks(image: np.ndarray) -> tuple[np.ndarray, tuple[int, int]]:
|
|||||||
padded = np.zeros((new_h, new_w), dtype=image.dtype)
|
padded = np.zeros((new_h, new_w), dtype=image.dtype)
|
||||||
padded[:h, :w] = image
|
padded[:h, :w] = image
|
||||||
|
|
||||||
# Mirror padding for smoother edges
|
|
||||||
if new_h > h:
|
if new_h > h:
|
||||||
padded[h:, :w] = image[h-(new_h-h):h, :w][::-1, :]
|
padded[h:, :w] = image[h-(new_h-h):h, :w][::-1, :]
|
||||||
if new_w > w:
|
if new_w > w:
|
||||||
@@ -161,82 +189,125 @@ def _pad_to_blocks(image: np.ndarray) -> tuple[np.ndarray, tuple[int, int]]:
|
|||||||
return padded, (h, w)
|
return padded, (h, w)
|
||||||
|
|
||||||
|
|
||||||
def _unpad_image(image: np.ndarray, original_size: tuple[int, int]) -> np.ndarray:
|
def _unpad_image(image: np.ndarray, original_size: Tuple[int, int]) -> np.ndarray:
|
||||||
"""Remove padding from image."""
|
"""Remove padding from image."""
|
||||||
h, w = original_size
|
h, w = original_size
|
||||||
return image[:h, :w]
|
return image[:h, :w]
|
||||||
|
|
||||||
|
|
||||||
def _embed_bit_in_coeff(coeff: float, bit: int, quant_step: int = QUANT_STEP) -> float:
|
def _embed_bit_in_coeff(coef: float, bit: int, quant_step: int = QUANT_STEP) -> float:
|
||||||
"""Embed a single bit into a DCT coefficient using QIM."""
|
"""Embed a single bit into a DCT coefficient using QIM."""
|
||||||
# Quantization Index Modulation
|
quantized = round(coef / quant_step)
|
||||||
quantized = round(coeff / quant_step)
|
|
||||||
if (quantized % 2) != bit:
|
if (quantized % 2) != bit:
|
||||||
# Adjust to embed the bit
|
|
||||||
if quantized % 2 == 0 and bit == 1:
|
if quantized % 2 == 0 and bit == 1:
|
||||||
quantized += 1 if coeff >= quantized * quant_step else -1
|
quantized += 1 if coef >= quantized * quant_step else -1
|
||||||
elif quantized % 2 == 1 and bit == 0:
|
elif quantized % 2 == 1 and bit == 0:
|
||||||
quantized += 1 if coeff >= quantized * quant_step else -1
|
quantized += 1 if coef >= quantized * quant_step else -1
|
||||||
return quantized * quant_step
|
return quantized * quant_step
|
||||||
|
|
||||||
|
|
||||||
def _extract_bit_from_coeff(coeff: float, quant_step: int = QUANT_STEP) -> int:
|
def _extract_bit_from_coeff(coef: float, quant_step: int = QUANT_STEP) -> int:
|
||||||
"""Extract a single bit from a DCT coefficient."""
|
"""Extract a single bit from a DCT coefficient."""
|
||||||
quantized = round(coeff / quant_step)
|
quantized = round(coef / quant_step)
|
||||||
return quantized % 2
|
return quantized % 2
|
||||||
|
|
||||||
|
|
||||||
def _generate_block_order(num_blocks: int, seed: bytes) -> list[int]:
|
def _generate_block_order(num_blocks: int, seed: bytes) -> list:
|
||||||
"""Generate pseudo-random block order from seed."""
|
"""Generate pseudo-random block order from seed."""
|
||||||
# Create deterministic RNG from seed
|
|
||||||
hash_bytes = hashlib.sha256(seed).digest()
|
hash_bytes = hashlib.sha256(seed).digest()
|
||||||
rng = np.random.RandomState(int.from_bytes(hash_bytes[:4], 'big'))
|
rng = np.random.RandomState(int.from_bytes(hash_bytes[:4], 'big'))
|
||||||
|
|
||||||
order = list(range(num_blocks))
|
order = list(range(num_blocks))
|
||||||
rng.shuffle(order)
|
rng.shuffle(order)
|
||||||
return order
|
return order
|
||||||
|
|
||||||
|
|
||||||
def _save_stego_image(
|
def _save_stego_image(image: np.ndarray, output_format: str = OUTPUT_FORMAT_PNG) -> bytes:
|
||||||
image: np.ndarray,
|
"""Save stego image in specified format (grayscale)."""
|
||||||
output_format: str = OUTPUT_FORMAT_PNG
|
|
||||||
) -> bytes:
|
|
||||||
"""Save stego image in specified format."""
|
|
||||||
# Clip to valid range and convert to uint8
|
|
||||||
clipped = np.clip(image, 0, 255).astype(np.uint8)
|
clipped = np.clip(image, 0, 255).astype(np.uint8)
|
||||||
img = Image.fromarray(clipped, mode='L')
|
img = Image.fromarray(clipped, mode='L')
|
||||||
|
|
||||||
buffer = io.BytesIO()
|
buffer = io.BytesIO()
|
||||||
|
|
||||||
if output_format == OUTPUT_FORMAT_JPEG:
|
if output_format == OUTPUT_FORMAT_JPEG:
|
||||||
# High-quality JPEG with no chroma subsampling
|
img.save(buffer, format='JPEG', quality=JPEG_OUTPUT_QUALITY,
|
||||||
img.save(
|
subsampling=0, optimize=True)
|
||||||
buffer,
|
|
||||||
format='JPEG',
|
|
||||||
quality=JPEG_OUTPUT_QUALITY,
|
|
||||||
subsampling=0, # 4:4:4 - no subsampling
|
|
||||||
optimize=True
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# PNG (lossless, default)
|
|
||||||
img.save(buffer, format='PNG', optimize=True)
|
img.save(buffer, format='PNG', optimize=True)
|
||||||
|
|
||||||
return buffer.getvalue()
|
return buffer.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def _save_color_image(rgb_array: np.ndarray, output_format: str = OUTPUT_FORMAT_PNG) -> bytes:
|
||||||
|
"""Save color RGB image in specified format."""
|
||||||
|
clipped = np.clip(rgb_array, 0, 255).astype(np.uint8)
|
||||||
|
img = Image.fromarray(clipped, mode='RGB')
|
||||||
|
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
|
||||||
|
if output_format == OUTPUT_FORMAT_JPEG:
|
||||||
|
img.save(buffer, format='JPEG', quality=JPEG_OUTPUT_QUALITY,
|
||||||
|
subsampling=0, optimize=True)
|
||||||
|
else:
|
||||||
|
img.save(buffer, format='PNG', optimize=True)
|
||||||
|
|
||||||
|
return buffer.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def _rgb_to_ycbcr(rgb: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||||
|
"""
|
||||||
|
Convert RGB array to YCbCr components.
|
||||||
|
|
||||||
|
Uses ITU-R BT.601 conversion (standard for JPEG).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
rgb: RGB image array (H, W, 3), float64
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (Y, Cb, Cr) arrays
|
||||||
|
"""
|
||||||
|
R = rgb[:, :, 0]
|
||||||
|
G = rgb[:, :, 1]
|
||||||
|
B = rgb[:, :, 2]
|
||||||
|
|
||||||
|
# ITU-R BT.601 conversion
|
||||||
|
Y = 0.299 * R + 0.587 * G + 0.114 * B
|
||||||
|
Cb = 128 - 0.168736 * R - 0.331264 * G + 0.5 * B
|
||||||
|
Cr = 128 + 0.5 * R - 0.418688 * G - 0.081312 * B
|
||||||
|
|
||||||
|
return Y, Cb, Cr
|
||||||
|
|
||||||
|
|
||||||
|
def _ycbcr_to_rgb(Y: np.ndarray, Cb: np.ndarray, Cr: np.ndarray) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Convert YCbCr components back to RGB array.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
Y: Luminance channel
|
||||||
|
Cb: Blue-difference chroma
|
||||||
|
Cr: Red-difference chroma
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
RGB array (H, W, 3)
|
||||||
|
"""
|
||||||
|
R = Y + 1.402 * (Cr - 128)
|
||||||
|
G = Y - 0.344136 * (Cb - 128) - 0.714136 * (Cr - 128)
|
||||||
|
B = Y + 1.772 * (Cb - 128)
|
||||||
|
|
||||||
|
rgb = np.stack([R, G, B], axis=-1)
|
||||||
|
return rgb
|
||||||
|
|
||||||
|
|
||||||
def _create_header(data_length: int, flags: int = 0) -> bytes:
|
def _create_header(data_length: int, flags: int = 0) -> bytes:
|
||||||
"""Create DCT stego header."""
|
"""Create DCT stego header."""
|
||||||
# Header format: MAGIC(4) + VERSION(1) + FLAGS(1) + LENGTH(4)
|
|
||||||
version = 1
|
version = 1
|
||||||
return struct.pack('>4sBBI', DCT_MAGIC, version, flags, data_length)
|
return struct.pack('>4sBBI', DCT_MAGIC, version, flags, data_length)
|
||||||
|
|
||||||
|
|
||||||
def _parse_header(header_bits: list[int]) -> tuple[int, int, int]:
|
def _parse_header(header_bits: list) -> Tuple[int, int, int]:
|
||||||
"""Parse header from extracted bits. Returns (version, flags, data_length)."""
|
"""Parse header from extracted bits. Returns (version, flags, data_length)."""
|
||||||
if len(header_bits) < HEADER_SIZE * 8:
|
if len(header_bits) < HEADER_SIZE * 8:
|
||||||
raise ValueError("Insufficient header data")
|
raise ValueError("Insufficient header data")
|
||||||
|
|
||||||
# Convert bits to bytes
|
|
||||||
header_bytes = bytes([
|
header_bytes = bytes([
|
||||||
sum(header_bits[i*8:(i+1)*8][j] << (7-j) for j in range(8))
|
sum(header_bits[i*8:(i+1)*8][j] << (7-j) for j in range(8))
|
||||||
for i in range(HEADER_SIZE)
|
for i in range(HEADER_SIZE)
|
||||||
@@ -245,7 +316,80 @@ def _parse_header(header_bits: list[int]) -> tuple[int, int, int]:
|
|||||||
magic, version, flags, length = struct.unpack('>4sBBI', header_bytes)
|
magic, version, flags, length = struct.unpack('>4sBBI', header_bytes)
|
||||||
|
|
||||||
if magic != DCT_MAGIC:
|
if magic != DCT_MAGIC:
|
||||||
raise ValueError("Invalid DCT stego magic bytes - not a DCT stego image")
|
raise ValueError("Invalid DCT stego magic bytes")
|
||||||
|
|
||||||
|
return version, flags, length
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# JPEGIO HELPERS (for proper JPEG output)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def _jpegio_bytes_to_file(data: bytes, suffix: str = '.jpg') -> str:
|
||||||
|
"""Write bytes to temp file for jpegio."""
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
fd, path = tempfile.mkstemp(suffix=suffix)
|
||||||
|
try:
|
||||||
|
os.write(fd, data)
|
||||||
|
finally:
|
||||||
|
os.close(fd)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def _jpegio_file_to_bytes(path: str) -> bytes:
|
||||||
|
"""Read file to bytes and delete it."""
|
||||||
|
import os
|
||||||
|
try:
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
return f.read()
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
os.unlink(path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _jpegio_get_usable_positions(coef_array: np.ndarray) -> list:
|
||||||
|
"""Get usable coefficient positions for jpegio embedding."""
|
||||||
|
positions = []
|
||||||
|
h, w = coef_array.shape
|
||||||
|
|
||||||
|
for row in range(h):
|
||||||
|
for col in range(w):
|
||||||
|
# Skip DC coefficients
|
||||||
|
if (row % BLOCK_SIZE == 0) and (col % BLOCK_SIZE == 0):
|
||||||
|
continue
|
||||||
|
# Check magnitude
|
||||||
|
if abs(coef_array[row, col]) >= JPEGIO_MIN_COEF_MAGNITUDE:
|
||||||
|
positions.append((row, col))
|
||||||
|
|
||||||
|
return positions
|
||||||
|
|
||||||
|
|
||||||
|
def _jpegio_generate_order(num_positions: int, seed: bytes) -> list:
|
||||||
|
"""Generate pseudo-random order for jpegio embedding."""
|
||||||
|
hash_bytes = hashlib.sha256(seed + b"jpeg_coef_order").digest()
|
||||||
|
rng = np.random.RandomState(int.from_bytes(hash_bytes[:4], 'big'))
|
||||||
|
order = list(range(num_positions))
|
||||||
|
rng.shuffle(order)
|
||||||
|
return order
|
||||||
|
|
||||||
|
|
||||||
|
def _jpegio_create_header(data_length: int) -> bytes:
|
||||||
|
"""Create header for jpegio embedding."""
|
||||||
|
return struct.pack('>4sBBI', JPEGIO_MAGIC, 1, 0, data_length)
|
||||||
|
|
||||||
|
|
||||||
|
def _jpegio_parse_header(header_bytes: bytes) -> Tuple[int, int, int]:
|
||||||
|
"""Parse jpegio header."""
|
||||||
|
if len(header_bytes) < HEADER_SIZE:
|
||||||
|
raise ValueError("Insufficient header data")
|
||||||
|
|
||||||
|
magic, version, flags, length = struct.unpack('>4sBBI', header_bytes[:HEADER_SIZE])
|
||||||
|
|
||||||
|
if magic != JPEGIO_MAGIC:
|
||||||
|
raise ValueError(f"Invalid JPEG stego magic: {magic}")
|
||||||
|
|
||||||
return version, flags, length
|
return version, flags, length
|
||||||
|
|
||||||
@@ -254,11 +398,6 @@ def _parse_header(header_bits: list[int]) -> tuple[int, int, int]:
|
|||||||
# PUBLIC API
|
# PUBLIC API
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
def has_dct_support() -> bool:
|
|
||||||
"""Check if DCT steganography is available."""
|
|
||||||
return HAS_SCIPY
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_dct_capacity(image_data: bytes) -> DCTCapacityInfo:
|
def calculate_dct_capacity(image_data: bytes) -> DCTCapacityInfo:
|
||||||
"""
|
"""
|
||||||
Calculate the DCT embedding capacity of an image.
|
Calculate the DCT embedding capacity of an image.
|
||||||
@@ -274,19 +413,13 @@ def calculate_dct_capacity(image_data: bytes) -> DCTCapacityInfo:
|
|||||||
img = Image.open(io.BytesIO(image_data))
|
img = Image.open(io.BytesIO(image_data))
|
||||||
width, height = img.size
|
width, height = img.size
|
||||||
|
|
||||||
# Calculate blocks
|
|
||||||
blocks_x = width // BLOCK_SIZE
|
blocks_x = width // BLOCK_SIZE
|
||||||
blocks_y = height // BLOCK_SIZE
|
blocks_y = height // BLOCK_SIZE
|
||||||
total_blocks = blocks_x * blocks_y
|
total_blocks = blocks_x * blocks_y
|
||||||
|
|
||||||
# Bits per block (using selected coefficient positions)
|
|
||||||
bits_per_block = len(DEFAULT_EMBED_POSITIONS)
|
bits_per_block = len(DEFAULT_EMBED_POSITIONS)
|
||||||
|
|
||||||
# Total capacity
|
|
||||||
total_bits = total_blocks * bits_per_block
|
total_bits = total_blocks * bits_per_block
|
||||||
total_bytes = total_bits // 8
|
total_bytes = total_bits // 8
|
||||||
|
|
||||||
# Usable capacity (minus header)
|
|
||||||
usable_bytes = max(0, total_bytes - HEADER_SIZE)
|
usable_bytes = max(0, total_bytes - HEADER_SIZE)
|
||||||
|
|
||||||
return DCTCapacityInfo(
|
return DCTCapacityInfo(
|
||||||
@@ -303,43 +436,23 @@ def calculate_dct_capacity(image_data: bytes) -> DCTCapacityInfo:
|
|||||||
|
|
||||||
|
|
||||||
def will_fit_dct(data_length: int, image_data: bytes) -> bool:
|
def will_fit_dct(data_length: int, image_data: bytes) -> bool:
|
||||||
"""
|
"""Check if data will fit in the image using DCT embedding."""
|
||||||
Check if data will fit in the image using DCT embedding.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data_length: Length of data in bytes
|
|
||||||
image_data: Carrier image bytes
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if data fits, False otherwise
|
|
||||||
"""
|
|
||||||
capacity = calculate_dct_capacity(image_data)
|
capacity = calculate_dct_capacity(image_data)
|
||||||
return data_length <= capacity.usable_capacity_bytes
|
return data_length <= capacity.usable_capacity_bytes
|
||||||
|
|
||||||
|
|
||||||
def estimate_capacity_comparison(image_data: bytes) -> dict:
|
def estimate_capacity_comparison(image_data: bytes) -> dict:
|
||||||
"""
|
"""Compare LSB and DCT capacity for an image."""
|
||||||
Compare LSB and DCT capacity for an image.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
image_data: Image file bytes
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict with 'lsb' and 'dct' capacity info
|
|
||||||
"""
|
|
||||||
img = Image.open(io.BytesIO(image_data))
|
img = Image.open(io.BytesIO(image_data))
|
||||||
width, height = img.size
|
width, height = img.size
|
||||||
pixels = width * height
|
pixels = width * height
|
||||||
|
|
||||||
# LSB capacity (3 bits per pixel for RGB, simplified)
|
|
||||||
lsb_bytes = (pixels * 3) // 8
|
lsb_bytes = (pixels * 3) // 8
|
||||||
|
|
||||||
# DCT capacity
|
|
||||||
if HAS_SCIPY:
|
if HAS_SCIPY:
|
||||||
dct_info = calculate_dct_capacity(image_data)
|
dct_info = calculate_dct_capacity(image_data)
|
||||||
dct_bytes = dct_info.usable_capacity_bytes
|
dct_bytes = dct_info.usable_capacity_bytes
|
||||||
else:
|
else:
|
||||||
# Estimate without scipy
|
|
||||||
blocks = (width // 8) * (height // 8)
|
blocks = (width // 8) * (height // 8)
|
||||||
dct_bytes = (blocks * 16) // 8 - HEADER_SIZE
|
dct_bytes = (blocks * 16) // 8 - HEADER_SIZE
|
||||||
|
|
||||||
@@ -357,6 +470,10 @@ def estimate_capacity_comparison(image_data: bytes) -> dict:
|
|||||||
'output': 'PNG or JPEG (grayscale)',
|
'output': 'PNG or JPEG (grayscale)',
|
||||||
'ratio_vs_lsb': (dct_bytes / lsb_bytes * 100) if lsb_bytes > 0 else 0,
|
'ratio_vs_lsb': (dct_bytes / lsb_bytes * 100) if lsb_bytes > 0 else 0,
|
||||||
'available': HAS_SCIPY,
|
'available': HAS_SCIPY,
|
||||||
|
},
|
||||||
|
'jpeg_native': {
|
||||||
|
'available': HAS_JPEGIO,
|
||||||
|
'note': 'Uses jpegio for proper JPEG coefficient embedding',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,30 +483,60 @@ def embed_in_dct(
|
|||||||
carrier_image: bytes,
|
carrier_image: bytes,
|
||||||
seed: bytes,
|
seed: bytes,
|
||||||
output_format: str = OUTPUT_FORMAT_PNG,
|
output_format: str = OUTPUT_FORMAT_PNG,
|
||||||
) -> tuple[bytes, DCTEmbedStats]:
|
color_mode: str = 'color', # v3.0.1: 'color' or 'grayscale'
|
||||||
|
) -> Tuple[bytes, DCTEmbedStats]:
|
||||||
"""
|
"""
|
||||||
Embed data into image using DCT coefficient modification.
|
Embed data into image using DCT coefficient modification.
|
||||||
|
|
||||||
|
For PNG output: Uses scipy DCT transform
|
||||||
|
For JPEG output: Uses jpegio if available for proper coefficient embedding
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data: Data to embed
|
data: Data to embed
|
||||||
carrier_image: Carrier image bytes
|
carrier_image: Carrier image bytes
|
||||||
seed: Seed for pseudo-random block selection
|
seed: Seed for pseudo-random selection
|
||||||
output_format: Output format - 'png' (default, lossless) or 'jpeg' (smaller)
|
output_format: 'png' (default, lossless) or 'jpeg'
|
||||||
|
color_mode: 'color' (preserve colors) or 'grayscale' (v3.0.1+)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (stego_image_bytes, stats)
|
Tuple of (stego_image_bytes, stats)
|
||||||
|
|
||||||
Raises:
|
|
||||||
ImportError: If scipy is not available
|
|
||||||
ValueError: If data is too large for carrier
|
|
||||||
"""
|
"""
|
||||||
_check_scipy()
|
|
||||||
|
|
||||||
# Validate output format
|
# Validate output format
|
||||||
if output_format not in (OUTPUT_FORMAT_PNG, OUTPUT_FORMAT_JPEG):
|
if output_format not in (OUTPUT_FORMAT_PNG, OUTPUT_FORMAT_JPEG):
|
||||||
raise ValueError(f"Invalid output format: {output_format}. Use 'png' or 'jpeg'")
|
raise ValueError(f"Invalid output format: {output_format}")
|
||||||
|
|
||||||
# Calculate capacity
|
# Validate color mode
|
||||||
|
if color_mode not in ('color', 'grayscale'):
|
||||||
|
color_mode = 'color' # Default to color
|
||||||
|
|
||||||
|
# For JPEG output, try to use jpegio for proper coefficient embedding
|
||||||
|
# Note: jpegio naturally preserves color (works in YCbCr space)
|
||||||
|
if output_format == OUTPUT_FORMAT_JPEG:
|
||||||
|
if HAS_JPEGIO:
|
||||||
|
return _embed_jpegio(data, carrier_image, seed, color_mode)
|
||||||
|
else:
|
||||||
|
# Fall back to scipy + PIL JPEG (WARNING: may not decode properly)
|
||||||
|
import warnings
|
||||||
|
warnings.warn(
|
||||||
|
"jpegio not available. JPEG output may not decode correctly. "
|
||||||
|
"Install jpegio for proper JPEG steganography support.",
|
||||||
|
RuntimeWarning
|
||||||
|
)
|
||||||
|
# Continue with scipy method but output as JPEG
|
||||||
|
|
||||||
|
# PNG output or JPEG fallback: use scipy DCT method
|
||||||
|
_check_scipy()
|
||||||
|
return _embed_scipy_dct(data, carrier_image, seed, output_format, color_mode)
|
||||||
|
|
||||||
|
|
||||||
|
def _embed_scipy_dct(
|
||||||
|
data: bytes,
|
||||||
|
carrier_image: bytes,
|
||||||
|
seed: bytes,
|
||||||
|
output_format: str,
|
||||||
|
color_mode: str = 'color',
|
||||||
|
) -> Tuple[bytes, DCTEmbedStats]:
|
||||||
|
"""Embed using scipy DCT (for PNG output), with color preservation option."""
|
||||||
capacity_info = calculate_dct_capacity(carrier_image)
|
capacity_info = calculate_dct_capacity(carrier_image)
|
||||||
|
|
||||||
if len(data) > capacity_info.usable_capacity_bytes:
|
if len(data) > capacity_info.usable_capacity_bytes:
|
||||||
@@ -398,69 +545,216 @@ def embed_in_dct(
|
|||||||
f"(capacity: {capacity_info.usable_capacity_bytes} bytes)"
|
f"(capacity: {capacity_info.usable_capacity_bytes} bytes)"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Prepare image
|
# Load image
|
||||||
image = _to_grayscale(carrier_image)
|
img = Image.open(io.BytesIO(carrier_image))
|
||||||
padded, original_size = _pad_to_blocks(image)
|
width, height = img.size
|
||||||
|
|
||||||
# Create header + data
|
if color_mode == 'color' and img.mode in ('RGB', 'RGBA'):
|
||||||
|
# Color mode: convert to YCbCr, embed in Y only, preserve Cb/Cr
|
||||||
|
if img.mode == 'RGBA':
|
||||||
|
img = img.convert('RGB')
|
||||||
|
|
||||||
|
rgb_array = np.array(img, dtype=np.float64)
|
||||||
|
Y, Cb, Cr = _rgb_to_ycbcr(rgb_array)
|
||||||
|
|
||||||
|
# Pad Y channel
|
||||||
|
Y_padded, original_size = _pad_to_blocks(Y)
|
||||||
|
|
||||||
|
# Embed in Y channel
|
||||||
|
Y_embedded = _embed_in_channel(Y_padded, data, seed, capacity_info)
|
||||||
|
|
||||||
|
# Unpad
|
||||||
|
Y_result = _unpad_image(Y_embedded, original_size)
|
||||||
|
|
||||||
|
# Convert back to RGB
|
||||||
|
result_rgb = _ycbcr_to_rgb(Y_result, Cb, Cr)
|
||||||
|
|
||||||
|
# Save as color image
|
||||||
|
stego_bytes = _save_color_image(result_rgb, output_format)
|
||||||
|
else:
|
||||||
|
# Grayscale mode: original behavior
|
||||||
|
image = _to_grayscale(carrier_image)
|
||||||
|
padded, original_size = _pad_to_blocks(image)
|
||||||
|
|
||||||
|
embedded = _embed_in_channel(padded, data, seed, capacity_info)
|
||||||
|
|
||||||
|
result = _unpad_image(embedded, original_size)
|
||||||
|
stego_bytes = _save_stego_image(result, output_format)
|
||||||
|
|
||||||
|
# Calculate stats
|
||||||
|
header = _create_header(len(data))
|
||||||
|
payload = header + data
|
||||||
|
bits = len(payload) * 8
|
||||||
|
|
||||||
|
stats = DCTEmbedStats(
|
||||||
|
blocks_used=(bits + len(DEFAULT_EMBED_POSITIONS) - 1) // len(DEFAULT_EMBED_POSITIONS),
|
||||||
|
blocks_available=capacity_info.total_blocks,
|
||||||
|
bits_embedded=bits,
|
||||||
|
capacity_bits=capacity_info.total_capacity_bits,
|
||||||
|
usage_percent=(bits / capacity_info.total_capacity_bits) * 100,
|
||||||
|
image_width=width,
|
||||||
|
image_height=height,
|
||||||
|
output_format=output_format,
|
||||||
|
jpeg_native=False,
|
||||||
|
color_mode=color_mode,
|
||||||
|
)
|
||||||
|
|
||||||
|
return stego_bytes, stats
|
||||||
|
|
||||||
|
|
||||||
|
def _embed_in_channel(
|
||||||
|
channel: np.ndarray,
|
||||||
|
data: bytes,
|
||||||
|
seed: bytes,
|
||||||
|
capacity_info: DCTCapacityInfo,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Embed data in a single channel using DCT."""
|
||||||
header = _create_header(len(data))
|
header = _create_header(len(data))
|
||||||
payload = header + data
|
payload = header + data
|
||||||
|
|
||||||
# Convert payload to bits
|
|
||||||
bits = []
|
bits = []
|
||||||
for byte in payload:
|
for byte in payload:
|
||||||
for i in range(7, -1, -1):
|
for i in range(7, -1, -1):
|
||||||
bits.append((byte >> i) & 1)
|
bits.append((byte >> i) & 1)
|
||||||
|
|
||||||
# Generate block order
|
|
||||||
num_blocks = capacity_info.total_blocks
|
num_blocks = capacity_info.total_blocks
|
||||||
block_order = _generate_block_order(num_blocks, seed)
|
block_order = _generate_block_order(num_blocks, seed)
|
||||||
|
|
||||||
# Embed bits
|
h, w = channel.shape
|
||||||
bit_idx = 0
|
result = channel.copy()
|
||||||
blocks_used = 0
|
|
||||||
h, w = padded.shape
|
|
||||||
|
|
||||||
|
bit_idx = 0
|
||||||
for block_num in block_order:
|
for block_num in block_order:
|
||||||
if bit_idx >= len(bits):
|
if bit_idx >= len(bits):
|
||||||
break
|
break
|
||||||
|
|
||||||
# Calculate block position
|
|
||||||
by = (block_num // (w // BLOCK_SIZE)) * BLOCK_SIZE
|
by = (block_num // (w // BLOCK_SIZE)) * BLOCK_SIZE
|
||||||
bx = (block_num % (w // BLOCK_SIZE)) * BLOCK_SIZE
|
bx = (block_num % (w // BLOCK_SIZE)) * BLOCK_SIZE
|
||||||
|
|
||||||
# Extract and transform block
|
block = result[by:by+BLOCK_SIZE, bx:bx+BLOCK_SIZE].copy()
|
||||||
block = padded[by:by+BLOCK_SIZE, bx:bx+BLOCK_SIZE].copy()
|
|
||||||
dct_block = _dct2(block)
|
dct_block = _dct2(block)
|
||||||
|
|
||||||
# Embed bits in selected coefficients
|
|
||||||
for pos in DEFAULT_EMBED_POSITIONS:
|
for pos in DEFAULT_EMBED_POSITIONS:
|
||||||
if bit_idx >= len(bits):
|
if bit_idx >= len(bits):
|
||||||
break
|
break
|
||||||
dct_block[pos] = _embed_bit_in_coeff(dct_block[pos], bits[bit_idx])
|
dct_block[pos] = _embed_bit_in_coeff(dct_block[pos], bits[bit_idx])
|
||||||
bit_idx += 1
|
bit_idx += 1
|
||||||
|
|
||||||
# Inverse transform and store
|
|
||||||
modified_block = _idct2(dct_block)
|
modified_block = _idct2(dct_block)
|
||||||
padded[by:by+BLOCK_SIZE, bx:bx+BLOCK_SIZE] = modified_block
|
result[by:by+BLOCK_SIZE, bx:bx+BLOCK_SIZE] = modified_block
|
||||||
blocks_used += 1
|
|
||||||
|
|
||||||
# Remove padding and save
|
return result
|
||||||
result = _unpad_image(padded, original_size)
|
|
||||||
stego_bytes = _save_stego_image(result, output_format)
|
|
||||||
|
|
||||||
stats = DCTEmbedStats(
|
|
||||||
blocks_used=blocks_used,
|
|
||||||
blocks_available=capacity_info.total_blocks,
|
|
||||||
bits_embedded=len(bits),
|
|
||||||
capacity_bits=capacity_info.total_capacity_bits,
|
|
||||||
usage_percent=(len(bits) / capacity_info.total_capacity_bits) * 100,
|
|
||||||
image_width=original_size[1],
|
|
||||||
image_height=original_size[0],
|
|
||||||
output_format=output_format,
|
|
||||||
)
|
|
||||||
|
|
||||||
return stego_bytes, stats
|
def _embed_jpegio(
|
||||||
|
data: bytes,
|
||||||
|
carrier_image: bytes,
|
||||||
|
seed: bytes,
|
||||||
|
color_mode: str = 'color',
|
||||||
|
) -> Tuple[bytes, DCTEmbedStats]:
|
||||||
|
"""
|
||||||
|
Embed using jpegio for proper JPEG coefficient modification.
|
||||||
|
|
||||||
|
Note: jpegio naturally preserves color since JPEG stores YCbCr
|
||||||
|
and we only modify Y channel coefficients.
|
||||||
|
"""
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Check if carrier is JPEG - if not, convert it
|
||||||
|
img = Image.open(io.BytesIO(carrier_image))
|
||||||
|
width, height = img.size
|
||||||
|
|
||||||
|
if img.format != 'JPEG':
|
||||||
|
# Convert to JPEG first
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
if img.mode != 'RGB':
|
||||||
|
img = img.convert('RGB')
|
||||||
|
img.save(buffer, format='JPEG', quality=95, subsampling=0)
|
||||||
|
carrier_image = buffer.getvalue()
|
||||||
|
|
||||||
|
# Write carrier to temp file
|
||||||
|
input_path = _jpegio_bytes_to_file(carrier_image, suffix='.jpg')
|
||||||
|
output_path = tempfile.mktemp(suffix='.jpg')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Read JPEG with jpegio
|
||||||
|
jpeg = jio.read(input_path)
|
||||||
|
|
||||||
|
# Get Y channel coefficients (channel 0)
|
||||||
|
# For grayscale mode, we could convert to grayscale, but jpegio
|
||||||
|
# works with the original JPEG which already has color info.
|
||||||
|
# The color_mode primarily affects the output interpretation.
|
||||||
|
coef_array = jpeg.coef_arrays[JPEGIO_EMBED_CHANNEL]
|
||||||
|
|
||||||
|
# Find usable positions
|
||||||
|
all_positions = _jpegio_get_usable_positions(coef_array)
|
||||||
|
|
||||||
|
# Generate pseudo-random order
|
||||||
|
order = _jpegio_generate_order(len(all_positions), seed)
|
||||||
|
|
||||||
|
# Create payload
|
||||||
|
header = _jpegio_create_header(len(data))
|
||||||
|
payload = header + data
|
||||||
|
|
||||||
|
# Convert to bits
|
||||||
|
bits = []
|
||||||
|
for byte in payload:
|
||||||
|
for i in range(7, -1, -1):
|
||||||
|
bits.append((byte >> i) & 1)
|
||||||
|
|
||||||
|
if len(bits) > len(all_positions):
|
||||||
|
raise ValueError(
|
||||||
|
f"Payload too large: {len(bits)} bits, "
|
||||||
|
f"only {len(all_positions)} usable coefficients"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Embed using LSB
|
||||||
|
coefs_used = 0
|
||||||
|
for bit_idx, pos_idx in enumerate(order):
|
||||||
|
if bit_idx >= len(bits):
|
||||||
|
break
|
||||||
|
|
||||||
|
row, col = all_positions[pos_idx]
|
||||||
|
coef = coef_array[row, col]
|
||||||
|
|
||||||
|
# Embed bit in LSB
|
||||||
|
if (coef & 1) != bits[bit_idx]:
|
||||||
|
if coef > 0:
|
||||||
|
coef_array[row, col] = coef - 1 if (coef & 1) else coef + 1
|
||||||
|
else:
|
||||||
|
coef_array[row, col] = coef + 1 if (coef & 1) else coef - 1
|
||||||
|
|
||||||
|
coefs_used += 1
|
||||||
|
|
||||||
|
# Write modified JPEG
|
||||||
|
jio.write(jpeg, output_path)
|
||||||
|
|
||||||
|
# Read back as bytes
|
||||||
|
with open(output_path, 'rb') as f:
|
||||||
|
stego_bytes = f.read()
|
||||||
|
|
||||||
|
stats = DCTEmbedStats(
|
||||||
|
blocks_used=coefs_used // 63, # Approximate blocks
|
||||||
|
blocks_available=len(all_positions) // 63,
|
||||||
|
bits_embedded=len(bits),
|
||||||
|
capacity_bits=len(all_positions),
|
||||||
|
usage_percent=(len(bits) / len(all_positions)) * 100 if all_positions else 0,
|
||||||
|
image_width=width,
|
||||||
|
image_height=height,
|
||||||
|
output_format=OUTPUT_FORMAT_JPEG,
|
||||||
|
jpeg_native=True,
|
||||||
|
color_mode=color_mode, # JPEG naturally preserves color
|
||||||
|
)
|
||||||
|
|
||||||
|
return stego_bytes, stats
|
||||||
|
|
||||||
|
finally:
|
||||||
|
for path in [input_path, output_path]:
|
||||||
|
try:
|
||||||
|
os.unlink(path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def extract_from_dct(
|
def extract_from_dct(
|
||||||
@@ -470,33 +764,43 @@ def extract_from_dct(
|
|||||||
"""
|
"""
|
||||||
Extract data from DCT stego image.
|
Extract data from DCT stego image.
|
||||||
|
|
||||||
|
Automatically detects whether image uses scipy DCT or jpegio embedding.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
stego_image: Stego image bytes
|
stego_image: Stego image bytes
|
||||||
seed: Same seed used for embedding
|
seed: Same seed used for embedding
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Extracted data bytes
|
Extracted data bytes
|
||||||
|
|
||||||
Raises:
|
|
||||||
ImportError: If scipy is not available
|
|
||||||
ValueError: If image is not a valid DCT stego image
|
|
||||||
"""
|
"""
|
||||||
_check_scipy()
|
# Check image format
|
||||||
|
img = Image.open(io.BytesIO(stego_image))
|
||||||
|
|
||||||
# Prepare image
|
if img.format == 'JPEG' and HAS_JPEGIO:
|
||||||
|
# Try jpegio extraction first
|
||||||
|
try:
|
||||||
|
return _extract_jpegio(stego_image, seed)
|
||||||
|
except ValueError:
|
||||||
|
# If jpegio magic not found, fall back to scipy method
|
||||||
|
pass
|
||||||
|
|
||||||
|
# PNG or fallback: use scipy DCT method
|
||||||
|
_check_scipy()
|
||||||
|
return _extract_scipy_dct(stego_image, seed)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_scipy_dct(stego_image: bytes, seed: bytes) -> bytes:
|
||||||
|
"""Extract using scipy DCT (for PNG images)."""
|
||||||
image = _to_grayscale(stego_image)
|
image = _to_grayscale(stego_image)
|
||||||
padded, original_size = _pad_to_blocks(image)
|
padded, original_size = _pad_to_blocks(image)
|
||||||
|
|
||||||
# Calculate capacity
|
|
||||||
h, w = padded.shape
|
h, w = padded.shape
|
||||||
blocks_x = w // BLOCK_SIZE
|
blocks_x = w // BLOCK_SIZE
|
||||||
blocks_y = h // BLOCK_SIZE
|
blocks_y = h // BLOCK_SIZE
|
||||||
num_blocks = blocks_x * blocks_y
|
num_blocks = blocks_x * blocks_y
|
||||||
|
|
||||||
# Generate same block order
|
|
||||||
block_order = _generate_block_order(num_blocks, seed)
|
block_order = _generate_block_order(num_blocks, seed)
|
||||||
|
|
||||||
# Extract all bits (we'll stop when we have enough based on header)
|
|
||||||
all_bits = []
|
all_bits = []
|
||||||
|
|
||||||
for block_num in block_order:
|
for block_num in block_order:
|
||||||
@@ -510,7 +814,6 @@ def extract_from_dct(
|
|||||||
bit = _extract_bit_from_coeff(dct_block[pos])
|
bit = _extract_bit_from_coeff(dct_block[pos])
|
||||||
all_bits.append(bit)
|
all_bits.append(bit)
|
||||||
|
|
||||||
# Check if we have enough for header
|
|
||||||
if len(all_bits) >= HEADER_SIZE * 8:
|
if len(all_bits) >= HEADER_SIZE * 8:
|
||||||
try:
|
try:
|
||||||
_, _, data_length = _parse_header(all_bits[:HEADER_SIZE * 8])
|
_, _, data_length = _parse_header(all_bits[:HEADER_SIZE * 8])
|
||||||
@@ -518,16 +821,12 @@ def extract_from_dct(
|
|||||||
if len(all_bits) >= total_needed:
|
if len(all_bits) >= total_needed:
|
||||||
break
|
break
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# Not enough data yet or invalid, continue
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Parse header
|
|
||||||
version, flags, data_length = _parse_header(all_bits)
|
version, flags, data_length = _parse_header(all_bits)
|
||||||
|
|
||||||
# Extract data bits
|
|
||||||
data_bits = all_bits[HEADER_SIZE * 8:(HEADER_SIZE + data_length) * 8]
|
data_bits = all_bits[HEADER_SIZE * 8:(HEADER_SIZE + data_length) * 8]
|
||||||
|
|
||||||
# Convert bits to bytes
|
|
||||||
data = bytes([
|
data = bytes([
|
||||||
sum(data_bits[i*8:(i+1)*8][j] << (7-j) for j in range(8))
|
sum(data_bits[i*8:(i+1)*8][j] << (7-j) for j in range(8))
|
||||||
for i in range(data_length)
|
for i in range(data_length)
|
||||||
@@ -536,6 +835,61 @@ def extract_from_dct(
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_jpegio(stego_image: bytes, seed: bytes) -> bytes:
|
||||||
|
"""Extract using jpegio for JPEG images."""
|
||||||
|
import os
|
||||||
|
|
||||||
|
temp_path = _jpegio_bytes_to_file(stego_image, suffix='.jpg')
|
||||||
|
|
||||||
|
try:
|
||||||
|
jpeg = jio.read(temp_path)
|
||||||
|
coef_array = jpeg.coef_arrays[JPEGIO_EMBED_CHANNEL]
|
||||||
|
|
||||||
|
all_positions = _jpegio_get_usable_positions(coef_array)
|
||||||
|
order = _jpegio_generate_order(len(all_positions), seed)
|
||||||
|
|
||||||
|
# Extract header bits
|
||||||
|
header_bits = []
|
||||||
|
for pos_idx in order[:HEADER_SIZE * 8]:
|
||||||
|
row, col = all_positions[pos_idx]
|
||||||
|
coef = coef_array[row, col]
|
||||||
|
header_bits.append(coef & 1)
|
||||||
|
|
||||||
|
header_bytes = bytes([
|
||||||
|
sum(header_bits[i*8:(i+1)*8][j] << (7-j) for j in range(8))
|
||||||
|
for i in range(HEADER_SIZE)
|
||||||
|
])
|
||||||
|
|
||||||
|
version, flags, data_length = _jpegio_parse_header(header_bytes)
|
||||||
|
|
||||||
|
# Extract all needed bits
|
||||||
|
total_bits_needed = (HEADER_SIZE + data_length) * 8
|
||||||
|
|
||||||
|
all_bits = []
|
||||||
|
for bit_idx, pos_idx in enumerate(order):
|
||||||
|
if bit_idx >= total_bits_needed:
|
||||||
|
break
|
||||||
|
row, col = all_positions[pos_idx]
|
||||||
|
coef = coef_array[row, col]
|
||||||
|
all_bits.append(coef & 1)
|
||||||
|
|
||||||
|
# Extract data
|
||||||
|
data_bits = all_bits[HEADER_SIZE * 8:]
|
||||||
|
|
||||||
|
data = bytes([
|
||||||
|
sum(data_bits[i*8:(i+1)*8][j] << (7-j) for j in range(8))
|
||||||
|
for i in range(data_length)
|
||||||
|
])
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
os.unlink(temp_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# CONVENIENCE FUNCTIONS
|
# CONVENIENCE FUNCTIONS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ New in v3.0:
|
|||||||
|
|
||||||
New in v3.0.1:
|
New in v3.0.1:
|
||||||
- dct_output_format parameter for DCT mode ('png' or 'jpeg')
|
- dct_output_format parameter for DCT mode ('png' or 'jpeg')
|
||||||
|
- dct_color_mode parameter for DCT mode ('grayscale' or 'color')
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
@@ -59,6 +60,10 @@ ENCRYPTION_OVERHEAD = HEADER_OVERHEAD + LENGTH_PREFIX
|
|||||||
DCT_OUTPUT_PNG = 'png'
|
DCT_OUTPUT_PNG = 'png'
|
||||||
DCT_OUTPUT_JPEG = 'jpeg'
|
DCT_OUTPUT_JPEG = 'jpeg'
|
||||||
|
|
||||||
|
# DCT color mode options (v3.0.1)
|
||||||
|
DCT_COLOR_GRAYSCALE = 'grayscale'
|
||||||
|
DCT_COLOR_COLOR = 'color'
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# DCT MODULE LAZY LOADING
|
# DCT MODULE LAZY LOADING
|
||||||
@@ -477,6 +482,7 @@ def embed_in_image(
|
|||||||
output_format: Optional[str] = None,
|
output_format: Optional[str] = None,
|
||||||
embed_mode: str = EMBED_MODE_LSB,
|
embed_mode: str = EMBED_MODE_LSB,
|
||||||
dct_output_format: str = DCT_OUTPUT_PNG, # NEW in v3.0.1
|
dct_output_format: str = DCT_OUTPUT_PNG, # NEW in v3.0.1
|
||||||
|
dct_color_mode: str = 'grayscale', # NEW in v3.0.1: 'grayscale' or 'color'
|
||||||
) -> Tuple[bytes, Union[EmbedStats, 'DCTEmbedStats'], str]:
|
) -> Tuple[bytes, Union[EmbedStats, 'DCTEmbedStats'], str]:
|
||||||
"""
|
"""
|
||||||
Embed data into an image using specified mode.
|
Embed data into an image using specified mode.
|
||||||
@@ -489,6 +495,7 @@ def embed_in_image(
|
|||||||
output_format: Force output format (LSB mode only)
|
output_format: Force output format (LSB mode only)
|
||||||
embed_mode: 'lsb' (default) or 'dct'
|
embed_mode: 'lsb' (default) or 'dct'
|
||||||
dct_output_format: For DCT mode - 'png' (lossless) or 'jpeg' (smaller)
|
dct_output_format: For DCT mode - 'png' (lossless) or 'jpeg' (smaller)
|
||||||
|
dct_color_mode: For DCT mode - 'grayscale' (default) or 'color' (preserves colors)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (stego image bytes, stats, file extension)
|
Tuple of (stego image bytes, stats, file extension)
|
||||||
@@ -515,14 +522,20 @@ def embed_in_image(
|
|||||||
debug.print(f"Invalid dct_output_format '{dct_output_format}', defaulting to PNG")
|
debug.print(f"Invalid dct_output_format '{dct_output_format}', defaulting to PNG")
|
||||||
dct_output_format = DCT_OUTPUT_PNG
|
dct_output_format = DCT_OUTPUT_PNG
|
||||||
|
|
||||||
|
# Validate DCT color mode (v3.0.1)
|
||||||
|
if dct_color_mode not in ('grayscale', 'color'):
|
||||||
|
debug.print(f"Invalid dct_color_mode '{dct_color_mode}', defaulting to grayscale")
|
||||||
|
dct_color_mode = 'grayscale'
|
||||||
|
|
||||||
dct_mod = _get_dct_module()
|
dct_mod = _get_dct_module()
|
||||||
|
|
||||||
# Pass output_format to DCT module (v3.0.1)
|
# Pass output_format and color_mode to DCT module (v3.0.1)
|
||||||
stego_bytes, dct_stats = dct_mod.embed_in_dct(
|
stego_bytes, dct_stats = dct_mod.embed_in_dct(
|
||||||
data,
|
data,
|
||||||
image_data,
|
image_data,
|
||||||
pixel_key,
|
pixel_key,
|
||||||
output_format=dct_output_format,
|
output_format=dct_output_format,
|
||||||
|
color_mode=dct_color_mode, # NEW in v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
# Determine extension based on output format
|
# Determine extension based on output format
|
||||||
@@ -531,7 +544,8 @@ def embed_in_image(
|
|||||||
else:
|
else:
|
||||||
ext = 'png'
|
ext = 'png'
|
||||||
|
|
||||||
debug.print(f"DCT embedding complete: {dct_output_format.upper()} output, ext={ext}")
|
debug.print(f"DCT embedding complete: {dct_output_format.upper()} output, "
|
||||||
|
f"color_mode={dct_color_mode}, ext={ext}")
|
||||||
return stego_bytes, dct_stats, ext
|
return stego_bytes, dct_stats, ext
|
||||||
|
|
||||||
# LSB MODE
|
# LSB MODE
|
||||||
|
|||||||
Reference in New Issue
Block a user