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

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

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 %}