New Version 2 -- prolly doesn't work.

This commit is contained in:
Aaron D. Lee
2025-12-27 22:40:31 -05:00
parent ee937c832f
commit 8581b86104
55 changed files with 5970 additions and 113 deletions

43
.gitignore vendored
View File

@@ -4,23 +4,50 @@ __pycache__/
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
venv/
.venv/
ENV/
env/
# IDE
.vscode/
.idea/
.vscode/
*.swp
*.swo
# Uploads (user data)
uploads/*
!uploads/.gitkeep
# Testing
.pytest_cache/
.coverage
htmlcov/
.tox/
.nox/
# Type checking
.mypy_cache/
.dmypy.json
# Environment
.env
.env.local
.env.*
*.log
# OS
.DS_Store
Thumbs.db
# Distribution
*.manifest
*.spec

View File

@@ -1,12 +1,12 @@
FROM python:3.11-slim
# Stegasoo Docker Image
# Multi-stage build for smaller image size
FROM python:3.11-slim as base
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Set work directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
@@ -14,25 +14,47 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libffi-dev \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
# ============================================================================
# Builder stage - install Python packages
# ============================================================================
FROM base as builder
COPY requirements.txt .
#COPY requirements-ml.txt .
WORKDIR /build
RUN pip install --upgrade pip
RUN pip install --no-cache-dir -r requirements.txt --root-user-action=ignore
#RUN pip install --no-cache-dir -r requirements-ml.txt
# Copy package files (including README.md which pyproject.toml references)
COPY pyproject.toml README.md ./
COPY src/ src/
COPY data/ data/
# Copy application
COPY . .
# Install the package with web extras
RUN 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 for security
# 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
@@ -40,5 +62,68 @@ EXPOSE 5000
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 in production
# 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
COPY pyproject.toml README.md ./
COPY src/ src/
COPY data/ data/
RUN 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/
RUN pip install --no-cache-dir ".[cli]"
# 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"]

282
README.md
View File

@@ -1,132 +1,268 @@
# Stegasoo Web Service
# Stegasoo
A containerized Flask + Bootstrap web UI for hybrid Photo + Day-Phrase + PIN steganography.
A secure steganography system for hiding encrypted messages in images using hybrid authentication.
![Python](https://img.shields.io/badge/Python-3.11+-blue)
![Flask](https://img.shields.io/badge/Flask-3.0+-green)
![Docker](https://img.shields.io/badge/Docker-Ready-blue)
![Python](https://img.shields.io/badge/Python-3.10+-blue)
![License](https://img.shields.io/badge/License-MIT-green)
![Security](https://img.shields.io/badge/Security-AES--256--GCM-red)
## Features
- 🔐 **AES-256-GCM** authenticated encryption
- 🧠 **Argon2id** memory-hard key derivation (256MB)
- 🧠 **Argon2id** memory-hard key derivation (256MB RAM requirement)
- 🎲 **Pseudo-random pixel selection** defeats steganalysis
- 📅 **Daily key rotation** with customizable phrases (3-12 words)
- 🔢 **Static PIN** for additional entropy (6-8 digits)
- 📅 **Daily key rotation** with BIP-39 passphrases
- 🔑 **Multi-factor authentication**: PIN, RSA key, or both
- 🖼️ **Reference photo** as "something you have"
- 🌐 **Web UI** with Bootstrap 5 dark theme
- 📖 **Memory aid stories** to help memorize phrases (template or AI-powered)
- 🌐 **Multiple interfaces**: CLI, Web UI, REST API
## Installation
### From PyPI (coming soon)
```bash
# Core library only
pip install stegasoo
# With CLI
pip install stegasoo[cli]
# With Web UI
pip install stegasoo[web]
# With REST API
pip install stegasoo[api]
# Everything
pip install stegasoo[all]
```
### From Source
```bash
git clone https://github.com/example/stegasoo.git
cd stegasoo
# Install with all extras
pip install -e ".[all]"
```
### Docker
```bash
# Web UI only
docker-compose up web
# REST API only
docker-compose up api
# Both
docker-compose up
```
## Quick Start
### Docker (Recommended)
### Python Library
```bash
# Build and run
docker-compose up -d
```python
import stegasoo
# Access at http://localhost:5000
# Generate credentials
creds = stegasoo.generate_credentials(use_pin=True, use_rsa=False)
print(f"Today's phrase: {creds.phrases['Monday']}")
print(f"PIN: {creds.pin}")
# Encode a message
with open('secret_photo.jpg', 'rb') as f:
ref_photo = f.read()
with open('meme.png', 'rb') as f:
carrier = f.read()
result = stegasoo.encode(
message="Meet at midnight",
reference_photo=ref_photo,
carrier_image=carrier,
day_phrase="apple forest thunder",
pin="123456"
)
with open('stego.png', 'wb') as f:
f.write(result.stego_image)
# Decode a message
message = stegasoo.decode(
stego_image=result.stego_image,
reference_photo=ref_photo,
day_phrase="apple forest thunder",
pin="123456"
)
print(message) # "Meet at midnight"
```
### Manual Installation
### CLI
```bash
# Create virtual environment
python -m venv venv
source venv/bin/activate # Linux/Mac
# or: venv\Scripts\activate # Windows
# Generate credentials
stegasoo generate --pin --words 3
# Install dependencies
pip install -r requirements.txt
# With RSA key
stegasoo generate --rsa --rsa-bits 4096 -o mykey.pem -p "secretpassword"
# Optional: Enable AI-powered story generation
pip install -r requirements-ml.txt
# Encode
stegasoo encode \
--ref photo.jpg \
--carrier meme.png \
--phrase "apple forest thunder" \
--pin 123456 \
--message "Secret message"
# Run development server
# Decode
stegasoo decode \
--ref photo.jpg \
--stego stego.png \
--phrase "apple forest thunder" \
--pin 123456
# Pipe-friendly
echo "secret" | stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 > stego.png
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 -q
```
### Web UI
```bash
# Development
cd frontends/web
python app.py
# Or production with gunicorn
# Production
gunicorn --bind 0.0.0.0:5000 app:app
```
## Usage
Visit http://localhost:5000
### 1. Generate Credentials
### REST API
Visit `/generate` to create:
- **7 phrases** (one per day of week, 3-12 words each)
- **1 PIN** (6-8 digits, same every day)
- **Memory aid stories** (optional, helps memorize phrases)
```bash
# Development
cd frontends/api
python main.py
Memorize these! Don't save them.
# Production
uvicorn main:app --host 0.0.0.0 --port 8000
```
### 2. Encode a Message
API docs at http://localhost:8000/docs
Visit `/encode` and provide:
- **Reference photo** - A photo both parties have (NOT transmitted)
- **Carrier image** - The image to hide your message in
- **Message** - Your secret text
- **Day phrase** - Today's phrase
- **PIN** - Your static PIN
#### Example API Calls
Download the stego image and share it through any channel.
```bash
# Generate credentials
curl -X POST http://localhost:8000/generate \
-H "Content-Type: application/json" \
-d '{"use_pin": true, "use_rsa": false}'
### 3. Decode a Message
# 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
Visit `/decode` and provide:
- **Reference photo** - Same photo used for encoding
- **Stego image** - The image containing the hidden message
- **Day phrase** - The phrase for the day it was encoded
- **PIN** - Your static PIN
# 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
| Component | Entropy | Purpose |
|-----------|---------|---------|
| Reference Photo | ~80-256 bits | Something you have |
| Day Phrase | ~33-132 bits | Something you know (rotates daily) |
| PIN | ~20-27 bits | Something you know (static) |
| **Combined** | **133-415+ bits** | **Beyond brute force** |
| Day Phrase (3 words) | ~33 bits | Something you know (rotates daily) |
| PIN (6 digits) | ~20 bits | Something you know (static) |
| RSA Key (2048-bit) | ~128 bits | Something you have |
| **Combined** | **133-400+ bits** | **Beyond brute force** |
### Attack Resistance
| Attack | Result |
|--------|--------|
| Brute force | 2^133+ combinations = impossible |
| Attack | Protection |
|--------|------------|
| Brute force | 2^133+ combinations |
| Rainbow tables | Random salt per message |
| Steganalysis | Random pixel selection defeats detection |
| GPU cracking | Argon2 requires 256MB RAM per attempt |
| Steganalysis | Random pixel selection |
| GPU cracking | Argon2id requires 256MB RAM per attempt |
| Side-channel | Constant-time operations in crypto |
## Memory Aid Stories
## Project Structure
The generate page can create stories to help you memorize your phrases:
**Template-based** (default):
> Monday morning began when I discovered a **APPLE** near the **FOREST**. I had to **THUNDER** quickly, then grab the **CRYSTAL** before reaching the **BRAVE**.
**AI-powered** (with `requirements-ml.txt`):
- Uses DistilGPT-2 (~350MB model)
- Generates more coherent, natural stories
- Words highlighted in RED CAPS
```
stegasoo/
├── src/stegasoo/ # Core library
│ ├── __init__.py # Public API
│ ├── constants.py # Configuration
│ ├── crypto.py # Encryption/decryption
│ ├── steganography.py # Image embedding
│ ├── keygen.py # Credential generation
│ ├── validation.py # Input validation
│ ├── models.py # Data classes
│ ├── exceptions.py # Custom exceptions
│ └── utils.py # Utilities
├── frontends/
│ ├── web/ # Flask web UI
│ ├── cli/ # Command-line interface
│ └── api/ # FastAPI REST API
├── data/
│ └── bip39-words.txt # BIP-39 wordlist
├── pyproject.toml # Package configuration
├── Dockerfile # Multi-stage Docker build
└── docker-compose.yml # Container orchestration
```
## Configuration
Environment variables:
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `FLASK_ENV` | production | Flask environment |
| `SECRET_KEY` | random | Session secret (auto-generated) |
| `PYTHONPATH` | - | Include src/ for development |
## Production Deployment
### Limits
For production, consider:
| Limit | Value |
|-------|-------|
| Max image size | 4 megapixels |
| Max message size | 50 KB |
| Max file upload | 5 MB |
| PIN length | 6-9 digits |
| Phrase length | 3-12 words |
| RSA key sizes | 2048, 3072, 4096 bits |
1. **HTTPS** - Use nginx reverse proxy with SSL
2. **Rate limiting** - Prevent abuse
3. **Logging** - Monitor for security events
4. **Memory** - Allocate at least 512MB (Argon2 needs 256MB)
## Development
```bash
# Install dev dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Format code
black src/ frontends/
ruff check src/ frontends/
# Type checking
mypy src/
```
## License

View File

@@ -1,16 +1,18 @@
version: '3.8'
services:
stegasoo:
build: .
container_name: stegasoo
# ============================================================================
# Web UI (Flask)
# ============================================================================
web:
build:
context: .
target: web
container_name: stegasoo-web
ports:
- "5000:5000"
environment:
- FLASK_ENV=production
# Uncomment to persist uploads between restarts:
# volumes:
# - ./uploads:/tmp/stego_uploads
restart: unless-stopped
deploy:
resources:
@@ -19,9 +21,30 @@ services:
reservations:
memory: 256M
# Optional: Add nginx reverse proxy for production
# ============================================================================
# REST API (FastAPI)
# ============================================================================
api:
build:
context: .
target: api
container_name: stegasoo-api
ports:
- "8000:8000"
restart: unless-stopped
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
# ============================================================================
# Nginx Reverse Proxy (optional, for production)
# ============================================================================
# nginx:
# image: nginx:alpine
# container_name: stegasoo-nginx
# ports:
# - "80:80"
# - "443:443"
@@ -29,5 +52,11 @@ services:
# - ./nginx.conf:/etc/nginx/nginx.conf:ro
# - ./certs:/etc/nginx/certs:ro
# depends_on:
# - stegasoo
# - web
# - api
# restart: unless-stopped
# ============================================================================
# Development overrides
# ============================================================================
# Use: docker-compose -f docker-compose.yml -f docker-compose.dev.yml up

368
frontends/api/main.py Normal file
View File

@@ -0,0 +1,368 @@
#!/usr/bin/env python3
"""
Stegasoo REST API
FastAPI-based REST API for steganography operations.
Designed for integration with other services and automation.
"""
import io
import sys
import base64
from pathlib import Path
from typing import Optional
from datetime import date
from fastapi import FastAPI, HTTPException, UploadFile, File, Form
from fastapi.responses import Response, JSONResponse
from pydantic import BaseModel, Field
# 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,
validate_image, calculate_capacity,
DAY_NAMES, __version__,
StegasooError, DecryptionError, CapacityError,
has_argon2,
)
from stegasoo.constants import (
MIN_PIN_LENGTH, MAX_PIN_LENGTH,
MIN_PHRASE_WORDS, MAX_PHRASE_WORDS,
VALID_RSA_SIZES,
)
# ============================================================================
# FASTAPI APP
# ============================================================================
app = FastAPI(
title="Stegasoo API",
description="Secure steganography with hybrid authentication",
version=__version__,
docs_url="/docs",
redoc_url="/redoc",
)
# ============================================================================
# MODELS
# ============================================================================
class GenerateRequest(BaseModel):
use_pin: bool = True
use_rsa: bool = False
pin_length: int = Field(default=6, ge=MIN_PIN_LENGTH, le=MAX_PIN_LENGTH)
rsa_bits: int = Field(default=2048)
words_per_phrase: int = Field(default=3, ge=MIN_PHRASE_WORDS, le=MAX_PHRASE_WORDS)
class GenerateResponse(BaseModel):
phrases: dict[str, str]
pin: Optional[str] = None
rsa_key_pem: Optional[str] = None
entropy: dict[str, int]
class EncodeRequest(BaseModel):
message: str
reference_photo_base64: str
carrier_image_base64: str
day_phrase: str
pin: str = ""
rsa_key_base64: Optional[str] = None
rsa_password: Optional[str] = None
date_str: Optional[str] = None
class EncodeResponse(BaseModel):
stego_image_base64: str
filename: str
capacity_used_percent: float
date_used: str
class DecodeRequest(BaseModel):
stego_image_base64: str
reference_photo_base64: str
day_phrase: str
pin: str = ""
rsa_key_base64: Optional[str] = None
rsa_password: Optional[str] = None
class DecodeResponse(BaseModel):
message: str
class ImageInfoResponse(BaseModel):
width: int
height: int
pixels: int
capacity_bytes: int
capacity_kb: int
class StatusResponse(BaseModel):
version: str
has_argon2: bool
day_names: list[str]
class ErrorResponse(BaseModel):
error: str
detail: Optional[str] = None
# ============================================================================
# ROUTES
# ============================================================================
@app.get("/", response_model=StatusResponse)
async def root():
"""Get API status and configuration."""
return StatusResponse(
version=__version__,
has_argon2=has_argon2(),
day_names=list(DAY_NAMES)
)
@app.post("/generate", response_model=GenerateResponse)
async def api_generate(request: GenerateRequest):
"""
Generate credentials for encoding/decoding.
At least one of use_pin or use_rsa must be True.
"""
if not request.use_pin and not request.use_rsa:
raise HTTPException(400, "Must enable at least one of use_pin or use_rsa")
if request.rsa_bits not in VALID_RSA_SIZES:
raise HTTPException(400, f"rsa_bits must be one of {VALID_RSA_SIZES}")
try:
creds = generate_credentials(
use_pin=request.use_pin,
use_rsa=request.use_rsa,
pin_length=request.pin_length,
rsa_bits=request.rsa_bits,
words_per_phrase=request.words_per_phrase
)
return GenerateResponse(
phrases=creds.phrases,
pin=creds.pin,
rsa_key_pem=creds.rsa_key_pem,
entropy={
"phrase": creds.phrase_entropy,
"pin": creds.pin_entropy,
"rsa": creds.rsa_entropy,
"total": creds.total_entropy
}
)
except Exception as e:
raise HTTPException(500, str(e))
@app.post("/encode", response_model=EncodeResponse)
async def api_encode(request: EncodeRequest):
"""
Encode a secret message into an image.
Images must be base64-encoded. Returns base64-encoded stego image.
"""
try:
ref_photo = base64.b64decode(request.reference_photo_base64)
carrier = base64.b64decode(request.carrier_image_base64)
rsa_key = base64.b64decode(request.rsa_key_base64) if request.rsa_key_base64 else None
result = encode(
message=request.message,
reference_photo=ref_photo,
carrier_image=carrier,
day_phrase=request.day_phrase,
pin=request.pin,
rsa_key_data=rsa_key,
rsa_password=request.rsa_password,
date_str=request.date_str
)
stego_b64 = base64.b64encode(result.stego_image).decode('utf-8')
return EncodeResponse(
stego_image_base64=stego_b64,
filename=result.filename,
capacity_used_percent=result.capacity_percent,
date_used=result.date_used
)
except CapacityError as e:
raise HTTPException(400, str(e))
except StegasooError as e:
raise HTTPException(400, str(e))
except Exception as e:
raise HTTPException(500, str(e))
@app.post("/decode", response_model=DecodeResponse)
async def api_decode(request: DecodeRequest):
"""
Decode a secret message from a stego image.
Images must be base64-encoded.
"""
try:
stego = base64.b64decode(request.stego_image_base64)
ref_photo = base64.b64decode(request.reference_photo_base64)
rsa_key = base64.b64decode(request.rsa_key_base64) if request.rsa_key_base64 else None
message = decode(
stego_image=stego,
reference_photo=ref_photo,
day_phrase=request.day_phrase,
pin=request.pin,
rsa_key_data=rsa_key,
rsa_password=request.rsa_password
)
return DecodeResponse(message=message)
except DecryptionError as e:
raise HTTPException(401, "Decryption failed. Check credentials.")
except StegasooError as e:
raise HTTPException(400, str(e))
except Exception as e:
raise HTTPException(500, str(e))
@app.post("/encode/multipart")
async def api_encode_multipart(
message: str = Form(...),
day_phrase: str = Form(...),
reference_photo: UploadFile = File(...),
carrier: UploadFile = File(...),
pin: str = Form(""),
rsa_key: Optional[UploadFile] = File(None),
rsa_password: str = Form(""),
date_str: str = Form("")
):
"""
Encode using multipart form data (file uploads).
Returns the stego image directly as PNG.
"""
try:
ref_data = await reference_photo.read()
carrier_data = await carrier.read()
rsa_key_data = await rsa_key.read() if rsa_key else None
result = encode(
message=message,
reference_photo=ref_data,
carrier_image=carrier_data,
day_phrase=day_phrase,
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=rsa_password if rsa_password else None,
date_str=date_str if date_str else None
)
return Response(
content=result.stego_image,
media_type="image/png",
headers={"Content-Disposition": f"attachment; filename={result.filename}"}
)
except CapacityError as e:
raise HTTPException(400, str(e))
except StegasooError as e:
raise HTTPException(400, str(e))
except Exception as e:
raise HTTPException(500, str(e))
@app.post("/decode/multipart", response_model=DecodeResponse)
async def api_decode_multipart(
day_phrase: str = Form(...),
reference_photo: UploadFile = File(...),
stego_image: UploadFile = File(...),
pin: str = Form(""),
rsa_key: Optional[UploadFile] = File(None),
rsa_password: str = Form("")
):
"""
Decode using multipart form data (file uploads).
"""
try:
ref_data = await reference_photo.read()
stego_data = await stego_image.read()
rsa_key_data = await rsa_key.read() if rsa_key else None
message = decode(
stego_image=stego_data,
reference_photo=ref_data,
day_phrase=day_phrase,
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=rsa_password if rsa_password else None
)
return DecodeResponse(message=message)
except DecryptionError:
raise HTTPException(401, "Decryption failed. Check credentials.")
except StegasooError as e:
raise HTTPException(400, str(e))
except Exception as e:
raise HTTPException(500, str(e))
@app.post("/image/info", response_model=ImageInfoResponse)
async def api_image_info(image: UploadFile = File(...)):
"""Get information about an image's capacity."""
try:
image_data = await image.read()
result = validate_image(image_data, check_size=False)
if not result.is_valid:
raise HTTPException(400, result.error_message)
capacity = calculate_capacity(image_data)
return ImageInfoResponse(
width=result.details['width'],
height=result.details['height'],
pixels=result.details['pixels'],
capacity_bytes=capacity,
capacity_kb=capacity // 1024
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(500, str(e))
# ============================================================================
# ERROR HANDLERS
# ============================================================================
@app.exception_handler(StegasooError)
async def stegasoo_error_handler(request, exc):
return JSONResponse(
status_code=400,
content={"error": type(exc).__name__, "detail": str(exc)}
)
# ============================================================================
# MAIN
# ============================================================================
if __name__ == '__main__':
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

371
frontends/cli/main.py Normal file
View File

@@ -0,0 +1,371 @@
#!/usr/bin/env python3
"""
Stegasoo CLI - Command-line interface for steganography operations.
Usage:
stegasoo generate [OPTIONS]
stegasoo encode [OPTIONS]
stegasoo decode [OPTIONS]
stegasoo info [OPTIONS]
"""
import sys
from pathlib import Path
from typing import Optional
import click
# 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_image, calculate_capacity,
get_day_from_date, parse_date_from_filename,
DAY_NAMES, __version__,
StegasooError, DecryptionError, ExtractionError,
)
# ============================================================================
# CLI SETUP
# ============================================================================
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
@click.group(context_settings=CONTEXT_SETTINGS)
@click.version_option(__version__, '-v', '--version')
def cli():
"""
Stegasoo - Secure steganography with hybrid authentication.
Hide encrypted messages in images using a combination of:
\b
• Reference photo (something you have)
• Daily passphrase (something you know)
• Static PIN or RSA key (additional security)
"""
pass
# ============================================================================
# GENERATE COMMAND
# ============================================================================
@cli.command()
@click.option('--pin/--no-pin', default=True, help='Generate a PIN (default: yes)')
@click.option('--rsa/--no-rsa', default=False, help='Generate an RSA key')
@click.option('--pin-length', type=click.IntRange(6, 9), default=6, help='PIN length (6-9)')
@click.option('--rsa-bits', type=click.Choice(['2048', '3072', '4096']), default='2048', help='RSA key size')
@click.option('--words', type=click.IntRange(3, 12), default=3, help='Words per phrase (3-12)')
@click.option('--output', '-o', type=click.Path(), help='Save RSA key to file (requires password)')
@click.option('--password', '-p', help='Password for RSA key file')
@click.option('--json', 'as_json', is_flag=True, help='Output as JSON')
def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
"""
Generate credentials for encoding/decoding.
Creates daily passphrases and optionally a PIN and/or RSA key.
At least one of --pin or --rsa must be enabled.
\b
Examples:
stegasoo generate
stegasoo generate --rsa --rsa-bits 4096
stegasoo generate --rsa -o mykey.pem -p "secretpassword"
stegasoo generate --no-pin --rsa
"""
if not pin and not rsa:
raise click.UsageError("Must enable at least one of --pin or --rsa")
if output and not password:
raise click.UsageError("--password is required when saving RSA key to file")
if password and len(password) < 8:
raise click.UsageError("Password must be at least 8 characters")
try:
creds = generate_credentials(
use_pin=pin,
use_rsa=rsa,
pin_length=pin_length,
rsa_bits=int(rsa_bits),
words_per_phrase=words
)
if as_json:
import json
data = {
'phrases': creds.phrases,
'pin': creds.pin,
'rsa_key': creds.rsa_key_pem,
'entropy': {
'phrase': creds.phrase_entropy,
'pin': creds.pin_entropy,
'rsa': creds.rsa_entropy,
'total': creds.total_entropy,
}
}
click.echo(json.dumps(data, indent=2))
return
# Pretty output
click.echo()
click.secho("" * 60, fg='cyan')
click.secho(" STEGASOO CREDENTIALS", fg='cyan', bold=True)
click.secho("" * 60, fg='cyan')
click.echo()
click.secho("⚠️ MEMORIZE THESE AND CLOSE THIS WINDOW", fg='yellow', bold=True)
click.secho(" Do not screenshot or save to file!", fg='yellow')
click.echo()
if creds.pin:
click.secho("─── STATIC PIN ───", fg='green')
click.secho(f" {creds.pin}", fg='bright_yellow', bold=True)
click.echo()
click.secho("─── DAILY PHRASES ───", fg='green')
for day in DAY_NAMES:
phrase = creds.phrases[day]
click.echo(f" {day:9}", nl=False)
click.secho(phrase, fg='bright_white')
click.echo()
if creds.rsa_key_pem:
click.secho("─── RSA KEY ───", fg='green')
if output:
# Save to file
private_key = load_rsa_key(creds.rsa_key_pem.encode())
encrypted_pem = export_rsa_key_pem(private_key, password)
Path(output).write_bytes(encrypted_pem)
click.secho(f" Saved to: {output}", fg='bright_white')
click.secho(f" Password: {'*' * len(password)}", fg='dim')
else:
click.echo(creds.rsa_key_pem)
click.echo()
click.secho("─── SECURITY ───", fg='green')
click.echo(f" Phrase entropy: {creds.phrase_entropy} bits")
if creds.pin:
click.echo(f" PIN entropy: {creds.pin_entropy} bits")
if creds.rsa_key_pem:
click.echo(f" RSA entropy: {creds.rsa_entropy} bits")
click.echo(f" Combined: {creds.total_entropy} bits")
click.secho(f" + photo entropy: 80-256 bits", fg='dim')
click.echo()
except Exception as e:
raise click.ClickException(str(e))
# ============================================================================
# ENCODE COMMAND
# ============================================================================
@cli.command()
@click.option('--ref', '-r', required=True, type=click.Path(exists=True), help='Reference photo')
@click.option('--carrier', '-c', required=True, type=click.Path(exists=True), help='Carrier image')
@click.option('--message', '-m', help='Message to encode (or use stdin)')
@click.option('--message-file', '-f', type=click.Path(exists=True), help='Read message from file')
@click.option('--phrase', '-p', required=True, help='Day phrase')
@click.option('--pin', help='Static PIN')
@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file')
@click.option('--key-password', help='RSA key password')
@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('--quiet', '-q', is_flag=True, help='Suppress output except errors')
def encode_cmd(ref, carrier, message, message_file, phrase, pin, key, key_password, output, date_str, quiet):
"""
Encode a secret message into an image.
Requires a reference photo, carrier image, and day phrase.
Must provide either --pin or --key (or both).
\b
Examples:
stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder" --pin 123456 -m "secret"
echo "secret" | stegasoo encode -r photo.jpg -c meme.png -p "word1 word2 word3" --pin 123456
stegasoo encode -r photo.jpg -c meme.png -p "words" -k mykey.pem --key-password "pass"
"""
# Get message
if message:
msg = message
elif message_file:
msg = Path(message_file).read_text()
elif not sys.stdin.isatty():
msg = sys.stdin.read()
else:
raise click.UsageError("Must provide message via -m, -f, or stdin")
# Load key if provided
rsa_key_data = None
if key:
rsa_key_data = Path(key).read_bytes()
# Validate security factors
if not pin and not rsa_key_data:
raise click.UsageError("Must provide --pin or --key (or both)")
try:
ref_photo = Path(ref).read_bytes()
carrier_image = Path(carrier).read_bytes()
result = encode(
message=msg,
reference_photo=ref_photo,
carrier_image=carrier_image,
day_phrase=phrase,
pin=pin or "",
rsa_key_data=rsa_key_data,
rsa_password=key_password,
date_str=date_str,
)
# Determine output path
if output:
out_path = Path(output)
else:
out_path = Path(result.filename)
# Write output
out_path.write_bytes(result.stego_image)
if not quiet:
click.secho(f"✓ Encoded successfully!", fg='green')
click.echo(f" Output: {out_path}")
click.echo(f" Size: {len(result.stego_image):,} bytes")
click.echo(f" Capacity used: {result.capacity_percent:.1f}%")
click.echo(f" Date: {result.date_used}")
except StegasooError as e:
raise click.ClickException(str(e))
except Exception as e:
raise click.ClickException(f"Error: {e}")
# ============================================================================
# DECODE COMMAND
# ============================================================================
@cli.command()
@click.option('--ref', '-r', required=True, type=click.Path(exists=True), help='Reference photo')
@click.option('--stego', '-s', required=True, type=click.Path(exists=True), help='Stego image')
@click.option('--phrase', '-p', required=True, help='Day phrase')
@click.option('--pin', help='Static PIN')
@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file')
@click.option('--key-password', help='RSA key password')
@click.option('--output', '-o', type=click.Path(), help='Save message to file')
@click.option('--quiet', '-q', is_flag=True, help='Output only the message')
def decode_cmd(ref, stego, phrase, pin, key, key_password, output, quiet):
"""
Decode a secret message from a stego image.
Must use the same credentials that were used for encoding.
\b
Examples:
stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder" --pin 123456
stegasoo decode -r photo.jpg -s stego.png -p "words" -k mykey.pem --key-password "pass"
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 -o message.txt
"""
# Load key if provided
rsa_key_data = None
if key:
rsa_key_data = Path(key).read_bytes()
# Validate security factors
if not pin and not rsa_key_data:
raise click.UsageError("Must provide --pin or --key (or both)")
try:
ref_photo = Path(ref).read_bytes()
stego_image = Path(stego).read_bytes()
message = decode(
stego_image=stego_image,
reference_photo=ref_photo,
day_phrase=phrase,
pin=pin or "",
rsa_key_data=rsa_key_data,
rsa_password=key_password,
)
if output:
Path(output).write_text(message)
if not quiet:
click.secho(f"✓ Decoded successfully!", fg='green')
click.echo(f" Saved to: {output}")
else:
if quiet:
click.echo(message)
else:
click.secho("✓ Decoded successfully!", fg='green')
click.echo()
click.echo(message)
except (DecryptionError, ExtractionError) as e:
raise click.ClickException(f"Decryption failed: {e}")
except StegasooError as e:
raise click.ClickException(str(e))
except Exception as e:
raise click.ClickException(f"Error: {e}")
# ============================================================================
# INFO COMMAND
# ============================================================================
@cli.command()
@click.argument('image', type=click.Path(exists=True))
def info(image):
"""
Show information about an image.
Displays dimensions, capacity, and attempts to detect date from filename.
"""
try:
image_data = Path(image).read_bytes()
result = validate_image(image_data, check_size=False)
if not result.is_valid:
raise click.ClickException(result.error_message)
capacity = calculate_capacity(image_data)
# Try to get date from filename
date_str = parse_date_from_filename(image)
day_name = get_day_from_date(date_str) if date_str else None
click.echo()
click.secho(f"Image: {image}", bold=True)
click.echo(f" Dimensions: {result.details['width']} × {result.details['height']}")
click.echo(f" Pixels: {result.details['pixels']:,}")
click.echo(f" Mode: {result.details['mode']}")
click.echo(f" Format: {result.details['format']}")
click.echo(f" Capacity: ~{capacity:,} bytes ({capacity // 1024} KB)")
if date_str:
click.echo(f" Embed date: {date_str} ({day_name})")
click.echo()
except Exception as e:
raise click.ClickException(str(e))
# ============================================================================
# MAIN
# ============================================================================
def main():
"""Entry point."""
cli()
if __name__ == '__main__':
main()

405
frontends/web/app.py Normal file
View File

@@ -0,0 +1,405 @@
#!/usr/bin/env python3
"""
Stegasoo Web Frontend
Flask-based web UI for steganography operations.
This is a thin wrapper around the stegasoo library.
"""
import io
import sys
import time
import secrets
from pathlib import Path
from datetime import datetime
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,
get_today_day, generate_filename,
DAY_NAMES, __version__,
StegasooError, DecryptionError, CapacityError,
has_argon2,
)
from stegasoo.constants import (
MAX_MESSAGE_SIZE, MIN_PIN_LENGTH, MAX_PIN_LENGTH,
VALID_RSA_SIZES,
)
# ============================================================================
# FLASK APP CONFIGURATION
# ============================================================================
app = Flask(__name__)
app.secret_key = secrets.token_hex(32)
app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024 # 5MB max upload
# Temporary file storage for sharing (file_id -> {data, timestamp, filename})
TEMP_FILES: dict[str, dict] = {}
TEMP_FILE_EXPIRY = 300 # 5 minutes
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)
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'}
# ============================================================================
# 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_ml=False)
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
)
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_ml=False
)
except Exception as e:
flash(f'Error generating credentials: {e}', 'error')
return render_template('generate.html', generated=False, has_ml=False)
return render_template('generate.html', generated=False, has_ml=False)
@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())
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('/encode', methods=['GET', 'POST'])
def encode_page():
day_of_week = get_today_day()
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')
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)
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)
# 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', '')
# Validate 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)
if not day_phrase:
flash('Day phrase is required', 'error')
return render_template('encode.html', day_of_week=day_of_week)
# Read files
ref_data = ref_photo.read()
carrier_data = carrier.read()
rsa_key_data = rsa_key_file.read() if rsa_key_file and rsa_key_file.filename else None
# 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)
# 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)
# Validate RSA key if provided
if rsa_key_data:
result = validate_rsa_key(rsa_key_data, rsa_password if rsa_password else None)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week)
# 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)
# 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=message,
reference_photo=ref_data,
carrier_image=carrier_data,
day_phrase=day_phrase,
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=rsa_password if rsa_password else None,
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)
except StegasooError as e:
flash(str(e), 'error')
return render_template('encode.html', day_of_week=day_of_week)
except Exception as e:
flash(f'Error: {e}', 'error')
return render_template('encode.html', day_of_week=day_of_week)
return render_template('encode.html', day_of_week=day_of_week)
@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]
return render_template('encode_result.html',
file_id=file_id,
filename=file_info['filename']
)
@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(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)
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')
# 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')
# Read files
ref_data = ref_photo.read()
stego_data = stego_image.read()
rsa_key_data = rsa_key_file.read() if rsa_key_file and rsa_key_file.filename else None
# 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')
# 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')
# Validate RSA key if provided
if rsa_key_data:
result = validate_rsa_key(rsa_key_data, rsa_password if rsa_password else None)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('decode.html')
# Decode
message = decode(
stego_image=stego_data,
reference_photo=ref_data,
day_phrase=day_phrase,
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=rsa_password if rsa_password else None
)
return render_template('decode.html', decoded_message=message)
except DecryptionError:
flash('Decryption failed. Check your phrase, PIN, RSA key, and reference photo.', 'error')
return render_template('decode.html')
except StegasooError as e:
flash(str(e), 'error')
return render_template('decode.html')
except Exception as e:
flash(f'Error: {e}', 'error')
return render_template('decode.html')
return render_template('decode.html')
@app.route('/about')
def about():
return render_template('about.html', has_argon2=has_argon2())
# ============================================================================
# MAIN
# ============================================================================
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)

View File

@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#667eea"/>
<stop offset="100%" style="stop-color:#764ba2"/>
</linearGradient>
</defs>
<path d="M32 4 L56 14 L56 32 C56 48 44 58 32 62 C20 58 8 48 8 32 L8 14 Z" fill="url(#grad)"/>
<rect x="16" y="18" width="32" height="24" rx="2" fill="#1a1a2e" stroke="#fff" stroke-width="1.5"/>
<polygon points="16,42 26,30 34,36 48,22 48,42" fill="#667eea" opacity="0.5"/>
<rect x="24" y="30" width="16" height="12" rx="2" fill="#fff"/>
<path d="M27 30 L27 25 C27 20 37 20 37 25 L37 30" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
<circle cx="32" cy="35" r="2.5" fill="url(#grad)"/>
<rect x="31" y="35" width="2" height="4" fill="url(#grad)"/>
</svg>

After

Width:  |  Height:  |  Size: 866 B

View File

@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<defs>
<linearGradient id="shieldGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#667eea"/>
<stop offset="100%" style="stop-color:#764ba2"/>
</linearGradient>
<linearGradient id="photoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#1a1a2e"/>
<stop offset="100%" style="stop-color:#16213e"/>
</linearGradient>
</defs>
<circle cx="100" cy="100" r="95" fill="none" stroke="url(#shieldGrad)" stroke-width="2" opacity="0.3"/>
<path d="M100 20 L170 45 L170 100 C170 145 140 175 100 185 C60 175 30 145 30 100 L30 45 Z" fill="url(#shieldGrad)" opacity="0.95"/>
<rect x="50" y="55" width="100" height="75" rx="4" ry="4" fill="url(#photoGrad)" stroke="#fff" stroke-width="2" opacity="0.9"/>
<polygon points="50,130 75,95 95,115 130,75 150,130" fill="#667eea" opacity="0.6"/>
<circle cx="125" cy="75" r="12" fill="#ffd700" opacity="0.8"/>
<g transform="translate(100, 105)">
<rect x="-18" y="-5" width="36" height="28" rx="4" ry="4" fill="#fff" opacity="0.95"/>
<path d="M-10 -5 L-10 -18 C-10 -30 10 -30 10 -18 L10 -5" fill="none" stroke="#fff" stroke-width="6" stroke-linecap="round" opacity="0.95"/>
<circle cx="0" cy="8" r="5" fill="url(#shieldGrad)"/>
<rect x="-2.5" y="8" width="5" height="10" rx="1" fill="url(#shieldGrad)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

105
pyproject.toml Normal file
View File

@@ -0,0 +1,105 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "stegasoo"
version = "2.0.0"
description = "Secure steganography with hybrid photo + passphrase + PIN authentication"
readme = "README.md"
license = "MIT"
requires-python = ">=3.10"
authors = [
{ name = "Aaron D. Lee" }
]
keywords = [
"steganography",
"encryption",
"security",
"privacy",
"cryptography",
"image",
]
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
"Environment :: Web Environment",
"Intended Audience :: Developers",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Security :: Cryptography",
"Topic :: Multimedia :: Graphics",
]
dependencies = [
"pillow>=10.0.0",
"cryptography>=41.0.0",
"argon2-cffi>=23.0.0",
]
[project.optional-dependencies]
cli = [
"click>=8.0.0",
]
web = [
"flask>=3.0.0",
"gunicorn>=21.0.0",
]
api = [
"fastapi>=0.100.0",
"uvicorn[standard]>=0.20.0",
"python-multipart>=0.0.6",
]
all = [
"stegasoo[cli,web,api]",
]
dev = [
"stegasoo[all]",
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"black>=23.0.0",
"ruff>=0.1.0",
"mypy>=1.0.0",
]
[project.scripts]
stegasoo = "stegasoo.cli:main"
[project.urls]
Homepage = "https://github.com/example/stegasoo"
Documentation = "https://github.com/example/stegasoo#readme"
Repository = "https://github.com/example/stegasoo"
[tool.hatch.build.targets.sdist]
include = [
"/src",
"/data",
]
[tool.hatch.build.targets.wheel]
packages = ["src/stegasoo"]
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
addopts = "-v --cov=stegasoo --cov-report=term-missing"
[tool.black]
line-length = 100
target-version = ["py310", "py311", "py312"]
[tool.ruff]
line-length = 100
select = ["E", "F", "I", "N", "W", "UP"]
ignore = ["E501"]
[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
ignore_missing_imports = true

View File

@@ -1,16 +1,16 @@
# Core dependencies
flask>=3.0.0
gunicorn>=21.0.0
pillow>=10.0.0
cryptography>=41.0.0
# Memory-hard key derivation (highly recommended)
argon2-cffi>=23.0.0
# Optional: ML story generation (adds ~1GB disk space)
# Uncomment for AI-powered memory aid stories
# transformers>=4.35.0
# torch>=2.0.0
# CLI (optional)
click>=8.0.0
# Optional: For production deployment
# gevent>=23.0.0
# Web UI (optional)
flask>=3.0.0
gunicorn>=21.0.0
# REST API (optional)
# fastapi>=0.100.0
# uvicorn[standard]>=0.20.0
# python-multipart>=0.0.6

11
src/main.py Normal file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env python3
"""Main entry point."""
def main():
"""Main function."""
print("Hello, World!")
if __name__ == "__main__":
main()

357
src/stegasoo/__init__.py Normal file
View File

@@ -0,0 +1,357 @@
"""
Stegasoo - Secure Steganography Library
A Python library for hiding encrypted messages in images using
hybrid photo + passphrase + PIN authentication.
Basic Usage:
from stegasoo import encode, decode, generate_credentials
# Generate credentials
creds = generate_credentials(use_pin=True, use_rsa=False)
print(creds.phrases['Monday'])
print(creds.pin)
# Encode a message
with open('secret.jpg', 'rb') as f:
ref_photo = f.read()
with open('meme.png', 'rb') as f:
carrier = f.read()
result = encode(
message="Meet at midnight",
reference_photo=ref_photo,
carrier_image=carrier,
day_phrase="apple forest thunder",
pin="123456"
)
with open('stego.png', 'wb') as f:
f.write(result.stego_image)
# Decode a message
message = decode(
stego_image=result.stego_image,
reference_photo=ref_photo,
day_phrase="apple forest thunder",
pin="123456"
)
print(message) # "Meet at midnight"
"""
from .constants import __version__, DAY_NAMES
from .models import (
Credentials,
EncodeInput,
EncodeResult,
DecodeInput,
DecodeResult,
EmbedStats,
KeyInfo,
ValidationResult,
)
from .exceptions import (
StegasooError,
ValidationError,
PinValidationError,
MessageValidationError,
ImageValidationError,
KeyValidationError,
SecurityFactorError,
CryptoError,
EncryptionError,
DecryptionError,
KeyDerivationError,
KeyGenerationError,
KeyPasswordError,
SteganographyError,
CapacityError,
ExtractionError,
EmbeddingError,
InvalidHeaderError,
)
from .keygen import (
generate_credentials,
generate_pin,
generate_phrase,
generate_day_phrases,
generate_rsa_key,
export_rsa_key_pem,
load_rsa_key,
get_key_info,
)
from .validation import (
validate_pin,
validate_message,
validate_image,
validate_rsa_key,
validate_security_factors,
validate_phrase,
validate_date_string,
require_valid_pin,
require_valid_message,
require_valid_image,
require_valid_rsa_key,
require_security_factors,
)
from .crypto import (
encrypt_message,
decrypt_message,
derive_hybrid_key,
derive_pixel_key,
hash_photo,
parse_header,
get_date_from_encrypted,
has_argon2,
)
from .steganography import (
embed_in_image,
extract_from_image,
calculate_capacity,
get_image_dimensions,
)
from .utils import (
generate_filename,
parse_date_from_filename,
get_day_from_date,
get_today_date,
get_today_day,
secure_delete,
SecureDeleter,
format_file_size,
)
from datetime import date
from typing import Optional
def encode(
message: str,
reference_photo: bytes,
carrier_image: bytes,
day_phrase: str,
pin: str = "",
rsa_key_data: Optional[bytes] = None,
rsa_password: Optional[str] = None,
date_str: Optional[str] = None,
) -> EncodeResult:
"""
Encode a secret message into an image.
High-level convenience function that handles validation,
encryption, and embedding in one call.
Args:
message: Secret message to hide
reference_photo: Shared reference photo bytes
carrier_image: Image to hide message in
day_phrase: Today's passphrase
pin: Static PIN (optional if using RSA key)
rsa_key_data: RSA private key PEM bytes (optional if using PIN)
rsa_password: Password for RSA key if encrypted
date_str: Date string YYYY-MM-DD (defaults to today)
Returns:
EncodeResult with stego image and metadata
Raises:
ValidationError: If inputs are invalid
SecurityFactorError: If no PIN or RSA key provided
CapacityError: If carrier is too small
EncryptionError: If encryption fails
"""
# Validate inputs
require_valid_message(message)
require_valid_image(carrier_image, "Carrier image")
require_security_factors(pin, rsa_key_data)
if pin:
require_valid_pin(pin)
if rsa_key_data:
require_valid_rsa_key(rsa_key_data, rsa_password)
# Default date to today
if date_str is None:
date_str = date.today().isoformat()
# Encrypt message
encrypted = encrypt_message(
message, reference_photo, day_phrase, date_str, pin, rsa_key_data
)
# Get pixel key
pixel_key = derive_pixel_key(
reference_photo, day_phrase, date_str, pin, rsa_key_data
)
# Embed in image
stego_data, stats = embed_in_image(carrier_image, encrypted, pixel_key)
# Generate filename
filename = generate_filename(date_str)
return EncodeResult(
stego_image=stego_data,
filename=filename,
pixels_modified=stats.pixels_modified,
total_pixels=stats.total_pixels,
capacity_used=stats.capacity_used,
date_used=date_str
)
def decode(
stego_image: bytes,
reference_photo: bytes,
day_phrase: str,
pin: str = "",
rsa_key_data: Optional[bytes] = None,
rsa_password: Optional[str] = None,
) -> str:
"""
Decode a secret message from a stego image.
High-level convenience function that handles extraction
and decryption in one call.
Args:
stego_image: Image containing hidden message
reference_photo: Shared reference photo bytes
day_phrase: Passphrase for the day message was encoded
pin: Static PIN (if used during encoding)
rsa_key_data: RSA private key PEM bytes (if used during encoding)
rsa_password: Password for RSA key if encrypted
Returns:
Decrypted message string
Raises:
ValidationError: If inputs are invalid
SecurityFactorError: If no PIN or RSA key provided
ExtractionError: If data cannot be extracted
DecryptionError: If decryption fails
"""
# Validate inputs
require_security_factors(pin, rsa_key_data)
if pin:
require_valid_pin(pin)
if rsa_key_data:
require_valid_rsa_key(rsa_key_data, rsa_password)
# Try to extract with today's date first
date_str = date.today().isoformat()
pixel_key = derive_pixel_key(
reference_photo, day_phrase, date_str, pin, rsa_key_data
)
encrypted = extract_from_image(stego_image, pixel_key)
# If we got data, check if it's from a different date
if encrypted:
header = parse_header(encrypted)
if header and header['date'] != date_str:
# Re-extract with correct date
pixel_key = derive_pixel_key(
reference_photo, day_phrase, header['date'], pin, rsa_key_data
)
encrypted = extract_from_image(stego_image, pixel_key)
if not encrypted:
raise ExtractionError("Could not extract data. Check your inputs.")
# Decrypt
return decrypt_message(encrypted, reference_photo, day_phrase, pin, rsa_key_data)
__all__ = [
# Version
'__version__',
# High-level API
'encode',
'decode',
'generate_credentials',
# Constants
'DAY_NAMES',
# Models
'Credentials',
'EncodeInput',
'EncodeResult',
'DecodeInput',
'DecodeResult',
'EmbedStats',
'KeyInfo',
'ValidationResult',
# Exceptions
'StegasooError',
'ValidationError',
'PinValidationError',
'MessageValidationError',
'ImageValidationError',
'KeyValidationError',
'SecurityFactorError',
'CryptoError',
'EncryptionError',
'DecryptionError',
'KeyDerivationError',
'KeyGenerationError',
'KeyPasswordError',
'SteganographyError',
'CapacityError',
'ExtractionError',
'EmbeddingError',
'InvalidHeaderError',
# Key generation
'generate_pin',
'generate_phrase',
'generate_day_phrases',
'generate_rsa_key',
'export_rsa_key_pem',
'load_rsa_key',
'get_key_info',
# Validation
'validate_pin',
'validate_message',
'validate_image',
'validate_rsa_key',
'validate_security_factors',
'validate_phrase',
'validate_date_string',
'require_valid_pin',
'require_valid_message',
'require_valid_image',
'require_valid_rsa_key',
'require_security_factors',
# Crypto
'encrypt_message',
'decrypt_message',
'derive_hybrid_key',
'derive_pixel_key',
'hash_photo',
'parse_header',
'get_date_from_encrypted',
'has_argon2',
# Steganography
'embed_in_image',
'extract_from_image',
'calculate_capacity',
'get_image_dimensions',
# Utilities
'generate_filename',
'parse_date_from_filename',
'get_day_from_date',
'get_today_date',
'get_today_day',
'secure_delete',
'SecureDeleter',
'format_file_size',
]

66
src/stegasoo/cli.py Normal file
View File

@@ -0,0 +1,66 @@
"""
Stegasoo CLI - Command-line interface for steganography operations.
This is the package entry point. For full CLI, install with: pip install stegasoo[cli]
"""
def main():
"""Main entry point for the CLI."""
try:
import click
except ImportError:
print("CLI requires click. Install with: pip install stegasoo[cli]")
return 1
# Import the CLI from frontends
import sys
from pathlib import Path
# Add frontends to path for development
root = Path(__file__).parent.parent.parent
cli_path = root / 'frontends' / 'cli'
if cli_path.exists():
sys.path.insert(0, str(cli_path))
try:
from main import cli
cli()
except ImportError:
# Minimal fallback CLI
_minimal_cli()
def _minimal_cli():
"""Minimal CLI when full CLI is not available."""
import sys
from . import __version__, generate_credentials, DAY_NAMES
if len(sys.argv) < 2 or sys.argv[1] in ['-h', '--help']:
print(f"Stegasoo v{__version__} - Secure Steganography")
print()
print("Usage: stegasoo <command>")
print()
print("Commands:")
print(" generate Generate credentials")
print(" encode Encode a message (requires full CLI)")
print(" decode Decode a message (requires full CLI)")
print()
print("For full CLI functionality:")
print(" pip install stegasoo[cli]")
return
if sys.argv[1] == 'generate':
creds = generate_credentials(use_pin=True, use_rsa=False)
print("\n=== STEGASOO CREDENTIALS ===\n")
print(f"PIN: {creds.pin}\n")
print("Daily Phrases:")
for day in DAY_NAMES:
print(f" {day:9} | {creds.phrases[day]}")
print(f"\nEntropy: {creds.total_entropy} bits (+ photo)")
else:
print(f"Command '{sys.argv[1]}' requires full CLI.")
print("Install with: pip install stegasoo[cli]")
if __name__ == '__main__':
main()

119
src/stegasoo/constants.py Normal file
View File

@@ -0,0 +1,119 @@
"""
Stegasoo Constants and Configuration
Central location for all magic numbers, limits, and crypto parameters.
"""
import os
from pathlib import Path
# ============================================================================
# VERSION
# ============================================================================
__version__ = "2.0.0"
# ============================================================================
# FILE FORMAT
# ============================================================================
MAGIC_HEADER = b'\x89ST3'
FORMAT_VERSION = 3
# ============================================================================
# CRYPTO PARAMETERS
# ============================================================================
SALT_SIZE = 32
IV_SIZE = 12
TAG_SIZE = 16
# Argon2 parameters (memory-hard KDF)
ARGON2_TIME_COST = 4
ARGON2_MEMORY_COST = 256 * 1024 # 256 MB
ARGON2_PARALLELISM = 4
# PBKDF2 fallback parameters
PBKDF2_ITERATIONS = 600000
# ============================================================================
# INPUT LIMITS
# ============================================================================
MAX_IMAGE_PIXELS = 4_000_000 # ~4 megapixels (2000x2000)
MAX_MESSAGE_SIZE = 50_000 # 50 KB
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB
MIN_PIN_LENGTH = 6
MAX_PIN_LENGTH = 9
DEFAULT_PIN_LENGTH = 6
MIN_PHRASE_WORDS = 3
MAX_PHRASE_WORDS = 12
DEFAULT_PHRASE_WORDS = 3
MIN_RSA_BITS = 2048
VALID_RSA_SIZES = (2048, 3072, 4096)
DEFAULT_RSA_BITS = 2048
MIN_KEY_PASSWORD_LENGTH = 8
# ============================================================================
# FILE TYPES
# ============================================================================
ALLOWED_IMAGE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'bmp', 'gif'}
ALLOWED_KEY_EXTENSIONS = {'pem', 'key'}
# ============================================================================
# DAYS
# ============================================================================
DAY_NAMES = ('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday')
# ============================================================================
# DATA FILES
# ============================================================================
def get_data_dir() -> Path:
"""Get the data directory path."""
# Check multiple locations
candidates = [
Path(__file__).parent.parent.parent.parent / 'data', # Development
Path(__file__).parent / 'data', # Installed package
Path('/app/data'), # Docker
Path.cwd() / 'data', # Current directory
]
for path in candidates:
if path.exists():
return path
# Default to first candidate
return candidates[0]
def get_bip39_words() -> list[str]:
"""Load BIP-39 wordlist."""
wordlist_path = get_data_dir() / 'bip39-words.txt'
if not wordlist_path.exists():
raise FileNotFoundError(
f"BIP-39 wordlist not found at {wordlist_path}. "
"Please ensure bip39-words.txt is in the data directory."
)
with open(wordlist_path, 'r') as f:
return [line.strip() for line in f if line.strip()]
# Lazy-loaded wordlist
_bip39_words: list[str] | None = None
def get_wordlist() -> list[str]:
"""Get the BIP-39 wordlist (cached)."""
global _bip39_words
if _bip39_words is None:
_bip39_words = get_bip39_words()
return _bip39_words

358
src/stegasoo/crypto.py Normal file
View File

@@ -0,0 +1,358 @@
"""
Stegasoo Cryptographic Functions
Key derivation, encryption, and decryption using AES-256-GCM.
"""
import io
import hashlib
import secrets
import struct
from typing import Optional
from PIL import Image
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from .constants import (
MAGIC_HEADER, FORMAT_VERSION,
SALT_SIZE, IV_SIZE, TAG_SIZE,
ARGON2_TIME_COST, ARGON2_MEMORY_COST, ARGON2_PARALLELISM,
PBKDF2_ITERATIONS,
)
from .exceptions import (
EncryptionError, DecryptionError, KeyDerivationError, InvalidHeaderError
)
# Check for Argon2 availability
try:
from argon2.low_level import hash_secret_raw, Type
HAS_ARGON2 = True
except ImportError:
HAS_ARGON2 = False
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
def hash_photo(image_data: bytes) -> bytes:
"""
Compute deterministic hash of photo pixel content.
This normalizes the image to RGB and hashes the raw pixel data,
making it resistant to metadata changes.
Args:
image_data: Raw image file bytes
Returns:
32-byte SHA-256 hash
"""
img = Image.open(io.BytesIO(image_data))
img = img.convert('RGB')
pixels = img.tobytes()
# Double-hash with prefix for additional mixing
h = hashlib.sha256(pixels).digest()
h = hashlib.sha256(h + pixels[:1024]).digest()
return h
def derive_hybrid_key(
photo_data: bytes,
day_phrase: str,
date_str: str,
salt: bytes,
pin: str = "",
rsa_key_data: Optional[bytes] = None
) -> bytes:
"""
Derive encryption key from multiple factors.
Combines:
- Photo hash (something you have)
- Day phrase (something you know, rotates daily)
- PIN (something you know, static)
- RSA key (something you have)
- Date (automatic rotation)
- Salt (random per message)
Uses Argon2id if available, falls back to PBKDF2.
Args:
photo_data: Reference photo bytes
day_phrase: The day's phrase
date_str: Date string (YYYY-MM-DD)
salt: Random salt for this message
pin: Optional static PIN
rsa_key_data: Optional RSA key bytes
Returns:
32-byte derived key
Raises:
KeyDerivationError: If key derivation fails
"""
try:
photo_hash = hash_photo(photo_data)
key_material = (
photo_hash +
day_phrase.lower().encode() +
pin.encode() +
date_str.encode() +
salt
)
# Add RSA key hash if provided
if rsa_key_data:
key_material += hashlib.sha256(rsa_key_data).digest()
if HAS_ARGON2:
key = hash_secret_raw(
secret=key_material,
salt=salt[:32],
time_cost=ARGON2_TIME_COST,
memory_cost=ARGON2_MEMORY_COST,
parallelism=ARGON2_PARALLELISM,
hash_len=32,
type=Type.ID
)
else:
kdf = PBKDF2HMAC(
algorithm=hashes.SHA512(),
length=32,
salt=salt,
iterations=PBKDF2_ITERATIONS,
backend=default_backend()
)
key = kdf.derive(key_material)
return key
except Exception as e:
raise KeyDerivationError(f"Failed to derive key: {e}") from e
def derive_pixel_key(
photo_data: bytes,
day_phrase: str,
date_str: str,
pin: str = "",
rsa_key_data: Optional[bytes] = None
) -> bytes:
"""
Derive key for pseudo-random pixel selection.
This key determines which pixels are used for embedding,
making the message location unpredictable without the correct inputs.
Args:
photo_data: Reference photo bytes
day_phrase: The day's phrase
date_str: Date string (YYYY-MM-DD)
pin: Optional static PIN
rsa_key_data: Optional RSA key bytes
Returns:
32-byte key for pixel selection
"""
photo_hash = hash_photo(photo_data)
material = (
photo_hash +
day_phrase.lower().encode() +
pin.encode() +
date_str.encode()
)
if rsa_key_data:
material += hashlib.sha256(rsa_key_data).digest()
return hashlib.sha256(material + b"pixel_selection").digest()
def encrypt_message(
message: str | bytes,
photo_data: bytes,
day_phrase: str,
date_str: str,
pin: str = "",
rsa_key_data: Optional[bytes] = None
) -> bytes:
"""
Encrypt message using AES-256-GCM with hybrid key derivation.
Message format:
- Magic header (4 bytes)
- Version (1 byte)
- Date length (1 byte)
- Date string (variable)
- Salt (32 bytes)
- IV (12 bytes)
- Auth tag (16 bytes)
- Ciphertext (variable, padded)
Args:
message: Message to encrypt
photo_data: Reference photo bytes
day_phrase: The day's phrase
date_str: Date string (YYYY-MM-DD)
pin: Optional static PIN
rsa_key_data: Optional RSA key bytes
Returns:
Encrypted message bytes
Raises:
EncryptionError: If encryption fails
"""
try:
salt = secrets.token_bytes(SALT_SIZE)
key = derive_hybrid_key(photo_data, day_phrase, date_str, salt, pin, rsa_key_data)
iv = secrets.token_bytes(IV_SIZE)
if isinstance(message, str):
message = message.encode('utf-8')
# Random padding to hide message length
padding_len = secrets.randbelow(256) + 64
padded_len = ((len(message) + padding_len + 255) // 256) * 256
padding_needed = padded_len - len(message)
padding = secrets.token_bytes(padding_needed - 4) + struct.pack('>I', len(message))
padded_message = message + padding
# Encrypt with AES-256-GCM
cipher = Cipher(algorithms.AES(key), modes.GCM(iv), backend=default_backend())
encryptor = cipher.encryptor()
encryptor.authenticate_additional_data(MAGIC_HEADER + bytes([FORMAT_VERSION]))
ciphertext = encryptor.update(padded_message) + encryptor.finalize()
date_bytes = date_str.encode()
return (
MAGIC_HEADER +
bytes([FORMAT_VERSION]) +
bytes([len(date_bytes)]) +
date_bytes +
salt +
iv +
encryptor.tag +
ciphertext
)
except Exception as e:
raise EncryptionError(f"Encryption failed: {e}") from e
def parse_header(encrypted_data: bytes) -> Optional[dict]:
"""
Parse the header from encrypted data.
Args:
encrypted_data: Raw encrypted bytes
Returns:
Dict with date, salt, iv, tag, ciphertext or None if invalid
"""
if len(encrypted_data) < 10 or encrypted_data[:4] != MAGIC_HEADER:
return None
try:
version = encrypted_data[4]
if version != FORMAT_VERSION:
return None
date_len = encrypted_data[5]
date_str = encrypted_data[6:6 + date_len].decode()
offset = 6 + date_len
salt = encrypted_data[offset:offset + SALT_SIZE]
offset += SALT_SIZE
iv = encrypted_data[offset:offset + IV_SIZE]
offset += IV_SIZE
tag = encrypted_data[offset:offset + TAG_SIZE]
offset += TAG_SIZE
ciphertext = encrypted_data[offset:]
return {
'date': date_str,
'salt': salt,
'iv': iv,
'tag': tag,
'ciphertext': ciphertext
}
except Exception:
return None
def decrypt_message(
encrypted_data: bytes,
photo_data: bytes,
day_phrase: str,
pin: str = "",
rsa_key_data: Optional[bytes] = None
) -> str:
"""
Decrypt message using the embedded date from the header.
Args:
encrypted_data: Encrypted message bytes
photo_data: Reference photo bytes
day_phrase: The day's phrase (must match encoding day)
pin: Optional static PIN
rsa_key_data: Optional RSA key bytes
Returns:
Decrypted message string
Raises:
InvalidHeaderError: If data doesn't have valid Stegasoo header
DecryptionError: If decryption fails (wrong credentials)
"""
header = parse_header(encrypted_data)
if not header:
raise InvalidHeaderError("Invalid or missing Stegasoo header")
try:
key = derive_hybrid_key(
photo_data, day_phrase, header['date'], header['salt'], pin, rsa_key_data
)
cipher = Cipher(
algorithms.AES(key),
modes.GCM(header['iv'], header['tag']),
backend=default_backend()
)
decryptor = cipher.decryptor()
decryptor.authenticate_additional_data(MAGIC_HEADER + bytes([FORMAT_VERSION]))
padded_plaintext = decryptor.update(header['ciphertext']) + decryptor.finalize()
original_length = struct.unpack('>I', padded_plaintext[-4:])[0]
return padded_plaintext[:original_length].decode('utf-8')
except Exception as e:
raise DecryptionError(
"Decryption failed. Check your phrase, PIN, RSA key, and reference photo."
) from e
def get_date_from_encrypted(encrypted_data: bytes) -> Optional[str]:
"""
Extract the date string from encrypted data without decrypting.
Useful for determining which day's phrase to use.
Args:
encrypted_data: Encrypted message bytes
Returns:
Date string (YYYY-MM-DD) or None if invalid
"""
header = parse_header(encrypted_data)
return header['date'] if header else None
def has_argon2() -> bool:
"""Check if Argon2 is available."""
return HAS_ARGON2

150
src/stegasoo/exceptions.py Normal file
View File

@@ -0,0 +1,150 @@
"""
Stegasoo Exceptions
Custom exception classes for clear error handling across all frontends.
"""
class StegasooError(Exception):
"""Base exception for all Stegasoo errors."""
pass
# ============================================================================
# VALIDATION ERRORS
# ============================================================================
class ValidationError(StegasooError):
"""Base class for validation errors."""
pass
class PinValidationError(ValidationError):
"""PIN validation failed."""
pass
class MessageValidationError(ValidationError):
"""Message validation failed."""
pass
class ImageValidationError(ValidationError):
"""Image validation failed."""
pass
class KeyValidationError(ValidationError):
"""RSA key validation failed."""
pass
class SecurityFactorError(ValidationError):
"""Security factor requirements not met."""
pass
# ============================================================================
# CRYPTO ERRORS
# ============================================================================
class CryptoError(StegasooError):
"""Base class for cryptographic errors."""
pass
class EncryptionError(CryptoError):
"""Encryption failed."""
pass
class DecryptionError(CryptoError):
"""Decryption failed (wrong key, corrupted data, etc.)."""
pass
class KeyDerivationError(CryptoError):
"""Key derivation failed."""
pass
class KeyGenerationError(CryptoError):
"""Key generation failed."""
pass
class KeyPasswordError(CryptoError):
"""RSA key password is incorrect or missing."""
pass
# ============================================================================
# STEGANOGRAPHY ERRORS
# ============================================================================
class SteganographyError(StegasooError):
"""Base class for steganography errors."""
pass
class CapacityError(SteganographyError):
"""Carrier image too small for message."""
def __init__(self, needed: int, available: int):
self.needed = needed
self.available = available
super().__init__(
f"Carrier image too small. Need {needed:,} bytes, have {available:,} bytes capacity."
)
class ExtractionError(SteganographyError):
"""Failed to extract hidden data from image."""
pass
class EmbeddingError(SteganographyError):
"""Failed to embed data in image."""
pass
class InvalidHeaderError(SteganographyError):
"""Invalid or missing Stegasoo header in extracted data."""
pass
# ============================================================================
# FILE ERRORS
# ============================================================================
class FileError(StegasooError):
"""Base class for file-related errors."""
pass
class FileNotFoundError(FileError):
"""Required file not found."""
pass
class FileTooLargeError(FileError):
"""File exceeds size limit."""
def __init__(self, size: int, limit: int, filename: str = "File"):
self.size = size
self.limit = limit
self.filename = filename
super().__init__(
f"{filename} too large ({size:,} bytes). Maximum allowed: {limit:,} bytes."
)
class UnsupportedFileTypeError(FileError):
"""File type not supported."""
def __init__(self, extension: str, allowed: set[str]):
self.extension = extension
self.allowed = allowed
super().__init__(
f"Unsupported file type: .{extension}. Allowed: {', '.join(sorted(allowed))}"
)

228
src/stegasoo/keygen.py Normal file
View File

@@ -0,0 +1,228 @@
"""
Stegasoo Key Generation
Generate PINs, passphrases, and RSA keys.
"""
import secrets
from typing import Optional
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.hazmat.backends import default_backend
from .constants import (
DAY_NAMES,
MIN_PIN_LENGTH, MAX_PIN_LENGTH, DEFAULT_PIN_LENGTH,
MIN_PHRASE_WORDS, MAX_PHRASE_WORDS, DEFAULT_PHRASE_WORDS,
MIN_RSA_BITS, VALID_RSA_SIZES, DEFAULT_RSA_BITS,
get_wordlist,
)
from .models import Credentials, KeyInfo
from .exceptions import KeyGenerationError, KeyPasswordError
def generate_pin(length: int = DEFAULT_PIN_LENGTH) -> str:
"""
Generate a random PIN.
PINs never start with zero for usability.
Args:
length: PIN length (6-9 digits)
Returns:
PIN string
"""
length = max(MIN_PIN_LENGTH, min(MAX_PIN_LENGTH, length))
# First digit: 1-9 (no leading zero)
first_digit = str(secrets.randbelow(9) + 1)
# Remaining digits: 0-9
rest = ''.join(str(secrets.randbelow(10)) for _ in range(length - 1))
return first_digit + rest
def generate_phrase(words_per_phrase: int = DEFAULT_PHRASE_WORDS) -> str:
"""
Generate a random passphrase from BIP-39 wordlist.
Args:
words_per_phrase: Number of words (3-12)
Returns:
Space-separated phrase
"""
words_per_phrase = max(MIN_PHRASE_WORDS, min(MAX_PHRASE_WORDS, words_per_phrase))
wordlist = get_wordlist()
words = [secrets.choice(wordlist) for _ in range(words_per_phrase)]
return ' '.join(words)
def generate_day_phrases(words_per_phrase: int = DEFAULT_PHRASE_WORDS) -> dict[str, str]:
"""
Generate phrases for all days of the week.
Args:
words_per_phrase: Number of words per phrase (3-12)
Returns:
Dict mapping day names to phrases
"""
return {day: generate_phrase(words_per_phrase) for day in DAY_NAMES}
def generate_rsa_key(bits: int = DEFAULT_RSA_BITS) -> rsa.RSAPrivateKey:
"""
Generate an RSA private key.
Args:
bits: Key size (2048, 3072, or 4096)
Returns:
RSA private key object
Raises:
KeyGenerationError: If generation fails
"""
if bits not in VALID_RSA_SIZES:
bits = DEFAULT_RSA_BITS
try:
return rsa.generate_private_key(
public_exponent=65537,
key_size=bits,
backend=default_backend()
)
except Exception as e:
raise KeyGenerationError(f"Failed to generate RSA key: {e}") from e
def export_rsa_key_pem(
private_key: rsa.RSAPrivateKey,
password: Optional[str] = None
) -> bytes:
"""
Export RSA key to PEM format.
Args:
private_key: RSA private key object
password: Optional password for encryption
Returns:
PEM-encoded key bytes
"""
if password:
encryption = serialization.BestAvailableEncryption(password.encode())
else:
encryption = serialization.NoEncryption()
return private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=encryption
)
def load_rsa_key(
key_data: bytes,
password: Optional[str] = None
) -> rsa.RSAPrivateKey:
"""
Load RSA private key from PEM data.
Args:
key_data: PEM-encoded key bytes
password: Password if key is encrypted
Returns:
RSA private key object
Raises:
KeyPasswordError: If password is wrong or missing
KeyGenerationError: If key is invalid
"""
try:
pwd_bytes = password.encode() if password else None
return load_pem_private_key(key_data, password=pwd_bytes, backend=default_backend())
except TypeError:
raise KeyPasswordError("RSA key is password-protected. Please provide the password.")
except ValueError as e:
if "password" in str(e).lower() or "encrypted" in str(e).lower():
raise KeyPasswordError("Incorrect password for RSA key.")
raise KeyGenerationError(f"Invalid RSA key: {e}") from e
except Exception as e:
raise KeyGenerationError(f"Could not load RSA key: {e}") from e
def get_key_info(key_data: bytes, password: Optional[str] = None) -> KeyInfo:
"""
Get information about an RSA key.
Args:
key_data: PEM-encoded key bytes
password: Password if key is encrypted
Returns:
KeyInfo with key size and encryption status
"""
# Check if encrypted
is_encrypted = b'ENCRYPTED' in key_data
private_key = load_rsa_key(key_data, password)
return KeyInfo(
key_size=private_key.key_size,
is_encrypted=is_encrypted,
pem_data=key_data
)
def generate_credentials(
use_pin: bool = True,
use_rsa: bool = False,
pin_length: int = DEFAULT_PIN_LENGTH,
rsa_bits: int = DEFAULT_RSA_BITS,
words_per_phrase: int = DEFAULT_PHRASE_WORDS
) -> Credentials:
"""
Generate a complete set of credentials.
At least one of use_pin or use_rsa must be True.
Args:
use_pin: Whether to generate a PIN
use_rsa: Whether to generate an RSA key
pin_length: PIN length if generating
rsa_bits: RSA key size if generating
words_per_phrase: Words per daily phrase
Returns:
Credentials object
Raises:
ValueError: If neither PIN nor RSA is selected
"""
if not use_pin and not use_rsa:
raise ValueError("Must select at least one security factor (PIN or RSA key)")
phrases = generate_day_phrases(words_per_phrase)
pin = generate_pin(pin_length) if use_pin else None
rsa_key_pem = None
if use_rsa:
private_key = generate_rsa_key(rsa_bits)
rsa_key_pem = export_rsa_key_pem(private_key).decode('utf-8')
return Credentials(
phrases=phrases,
pin=pin,
rsa_key_pem=rsa_key_pem,
rsa_bits=rsa_bits if use_rsa else None,
words_per_phrase=words_per_phrase
)

134
src/stegasoo/models.py Normal file
View File

@@ -0,0 +1,134 @@
"""
Stegasoo Data Models
Dataclasses for structured data exchange between modules and frontends.
"""
from dataclasses import dataclass, field
from datetime import date
from typing import Optional
@dataclass
class Credentials:
"""Generated credentials for encoding/decoding."""
phrases: dict[str, str] # Day -> phrase mapping
pin: Optional[str] = None
rsa_key_pem: Optional[str] = None
rsa_bits: Optional[int] = None
words_per_phrase: int = 3
@property
def phrase_entropy(self) -> int:
"""Entropy in bits from phrases (~11 bits per BIP-39 word)."""
return self.words_per_phrase * 11
@property
def pin_entropy(self) -> int:
"""Entropy in bits from PIN (~3.32 bits per digit)."""
if self.pin:
return int(len(self.pin) * 3.32)
return 0
@property
def rsa_entropy(self) -> int:
"""Effective entropy from RSA key."""
if self.rsa_key_pem and self.rsa_bits:
return min(self.rsa_bits // 16, 128)
return 0
@property
def total_entropy(self) -> int:
"""Total entropy in bits (excluding reference photo)."""
return self.phrase_entropy + self.pin_entropy + self.rsa_entropy
@dataclass
class EncodeInput:
"""Input parameters for encoding a message."""
message: str
reference_photo: bytes
carrier_image: bytes
day_phrase: str
pin: str = ""
rsa_key_data: Optional[bytes] = None
rsa_password: Optional[str] = None
date_str: Optional[str] = None # YYYY-MM-DD, defaults to today
def __post_init__(self):
if self.date_str is None:
self.date_str = date.today().isoformat()
@dataclass
class EncodeResult:
"""Result of encoding operation."""
stego_image: bytes
filename: str
pixels_modified: int
total_pixels: int
capacity_used: float # 0.0 - 1.0
date_used: str
@property
def capacity_percent(self) -> float:
"""Capacity used as percentage."""
return self.capacity_used * 100
@dataclass
class DecodeInput:
"""Input parameters for decoding a message."""
stego_image: bytes
reference_photo: bytes
day_phrase: str
pin: str = ""
rsa_key_data: Optional[bytes] = None
rsa_password: Optional[str] = None
@dataclass
class DecodeResult:
"""Result of decoding operation."""
message: str
date_encoded: str
@dataclass
class EmbedStats:
"""Statistics from image embedding."""
pixels_modified: int
total_pixels: int
capacity_used: float
bytes_embedded: int
@property
def modification_percent(self) -> float:
"""Percentage of pixels modified."""
return (self.pixels_modified / self.total_pixels) * 100 if self.total_pixels > 0 else 0
@dataclass
class KeyInfo:
"""Information about an RSA key."""
key_size: int
is_encrypted: bool
pem_data: bytes
@dataclass
class ValidationResult:
"""Result of input validation."""
is_valid: bool
error_message: str = ""
details: dict = field(default_factory=dict)
@classmethod
def ok(cls, **details) -> 'ValidationResult':
"""Create a successful validation result."""
return cls(is_valid=True, details=details)
@classmethod
def error(cls, message: str, **details) -> 'ValidationResult':
"""Create a failed validation result."""
return cls(is_valid=False, error_message=message, details=details)

View File

@@ -0,0 +1,286 @@
"""
Stegasoo Steganography Functions
LSB embedding and extraction with pseudo-random pixel selection.
"""
import io
import struct
from typing import Optional
from PIL import Image
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
from cryptography.hazmat.backends import default_backend
from .models import EmbedStats
from .exceptions import CapacityError, ExtractionError, EmbeddingError
def generate_pixel_indices(key: bytes, num_pixels: int, num_needed: int) -> list[int]:
"""
Generate pseudo-random pixel indices for embedding.
Uses ChaCha20 as a CSPRNG seeded by the key to deterministically
select which pixels will hold hidden data.
Args:
key: 32-byte key for pixel selection
num_pixels: Total pixels in image
num_needed: Number of pixels needed for embedding
Returns:
List of pixel indices
"""
if num_needed >= num_pixels // 2:
# If we need many pixels, shuffle all indices
nonce = b'\x00' * 16
cipher = Cipher(algorithms.ChaCha20(key, nonce), mode=None, backend=default_backend())
encryptor = cipher.encryptor()
indices = list(range(num_pixels))
random_bytes = encryptor.update(b'\x00' * (num_pixels * 4))
for i in range(num_pixels - 1, 0, -1):
j_bytes = random_bytes[(num_pixels - 1 - i) * 4:(num_pixels - i) * 4]
j = int.from_bytes(j_bytes, 'big') % (i + 1)
indices[i], indices[j] = indices[j], indices[i]
return indices[:num_needed]
# Optimized path: generate indices directly
selected = []
used = set()
nonce = b'\x00' * 16
cipher = Cipher(algorithms.ChaCha20(key, nonce), mode=None, backend=default_backend())
encryptor = cipher.encryptor()
# Generate more than needed to handle collisions
bytes_needed = (num_needed * 2) * 4
random_bytes = encryptor.update(b'\x00' * bytes_needed)
byte_offset = 0
while len(selected) < num_needed and byte_offset < len(random_bytes) - 4:
idx = int.from_bytes(random_bytes[byte_offset:byte_offset + 4], 'big') % num_pixels
byte_offset += 4
if idx not in used:
used.add(idx)
selected.append(idx)
# Generate additional if needed (rare)
while len(selected) < num_needed:
extra_bytes = encryptor.update(b'\x00' * 4)
idx = int.from_bytes(extra_bytes, 'big') % num_pixels
if idx not in used:
used.add(idx)
selected.append(idx)
return selected
def embed_in_image(
carrier_data: bytes,
encrypted_data: bytes,
pixel_key: bytes,
bits_per_channel: int = 1
) -> tuple[bytes, EmbedStats]:
"""
Embed encrypted data in carrier image using LSB steganography.
Uses pseudo-random pixel selection based on pixel_key to scatter
the data across the image, defeating statistical analysis.
Args:
carrier_data: Carrier image bytes
encrypted_data: Data to embed
pixel_key: Key for pixel selection
bits_per_channel: Bits to use per color channel (1-2)
Returns:
Tuple of (PNG image bytes, EmbedStats)
Raises:
CapacityError: If carrier is too small
EmbeddingError: If embedding fails
"""
try:
img = Image.open(io.BytesIO(carrier_data))
if img.mode != 'RGB':
img = img.convert('RGB')
pixels = list(img.getdata())
num_pixels = len(pixels)
bits_per_pixel = 3 * bits_per_channel
max_bytes = (num_pixels * bits_per_pixel) // 8
# Prepend length
data_with_len = struct.pack('>I', len(encrypted_data)) + encrypted_data
if len(data_with_len) > max_bytes:
raise CapacityError(len(data_with_len), max_bytes)
# Convert to binary string
binary_data = ''.join(format(b, '08b') for b in data_with_len)
pixels_needed = (len(binary_data) + bits_per_pixel - 1) // bits_per_pixel
# Get pixel indices
selected_indices = generate_pixel_indices(pixel_key, num_pixels, pixels_needed)
# Embed data
new_pixels = list(pixels)
clear_mask = 0xFF ^ ((1 << bits_per_channel) - 1)
bit_idx = 0
for pixel_idx in selected_indices:
if bit_idx >= len(binary_data):
break
r, g, b = new_pixels[pixel_idx]
for channel_idx, channel_val in enumerate([r, g, b]):
if bit_idx >= len(binary_data):
break
bits = binary_data[bit_idx:bit_idx + bits_per_channel].ljust(bits_per_channel, '0')
new_val = (channel_val & clear_mask) | int(bits, 2)
if channel_idx == 0:
r = new_val
elif channel_idx == 1:
g = new_val
else:
b = new_val
bit_idx += bits_per_channel
new_pixels[pixel_idx] = (r, g, b)
# Create output image
stego_img = Image.new('RGB', img.size)
stego_img.putdata(new_pixels)
output = io.BytesIO()
stego_img.save(output, 'PNG')
output.seek(0)
stats = EmbedStats(
pixels_modified=len(selected_indices),
total_pixels=num_pixels,
capacity_used=len(data_with_len) / max_bytes,
bytes_embedded=len(data_with_len)
)
return output.getvalue(), stats
except CapacityError:
raise
except Exception as e:
raise EmbeddingError(f"Failed to embed data: {e}") from e
def extract_from_image(
image_data: bytes,
pixel_key: bytes,
bits_per_channel: int = 1
) -> Optional[bytes]:
"""
Extract hidden data from a stego image.
Args:
image_data: Stego image bytes
pixel_key: Key for pixel selection (must match encoding)
bits_per_channel: Bits per channel (must match encoding)
Returns:
Extracted data bytes, or None if extraction fails
Raises:
ExtractionError: If extraction fails critically
"""
try:
img = Image.open(io.BytesIO(image_data))
if img.mode != 'RGB':
img = img.convert('RGB')
pixels = list(img.getdata())
num_pixels = len(pixels)
bits_per_pixel = 3 * bits_per_channel
# First, extract enough to get the length (4 bytes = 32 bits)
initial_pixels = (32 + bits_per_pixel - 1) // bits_per_pixel + 10
initial_indices = generate_pixel_indices(pixel_key, num_pixels, initial_pixels)
binary_data = ''
for pixel_idx in initial_indices:
r, g, b = pixels[pixel_idx]
for channel in [r, g, b]:
for bit_pos in range(bits_per_channel - 1, -1, -1):
binary_data += str((channel >> bit_pos) & 1)
# Parse length
try:
length_bits = binary_data[:32]
data_length = struct.unpack('>I', int(length_bits, 2).to_bytes(4, 'big'))[0]
except Exception:
return None
# Sanity check
max_possible = (num_pixels * bits_per_pixel) // 8 - 4
if data_length > max_possible or data_length < 10:
return None
# Extract full data
total_bits = (4 + data_length) * 8
pixels_needed = (total_bits + bits_per_pixel - 1) // bits_per_pixel
selected_indices = generate_pixel_indices(pixel_key, num_pixels, pixels_needed)
binary_data = ''
for pixel_idx in selected_indices:
r, g, b = pixels[pixel_idx]
for channel in [r, g, b]:
for bit_pos in range(bits_per_channel - 1, -1, -1):
binary_data += str((channel >> bit_pos) & 1)
data_bits = binary_data[32:32 + (data_length * 8)]
data_bytes = bytearray()
for i in range(0, len(data_bits), 8):
byte_bits = data_bits[i:i + 8]
if len(byte_bits) == 8:
data_bytes.append(int(byte_bits, 2))
return bytes(data_bytes)
except Exception as e:
raise ExtractionError(f"Failed to extract data: {e}") from e
def calculate_capacity(image_data: bytes, bits_per_channel: int = 1) -> int:
"""
Calculate the maximum message capacity of an image.
Args:
image_data: Image bytes
bits_per_channel: Bits to use per color channel
Returns:
Maximum bytes that can be embedded (minus overhead)
"""
img = Image.open(io.BytesIO(image_data))
if img.mode != 'RGB':
img = img.convert('RGB')
num_pixels = img.size[0] * img.size[1]
bits_per_pixel = 3 * bits_per_channel
max_bytes = (num_pixels * bits_per_pixel) // 8
# Subtract overhead: 4 bytes length + ~100 bytes header
return max(0, max_bytes - 104)
def get_image_dimensions(image_data: bytes) -> tuple[int, int]:
"""Get image dimensions without loading full image."""
img = Image.open(io.BytesIO(image_data))
return img.size

201
src/stegasoo/utils.py Normal file
View File

@@ -0,0 +1,201 @@
"""
Stegasoo Utilities
Secure deletion, filename generation, and other helpers.
"""
import os
import random
import secrets
import shutil
from datetime import date, datetime
from pathlib import Path
from typing import Optional
from .constants import DAY_NAMES
def generate_filename(date_str: Optional[str] = None, prefix: str = "") -> str:
"""
Generate a filename for stego images.
Format: {prefix}{random}_{YYYYMMDD}.png
Args:
date_str: Date string (YYYY-MM-DD), defaults to today
prefix: Optional prefix
Returns:
Filename string
"""
if date_str is None:
date_str = date.today().isoformat()
date_compact = date_str.replace('-', '')
random_hex = secrets.token_hex(4)
return f"{prefix}{random_hex}_{date_compact}.png"
def parse_date_from_filename(filename: str) -> Optional[str]:
"""
Extract date from a stego filename.
Looks for patterns like _20251227 or _2025-12-27
Args:
filename: Filename to parse
Returns:
Date string (YYYY-MM-DD) or None
"""
import re
# Try YYYYMMDD format
match = re.search(r'_(\d{4})(\d{2})(\d{2})(?:\.|$)', filename)
if match:
year, month, day = match.groups()
return f"{year}-{month}-{day}"
# Try YYYY-MM-DD format
match = re.search(r'_(\d{4})-(\d{2})-(\d{2})(?:\.|$)', filename)
if match:
year, month, day = match.groups()
return f"{year}-{month}-{day}"
return None
def get_day_from_date(date_str: str) -> str:
"""
Get day of week name from date string.
Args:
date_str: Date string (YYYY-MM-DD)
Returns:
Day name (e.g., "Monday")
"""
try:
year, month, day = map(int, date_str.split('-'))
d = date(year, month, day)
return DAY_NAMES[d.weekday()]
except Exception:
return ""
def get_today_date() -> str:
"""Get today's date as YYYY-MM-DD."""
return date.today().isoformat()
def get_today_day() -> str:
"""Get today's day name."""
return DAY_NAMES[date.today().weekday()]
class SecureDeleter:
"""
Securely delete files by overwriting with random data.
Implements multi-pass overwriting before deletion.
"""
def __init__(self, path: str | Path, passes: int = 7):
"""
Initialize secure deleter.
Args:
path: Path to file or directory
passes: Number of overwrite passes
"""
self.path = Path(path)
self.passes = passes
def _overwrite_file(self, file_path: Path) -> None:
"""Overwrite file with random data multiple times."""
if not file_path.exists() or not file_path.is_file():
return
length = file_path.stat().st_size
if length == 0:
return
patterns = [b'\x00', b'\xFF', bytes([random.randint(0, 255)])]
for _ in range(self.passes):
with open(file_path, 'r+b') as f:
for pattern in patterns:
f.seek(0)
for _ in range(length):
f.write(pattern)
# Final pass with random data
f.seek(0)
f.write(os.urandom(length))
def delete_file(self) -> None:
"""Securely delete a single file."""
if self.path.is_file():
self._overwrite_file(self.path)
self.path.unlink()
def delete_directory(self) -> None:
"""Securely delete a directory and all contents."""
if not self.path.is_dir():
return
# First, securely overwrite all files
for file_path in self.path.rglob('*'):
if file_path.is_file():
self._overwrite_file(file_path)
# Then remove the directory tree
shutil.rmtree(self.path)
def execute(self) -> None:
"""Securely delete the path (file or directory)."""
if self.path.is_file():
self.delete_file()
elif self.path.is_dir():
self.delete_directory()
def secure_delete(path: str | Path, passes: int = 7) -> None:
"""
Convenience function for secure deletion.
Args:
path: Path to file or directory
passes: Number of overwrite passes
"""
SecureDeleter(path, passes).execute()
def format_file_size(size_bytes: int) -> str:
"""
Format file size for display.
Args:
size_bytes: Size in bytes
Returns:
Human-readable string (e.g., "1.5 MB")
"""
for unit in ['B', 'KB', 'MB', 'GB']:
if size_bytes < 1024:
if unit == 'B':
return f"{size_bytes} {unit}"
return f"{size_bytes:.1f} {unit}"
size_bytes /= 1024
return f"{size_bytes:.1f} TB"
def format_number(n: int) -> str:
"""Format number with commas."""
return f"{n:,}"
def clamp(value: int, min_val: int, max_val: int) -> int:
"""Clamp value to range."""
return max(min_val, min(max_val, value))

344
src/stegasoo/validation.py Normal file
View File

@@ -0,0 +1,344 @@
"""
Stegasoo Input Validation
Validators for all user inputs with clear error messages.
"""
import io
from typing import Optional
from PIL import Image
from .constants import (
MIN_PIN_LENGTH, MAX_PIN_LENGTH,
MAX_MESSAGE_SIZE, MAX_IMAGE_PIXELS, MAX_FILE_SIZE,
MIN_RSA_BITS, MIN_KEY_PASSWORD_LENGTH,
ALLOWED_IMAGE_EXTENSIONS, ALLOWED_KEY_EXTENSIONS,
)
from .models import ValidationResult
from .exceptions import (
ValidationError, PinValidationError, MessageValidationError,
ImageValidationError, KeyValidationError, SecurityFactorError,
FileTooLargeError, UnsupportedFileTypeError,
)
from .keygen import load_rsa_key
def validate_pin(pin: str, required: bool = False) -> ValidationResult:
"""
Validate PIN format.
Rules:
- 6-9 digits only
- Cannot start with zero
- Empty is OK if not required
Args:
pin: PIN string to validate
required: Whether PIN is required
Returns:
ValidationResult
"""
if not pin:
if required:
return ValidationResult.error("PIN is required")
return ValidationResult.ok()
if not pin.isdigit():
return ValidationResult.error("PIN must contain only digits")
if len(pin) < MIN_PIN_LENGTH or len(pin) > MAX_PIN_LENGTH:
return ValidationResult.error(
f"PIN must be {MIN_PIN_LENGTH}-{MAX_PIN_LENGTH} digits"
)
if pin[0] == '0':
return ValidationResult.error("PIN cannot start with zero")
return ValidationResult.ok(length=len(pin))
def validate_message(message: str) -> ValidationResult:
"""
Validate message content and size.
Args:
message: Message text
Returns:
ValidationResult
"""
if not message:
return ValidationResult.error("Message is required")
if len(message) > MAX_MESSAGE_SIZE:
return ValidationResult.error(
f"Message too long ({len(message):,} chars). Maximum: {MAX_MESSAGE_SIZE:,} characters"
)
return ValidationResult.ok(length=len(message))
def validate_image(
image_data: bytes,
name: str = "Image",
check_size: bool = True
) -> ValidationResult:
"""
Validate image data and dimensions.
Args:
image_data: Raw image bytes
name: Name for error messages
check_size: Whether to check pixel dimensions
Returns:
ValidationResult with width, height, pixels
"""
if not image_data:
return ValidationResult.error(f"{name} is required")
if len(image_data) > MAX_FILE_SIZE:
return ValidationResult.error(
f"{name} too large ({len(image_data):,} bytes). Maximum: {MAX_FILE_SIZE:,} bytes"
)
try:
img = Image.open(io.BytesIO(image_data))
width, height = img.size
num_pixels = width * height
if check_size and num_pixels > MAX_IMAGE_PIXELS:
max_dim = int(MAX_IMAGE_PIXELS ** 0.5)
return ValidationResult.error(
f"{name} too large ({width}×{height} = {num_pixels:,} pixels). "
f"Maximum: ~{MAX_IMAGE_PIXELS:,} pixels ({max_dim}×{max_dim})"
)
return ValidationResult.ok(
width=width,
height=height,
pixels=num_pixels,
mode=img.mode,
format=img.format
)
except Exception as e:
return ValidationResult.error(f"Could not read {name}: {e}")
def validate_rsa_key(
key_data: bytes,
password: Optional[str] = None,
required: bool = False
) -> ValidationResult:
"""
Validate RSA private key.
Args:
key_data: PEM-encoded key bytes
password: Password if key is encrypted
required: Whether key is required
Returns:
ValidationResult with key_size
"""
if not key_data:
if required:
return ValidationResult.error("RSA key is required")
return ValidationResult.ok()
try:
private_key = load_rsa_key(key_data, password)
key_size = private_key.key_size
if key_size < MIN_RSA_BITS:
return ValidationResult.error(
f"RSA key must be at least {MIN_RSA_BITS} bits (got {key_size})"
)
return ValidationResult.ok(key_size=key_size)
except Exception as e:
return ValidationResult.error(str(e))
def validate_security_factors(
pin: str,
rsa_key_data: Optional[bytes]
) -> ValidationResult:
"""
Validate that at least one security factor is provided.
Args:
pin: PIN string (may be empty)
rsa_key_data: RSA key bytes (may be None/empty)
Returns:
ValidationResult
"""
has_pin = bool(pin and pin.strip())
has_key = bool(rsa_key_data and len(rsa_key_data) > 0)
if not has_pin and not has_key:
return ValidationResult.error(
"You must provide at least a PIN or RSA Key"
)
return ValidationResult.ok(has_pin=has_pin, has_key=has_key)
def validate_file_extension(
filename: str,
allowed: set[str],
file_type: str = "File"
) -> ValidationResult:
"""
Validate file extension.
Args:
filename: Filename to check
allowed: Set of allowed extensions (lowercase, no dot)
file_type: Name for error messages
Returns:
ValidationResult with extension
"""
if not filename or '.' not in filename:
return ValidationResult.error(f"{file_type} must have a file extension")
ext = filename.rsplit('.', 1)[1].lower()
if ext not in allowed:
return ValidationResult.error(
f"Unsupported {file_type.lower()} type: .{ext}. "
f"Allowed: {', '.join(sorted('.' + e for e in allowed))}"
)
return ValidationResult.ok(extension=ext)
def validate_image_file(filename: str) -> ValidationResult:
"""Validate image file extension."""
return validate_file_extension(filename, ALLOWED_IMAGE_EXTENSIONS, "Image")
def validate_key_file(filename: str) -> ValidationResult:
"""Validate key file extension."""
return validate_file_extension(filename, ALLOWED_KEY_EXTENSIONS, "Key file")
def validate_key_password(password: str) -> ValidationResult:
"""
Validate password for key encryption.
Args:
password: Password string
Returns:
ValidationResult
"""
if not password:
return ValidationResult.error("Password is required")
if len(password) < MIN_KEY_PASSWORD_LENGTH:
return ValidationResult.error(
f"Password must be at least {MIN_KEY_PASSWORD_LENGTH} characters"
)
return ValidationResult.ok(length=len(password))
def validate_phrase(phrase: str) -> ValidationResult:
"""
Validate day phrase.
Args:
phrase: Phrase string
Returns:
ValidationResult with word_count
"""
if not phrase or not phrase.strip():
return ValidationResult.error("Day phrase is required")
words = phrase.strip().split()
return ValidationResult.ok(word_count=len(words))
def validate_date_string(date_str: str) -> ValidationResult:
"""
Validate date string format (YYYY-MM-DD).
Args:
date_str: Date string
Returns:
ValidationResult
"""
if not date_str:
return ValidationResult.error("Date is required")
if len(date_str) != 10:
return ValidationResult.error("Date must be in YYYY-MM-DD format")
if date_str[4] != '-' or date_str[7] != '-':
return ValidationResult.error("Date must be in YYYY-MM-DD format")
try:
year = int(date_str[0:4])
month = int(date_str[5:7])
day = int(date_str[8:10])
if not (1 <= month <= 12 and 1 <= day <= 31 and 2000 <= year <= 2100):
return ValidationResult.error("Invalid date values")
return ValidationResult.ok(year=year, month=month, day=day)
except ValueError:
return ValidationResult.error("Date must contain valid numbers")
# ============================================================================
# EXCEPTION-RAISING VALIDATORS (for CLI/API use)
# ============================================================================
def require_valid_pin(pin: str, required: bool = False) -> None:
"""Validate PIN, raising exception on failure."""
result = validate_pin(pin, required)
if not result.is_valid:
raise PinValidationError(result.error_message)
def require_valid_message(message: str) -> None:
"""Validate message, raising exception on failure."""
result = validate_message(message)
if not result.is_valid:
raise MessageValidationError(result.error_message)
def require_valid_image(image_data: bytes, name: str = "Image") -> None:
"""Validate image, raising exception on failure."""
result = validate_image(image_data, name)
if not result.is_valid:
raise ImageValidationError(result.error_message)
def require_valid_rsa_key(
key_data: bytes,
password: Optional[str] = None,
required: bool = False
) -> None:
"""Validate RSA key, raising exception on failure."""
result = validate_rsa_key(key_data, password, required)
if not result.is_valid:
raise KeyValidationError(result.error_message)
def require_security_factors(pin: str, rsa_key_data: Optional[bytes]) -> None:
"""Validate security factors, raising exception on failure."""
result = validate_security_factors(pin, rsa_key_data)
if not result.is_valid:
raise SecurityFactorError(result.error_message)

220
tests/test_stegasoo.py Normal file
View File

@@ -0,0 +1,220 @@
"""
Basic tests for Stegasoo library.
"""
import io
import sys
from pathlib import Path
import pytest
# Add src to path for development
sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
import stegasoo
from stegasoo import (
generate_credentials,
generate_pin,
generate_phrase,
validate_pin,
validate_message,
encode,
decode,
DAY_NAMES,
)
class TestKeygen:
"""Test credential generation."""
def test_generate_pin_default(self):
pin = generate_pin()
assert len(pin) == 6
assert pin.isdigit()
assert pin[0] != '0'
def test_generate_pin_lengths(self):
for length in range(6, 10):
pin = generate_pin(length)
assert len(pin) == length
def test_generate_phrase_default(self):
phrase = generate_phrase()
words = phrase.split()
assert len(words) == 3
def test_generate_phrase_lengths(self):
for length in range(3, 13):
phrase = generate_phrase(length)
words = phrase.split()
assert len(words) == length
def test_generate_credentials_pin_only(self):
creds = generate_credentials(use_pin=True, use_rsa=False)
assert creds.pin is not None
assert creds.rsa_key_pem is None
assert len(creds.phrases) == 7
assert set(creds.phrases.keys()) == set(DAY_NAMES)
def test_generate_credentials_rsa_only(self):
creds = generate_credentials(use_pin=False, use_rsa=True)
assert creds.pin is None
assert creds.rsa_key_pem is not None
assert '-----BEGIN PRIVATE KEY-----' in creds.rsa_key_pem
def test_generate_credentials_both(self):
creds = generate_credentials(use_pin=True, use_rsa=True)
assert creds.pin is not None
assert creds.rsa_key_pem is not None
def test_generate_credentials_neither_fails(self):
with pytest.raises(ValueError):
generate_credentials(use_pin=False, use_rsa=False)
def test_entropy_calculation(self):
creds = generate_credentials(
use_pin=True,
use_rsa=True,
pin_length=6,
rsa_bits=2048,
words_per_phrase=3
)
assert creds.phrase_entropy == 33 # 3 * 11
assert creds.pin_entropy == 19 # floor(6 * 3.32)
assert creds.rsa_entropy == 128
assert creds.total_entropy == 33 + 19 + 128
class TestValidation:
"""Test input validation."""
def test_validate_pin_valid(self):
result = validate_pin("123456")
assert result.is_valid
def test_validate_pin_empty_ok(self):
result = validate_pin("")
assert result.is_valid
def test_validate_pin_too_short(self):
result = validate_pin("12345")
assert not result.is_valid
assert "6-9" in result.error_message
def test_validate_pin_too_long(self):
result = validate_pin("1234567890")
assert not result.is_valid
def test_validate_pin_leading_zero(self):
result = validate_pin("012345")
assert not result.is_valid
assert "zero" in result.error_message.lower()
def test_validate_pin_non_digits(self):
result = validate_pin("12345a")
assert not result.is_valid
def test_validate_message_valid(self):
result = validate_message("Hello, world!")
assert result.is_valid
def test_validate_message_empty(self):
result = validate_message("")
assert not result.is_valid
def test_validate_message_too_long(self):
result = validate_message("x" * 60000)
assert not result.is_valid
class TestEncodeDecode:
"""Test encoding and decoding (requires test images)."""
@pytest.fixture
def test_image(self):
"""Create a simple test image."""
from PIL import Image
img = Image.new('RGB', (100, 100), color='red')
buf = io.BytesIO()
img.save(buf, format='PNG')
return buf.getvalue()
def test_encode_decode_roundtrip(self, test_image):
"""Test full encode/decode cycle."""
message = "Secret message!"
phrase = "apple forest thunder"
pin = "123456"
result = encode(
message=message,
reference_photo=test_image,
carrier_image=test_image,
day_phrase=phrase,
pin=pin
)
assert result.stego_image is not None
assert len(result.stego_image) > 0
assert result.filename.endswith('.png')
decoded = decode(
stego_image=result.stego_image,
reference_photo=test_image,
day_phrase=phrase,
pin=pin
)
assert decoded == message
def test_wrong_pin_fails(self, test_image):
"""Test that wrong PIN fails to decode."""
result = encode(
message="Secret",
reference_photo=test_image,
carrier_image=test_image,
day_phrase="test phrase here",
pin="123456"
)
with pytest.raises(stegasoo.DecryptionError):
decode(
stego_image=result.stego_image,
reference_photo=test_image,
day_phrase="test phrase here",
pin="654321" # Wrong PIN
)
def test_wrong_phrase_fails(self, test_image):
"""Test that wrong phrase fails to decode."""
result = encode(
message="Secret",
reference_photo=test_image,
carrier_image=test_image,
day_phrase="correct phrase here",
pin="123456"
)
with pytest.raises(stegasoo.DecryptionError):
decode(
stego_image=result.stego_image,
reference_photo=test_image,
day_phrase="wrong phrase here",
pin="123456"
)
class TestVersion:
"""Test version information."""
def test_version_exists(self):
assert hasattr(stegasoo, '__version__')
assert stegasoo.__version__ == "2.0.0"
def test_day_names(self):
assert len(stegasoo.DAY_NAMES) == 7
assert stegasoo.DAY_NAMES[0] == 'Monday'
assert stegasoo.DAY_NAMES[6] == 'Sunday'
if __name__ == '__main__':
pytest.main([__file__, '-v'])

44
v1_old_files/Dockerfile Normal file
View File

@@ -0,0 +1,44 @@
FROM python:3.11-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Set work directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libc-dev \
libffi-dev \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
#COPY requirements-ml.txt .
RUN pip install --upgrade pip
RUN pip install --no-cache-dir -r requirements.txt --root-user-action=ignore
#RUN pip install --no-cache-dir -r requirements-ml.txt
# Copy application
COPY . .
# Create upload directory
RUN mkdir -p /tmp/stego_uploads
# Create non-root user for security
RUN useradd -m -u 1000 stego && chown -R stego:stego /app /tmp/stego_uploads
USER stego
# 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 in production
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "--threads", "4", "--timeout", "60", "app:app"]

137
v1_old_files/README.md Normal file
View File

@@ -0,0 +1,137 @@
# Stegasoo Web Service
A containerized Flask + Bootstrap web UI for hybrid Photo + Day-Phrase + PIN steganography.
![Python](https://img.shields.io/badge/Python-3.11+-blue)
![Flask](https://img.shields.io/badge/Flask-3.0+-green)
![Docker](https://img.shields.io/badge/Docker-Ready-blue)
![Security](https://img.shields.io/badge/Security-AES--256--GCM-red)
## Features
- 🔐 **AES-256-GCM** authenticated encryption
- 🧠 **Argon2id** memory-hard key derivation (256MB)
- 🎲 **Pseudo-random pixel selection** defeats steganalysis
- 📅 **Daily key rotation** with customizable phrases (3-12 words)
- 🔢 **Static PIN** for additional entropy (6-8 digits)
- 🖼️ **Reference photo** as "something you have"
- 🌐 **Web UI** with Bootstrap 5 dark theme
- 📖 **Memory aid stories** to help memorize phrases (template or AI-powered)
## Quick Start
### Docker (Recommended)
```bash
# Build and run
docker-compose up -d
# Access at http://localhost:5000
```
### Manual Installation
```bash
# Create virtual environment
python -m venv venv
source venv/bin/activate # Linux/Mac
# or: venv\Scripts\activate # Windows
# Install dependencies
pip install -r requirements.txt
# Optional: Enable AI-powered story generation
pip install -r requirements-ml.txt
# Run development server
python app.py
# Or production with gunicorn
gunicorn --bind 0.0.0.0:5000 app:app
```
## Usage
### 1. Generate Credentials
Visit `/generate` to create:
- **7 phrases** (one per day of week, 3-12 words each)
- **1 PIN** (6-8 digits, same every day)
- **Memory aid stories** (optional, helps memorize phrases)
Memorize these! Don't save them.
### 2. Encode a Message
Visit `/encode` and provide:
- **Reference photo** - A photo both parties have (NOT transmitted)
- **Carrier image** - The image to hide your message in
- **Message** - Your secret text
- **Day phrase** - Today's phrase
- **PIN** - Your static PIN
Download the stego image and share it through any channel.
### 3. Decode a Message
Visit `/decode` and provide:
- **Reference photo** - Same photo used for encoding
- **Stego image** - The image containing the hidden message
- **Day phrase** - The phrase for the day it was encoded
- **PIN** - Your static PIN
## Security Model
| Component | Entropy | Purpose |
|-----------|---------|---------|
| Reference Photo | ~80-256 bits | Something you have |
| Day Phrase | ~33-132 bits | Something you know (rotates daily) |
| PIN | ~20-27 bits | Something you know (static) |
| **Combined** | **133-415+ bits** | **Beyond brute force** |
### Attack Resistance
| Attack | Result |
|--------|--------|
| Brute force | 2^133+ combinations = impossible |
| Rainbow tables | Random salt per message |
| Steganalysis | Random pixel selection defeats detection |
| GPU cracking | Argon2 requires 256MB RAM per attempt |
## Memory Aid Stories
The generate page can create stories to help you memorize your phrases:
**Template-based** (default):
> Monday morning began when I discovered a **APPLE** near the **FOREST**. I had to **THUNDER** quickly, then grab the **CRYSTAL** before reaching the **BRAVE**.
**AI-powered** (with `requirements-ml.txt`):
- Uses DistilGPT-2 (~350MB model)
- Generates more coherent, natural stories
- Words highlighted in RED CAPS
## Configuration
Environment variables:
| Variable | Default | Description |
|----------|---------|-------------|
| `FLASK_ENV` | production | Flask environment |
| `SECRET_KEY` | random | Session secret (auto-generated) |
## Production Deployment
For production, consider:
1. **HTTPS** - Use nginx reverse proxy with SSL
2. **Rate limiting** - Prevent abuse
3. **Logging** - Monitor for security events
4. **Memory** - Allocate at least 512MB (Argon2 needs 256MB)
## License
MIT License - Use responsibly.
## ⚠️ Disclaimer
This tool is for educational and legitimate privacy purposes only. Users are responsible for complying with applicable laws in their jurisdiction.

View File

@@ -0,0 +1,16 @@
# Core dependencies
flask>=3.0.0
gunicorn>=21.0.0
pillow>=10.0.0
cryptography>=41.0.0
# Memory-hard key derivation (highly recommended)
argon2-cffi>=23.0.0
# Optional: ML story generation (adds ~1GB disk space)
# Uncomment for AI-powered memory aid stories
# transformers>=4.35.0
# torch>=2.0.0
# Optional: For production deployment
# gevent>=23.0.0

View File

View File

Before

Width:  |  Height:  |  Size: 964 B

After

Width:  |  Height:  |  Size: 964 B

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,257 @@
/* ============================================================================
Stegasoo - Main Stylesheet
============================================================================ */
/* ----------------------------------------------------------------------------
CSS Variables
---------------------------------------------------------------------------- */
:root {
--gradient-start: #667eea;
--gradient-end: #764ba2;
--bg-dark-1: #1a1a2e;
--bg-dark-2: #16213e;
--bg-dark-3: #0f3460;
--text-muted: rgba(255, 255, 255, 0.5);
--border-light: rgba(255, 255, 255, 0.1);
--overlay-dark: rgba(0, 0, 0, 0.3);
--overlay-light: rgba(255, 255, 255, 0.05);
}
/* ----------------------------------------------------------------------------
Base Styles
---------------------------------------------------------------------------- */
body {
min-height: 100vh;
background: linear-gradient(135deg, var(--bg-dark-1) 0%, var(--bg-dark-2) 50%, var(--bg-dark-3) 100%);
}
/* ----------------------------------------------------------------------------
Navigation
---------------------------------------------------------------------------- */
.navbar {
background: var(--overlay-dark) !important;
backdrop-filter: blur(10px);
}
/* ----------------------------------------------------------------------------
Cards
---------------------------------------------------------------------------- */
.card {
background: var(--overlay-light);
backdrop-filter: blur(10px);
border: 1px solid var(--border-light);
}
.card-header {
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
border-bottom: none;
}
.feature-card {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.feature-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 40px rgba(102, 126, 234, 0.2);
}
/* ----------------------------------------------------------------------------
Buttons
---------------------------------------------------------------------------- */
.btn-primary {
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
border: none;
}
.btn-primary:hover {
background: linear-gradient(135deg, var(--gradient-end), var(--gradient-start));
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
/* ----------------------------------------------------------------------------
Forms
---------------------------------------------------------------------------- */
.form-control,
.form-select {
background: var(--overlay-light);
border: 1px solid var(--border-light);
color: #fff;
}
.form-control:focus,
.form-select:focus {
background: rgba(255, 255, 255, 0.1);
border-color: var(--gradient-start);
box-shadow: 0 0 0 0.25rem rgba(102, 126, 234, 0.25);
color: #fff;
}
.form-control::placeholder {
color: var(--text-muted);
}
/* Fix dropdown options for dark theme */
.form-select option {
background: var(--bg-dark-1);
color: #fff;
}
/* ----------------------------------------------------------------------------
Hero & Icons
---------------------------------------------------------------------------- */
.hero-icon {
font-size: 4rem;
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* ----------------------------------------------------------------------------
Phrase Display
---------------------------------------------------------------------------- */
.phrase-display {
font-family: 'Courier New', monospace;
font-size: 1rem;
background: var(--overlay-dark);
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
border-left: 4px solid var(--gradient-start);
display: inline-block;
line-height: 1.6;
word-spacing: 0.3rem;
}
/* ----------------------------------------------------------------------------
PIN Display
---------------------------------------------------------------------------- */
.pin-display {
font-family: 'Courier New', monospace;
font-size: 3rem;
font-weight: bold;
letter-spacing: 0.75rem;
background: linear-gradient(135deg, #fef08a, #fcd34d, #fb923c);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
display: inline-block;
line-height: 1;
}
.pin-container {
background: var(--overlay-dark);
border: 1px solid var(--border-light);
border-radius: 0.75rem;
padding: 1.5rem 2rem;
display: inline-block;
}
/* ----------------------------------------------------------------------------
Story Cards (Memory Aid)
---------------------------------------------------------------------------- */
.story-word {
color: #ff6b6b;
font-weight: bold;
text-transform: uppercase;
}
.story-card {
background: rgba(0, 0, 0, 0.2);
border-left: 3px solid var(--gradient-start);
padding: 1rem;
margin-bottom: 0.75rem;
border-radius: 0.5rem;
font-size: 0.95rem;
line-height: 1.6;
}
.story-card .day-label {
font-weight: bold;
color: var(--gradient-start);
margin-bottom: 0.5rem;
}
/* ----------------------------------------------------------------------------
Alert / Message Display
---------------------------------------------------------------------------- */
.alert-message {
background: var(--overlay-dark);
border: 1px solid var(--border-light);
border-radius: 0.5rem;
padding: 1.5rem;
white-space: pre-wrap;
font-family: 'Courier New', monospace;
}
/* ----------------------------------------------------------------------------
Drop Zone (Drag & Drop File Upload)
---------------------------------------------------------------------------- */
.drop-zone {
position: relative;
border: 2px dashed rgba(255, 255, 255, 0.2);
border-radius: 0.5rem;
padding: 1.5rem;
text-align: center;
transition: all 0.2s ease;
}
.drop-zone.drag-over {
border-color: var(--gradient-start);
background: rgba(102, 126, 234, 0.1);
}
.drop-zone input[type="file"] {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
}
.drop-zone-label {
pointer-events: none;
}
.drop-zone-preview {
max-height: 120px;
border-radius: 0.375rem;
margin-top: 0.75rem;
}
/* ----------------------------------------------------------------------------
Footer
---------------------------------------------------------------------------- */
footer {
background: rgba(0, 0, 0, 0.2);
}
/* ----------------------------------------------------------------------------
Custom Alert Variants
---------------------------------------------------------------------------- */
.alert-success-bright {
background: rgba(34, 197, 94, 0.2);
border-color: #22c55e;
color: #4ade80;
}
/* ----------------------------------------------------------------------------
Utility Classes
---------------------------------------------------------------------------- */
.bg-dark-subtle {
background: rgba(0, 0, 0, 0.2);
}
.status-box {
background: rgba(0, 0, 0, 0.2);
padding: 1rem;
border-radius: 0.5rem;
}
.result-icon {
font-size: 4rem;
}
.footer-icon {
vertical-align: text-bottom;
}

View File

@@ -0,0 +1,178 @@
{% extends "base.html" %}
{% block title %}About - Stegasoo{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-10">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-info-circle me-2"></i>About Stegasoo</h5>
</div>
<div class="card-body">
<p>
Stegasoo is a hybrid steganography system that hides encrypted messages inside
ordinary images. It combines multiple security layers to create a system that is
both highly secure and practical to use.
</p>
<h6 class="mt-4 mb-3">System Status</h6>
<div class="row g-3">
<div class="col-md-6">
<div class="d-flex align-items-center p-3 rounded status-box">
{% if has_argon2 %}
<i class="bi bi-check-circle-fill text-success fs-4 me-3"></i>
<div>
<strong>Argon2id Available</strong>
<div class="small text-muted">Memory-hard key derivation (256MB)</div>
</div>
{% else %}
<i class="bi bi-exclamation-triangle-fill text-warning fs-4 me-3"></i>
<div>
<strong>Using PBKDF2 Fallback</strong>
<div class="small text-muted">Install argon2-cffi for better security</div>
</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="d-flex align-items-center p-3 rounded status-box">
<i class="bi bi-shield-fill-check text-success fs-4 me-3"></i>
<div>
<strong>AES-256-GCM</strong>
<div class="small text-muted">Authenticated encryption enabled</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-shield-lock me-2"></i>Security Model</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-dark">
<thead>
<tr>
<th>Component</th>
<th>Entropy</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><i class="bi bi-image text-info me-2"></i>Reference Photo</td>
<td>~80-256 bits</td>
<td>Something you have (plausible deniability)</td>
</tr>
<tr>
<td><i class="bi bi-chat-quote text-info me-2"></i>3-Word Phrase</td>
<td>~33 bits</td>
<td>Something you know (changes daily)</td>
</tr>
<tr>
<td><i class="bi bi-123 text-info me-2"></i>6-Digit PIN</td>
<td>~20 bits</td>
<td>Something you know (static)</td>
</tr>
<tr>
<td><i class="bi bi-calendar text-info me-2"></i>Date</td>
<td>N/A</td>
<td>Automatic key rotation</td>
</tr>
<tr class="table-active">
<td><strong>Combined</strong></td>
<td><strong>133+ bits</strong></td>
<td><strong>Beyond brute force</strong></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-cpu me-2"></i>Attack Resistance</h5>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-6">
<h6 class="text-danger"><i class="bi bi-x-circle me-2"></i>What Attackers Can't Do</h6>
<ul class="list-unstyled">
<li class="mb-2">
<i class="bi bi-shield-x text-muted me-2"></i>
Brute force the passphrase (2<sup>133</sup> combinations)
</li>
<li class="mb-2">
<i class="bi bi-shield-x text-muted me-2"></i>
Use rainbow tables (random salt per message)
</li>
<li class="mb-2">
<i class="bi bi-shield-x text-muted me-2"></i>
Detect hidden data (random pixel selection)
</li>
<li class="mb-2">
<i class="bi bi-shield-x text-muted me-2"></i>
Use GPU farms (Argon2 requires 256MB RAM per attempt)
</li>
</ul>
</div>
<div class="col-md-6">
<h6 class="text-warning"><i class="bi bi-exclamation-triangle me-2"></i>Real Threats</h6>
<ul class="list-unstyled">
<li class="mb-2">
<i class="bi bi-person-x text-muted me-2"></i>
Social engineering (someone tricks you)
</li>
<li class="mb-2">
<i class="bi bi-door-open text-muted me-2"></i>
Physical access to your devices
</li>
<li class="mb-2">
<i class="bi bi-bug text-muted me-2"></i>
Malware/keyloggers on your system
</li>
<li class="mb-2">
<i class="bi bi-camera-video text-muted me-2"></i>
Shoulder surfing while you type
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-book me-2"></i>Best Practices</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6 class="text-success"><i class="bi bi-check-lg me-2"></i>Do</h6>
<ul>
<li>Memorize your phrases and PIN, never write them down</li>
<li>Use a reference photo that both parties already have</li>
<li>Use different carrier images for each message</li>
<li>Share stego images through normal channels (looks innocent)</li>
</ul>
</div>
<div class="col-md-6">
<h6 class="text-danger"><i class="bi bi-x-lg me-2"></i>Don't</h6>
<ul>
<li>Don't transmit the reference photo</li>
<li>Don't reuse the same carrier image</li>
<li>Don't store phrases or PIN digitally</li>
<li>Don't resize or recompress stego images</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Stegasoo{% endblock %}</title>
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
<link href="{{ url_for('static', filename='style.css') }}" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark">
<div class="container">
<a class="navbar-brand d-flex align-items-center" href="/">
<img src="{{ url_for('static', filename='logo.svg') }}" alt="Stegasoo" height="36" class="me-2">
<span class="fw-bold">Stegasoo</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="/"><i class="bi bi-house me-1"></i> Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/encode"><i class="bi bi-lock me-1"></i> Encode</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/decode"><i class="bi bi-unlock me-1"></i> Decode</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/generate"><i class="bi bi-key me-1"></i> Generate</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/about"><i class="bi bi-info-circle me-1"></i> About</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="container py-5">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
<i class="bi bi-{{ 'exclamation-triangle' if category == 'error' else 'check-circle' }} me-2"></i>
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<footer class="py-4 mt-5">
<div class="container text-center text-muted">
<small>
<img src="{{ url_for('static', filename='favicon.svg') }}" alt="" height="16" class="me-1" style="vertical-align: text-bottom;">
Stegasoo v1.1 — Hybrid Photo + Day-Phrase + PIN Steganography
</small>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,259 @@
{% extends "base.html" %}
{% block title %}Decode Message - Stegasoo{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-unlock-fill me-2"></i>Decode Secret Message</h5>
</div>
<div class="card-body">
{% if decoded_message %}
<div class="alert alert-success">
<h6><i class="bi bi-check-circle me-2"></i>Message Decrypted Successfully!</h6>
</div>
<div class="mb-4">
<label class="form-label text-muted">Decoded Message:</label>
<div class="alert-message">{{ decoded_message }}</div>
</div>
<a href="/decode" class="btn btn-outline-light w-100">
<i class="bi bi-arrow-repeat me-2"></i>Decode Another Message
</a>
{% else %}
<form method="POST" enctype="multipart/form-data" id="decodeForm">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">
<i class="bi bi-image me-1"></i> Reference Photo
</label>
<div class="drop-zone">
<input type="file" name="reference_photo" accept="image/*" required>
<div class="drop-zone-label">
<i class="bi bi-cloud-arrow-up fs-3 d-block mb-2 text-muted"></i>
<span class="text-muted">Drop image or click to browse</span>
</div>
<img class="drop-zone-preview d-none">
</div>
<div class="form-text">
The same reference photo used for encoding
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">
<i class="bi bi-file-earmark-image me-1"></i> Stego Image
</label>
<div class="drop-zone" id="stegoDropZone">
<input type="file" name="stego_image" accept="image/*" required>
<div class="drop-zone-label">
<i class="bi bi-cloud-arrow-up fs-3 d-block mb-2 text-muted"></i>
<span class="text-muted">Drop image or click to browse</span>
</div>
<img class="drop-zone-preview d-none">
</div>
<div class="form-text">
The image containing the hidden message
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label" id="dayPhraseLabel">
<i class="bi bi-chat-quote me-1"></i> Day Phrase
</label>
<input type="text" name="day_phrase" class="form-control"
placeholder="e.g., correct horse battery" required>
<div class="form-text">
The phrase for the day the message was encoded
</div>
</div>
<hr class="my-4">
<h6 class="text-muted mb-3">
SECURITY FACTORS
<span class="text-warning small">(provide same factors used during encoding)</span>
</h6>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">
<i class="bi bi-123 me-1"></i> PIN
</label>
<input type="password" name="pin" class="form-control" id="pinInput"
placeholder="6-9 digits" maxlength="9">
<div class="form-text">
If PIN was used during encoding
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
</label>
<input type="file" name="rsa_key" class="form-control" id="rsaKeyInput"
accept=".pem,.key">
<div class="form-text">
If RSA key was used during encoding
</div>
</div>
</div>
<!-- RSA Key Password (shown when key selected) -->
<div class="mb-3 d-none" id="rsaPasswordGroup">
<label class="form-label">
<i class="bi bi-key me-1"></i> RSA Key Password
</label>
<input type="password" name="rsa_password" class="form-control"
placeholder="Password for the .pem file (if encrypted)">
<div class="form-text">
Leave blank if your key file is not password-protected
</div>
</div>
<button type="submit" class="btn btn-primary btn-lg w-100" id="decodeBtn">
<i class="bi bi-unlock me-2"></i>Decode Message
</button>
</form>
{% endif %}
</div>
</div>
<div class="card mt-4">
<div class="card-body">
<h6 class="text-muted mb-3"><i class="bi bi-question-circle me-2"></i>Troubleshooting</h6>
<ul class="list-unstyled text-muted small mb-0">
<li class="mb-2">
<i class="bi bi-dot"></i>
Make sure you're using the <strong>exact same reference photo</strong> file
</li>
<li class="mb-2">
<i class="bi bi-dot"></i>
Use the phrase for the <strong>day the message was encoded</strong>, not today
</li>
<li class="mb-2">
<i class="bi bi-dot"></i>
Provide the <strong>same security factors</strong> (PIN and/or RSA key) used during encoding
</li>
<li class="mb-2">
<i class="bi bi-dot"></i>
Ensure the stego image hasn't been <strong>resized or recompressed</strong>
</li>
<li class="mb-0">
<i class="bi bi-dot"></i>
If using an RSA key, make sure the <strong>password is correct</strong>
</li>
</ul>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Form submit loading state
document.getElementById('decodeForm')?.addEventListener('submit', function() {
const btn = document.getElementById('decodeBtn');
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Decoding...';
btn.disabled = true;
});
// Show RSA password field when key is selected
const rsaKeyInput = document.getElementById('rsaKeyInput');
const rsaPasswordGroup = document.getElementById('rsaPasswordGroup');
rsaKeyInput?.addEventListener('change', function() {
rsaPasswordGroup.classList.toggle('d-none', !this.files.length);
});
// Day names for date detection
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
// Detect day from filename
function detectDayFromFilename(filename) {
const dateMatch = filename.match(/_(\d{4})[-]?(\d{2})[-]?(\d{2})/);
if (dateMatch) {
const [, year, month, day] = dateMatch;
const date = new Date(year, month - 1, day);
return dayNames[date.getDay()];
}
return null;
}
// Update day phrase label
function updateDayLabel(dayName) {
const label = document.getElementById('dayPhraseLabel');
if (label && dayName) {
label.innerHTML = `<i class="bi bi-chat-quote me-1"></i> ${dayName}'s Phrase`;
}
}
// Drag & drop with preview
document.querySelectorAll('.drop-zone').forEach(zone => {
const input = zone.querySelector('input[type="file"]');
const label = zone.querySelector('.drop-zone-label');
const preview = zone.querySelector('.drop-zone-preview');
const isStegoZone = zone.id === 'stegoDropZone';
['dragenter', 'dragover'].forEach(evt => {
zone.addEventListener(evt, e => {
e.preventDefault();
zone.classList.add('drag-over');
});
});
['dragleave', 'drop'].forEach(evt => {
zone.addEventListener(evt, e => {
e.preventDefault();
zone.classList.remove('drag-over');
});
});
zone.addEventListener('drop', e => {
if (e.dataTransfer.files.length) {
input.files = e.dataTransfer.files;
const file = e.dataTransfer.files[0];
showPreview(file);
if (isStegoZone) {
const dayName = detectDayFromFilename(file.name);
updateDayLabel(dayName);
}
}
});
input.addEventListener('change', function() {
if (this.files && this.files[0]) {
const file = this.files[0];
showPreview(file);
if (isStegoZone) {
const dayName = detectDayFromFilename(file.name);
updateDayLabel(dayName);
}
}
});
function showPreview(file) {
if (!file.type.startsWith('image/')) return;
const reader = new FileReader();
reader.onload = e => {
preview.src = e.target.result;
preview.classList.remove('d-none');
label.innerHTML = '<i class="bi bi-check-circle text-success me-1"></i>' + file.name;
};
reader.readAsDataURL(file);
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,259 @@
{% extends "base.html" %}
{% block title %}Encode Message - Stegasoo{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-lock-fill me-2"></i>Encode Secret Message</h5>
</div>
<div class="card-body">
<form method="POST" enctype="multipart/form-data" id="encodeForm">
<input type="hidden" name="client_date" id="clientDate" value="">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">
<i class="bi bi-image me-1"></i> Reference Photo
</label>
<div class="drop-zone" id="refDropZone">
<input type="file" name="reference_photo" accept="image/*" required>
<div class="drop-zone-label">
<i class="bi bi-cloud-arrow-up fs-3 d-block mb-2 text-muted"></i>
<span class="text-muted">Drop image or click to browse</span>
</div>
<img class="drop-zone-preview d-none" id="refPreview">
</div>
<div class="form-text">
The secret photo both parties have (NOT transmitted)
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">
<i class="bi bi-file-image me-1"></i> Carrier Image
</label>
<div class="drop-zone" id="carrierDropZone">
<input type="file" name="carrier" accept="image/*" required>
<div class="drop-zone-label">
<i class="bi bi-cloud-arrow-up fs-3 d-block mb-2 text-muted"></i>
<span class="text-muted">Drop image or click to browse</span>
</div>
<img class="drop-zone-preview d-none" id="carrierPreview">
</div>
<div class="form-text">
The image to hide your message in (e.g., a meme)
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">
<i class="bi bi-chat-left-text me-1"></i> Secret Message
</label>
<textarea name="message" class="form-control" rows="4" id="messageInput"
placeholder="Enter your secret message here..." required></textarea>
<div class="d-flex justify-content-between form-text">
<span>
<span id="charCount">0</span> / 50,000 characters
<span id="charWarning" class="text-warning d-none ms-2">
<i class="bi bi-exclamation-triangle"></i> Getting long!
</span>
</span>
<span id="charPercent" class="text-muted">0%</span>
</div>
</div>
<div class="mb-3">
<label class="form-label" id="dayPhraseLabel">
<i class="bi bi-chat-quote me-1"></i> {{ day_of_week }}'s Phrase
</label>
<input type="text" name="day_phrase" class="form-control"
placeholder="e.g., correct horse battery" required>
<div class="form-text">
Your phrase for <strong>today</strong> (based on your local timezone)
</div>
</div>
<hr class="my-4">
<h6 class="text-muted mb-3">
SECURITY FACTORS
<span class="text-warning small">(provide at least one: PIN or RSA Key)</span>
</h6>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">
<i class="bi bi-123 me-1"></i> PIN
</label>
<input type="password" name="pin" class="form-control" id="pinInput"
placeholder="6-9 digits" maxlength="9">
<div class="form-text">
Your static 6-9 digit PIN (if configured)
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
</label>
<input type="file" name="rsa_key" class="form-control" id="rsaKeyInput"
accept=".pem,.key">
<div class="form-text">
Your shared .pem key file (if configured)
</div>
</div>
</div>
<!-- RSA Key Password (shown when key selected) -->
<div class="mb-3 d-none" id="rsaPasswordGroup">
<label class="form-label">
<i class="bi bi-key me-1"></i> RSA Key Password
</label>
<input type="password" name="rsa_password" class="form-control"
placeholder="Password for the .pem file (if encrypted)">
<div class="form-text">
Leave blank if your key file is not password-protected
</div>
</div>
<button type="submit" class="btn btn-primary btn-lg w-100" id="encodeBtn">
<i class="bi bi-lock me-2"></i>Encode Message
</button>
</form>
<hr class="my-4">
<div class="row text-center text-muted small">
<div class="col-4">
<i class="bi bi-shield-check fs-4 d-block mb-1 text-success"></i>
AES-256-GCM Encryption
</div>
<div class="col-4">
<i class="bi bi-shuffle fs-4 d-block mb-1 text-info"></i>
Random Pixel Embedding
</div>
<div class="col-4">
<i class="bi bi-eye-slash fs-4 d-block mb-1 text-warning"></i>
Undetectable by Analysis
</div>
</div>
<div class="alert alert-secondary mt-4 small">
<i class="bi bi-info-circle me-1"></i>
<strong>Limits:</strong>
Carrier image max ~4 megapixels (2000×2000).
Files max 5MB each.
Message max 50KB.
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Detect client's local date and day
const now = new Date();
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const localDay = dayNames[now.getDay()];
const localDate = now.getFullYear() + '-' +
String(now.getMonth() + 1).padStart(2, '0') + '-' +
String(now.getDate()).padStart(2, '0');
// Update day label to client's local day
const dayLabel = document.getElementById('dayPhraseLabel');
if (dayLabel) {
dayLabel.innerHTML = `<i class="bi bi-chat-quote me-1"></i> ${localDay}'s Phrase`;
}
// Set hidden field with client's local date for server
const dateInput = document.getElementById('clientDate');
if (dateInput) {
dateInput.value = localDate;
}
// Show RSA password field when key is selected
const rsaKeyInput = document.getElementById('rsaKeyInput');
const rsaPasswordGroup = document.getElementById('rsaPasswordGroup');
rsaKeyInput.addEventListener('change', function() {
rsaPasswordGroup.classList.toggle('d-none', !this.files.length);
});
// Form submit loading state
document.getElementById('encodeForm').addEventListener('submit', function(e) {
const btn = document.getElementById('encodeBtn');
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Encoding...';
btn.disabled = true;
});
// Character counter
const messageInput = document.getElementById('messageInput');
const charCount = document.getElementById('charCount');
const charWarning = document.getElementById('charWarning');
const charPercent = document.getElementById('charPercent');
const maxChars = 50000;
messageInput.addEventListener('input', function() {
const len = this.value.length;
charCount.textContent = len.toLocaleString();
const pct = Math.round((len / maxChars) * 100);
charPercent.textContent = pct + '%';
charWarning.classList.toggle('d-none', len < maxChars * 0.8);
charCount.classList.toggle('text-danger', len > maxChars * 0.95);
});
// Drag & drop with preview
document.querySelectorAll('.drop-zone').forEach(zone => {
const input = zone.querySelector('input[type="file"]');
const label = zone.querySelector('.drop-zone-label');
const preview = zone.querySelector('.drop-zone-preview');
['dragenter', 'dragover'].forEach(evt => {
zone.addEventListener(evt, e => {
e.preventDefault();
zone.classList.add('drag-over');
});
});
['dragleave', 'drop'].forEach(evt => {
zone.addEventListener(evt, e => {
e.preventDefault();
zone.classList.remove('drag-over');
});
});
zone.addEventListener('drop', e => {
if (e.dataTransfer.files.length) {
input.files = e.dataTransfer.files;
showPreview(e.dataTransfer.files[0]);
}
});
input.addEventListener('change', function() {
if (this.files && this.files[0]) {
showPreview(this.files[0]);
}
});
function showPreview(file) {
if (!file.type.startsWith('image/')) return;
const reader = new FileReader();
reader.onload = e => {
preview.src = e.target.result;
preview.classList.remove('d-none');
label.innerHTML = '<i class="bi bi-check-circle text-success me-1"></i>' + file.name;
};
reader.readAsDataURL(file);
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,142 @@
{% extends "base.html" %}
{% block title %}Message Encoded - Stegasoo{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-check-circle-fill me-2"></i>Message Encoded Successfully!</h5>
</div>
<div class="card-body text-center">
<div class="mb-4">
<i class="bi bi-file-earmark-image text-success result-icon"></i>
<h5 class="mt-3">{{ filename }}</h5>
<p class="text-muted">Your secret message is hidden in this image</p>
</div>
<div class="d-grid gap-3 mb-4">
<a href="{{ url_for('encode_download', file_id=file_id) }}"
class="btn btn-primary btn-lg" id="downloadBtn">
<i class="bi bi-download me-2"></i>Download Image
</a>
<button type="button" class="btn btn-outline-light btn-lg" id="shareBtn">
<i class="bi bi-share me-2"></i>Share Image
</button>
</div>
<!-- Fallback share options (shown if Web Share API unavailable) -->
<div id="shareFallback" class="d-none">
<p class="text-muted mb-3">Share via:</p>
<div class="d-flex justify-content-center gap-2 flex-wrap">
<a href="#" id="shareEmail" class="btn btn-outline-secondary">
<i class="bi bi-envelope me-1"></i>Email
</a>
<a href="#" id="shareTelegram" class="btn btn-outline-secondary">
<i class="bi bi-telegram me-1"></i>Telegram
</a>
<a href="#" id="shareWhatsapp" class="btn btn-outline-secondary">
<i class="bi bi-whatsapp me-1"></i>WhatsApp
</a>
<button type="button" id="copyLink" class="btn btn-outline-secondary">
<i class="bi bi-link-45deg me-1"></i>Copy Link
</button>
</div>
</div>
<hr class="my-4">
<div class="alert alert-warning small text-start">
<i class="bi bi-clock me-1"></i>
<strong>File expires in 5 minutes.</strong>
Download or share now. The file will be securely deleted after expiry.
</div>
<a href="{{ url_for('encode') }}" class="btn btn-outline-light">
<i class="bi bi-plus-circle me-2"></i>Encode Another Message
</a>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const fileId = "{{ file_id }}";
const filename = "{{ filename }}";
const fileUrl = "{{ url_for('encode_file', file_id=file_id, _external=True) }}";
const downloadUrl = "{{ url_for('encode_download', file_id=file_id, _external=True) }}";
const shareBtn = document.getElementById('shareBtn');
const shareFallback = document.getElementById('shareFallback');
// Check if Web Share API with files is supported
async function canShareFiles() {
if (!navigator.canShare) return false;
// Create a test file to check
const testFile = new File(['test'], 'test.png', { type: 'image/png' });
return navigator.canShare({ files: [testFile] });
}
shareBtn.addEventListener('click', async function() {
const canShare = await canShareFiles();
if (canShare) {
try {
// Fetch the image as a blob
const response = await fetch(fileUrl);
const blob = await response.blob();
const file = new File([blob], filename, { type: 'image/png' });
await navigator.share({
files: [file],
title: 'Shared Image',
});
// Cleanup after successful share
fetch("{{ url_for('encode_cleanup', file_id=file_id) }}", { method: 'POST' });
} catch (err) {
if (err.name !== 'AbortError') {
console.error('Share failed:', err);
shareFallback.classList.remove('d-none');
}
}
} else {
// Show fallback options
shareFallback.classList.remove('d-none');
}
});
// Fallback share links
document.getElementById('shareEmail').href =
`mailto:?subject=Shared Image&body=Check out this image: ${downloadUrl}`;
document.getElementById('shareTelegram').href =
`https://t.me/share/url?url=${encodeURIComponent(downloadUrl)}`;
document.getElementById('shareWhatsapp').href =
`https://wa.me/?text=${encodeURIComponent('Check this out: ' + downloadUrl)}`;
document.getElementById('copyLink').addEventListener('click', function() {
navigator.clipboard.writeText(downloadUrl).then(() => {
this.innerHTML = '<i class="bi bi-check me-1"></i>Copied!';
setTimeout(() => {
this.innerHTML = '<i class="bi bi-link-45deg me-1"></i>Copy Link';
}, 2000);
});
});
// Cleanup after download
document.getElementById('downloadBtn').addEventListener('click', function() {
// Give time for download to start, then cleanup
setTimeout(() => {
fetch("{{ url_for('encode_cleanup', file_id=file_id) }}", { method: 'POST' });
}, 3000);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,345 @@
{% extends "base.html" %}
{% block title %}Generate Credentials - Stegasoo{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-key-fill me-2"></i>Generate Credentials</h5>
</div>
<div class="card-body">
{% if not generated %}
<p class="text-muted mb-4">
Generate your weekly phrase card and security factors. You must choose at least one: PIN or RSA Key.
</p>
<form method="POST" id="generateForm">
<div class="mb-4">
<label class="form-label">Words per phrase</label>
<select name="words_per_phrase" class="form-select" id="wordsSelect">
<option value="3" selected>3 words (~33 bits)</option>
<option value="4">4 words (~44 bits)</option>
<option value="5">5 words (~55 bits)</option>
<option value="6">6 words (~66 bits)</option>
<option value="7">7 words (~77 bits)</option>
<option value="8">8 words (~88 bits)</option>
<option value="9">9 words (~99 bits)</option>
<option value="10">10 words (~110 bits)</option>
<option value="11">11 words (~121 bits)</option>
<option value="12">12 words (~132 bits)</option>
</select>
<div class="form-text">More words = more security, harder to memorize</div>
</div>
<hr class="my-4">
<h6 class="text-muted mb-3">SECURITY FACTORS <span class="text-warning">(select at least one)</span></h6>
<!-- PIN Option -->
<div class="card mb-3" style="background: rgba(0,0,0,0.2);">
<div class="card-body">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="use_pin" id="usePin" checked>
<label class="form-check-label fw-bold" for="usePin">
<i class="bi bi-123 me-1"></i> PIN
</label>
</div>
<div id="pinOptions">
<label class="form-label">PIN length</label>
<select name="pin_length" class="form-select" id="pinSelect">
<option value="6" selected>6 digits (~20 bits)</option>
<option value="7">7 digits (~23 bits)</option>
<option value="8">8 digits (~27 bits)</option>
<option value="9">9 digits (~30 bits)</option>
</select>
<div class="form-text">Memorizable, same PIN used every day</div>
</div>
</div>
</div>
<!-- RSA Key Option -->
<div class="card mb-3" style="background: rgba(0,0,0,0.2);">
<div class="card-body">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="use_rsa" id="useRsa">
<label class="form-check-label fw-bold" for="useRsa">
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
</label>
</div>
<div id="rsaOptions" class="d-none">
<label class="form-label">Key size</label>
<select name="rsa_bits" class="form-select" id="rsaSelect">
<option value="2048" selected>2048-bit (~128 bits effective)</option>
<option value="3072">3072-bit (~128 bits effective)</option>
<option value="4096">4096-bit (~128 bits effective)</option>
</select>
<div class="form-text">File-based key, both parties need the same .pem file</div>
</div>
</div>
</div>
<div class="alert alert-info mb-4">
<div class="d-flex justify-content-between align-items-center">
<span><i class="bi bi-calculator me-2"></i>Estimated entropy:</span>
<strong id="entropyDisplay">~53 bits</strong>
</div>
<div class="progress mt-2" style="height: 8px;">
<div class="progress-bar bg-success" id="entropyBar" style="width: 40%"></div>
</div>
<small class="text-muted mt-1 d-block">
<span id="entropyDesc">Good for most use cases</span>
• Reference photo adds ~80-256 bits more
</small>
</div>
<div class="alert alert-warning d-none" id="noFactorWarning">
<i class="bi bi-exclamation-triangle me-2"></i>
You must select at least one security factor (PIN or RSA Key)
</div>
<button type="submit" class="btn btn-primary btn-lg w-100" id="generateBtn">
<i class="bi bi-shuffle me-2"></i>Generate Credentials
</button>
</form>
{% else %}
<!-- Generated Results -->
<div class="alert alert-success-bright alert-dismissible fade show">
<i class="bi bi-check-circle me-2"></i>
<strong>Credentials Generated!</strong>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<div class="alert alert-warning alert-dismissible fade show">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Memorize phrases, save key securely, then close!</strong> - Do not screenshot
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% if pin %}
<hr class="my-4">
<div class="text-center mb-4">
<h6 class="text-muted mb-2">YOUR STATIC PIN</h6>
<div class="pin-container">
<div class="pin-display">{{ pin }}</div>
</div>
<div class="mt-2">
<small class="text-muted">Use this {{ pin_length }}-digit PIN every day</small>
</div>
</div>
{% endif %}
{% if rsa_key_pem %}
<hr class="my-4">
<div class="mb-4">
<h6 class="text-muted mb-3">
<i class="bi bi-file-earmark-lock me-2"></i>YOUR RSA KEY ({{ rsa_bits }}-bit)
</h6>
<div class="alert alert-danger small">
<i class="bi bi-shield-exclamation me-1"></i>
<strong>Save this key securely!</strong> Share it with your recipient through a secure channel. You cannot recover it later.
</div>
<!-- Key Display -->
<div class="mb-3">
<textarea class="form-control font-monospace" id="rsaKeyText" rows="6" readonly style="font-size: 0.75rem;">{{ rsa_key_pem }}</textarea>
</div>
<!-- Copy to Clipboard -->
<button type="button" class="btn btn-outline-light me-2" id="copyKeyBtn">
<i class="bi bi-clipboard me-1"></i> Copy to Clipboard
</button>
<!-- Download with Password -->
<button type="button" class="btn btn-outline-light" data-bs-toggle="collapse" data-bs-target="#downloadKeyForm">
<i class="bi bi-download me-1"></i> Download as .pem
</button>
<div class="collapse mt-3" id="downloadKeyForm">
<div class="card" style="background: rgba(0,0,0,0.2);">
<div class="card-body">
<form method="POST" action="{{ url_for('download_key') }}">
<input type="hidden" name="key_pem" value="{{ rsa_key_pem }}">
<div class="mb-3">
<label class="form-label">Password to protect key file</label>
<input type="password" name="key_password" class="form-control"
placeholder="Minimum 8 characters" minlength="8" required>
<div class="form-text">You'll need this password when using the key</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-file-earmark-lock me-1"></i> Download Protected Key
</button>
</form>
</div>
</div>
</div>
</div>
{% endif %}
<hr class="my-4">
<h6 class="text-muted mb-3">DAILY PHRASES ({{ words_per_phrase }} words each)</h6>
<div class="table-responsive">
<table class="table table-dark table-hover">
<thead>
<tr>
<th style="width: 140px;">Day</th>
<th>Phrase</th>
</tr>
</thead>
<tbody>
{% for day in days %}
<tr>
<td class="text-nowrap">
<i class="bi bi-calendar3 me-2"></i>{{ day }}
</td>
<td>
<span class="phrase-display">{{ phrases[day] }}</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="alert alert-success mt-4">
<h6><i class="bi bi-shield-check me-2"></i>Security Summary</h6>
<div class="row text-center mt-3">
<div class="col-3">
<div class="fs-4 fw-bold">{{ phrase_entropy }}</div>
<small class="text-muted">bits/phrase</small>
</div>
{% if pin %}
<div class="col-3">
<div class="fs-4 fw-bold">{{ pin_entropy }}</div>
<small class="text-muted">bits/PIN</small>
</div>
{% endif %}
{% if rsa_key_pem %}
<div class="col-3">
<div class="fs-4 fw-bold">{{ rsa_entropy }}</div>
<small class="text-muted">bits/RSA</small>
</div>
{% endif %}
<div class="col-3">
<div class="fs-4 fw-bold text-success">{{ total_entropy }}</div>
<small class="text-muted">bits total</small>
</div>
</div>
<small class="d-block mt-2 text-center text-muted">
+ reference photo (~80-256 bits) = <strong>{{ total_entropy + 80 }}+ bits combined</strong>
</small>
</div>
<a href="/generate" class="btn btn-outline-light btn-lg w-100 mt-3">
<i class="bi bi-arrow-repeat me-2"></i>Generate New Credentials
</a>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
{% if not generated %}
const usePinCheckbox = document.getElementById('usePin');
const useRsaCheckbox = document.getElementById('useRsa');
const pinOptions = document.getElementById('pinOptions');
const rsaOptions = document.getElementById('rsaOptions');
const noFactorWarning = document.getElementById('noFactorWarning');
const generateBtn = document.getElementById('generateBtn');
// Toggle option visibility
usePinCheckbox.addEventListener('change', function() {
pinOptions.classList.toggle('d-none', !this.checked);
validateFactors();
updateEntropy();
});
useRsaCheckbox.addEventListener('change', function() {
rsaOptions.classList.toggle('d-none', !this.checked);
validateFactors();
updateEntropy();
});
function validateFactors() {
const hasPin = usePinCheckbox.checked;
const hasRsa = useRsaCheckbox.checked;
const valid = hasPin || hasRsa;
noFactorWarning.classList.toggle('d-none', valid);
generateBtn.disabled = !valid;
}
function updateEntropy() {
const words = parseInt(document.getElementById('wordsSelect').value);
const usePin = usePinCheckbox.checked;
const useRsa = useRsaCheckbox.checked;
const pinLen = parseInt(document.getElementById('pinSelect').value);
const phraseEntropy = words * 11;
const pinEntropy = usePin ? Math.floor(pinLen * 3.32) : 0;
const rsaEntropy = useRsa ? 128 : 0;
const total = phraseEntropy + pinEntropy + rsaEntropy;
document.getElementById('entropyDisplay').textContent = '~' + total + ' bits';
// Update progress bar
const pct = Math.min(100, Math.max(10, (total - 30) * 0.5));
document.getElementById('entropyBar').style.width = pct + '%';
// Update description
let desc;
if (total < 50) desc = 'Basic security';
else if (total < 80) desc = 'Good for most use cases';
else if (total < 120) desc = 'Strong security';
else if (total < 180) desc = 'Very strong security';
else desc = 'Maximum security';
document.getElementById('entropyDesc').textContent = desc;
}
document.getElementById('wordsSelect').addEventListener('change', updateEntropy);
document.getElementById('pinSelect').addEventListener('change', updateEntropy);
document.getElementById('rsaSelect').addEventListener('change', updateEntropy);
// Form submit
document.getElementById('generateForm').addEventListener('submit', function(e) {
if (!usePinCheckbox.checked && !useRsaCheckbox.checked) {
e.preventDefault();
noFactorWarning.classList.remove('d-none');
return;
}
generateBtn.disabled = true;
generateBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Generating...';
});
// Initial state
validateFactors();
updateEntropy();
{% else %}
// Copy RSA key to clipboard
document.getElementById('copyKeyBtn')?.addEventListener('click', function() {
const keyText = document.getElementById('rsaKeyText');
navigator.clipboard.writeText(keyText.value).then(() => {
this.innerHTML = '<i class="bi bi-check me-1"></i> Copied!';
setTimeout(() => {
this.innerHTML = '<i class="bi bi-clipboard me-1"></i> Copy to Clipboard';
}, 2000);
});
});
{% endif %}
</script>
{% endblock %}

View File

@@ -0,0 +1,108 @@
{% extends "base.html" %}
{% block title %}Stegasoo - Secure Steganography{% endblock %}
{% block content %}
<div class="text-center mb-5">
<img src="{{ url_for('static', filename='logo.svg') }}" alt="Stegasoo" height="120" class="mb-3">
<h1 class="display-4 fw-bold">Stegasoo</h1>
<p class="lead text-muted">Create hidden encrypted messages in images and photos using advanced steganography.</p>
</div>
<div class="row g-4 mb-5">
<div class="col-md-4">
<div class="card h-100 feature-card">
<div class="card-header text-center py-3">
<i class="bi bi-lock-fill fs-1"></i>
</div>
<div class="card-body text-center">
<h5 class="card-title">Encode Message</h5>
<p class="card-text text-muted">
Hide your secret message inside an innocent-looking image using your daily phrase + PIN.
</p>
<a href="/encode" class="btn btn-primary">
<i class="bi bi-upload me-1"></i> Encode
</a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 feature-card">
<div class="card-header text-center py-3">
<i class="bi bi-unlock-fill fs-1"></i>
</div>
<div class="card-body text-center">
<h5 class="card-title">Decode Message</h5>
<p class="card-text text-muted">
Extract and decrypt hidden messages from Stegasoo-encoded images using your credentials.
</p>
<a href="/decode" class="btn btn-primary">
<i class="bi bi-download me-1"></i> Decode
</a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 feature-card">
<div class="card-header text-center py-3">
<i class="bi bi-key-fill fs-1"></i>
</div>
<div class="card-body text-center">
<h5 class="card-title">Generate Keys</h5>
<p class="card-text text-muted">
Create your weekly phrase card and PIN. Memorize 21 words + 6 digits for maximum security.
</p>
<a href="/generate" class="btn btn-primary">
<i class="bi bi-plus-circle me-1"></i> Generate
</a>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-diagram-3 me-2"></i>How It Works</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6 class="text-primary"><i class="bi bi-1-circle me-2"></i>Key Components</h6>
<ul class="list-unstyled">
<li class="mb-2">
<i class="bi bi-image text-info me-2"></i>
<strong>Reference Photo</strong> — Any photo you and recipient both have
</li>
<li class="mb-2">
<i class="bi bi-chat-quote text-info me-2"></i>
<strong>Day Phrase</strong> — 3 words, different each day of the week
</li>
<li class="mb-2">
<i class="bi bi-123 text-info me-2"></i>
<strong>Static PIN</strong> — 6 digits, same every day
</li>
</ul>
</div>
<div class="col-md-6">
<h6 class="text-primary"><i class="bi bi-2-circle me-2"></i>Security Features</h6>
<ul class="list-unstyled">
<li class="mb-2">
<i class="bi bi-shield-check text-success me-2"></i>
Argon2id memory-hard key derivation (256MB)
</li>
<li class="mb-2">
<i class="bi bi-shuffle text-success me-2"></i>
Pseudo-random pixel selection (defeats steganalysis)
</li>
<li class="mb-2">
<i class="bi bi-lock text-success me-2"></i>
AES-256-GCM authenticated encryption
</li>
</ul>
</div>
</div>
</div>
</div>
{% endblock %}

View File