diff --git a/.gitignore b/.gitignore index 1e611b0..1b17709 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,9 @@ venv/ ENV/ env/ +# Old versions of code files. +old_files/ + # IDE .idea/ .vscode/ @@ -51,3 +54,6 @@ htmlcov/ # Distribution *.manifest *.spec + +# Output test files. +*.png diff --git a/Dockerfile.txt b/Dockerfile.txt new file mode 100644 index 0000000..da619e3 --- /dev/null +++ b/Dockerfile.txt @@ -0,0 +1,129 @@ +# 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 + +# 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/* + +# ============================================================================ +# Builder stage - install Python packages +# ============================================================================ +FROM base as builder + +WORKDIR /build + +# Copy package files (including README.md which pyproject.toml references) +COPY pyproject.toml README.md ./ +COPY src/ src/ +COPY data/ data/ + +# Install 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 +RUN useradd -m -u 1000 stego && chown -R stego:stego /app /tmp/stego_uploads +USER stego + +# Set Python path +ENV PYTHONPATH=/app/src + +# Expose port +EXPOSE 5000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/')" || exit 1 + +# Run with gunicorn +WORKDIR /app/frontends/web +CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "--threads", "4", "--timeout", "60", "app:app"] + +# ============================================================================ +# API stage - REST API +# ============================================================================ +FROM base as api + +WORKDIR /app + +# Install API extras +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"] diff --git a/frontends/api/main.py b/frontends/api/main.py index 72d9f81..b45fe02 100644 --- a/frontends/api/main.py +++ b/frontends/api/main.py @@ -3,7 +3,7 @@ Stegasoo REST API FastAPI-based REST API for steganography operations. -Designed for integration with other services and automation. +Supports both text messages and file embedding. """ import io @@ -28,6 +28,8 @@ from stegasoo import ( DAY_NAMES, __version__, StegasooError, DecryptionError, CapacityError, has_argon2, + FilePayload, + MAX_FILE_PAYLOAD_SIZE, ) from stegasoo.constants import ( MIN_PIN_LENGTH, MAX_PIN_LENGTH, @@ -35,6 +37,17 @@ from stegasoo.constants import ( VALID_RSA_SIZES, ) +# QR Code utilities +try: + from stegasoo.qr_utils import ( + extract_key_from_qr, + has_qr_read, + ) + HAS_QR_READ = has_qr_read() +except ImportError: + HAS_QR_READ = False + extract_key_from_qr = None + # ============================================================================ # FASTAPI APP @@ -42,7 +55,7 @@ from stegasoo.constants import ( app = FastAPI( title="Stegasoo API", - description="Secure steganography with hybrid authentication", + description="Secure steganography with hybrid authentication. Supports text messages and file embedding.", version=__version__, docs_url="/docs", redoc_url="/redoc", @@ -79,6 +92,20 @@ class EncodeRequest(BaseModel): date_str: Optional[str] = None +class EncodeFileRequest(BaseModel): + """Request for embedding a file (base64-encoded).""" + file_data_base64: str + filename: str + mime_type: Optional[str] = None + 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 @@ -97,7 +124,12 @@ class DecodeRequest(BaseModel): class DecodeResponse(BaseModel): - message: str + """Response for decode - can be text or file.""" + payload_type: str # 'text' or 'file' + message: Optional[str] = None # For text + file_data_base64: Optional[str] = None # For file (base64-encoded) + filename: Optional[str] = None # For file + mime_type: Optional[str] = None # For file class ImageInfoResponse(BaseModel): @@ -111,7 +143,15 @@ class ImageInfoResponse(BaseModel): class StatusResponse(BaseModel): version: str has_argon2: bool + has_qrcode_read: bool day_names: list[str] + max_payload_kb: int + + +class QrExtractResponse(BaseModel): + success: bool + key_pem: Optional[str] = None + error: Optional[str] = None class ErrorResponse(BaseModel): @@ -129,10 +169,43 @@ async def root(): return StatusResponse( version=__version__, has_argon2=has_argon2(), - day_names=list(DAY_NAMES) + has_qrcode_read=HAS_QR_READ, + day_names=list(DAY_NAMES), + max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024 ) +@app.post("/extract-key-from-qr", response_model=QrExtractResponse) +async def api_extract_key_from_qr( + qr_image: UploadFile = File(..., description="QR code image containing RSA key") +): + """ + Extract RSA key from a QR code image. + + Supports both compressed (STEGASOO-Z: prefix) and uncompressed keys. + Returns the PEM-encoded key if found. + """ + if not HAS_QR_READ: + raise HTTPException( + 501, + "QR code reading not available. Install pyzbar and libzbar." + ) + + try: + image_data = await qr_image.read() + key_pem = extract_key_from_qr(image_data) + + if key_pem: + return QrExtractResponse(success=True, key_pem=key_pem) + else: + return QrExtractResponse( + success=False, + error="No valid RSA key found in QR code" + ) + except Exception as e: + return QrExtractResponse(success=False, error=str(e)) + + @app.post("/generate", response_model=GenerateResponse) async def api_generate(request: GenerateRequest): """ @@ -173,7 +246,7 @@ async def api_generate(request: GenerateRequest): @app.post("/encode", response_model=EncodeResponse) async def api_encode(request: EncodeRequest): """ - Encode a secret message into an image. + Encode a text message into an image. Images must be base64-encoded. Returns base64-encoded stego image. """ @@ -194,8 +267,55 @@ async def api_encode(request: EncodeRequest): ) stego_b64 = base64.b64encode(result.stego_image).decode('utf-8') + day_of_week = get_day_from_date(result.date_used) - # Get day of week from the date used + return EncodeResponse( + stego_image_base64=stego_b64, + filename=result.filename, + capacity_used_percent=result.capacity_percent, + date_used=result.date_used, + day_of_week=day_of_week + ) + + 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("/encode/file", response_model=EncodeResponse) +async def api_encode_file(request: EncodeFileRequest): + """ + Encode a file into an image (JSON with base64). + + File data must be base64-encoded. + """ + try: + file_data = base64.b64decode(request.file_data_base64) + 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 + + payload = FilePayload( + data=file_data, + filename=request.filename, + mime_type=request.mime_type + ) + + result = encode( + message=payload, + 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') day_of_week = get_day_from_date(result.date_used) return EncodeResponse( @@ -217,16 +337,16 @@ async def api_encode(request: EncodeRequest): @app.post("/decode", response_model=DecodeResponse) async def api_decode(request: DecodeRequest): """ - Decode a secret message from a stego image. + Decode a message or file from a stego image. - Images must be base64-encoded. + Returns payload_type to indicate if result is text or file. """ 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( + result = decode( stego_image=stego, reference_photo=ref_photo, day_phrase=request.day_phrase, @@ -235,7 +355,18 @@ async def api_decode(request: DecodeRequest): rsa_password=request.rsa_password ) - return DecodeResponse(message=message) + if result.is_file: + return DecodeResponse( + payload_type='file', + file_data_base64=base64.b64encode(result.file_data).decode('utf-8'), + filename=result.filename, + mime_type=result.mime_type + ) + else: + return DecodeResponse( + payload_type='text', + message=result.message + ) except DecryptionError as e: raise HTTPException(401, "Decryption failed. Check credentials.") @@ -247,37 +378,74 @@ async def api_decode(request: DecodeRequest): @app.post("/encode/multipart") async def api_encode_multipart( - message: str = Form(...), day_phrase: str = Form(...), reference_photo: UploadFile = File(...), carrier: UploadFile = File(...), + message: str = Form(""), + payload_file: Optional[UploadFile] = File(None), pin: str = Form(""), rsa_key: Optional[UploadFile] = File(None), + rsa_key_qr: Optional[UploadFile] = File(None), rsa_password: str = Form(""), date_str: str = Form("") ): """ Encode using multipart form data (file uploads). + Provide either 'message' (text) or 'payload_file' (binary file). + RSA key can be provided as 'rsa_key' (.pem file) or 'rsa_key_qr' (QR code image). Returns the stego image directly as PNG with metadata headers. """ try: ref_data = await reference_photo.read() carrier_data = await carrier.read() - rsa_key_data = await rsa_key.read() if rsa_key else None + + # Handle RSA key from .pem file or QR code image + rsa_key_data = None + rsa_key_from_qr = False + + if rsa_key and rsa_key.filename: + rsa_key_data = await rsa_key.read() + elif rsa_key_qr and rsa_key_qr.filename: + if not HAS_QR_READ: + raise HTTPException( + 501, + "QR code reading not available. Install pyzbar and libzbar." + ) + qr_image_data = await rsa_key_qr.read() + key_pem = extract_key_from_qr(qr_image_data) + if not key_pem: + raise HTTPException(400, "Could not extract RSA key from QR code image") + rsa_key_data = key_pem.encode('utf-8') + rsa_key_from_qr = True + + # QR code keys are never password-protected + effective_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None) + + # Determine payload + if payload_file and payload_file.filename: + file_data = await payload_file.read() + payload = FilePayload( + data=file_data, + filename=payload_file.filename, + mime_type=payload_file.content_type + ) + elif message: + payload = message + else: + raise HTTPException(400, "Must provide either 'message' or 'payload_file'") result = encode( - message=message, + message=payload, 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, + rsa_password=effective_password, date_str=date_str if date_str else None ) - # Get day of week from the date used day_of_week = get_day_from_date(result.date_used) return Response( @@ -306,26 +474,62 @@ async def api_decode_multipart( stego_image: UploadFile = File(...), pin: str = Form(""), rsa_key: Optional[UploadFile] = File(None), + rsa_key_qr: Optional[UploadFile] = File(None), rsa_password: str = Form("") ): """ Decode using multipart form data (file uploads). + + RSA key can be provided as 'rsa_key' (.pem file) or 'rsa_key_qr' (QR code image). + Returns JSON with payload_type indicating text or file. """ 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( + # Handle RSA key from .pem file or QR code image + rsa_key_data = None + rsa_key_from_qr = False + + if rsa_key and rsa_key.filename: + rsa_key_data = await rsa_key.read() + elif rsa_key_qr and rsa_key_qr.filename: + if not HAS_QR_READ: + raise HTTPException( + 501, + "QR code reading not available. Install pyzbar and libzbar." + ) + qr_image_data = await rsa_key_qr.read() + key_pem = extract_key_from_qr(qr_image_data) + if not key_pem: + raise HTTPException(400, "Could not extract RSA key from QR code image") + rsa_key_data = key_pem.encode('utf-8') + rsa_key_from_qr = True + + # QR code keys are never password-protected + effective_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None) + + result = 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 + rsa_password=effective_password ) - return DecodeResponse(message=message) + if result.is_file: + return DecodeResponse( + payload_type='file', + file_data_base64=base64.b64encode(result.file_data).decode('utf-8'), + filename=result.filename, + mime_type=result.mime_type + ) + else: + return DecodeResponse( + payload_type='text', + message=result.message + ) except DecryptionError: raise HTTPException(401, "Decryption failed. Check credentials.") diff --git a/frontends/cli/main.py b/frontends/cli/main.py index 6cb80d2..ce27a14 100644 --- a/frontends/cli/main.py +++ b/frontends/cli/main.py @@ -20,14 +20,30 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'src')) import stegasoo from stegasoo import ( - encode, decode, generate_credentials, + encode, encode_file, 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, + FilePayload, ) +# QR Code utilities +try: + from stegasoo.qr_utils import ( + extract_key_from_qr_file, + generate_qr_code, + has_qr_read, has_qr_write, + can_fit_in_qr, needs_compression, + ) + HAS_QR = True +except ImportError: + HAS_QR = False + has_qr_read = lambda: False + has_qr_write = lambda: False + # ============================================================================ # CLI SETUP @@ -42,7 +58,7 @@ def cli(): """ Stegasoo - Secure steganography with hybrid authentication. - Hide encrypted messages in images using a combination of: + Hide encrypted messages or files in images using a combination of: \b • Reference photo (something you have) @@ -170,59 +186,101 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json): @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('--message', '-m', help='Text message to encode') +@click.option('--message-file', '-f', type=click.Path(exists=True), help='Read text message from file') +@click.option('--embed-file', '-e', type=click.Path(exists=True), help='Embed a file (binary)') @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('--key', '-k', type=click.Path(exists=True), help='RSA key file (.pem)') +@click.option('--key-qr', type=click.Path(exists=True), help='RSA key from QR code image') +@click.option('--key-password', help='RSA key password (for encrypted .pem files)') @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): +def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key, key_qr, key_password, output, date_str, quiet): """ - Encode a secret message into an image. + Encode a secret message or file into an image. Requires a reference photo, carrier image, and day phrase. - Must provide either --pin or --key (or both). + Must provide either --pin or --key/--key-qr (or both). + + For text messages, use -m or -f or pipe via stdin. + For binary files, use -e/--embed-file. + RSA key can be provided as a .pem file (--key) or QR code image (--key-qr). \b Examples: + # Text message with PIN 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" + + # With RSA key file + stegasoo encode -r photo.jpg -c meme.png -p "words" -k mykey.pem -m "secret" + + # With RSA key from QR code image + stegasoo encode -r photo.jpg -c meme.png -p "words" --key-qr keyqr.png -m "secret" + + # Embed a binary file + stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -e secret.pdf """ - # 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") + # Determine what to encode + payload = None - # Load key if provided + if embed_file: + # Binary file embedding + payload = FilePayload.from_file(embed_file) + if not quiet: + click.echo(f"Embedding file: {payload.filename} ({len(payload.data):,} bytes)") + elif message: + payload = message + elif message_file: + payload = Path(message_file).read_text() + elif not sys.stdin.isatty(): + payload = sys.stdin.read() + else: + raise click.UsageError("Must provide message via -m, -f, -e, or stdin") + + # Load key if provided (from .pem file or QR code image) rsa_key_data = None + rsa_key_from_qr = False + + if key and key_qr: + raise click.UsageError("Cannot use both --key and --key-qr. Choose one.") + if key: rsa_key_data = Path(key).read_bytes() + elif key_qr: + if not HAS_QR or not has_qr_read(): + raise click.ClickException( + "QR code reading not available. Install: pip install pyzbar\n" + "Also requires system library: sudo apt-get install libzbar0" + ) + key_pem = extract_key_from_qr_file(key_qr) + if not key_pem: + raise click.ClickException(f"Could not extract RSA key from QR code: {key_qr}") + rsa_key_data = key_pem.encode('utf-8') + rsa_key_from_qr = True + if not quiet: + click.echo(f"Loaded RSA key from QR code: {key_qr}") + + # QR code keys are never password-protected + effective_key_password = None if rsa_key_from_qr else key_password # Validate security factors if not pin and not rsa_key_data: - raise click.UsageError("Must provide --pin or --key (or both)") + raise click.UsageError("Must provide --pin or --key/--key-qr (or both)") try: ref_photo = Path(ref).read_bytes() carrier_image = Path(carrier).read_bytes() result = encode( - message=msg, + message=payload, reference_photo=ref_photo, carrier_image=carrier_image, day_phrase=phrase, pin=pin or "", rsa_key_data=rsa_key_data, - rsa_password=key_password, + rsa_password=effective_key_password, date_str=date_str, ) @@ -257,56 +315,113 @@ def encode_cmd(ref, carrier, message, message_file, phrase, pin, key, key_passwo @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): +@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file (.pem)') +@click.option('--key-qr', type=click.Path(exists=True), help='RSA key from QR code image') +@click.option('--key-password', help='RSA key password (for encrypted .pem files)') +@click.option('--output', '-o', type=click.Path(), help='Save decoded content to file') +@click.option('--quiet', '-q', is_flag=True, help='Output only the content (for text) or suppress messages (for files)') +@click.option('--force', is_flag=True, help='Overwrite existing output file') +def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, quiet, force): """ - Decode a secret message from a stego image. + Decode a secret message or file from a stego image. Must use the same credentials that were used for encoding. + Automatically detects whether content is text or a file. + RSA key can be provided as a .pem file (--key) or QR code image (--key-qr). \b Examples: + # Decode with PIN 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 + + # Decode with RSA key file + stegasoo decode -r photo.jpg -s stego.png -p "words" -k mykey.pem + + # Decode with RSA key from QR code image + stegasoo decode -r photo.jpg -s stego.png -p "words" --key-qr keyqr.png + + # Save output to file + stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 -o output.txt """ - # Load key if provided + # Load key if provided (from .pem file or QR code image) rsa_key_data = None + rsa_key_from_qr = False + + if key and key_qr: + raise click.UsageError("Cannot use both --key and --key-qr. Choose one.") + if key: rsa_key_data = Path(key).read_bytes() + elif key_qr: + if not HAS_QR or not has_qr_read(): + raise click.ClickException( + "QR code reading not available. Install: pip install pyzbar\n" + "Also requires system library: sudo apt-get install libzbar0" + ) + key_pem = extract_key_from_qr_file(key_qr) + if not key_pem: + raise click.ClickException(f"Could not extract RSA key from QR code: {key_qr}") + rsa_key_data = key_pem.encode('utf-8') + rsa_key_from_qr = True + if not quiet: + click.echo(f"Loaded RSA key from QR code: {key_qr}") + + # QR code keys are never password-protected + effective_key_password = None if rsa_key_from_qr else key_password # Validate security factors if not pin and not rsa_key_data: - raise click.UsageError("Must provide --pin or --key (or both)") + raise click.UsageError("Must provide --pin or --key/--key-qr (or both)") try: ref_photo = Path(ref).read_bytes() stego_image = Path(stego).read_bytes() - message = decode( + result = 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, + rsa_password=effective_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) + if result.is_file: + # File content + if output: + out_path = Path(output) + elif result.filename: + out_path = Path(result.filename) else: - click.secho("✓ Decoded successfully!", fg='green') - click.echo() - click.echo(message) + out_path = Path("decoded_file") + + if out_path.exists() and not force: + raise click.ClickException( + f"Output file '{out_path}' exists. Use --force to overwrite." + ) + + out_path.write_bytes(result.file_data) + + if not quiet: + click.secho("✓ Decoded file successfully!", fg='green') + click.echo(f" Saved to: {out_path}") + click.echo(f" Size: {len(result.file_data):,} bytes") + if result.mime_type: + click.echo(f" Type: {result.mime_type}") + else: + # Text content + if output: + Path(output).write_text(result.message) + if not quiet: + click.secho("✓ Decoded successfully!", fg='green') + click.echo(f" Saved to: {output}") + else: + if quiet: + click.echo(result.message) + else: + click.secho("✓ Decoded successfully!", fg='green') + click.echo() + click.echo(result.message) except (DecryptionError, ExtractionError) as e: raise click.ClickException(f"Decryption failed: {e}") diff --git a/frontends/web/app.py b/frontends/web/app.py index f854d70..9cfc79b 100644 --- a/frontends/web/app.py +++ b/frontends/web/app.py @@ -3,15 +3,17 @@ Stegasoo Web Frontend Flask-based web UI for steganography operations. -This is a thin wrapper around the stegasoo library. +Supports both text messages and file embedding. """ import io import sys import time import secrets +import mimetypes from pathlib import Path from datetime import datetime +from PIL import Image from flask import ( Flask, render_template, request, send_file, @@ -27,14 +29,44 @@ from stegasoo import ( export_rsa_key_pem, load_rsa_key, validate_pin, validate_message, validate_image, validate_rsa_key, validate_security_factors, + validate_file_payload, get_today_day, generate_filename, DAY_NAMES, __version__, StegasooError, DecryptionError, CapacityError, has_argon2, + FilePayload, + MAX_FILE_PAYLOAD_SIZE, ) from stegasoo.constants import ( MAX_MESSAGE_SIZE, MIN_PIN_LENGTH, MAX_PIN_LENGTH, - VALID_RSA_SIZES, + VALID_RSA_SIZES, MAX_FILE_SIZE, +) + +# QR Code support +try: + import qrcode + from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_M + HAS_QRCODE = True +except ImportError: + HAS_QRCODE = False + +# QR Code reading +try: + from pyzbar.pyzbar import decode as pyzbar_decode + HAS_QRCODE_READ = True +except ImportError: + HAS_QRCODE_READ = False + +import zlib +import base64 + +# Import QR utilities +from stegasoo.qr_utils import ( + compress_data, decompress_data, auto_decompress, + is_compressed, can_fit_in_qr, needs_compression, + generate_qr_code, read_qr_code, extract_key_from_qr, + has_qr_write, has_qr_read, + QR_MAX_BINARY, COMPRESSION_PREFIX ) @@ -44,19 +76,83 @@ from stegasoo.constants import ( app = Flask(__name__) app.secret_key = secrets.token_hex(32) -app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024 # 5MB max upload +app.config['MAX_CONTENT_LENGTH'] = MAX_FILE_SIZE # 10MB max upload # Temporary file storage for sharing (file_id -> {data, timestamp, filename}) TEMP_FILES: dict[str, dict] = {} +THUMBNAIL_FILES: dict[str, bytes] = {} TEMP_FILE_EXPIRY = 300 # 5 minutes +THUMBNAIL_SIZE = (250, 250) # Maximum dimensions for thumbnail + +# ============================================================================ +# CONFIGURATION +# ============================================================================ + +# Override stegasoo limits for larger files +# Note: You might need to modify the stegasoo library itself +# to actually increase these limits in its internal calculations + +# Flask upload limit (30MB) +MAX_UPLOAD_SIZE = 30 * 1024 * 1024 + +# Try to import and override stegasoo constants if possible +try: + # Check current limits + print(f"Current MAX_FILE_SIZE from constants: {MAX_FILE_SIZE}") + print(f"Current MAX_FILE_PAYLOAD_SIZE: {MAX_FILE_PAYLOAD_SIZE}") + + # Try to increase payload size limit (in bytes) + # 15MB should be enough for 7.6MB files with overhead + DESIRED_PAYLOAD_SIZE = 15 * 1024 * 1024 # 15MB + + # Note: You might need to patch the stegasoo module + # if MAX_FILE_PAYLOAD_SIZE is used internally + import stegasoo + if hasattr(stegasoo, 'MAX_FILE_PAYLOAD_SIZE'): + print(f"Overriding MAX_FILE_PAYLOAD_SIZE to {DESIRED_PAYLOAD_SIZE}") + stegasoo.MAX_FILE_PAYLOAD_SIZE = DESIRED_PAYLOAD_SIZE + +except Exception as e: + print(f"Could not override stegasoo limits: {e}") + +def generate_thumbnail(image_data: bytes, size: tuple = THUMBNAIL_SIZE) -> bytes: + """Generate thumbnail from image data.""" + try: + with Image.open(io.BytesIO(image_data)) as img: + # Convert to RGB if necessary + if img.mode in ('RGBA', 'LA', 'P'): + # Create white background for transparent images + background = Image.new('RGB', img.size, (255, 255, 255)) + if img.mode == 'P': + img = img.convert('RGBA') + background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None) + img = background + elif img.mode != 'RGB': + img = img.convert('RGB') + + # Create thumbnail + img.thumbnail(size, Image.Resampling.LANCZOS) + + # Save to bytes + buffer = io.BytesIO() + img.save(buffer, format='JPEG', quality=85, optimize=True) + return buffer.getvalue() + except Exception as e: + # Log error but don't crash + print(f"Thumbnail generation error: {e}") + return None def cleanup_temp_files(): """Remove expired temporary files.""" now = time.time() expired = [fid for fid, info in TEMP_FILES.items() if now - info['timestamp'] > TEMP_FILE_EXPIRY] + for fid in expired: TEMP_FILES.pop(fid, None) + # Also clean up corresponding thumbnail + thumb_id = f"{fid}_thumb" + THUMBNAIL_FILES.pop(thumb_id, None) def allowed_image(filename: str) -> bool: @@ -67,6 +163,16 @@ def allowed_image(filename: str) -> bool: return ext in {'png', 'jpg', 'jpeg', 'bmp', 'gif'} +def format_size(size_bytes: int) -> str: + """Format file size for display.""" + if size_bytes < 1024: + return f"{size_bytes} B" + elif size_bytes < 1024 * 1024: + return f"{size_bytes / 1024:.1f} KB" + else: + return f"{size_bytes / (1024 * 1024):.1f} MB" + + # ============================================================================ # ROUTES # ============================================================================ @@ -85,7 +191,7 @@ def generate(): 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) + return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE) pin_length = int(request.form.get('pin_length', 6)) rsa_bits = int(request.form.get('rsa_bits', 2048)) @@ -105,6 +211,31 @@ def generate(): words_per_phrase=words_per_phrase ) + # Store RSA key temporarily for QR generation + qr_token = None + qr_needs_compression = False + qr_too_large = False + + if creds.rsa_key_pem and HAS_QRCODE: + # Check if key fits in QR code + if can_fit_in_qr(creds.rsa_key_pem, compress=False): + qr_needs_compression = False + elif can_fit_in_qr(creds.rsa_key_pem, compress=True): + qr_needs_compression = True + else: + qr_too_large = True + + if not qr_too_large: + qr_token = secrets.token_urlsafe(16) + cleanup_temp_files() + TEMP_FILES[qr_token] = { + 'data': creds.rsa_key_pem.encode(), + 'filename': 'rsa_key.pem', + 'timestamp': time.time(), + 'type': 'rsa_key', + 'compress': qr_needs_compression + } + return render_template('generate.html', phrases=creds.phrases, pin=creds.pin, @@ -120,13 +251,71 @@ def generate(): pin_entropy=creds.pin_entropy, rsa_entropy=creds.rsa_entropy, total_entropy=creds.total_entropy, - has_ml=False + has_qrcode=HAS_QRCODE, + qr_token=qr_token, + qr_needs_compression=qr_needs_compression, + qr_too_large=qr_too_large ) except Exception as e: flash(f'Error generating credentials: {e}', 'error') - return render_template('generate.html', generated=False, has_ml=False) + return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE) - return render_template('generate.html', generated=False, has_ml=False) + return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE) + + +@app.route('/generate/qr/') +def generate_qr(token): + """Generate QR code for RSA key.""" + if not HAS_QRCODE: + return "QR code support not available", 501 + + if token not in TEMP_FILES: + return "Token expired or invalid", 404 + + file_info = TEMP_FILES[token] + if file_info.get('type') != 'rsa_key': + return "Invalid token type", 400 + + try: + key_pem = file_info['data'].decode('utf-8') + compress = file_info.get('compress', False) + qr_png = generate_qr_code(key_pem, compress=compress) + + return send_file( + io.BytesIO(qr_png), + mimetype='image/png', + as_attachment=False + ) + except Exception as e: + return f"Error generating QR code: {e}", 500 + + +@app.route('/generate/qr-download/') +def generate_qr_download(token): + """Download QR code as PNG file.""" + if not HAS_QRCODE: + return "QR code support not available", 501 + + if token not in TEMP_FILES: + return "Token expired or invalid", 404 + + file_info = TEMP_FILES[token] + if file_info.get('type') != 'rsa_key': + return "Invalid token type", 400 + + try: + key_pem = file_info['data'].decode('utf-8') + compress = file_info.get('compress', False) + qr_png = generate_qr_code(key_pem, compress=compress) + + return send_file( + io.BytesIO(qr_png), + mimetype='image/png', + as_attachment=True, + download_name='stegasoo_rsa_key_qr.png' + ) + except Exception as e: + return f"Error generating QR code: {e}", 500 @app.route('/generate/download-key', methods=['POST']) @@ -134,22 +323,22 @@ 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()) + private_key = load_rsa_key(key_pem.encode('utf-8')) encrypted_pem = export_rsa_key_pem(private_key, password=password) - + key_id = secrets.token_hex(4) filename = f'stegasoo_key_{private_key.key_size}_{key_id}.pem' - + return send_file( io.BytesIO(encrypted_pem), mimetype='application/x-pem-file', @@ -161,9 +350,51 @@ def download_key(): return redirect(url_for('generate')) +@app.route('/extract-key-from-qr', methods=['POST']) +def extract_key_from_qr_route(): + """ + Extract RSA key from uploaded QR code image. + Returns JSON with the extracted key or error. + """ + if not HAS_QRCODE_READ: + return jsonify({ + 'success': False, + 'error': 'QR code reading not available. Install pyzbar and libzbar.' + }), 501 + + qr_image = request.files.get('qr_image') + if not qr_image: + return jsonify({ + 'success': False, + 'error': 'No QR image provided' + }), 400 + + try: + image_data = qr_image.read() + key_pem = extract_key_from_qr(image_data) + + if key_pem: + return jsonify({ + 'success': True, + 'key_pem': key_pem + }) + else: + return jsonify({ + 'success': False, + 'error': 'No valid RSA key found in QR code' + }), 400 + + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + + @app.route('/encode', methods=['GET', 'POST']) def encode_page(): day_of_week = get_today_day() + max_payload_kb = MAX_FILE_PAYLOAD_SIZE // 1024 if request.method == 'POST': try: @@ -171,61 +402,102 @@ def encode_page(): ref_photo = request.files.get('reference_photo') carrier = request.files.get('carrier') rsa_key_file = request.files.get('rsa_key') + payload_file = request.files.get('payload_file') if not ref_photo or not carrier: flash('Both reference photo and carrier image are required', 'error') - return render_template('encode.html', day_of_week=day_of_week) + return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ) if not allowed_image(ref_photo.filename) or not allowed_image(carrier.filename): flash('Invalid file type. Use PNG, JPG, or BMP', 'error') - return render_template('encode.html', day_of_week=day_of_week) + return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ) # Get form data message = request.form.get('message', '') day_phrase = request.form.get('day_phrase', '') pin = request.form.get('pin', '').strip() rsa_password = request.form.get('rsa_password', '') + payload_type = request.form.get('payload_type', 'text') - # 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) + # Determine payload + if payload_type == 'file' and payload_file and payload_file.filename: + # File payload + file_data = payload_file.read() + + result = validate_file_payload(file_data, payload_file.filename) + if not result.is_valid: + flash(result.error_message, 'error') + return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ) + + mime_type, _ = mimetypes.guess_type(payload_file.filename) + payload = FilePayload( + data=file_data, + filename=payload_file.filename, + mime_type=mime_type + ) + else: + # Text message + result = validate_message(message) + if not result.is_valid: + flash(result.error_message, 'error') + return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ) + payload = message if not day_phrase: flash('Day phrase is required', 'error') - return render_template('encode.html', day_of_week=day_of_week) + return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ) # Read files ref_data = ref_photo.read() carrier_data = carrier.read() - rsa_key_data = rsa_key_file.read() if rsa_key_file and rsa_key_file.filename else None + + # Handle RSA key - can come from .pem file or QR code image + rsa_key_data = None + rsa_key_qr = request.files.get('rsa_key_qr') + rsa_key_from_qr = False # Track source for password handling + + if rsa_key_file and rsa_key_file.filename: + # RSA key from .pem file + rsa_key_data = rsa_key_file.read() + elif rsa_key_qr and rsa_key_qr.filename and HAS_QRCODE_READ: + # RSA key from QR code image + qr_image_data = rsa_key_qr.read() + key_pem = extract_key_from_qr(qr_image_data) + if key_pem: + rsa_key_data = key_pem.encode('utf-8') + rsa_key_from_qr = True # QR keys are never password-protected + else: + flash('Could not extract RSA key from QR code image. Make sure the image contains a valid QR code.', 'error') + return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ) # Validate security factors result = validate_security_factors(pin, rsa_key_data) if not result.is_valid: flash(result.error_message, 'error') - return render_template('encode.html', day_of_week=day_of_week) + return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ) # Validate PIN if provided if pin: result = validate_pin(pin) if not result.is_valid: flash(result.error_message, 'error') - return render_template('encode.html', day_of_week=day_of_week) + return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ) + + # Determine key password - QR code keys are never password-protected + key_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None) # Validate RSA key if provided if rsa_key_data: - result = validate_rsa_key(rsa_key_data, rsa_password if rsa_password else None) + result = validate_rsa_key(rsa_key_data, key_password) if not result.is_valid: flash(result.error_message, 'error') - return render_template('encode.html', day_of_week=day_of_week) + return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ) # Validate carrier image result = validate_image(carrier_data, "Carrier image") if not result.is_valid: flash(result.error_message, 'error') - return render_template('encode.html', day_of_week=day_of_week) + return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ) # Get date client_date = request.form.get('client_date', '').strip() @@ -236,13 +508,13 @@ def encode_page(): # Encode encode_result = encode( - message=message, + message=payload, 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, + rsa_password=key_password, date_str=date_str ) @@ -259,15 +531,15 @@ def encode_page(): except CapacityError as e: flash(str(e), 'error') - return render_template('encode.html', day_of_week=day_of_week) + return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ) except StegasooError as e: flash(str(e), 'error') - return render_template('encode.html', day_of_week=day_of_week) + return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ) except Exception as e: flash(f'Error: {e}', 'error') - return render_template('encode.html', day_of_week=day_of_week) + return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ) - return render_template('encode.html', day_of_week=day_of_week) + return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ) @app.route('/encode/result/') @@ -277,9 +549,32 @@ def encode_result(file_id): return redirect(url_for('encode_page')) file_info = TEMP_FILES[file_id] + + # Generate thumbnail + thumbnail_data = generate_thumbnail(file_info['data']) + thumbnail_id = None + + if thumbnail_data: + thumbnail_id = f"{file_id}_thumb" + THUMBNAIL_FILES[thumbnail_id] = thumbnail_data + return render_template('encode_result.html', file_id=file_id, - filename=file_info['filename'] + filename=file_info['filename'], + thumbnail_url=url_for('encode_thumbnail', thumb_id=thumbnail_id) if thumbnail_id else None + ) + + +@app.route('/encode/thumbnail/') +def encode_thumbnail(thumb_id): + """Serve thumbnail image.""" + if thumb_id not in THUMBNAIL_FILES: + return "Thumbnail not found", 404 + + return send_file( + io.BytesIO(THUMBNAIL_FILES[thumb_id]), + mimetype='image/jpeg', + as_attachment=False ) @@ -299,7 +594,7 @@ def encode_download(file_id): @app.route('/encode/file/') -def encode_file(file_id): +def encode_file_route(file_id): """Serve file for Web Share API.""" if file_id not in TEMP_FILES: return "Not found", 404 @@ -317,6 +612,11 @@ def encode_file(file_id): def encode_cleanup(file_id): """Manually cleanup a file after sharing.""" TEMP_FILES.pop(file_id, None) + + # Also cleanup thumbnail if exists + thumb_id = f"{file_id}_thumb" + THUMBNAIL_FILES.pop(thumb_id, None) + return jsonify({'status': 'ok'}) @@ -331,7 +631,7 @@ def decode_page(): if not ref_photo or not stego_image: flash('Both reference photo and stego image are required', 'error') - return render_template('decode.html') + return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ) # Get form data day_phrase = request.form.get('day_phrase', '') @@ -340,61 +640,126 @@ def decode_page(): if not day_phrase: flash('Day phrase is required', 'error') - return render_template('decode.html') + return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ) # 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 + + # Handle RSA key - can come from .pem file or QR code image + rsa_key_data = None + rsa_key_qr = request.files.get('rsa_key_qr') + rsa_key_from_qr = False # Track source for password handling + + if rsa_key_file and rsa_key_file.filename: + # RSA key from .pem file + rsa_key_data = rsa_key_file.read() + elif rsa_key_qr and rsa_key_qr.filename and HAS_QRCODE_READ: + # RSA key from QR code image + qr_image_data = rsa_key_qr.read() + key_pem = extract_key_from_qr(qr_image_data) + if key_pem: + rsa_key_data = key_pem.encode('utf-8') + rsa_key_from_qr = True # QR keys are never password-protected + else: + flash('Could not extract RSA key from QR code image. Make sure the image contains a valid QR code.', 'error') + return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ) # Validate security factors result = validate_security_factors(pin, rsa_key_data) if not result.is_valid: flash(result.error_message, 'error') - return render_template('decode.html') + return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ) # Validate PIN if provided if pin: result = validate_pin(pin) if not result.is_valid: flash(result.error_message, 'error') - return render_template('decode.html') + return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ) + + # Determine key password - QR code keys are never password-protected + key_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None) # Validate RSA key if provided if rsa_key_data: - result = validate_rsa_key(rsa_key_data, rsa_password if rsa_password else None) + result = validate_rsa_key(rsa_key_data, key_password) if not result.is_valid: flash(result.error_message, 'error') - return render_template('decode.html') + return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ) # Decode - message = decode( + decode_result = decode( stego_image=stego_data, reference_photo=ref_data, day_phrase=day_phrase, pin=pin, rsa_key_data=rsa_key_data, - rsa_password=rsa_password if rsa_password else None + rsa_password=key_password ) - return render_template('decode.html', decoded_message=message) + if decode_result.is_file: + # File content - store temporarily for download + file_id = secrets.token_urlsafe(16) + cleanup_temp_files() + + filename = decode_result.filename or 'decoded_file' + TEMP_FILES[file_id] = { + 'data': decode_result.file_data, + 'filename': filename, + 'mime_type': decode_result.mime_type, + 'timestamp': time.time() + } + + return render_template('decode.html', + decoded_file=True, + file_id=file_id, + filename=filename, + file_size=format_size(len(decode_result.file_data)), + mime_type=decode_result.mime_type + ) + else: + # Text content + return render_template('decode.html', decoded_message=decode_result.message) except DecryptionError: flash('Decryption failed. Check your phrase, PIN, RSA key, and reference photo.', 'error') - return render_template('decode.html') + return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ) except StegasooError as e: flash(str(e), 'error') - return render_template('decode.html') + return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ) except Exception as e: flash(f'Error: {e}', 'error') - return render_template('decode.html') + return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ) - return render_template('decode.html') + return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ) + + +@app.route('/decode/download/') +def decode_download(file_id): + """Download decoded file.""" + if file_id not in TEMP_FILES: + flash('File expired or not found.', 'error') + return redirect(url_for('decode_page')) + + file_info = TEMP_FILES[file_id] + mime_type = file_info.get('mime_type', 'application/octet-stream') + + return send_file( + io.BytesIO(file_info['data']), + mimetype=mime_type, + as_attachment=True, + download_name=file_info['filename'] + ) @app.route('/about') def about(): - return render_template('about.html', has_argon2=has_argon2()) + return render_template('about.html', + has_argon2=has_argon2(), + has_qrcode_read=HAS_QRCODE_READ, + max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024 + ) # ============================================================================ diff --git a/frontends/web/static/favicon.svg b/frontends/web/static/favicon.svg index 4124d8b..53fc308 100644 --- a/frontends/web/static/favicon.svg +++ b/frontends/web/static/favicon.svg @@ -1,112 +1,86 @@ + - - - - + + + + + - - + + + - - - + + + - - + + + - + - + - - + + - + - - - - - + + + + + - + - + - + - + - - + + - + - + - + - - + + - + - + + v3.7" fill="none" stroke="#C28A24" stroke-width="2.4" stroke-linecap="round"> - - + + - - diff --git a/frontends/web/static/logo.svg b/frontends/web/static/logo.svg index 288b8ed..db58c33 100644 --- a/frontends/web/static/logo.svg +++ b/frontends/web/static/logo.svg @@ -1,165 +1,131 @@ + - - - - + + + + + - - + + + - + + - - - + + + - - + + + + - + - + - + - + - + - - - - - + + + + + - + - + - - + + - - - - + + + + - - - + + + - + - + - - - + + + + + + - + - + + - + + - - + + + + + - - + + - - - + + + + v4" fill="none" stroke="#C28A24" stroke-width="2.4" stroke-linecap="round"> - + + - - + + + - - diff --git a/frontends/web/static/style.css b/frontends/web/static/style.css index 9f8a840..95cc22a 100644 --- a/frontends/web/static/style.css +++ b/frontends/web/static/style.css @@ -47,6 +47,15 @@ body { border-bottom: none; } +.card-link .card-header.text-center { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + height: 4.5rem; /* Fixed height to match original */ + display: flex; + align-items: center; + justify-content: center; +} + .feature-card { transition: transform 0.3s ease, box-shadow 0.3s ease; } @@ -255,3 +264,137 @@ footer { .footer-icon { vertical-align: text-bottom; } + +/* ---------------------------------------------------------------------------- + Card Stuff Icons + ---------------------------------------------------------------------------- */ +.action-card { + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.action-card:hover { + transform: translateY(-5px); + box-shadow: 0 10px 20px rgba(0,0,0,0.1) !important; +} + +.feature-card { + border-radius: 10px; + overflow: hidden; +} + +/* Ensure buttons are easily tappable on mobile */ +@media (max-width: 768px) { + .btn-lg { + padding: 1rem 1.5rem; + font-size: 1.1rem; + } + + .card { + margin-bottom: 1rem; + } +} + +/* ---------------------------------------------------------------------------- + Dramatic Gold Glow Hover Effect + ---------------------------------------------------------------------------- */ +.embossed-icon { + color: #f8f8f8 !important; + text-shadow: + /* Subtle emboss only by default */ + 0 0.5px 0 rgba(255, 255, 255, 0.4), /* Soft top highlight */ + 0 -0.5px 0 rgba(0, 0, 0, 0.15), /* Very soft bottom shadow */ + 0 1px 2px rgba(0, 0, 0, 0.1); /* Minimal drop shadow */ + + position: relative; + display: inline-block; + transition: all 0.3s ease; + font-size: 3.5rem !important; + line-height: 1; + padding: 10px 0; +} + +.card-link:hover .embossed-icon { + color: #ffffff !important; + text-shadow: + /* GOLD GLOW LAYERS - Only on hover */ + 0 0 10px rgba(255, 215, 0, 0.5), /* Soft inner glow */ + 0 0 20px rgba(255, 215, 0, 0.3), /* Medium glow */ + 0 0 30px rgba(255, 215, 0, 0.2), /* Outer glow */ + 0 0 40px rgba(255, 215, 0, 0.1), /* Far outer glow */ + + /* Enhanced emboss on hover */ + 0 1px 0 rgba(255, 255, 255, 0.8), /* Bright top highlight */ + 0 -1px 0 rgba(0, 0, 0, 0.25), /* Deeper bottom shadow */ + 0 2px 4px rgba(0, 0, 0, 0.15), /* Soft drop shadow */ + + /* Gold accent shadows */ + 0 1px 2px rgba(255, 215, 0, 0.3), /* Gold highlight layer */ + 0 -1px 1px rgba(255, 215, 0, 0.15); /* Gold shadow layer */ + + filter: drop-shadow(0 0 8px rgba(255, 215, 0, 0.25)); + transform: scale(1.02); /* Slight grow on hover */ +} + +/* Card header adjustments for dramatic effect */ +.card-link .card-header.text-center { + padding-top: 1.25rem !important; + padding-bottom: 1.25rem !important; + min-height: 6.5rem; + display: flex; + align-items: center; + justify-content: center; + position: relative; + transition: all 0.3s ease; +} + +/* Enhance the gradient on hover for dramatic effect */ +.card-link:hover .card-header.text-center { + background: linear-gradient(135deg, + var(--gradient-start) 0%, + #5a67d8 20%, + var(--gradient-end) 80%, + #8a2be2 100%); + box-shadow: inset 0 0 20px rgba(255, 215, 0, 0.1); +} + +/* Mobile adjustments */ +@media (max-width: 768px) { + .embossed-icon { + font-size: 2.8rem !important; + padding: 8px 0; + } + + .card-link:hover .embossed-icon { + transform: scale(1.01); + } + + .card-link .card-header.text-center { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + min-height: 5.5rem; + } +} + +/* ---------------------------------------------------------------------------- + Card Links + ---------------------------------------------------------------------------- */ +.card-link { + text-decoration: none; + color: inherit; + display: block; + height: 100%; +} + +.card-link:hover { + color: inherit; +} + +/* Optional: Add a slight scale effect to the entire card on hover */ +.card-link .feature-card { + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.card-link:hover .feature-card { + transform: translateY(-5px); + box-shadow: 0 10px 40px rgba(102, 126, 234, 0.3); +} \ No newline at end of file diff --git a/frontends/web/templates/about.html b/frontends/web/templates/about.html index 7ac6af0..c275f0e 100644 --- a/frontends/web/templates/about.html +++ b/frontends/web/templates/about.html @@ -10,37 +10,239 @@
About Stegasoo
-

- 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. +

+ Stegasoo is a secure steganography tool that hides encrypted messages and files + inside ordinary images using multi-factor authentication.

-
System Status
-
+
Key Features
+
-
- {% if has_argon2 %} - -
- Argon2id Available -
Memory-hard key derivation (256MB)
-
- {% else %} - -
- Using PBKDF2 Fallback -
Install argon2-cffi for better security
-
- {% endif %} -
+
    +
  • + + Text & File Embedding — Hide messages or any file type (PDF, ZIP, documents) +
  • +
  • + + Multi-Factor Security — Combines photo + phrase + PIN/RSA key +
  • +
  • + + AES-256-GCM Encryption — Military-grade authenticated encryption +
  • +
  • + + Daily Rotating Phrases — Different passphrase each day of the week +
  • +
-
- -
- AES-256-GCM -
Authenticated encryption enabled
+
    +
  • + + Random Pixel Embedding — Defeats statistical steganalysis +
  • +
  • + + Format Preservation — Maintains PNG/BMP lossless formats +
  • +
  • + + Large Capacity — Up to {{ max_payload_kb }} KB payload, 16MP images +
  • +
  • + + Zero Server Storage — Nothing saved, files auto-expire +
  • +
+
+
+
+
+ +
+
+
How Security Works
+
+
+

Stegasoo uses hybrid multi-factor authentication to derive encryption keys:

+ +
+
+
+ + Reference Photo +
Something you have
+
~80-256 bits
+
+
+
+
+ + Daily Phrase +
Something you know (rotates)
+
~33 bits (3 words)
+
+
+
+
+ + Static PIN +
Something you know (fixed)
+
~20 bits (6 digits)
+
+
+
+
+ + RSA Key +
Something you have (optional)
+
~128 bits (2048-bit)
+
+
+
+ +
+ + Combined entropy: 130-400+ bits depending on configuration. + For reference, 128 bits is considered computationally infeasible to brute force. +
+ +
Key Derivation
+

+ {% if has_argon2 %} + Argon2id Available + Using Argon2id with 256MB memory cost — the winner of the Password Hashing Competition + and current best practice for key derivation. + {% else %} + Argon2 Not Available + Falling back to PBKDF2-SHA512 with 600,000 iterations. + Install argon2-cffi for stronger security. + {% endif %} +

+ +
Steganography Technique
+

+ Uses LSB (Least Significant Bit) embedding with pseudo-random pixel selection. + The pixel locations are determined by a key derived from your credentials, making the + hidden data's location unpredictable without the correct inputs. +

+
+
+ +
+
+
File Embedding
+
+
+

+ New in v2.1 + Stegasoo now supports embedding any file type, not just text messages. +

+ +
+
+
Supported
+
    +
  • PDF documents
  • +
  • ZIP/RAR archives
  • +
  • Office documents (DOCX, XLSX, PPTX)
  • +
  • Source code files
  • +
  • Any binary file up to {{ max_payload_kb }} KB
  • +
+
+
+
How It Works
+
    +
  • Original filename is preserved
  • +
  • MIME type is stored for proper handling
  • +
  • File is encrypted identically to text
  • +
  • Decoding auto-detects text vs. file
  • +
+
+
+ +
+ + Tip: For larger files, compress them first (ZIP) to maximize capacity. + A 16MP carrier image can hold approximately 6MB of raw data, but we limit payloads + to {{ max_payload_kb }} KB for reasonable processing times. +
+
+
+ +
+
+
Usage Guide
+
+
+
+
+

+ +

+
+
+
    +
  1. Both parties agree on a reference photo (shared secretly, never transmitted)
  2. +
  3. Go to Generate and create credentials
  4. +
  5. Memorize the 7 daily phrases and PIN
  6. +
  7. If using RSA, download and securely store the key file
  8. +
  9. Share credentials with your contact through a secure channel
  10. +
+
+
+
+ +
+

+ +

+
+
+
    +
  1. Go to Encode
  2. +
  3. Upload your reference photo
  4. +
  5. Upload a carrier image (the image to hide data in)
  6. +
  7. Choose Text or File mode
  8. +
  9. Enter your message or select a file to embed
  10. +
  11. Enter today's phrase and your PIN/key
  12. +
  13. Download the resulting stego image
  14. +
  15. Send the stego image through any channel (email, social media, etc.)
  16. +
+
+
+
+ +
+

+ +

+
+
+
    +
  1. Go to Decode
  2. +
  3. Upload your reference photo (same one used for encoding)
  4. +
  5. Upload the stego image you received
  6. +
  7. Enter the phrase for the day it was encoded (check the filename for date)
  8. +
  9. Enter your PIN and/or RSA key
  10. +
  11. View the decoded message or download the extracted file
  12. +
+
+ + The stego image filename contains the encoding date (e.g., abc123_20251228.png). + Use this to determine which day's phrase to use! +
@@ -50,129 +252,100 @@
-
Security Model
+
Limits & Specifications
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ComponentEntropyPurpose
Reference Photo~80-256 bitsSomething you have (plausible deniability)
3-Word Phrase~33 bitsSomething you know (changes daily)
6-Digit PIN~20 bitsSomething you know (static)
DateN/AAutomatic key rotation
Combined133+ bitsBeyond brute force
-
-
-
- -
-
-
Attack Resistance
-
-
-
-
-
What Attackers Can't Do
-
    -
  • - - Brute force the passphrase (2133 combinations) -
  • -
  • - - Use rainbow tables (random salt per message) -
  • -
  • - - Detect hidden data (random pixel selection) -
  • -
  • - - Use GPU farms (Argon2 requires 256MB RAM per attempt) -
  • -
-
-
-
Real Threats
-
    -
  • - - Social engineering (someone tricks you) -
  • -
  • - - Physical access to your devices -
  • -
  • - - Malware/keyloggers on your system -
  • -
  • - - Shoulder surfing while you type -
  • -
-
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Max text message250,000 characters (~250 KB)
Max file payload{{ max_payload_kb }} KB
Max carrier image16 megapixels (~4000×4000)
Max upload size10 MB
Temp file expiry5 minutes
PIN length6-9 digits
RSA key sizes2048, 3072, 4096 bits
Phrase length3-12 words (BIP-39 wordlist)
-
Best Practices
+
CLI & API
-
-
-
Do
-
    -
  • Memorize your phrases and PIN, never write them down
  • -
  • Use a reference photo that both parties already have
  • -
  • Use different carrier images for each message
  • -
  • Share stego images through normal channels (looks innocent)
  • -
-
-
-
Don't
-
    -
  • Don't transmit the reference photo
  • -
  • Don't reuse the same carrier image
  • -
  • Don't store phrases or PIN digitally
  • -
  • Don't resize or recompress stego images
  • -
-
-
+

Stegasoo is also available as a command-line tool and REST API:

+ +
Command Line
+
# Generate credentials
+stegasoo generate --pin --rsa
+
+# Encode a text message
+stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder" --pin 123456 -m "secret"
+
+# Encode a file
+stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder" --pin 123456 -e document.pdf
+
+# Decode (auto-detects text vs file)
+stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder" --pin 123456
+ +
REST API
+
# Encode with multipart upload
+curl -X POST http://localhost:8000/encode/multipart \
+  -F "reference_photo=@photo.jpg" \
+  -F "carrier=@meme.png" \
+  -F "message=secret" \
+  -F "day_phrase=apple forest thunder" \
+  -F "pin=123456" \
+  --output stego.png
+
+# Encode a file
+curl -X POST http://localhost:8000/encode/multipart \
+  -F "reference_photo=@photo.jpg" \
+  -F "carrier=@meme.png" \
+  -F "payload_file=@document.pdf" \
+  -F "day_phrase=apple forest thunder" \
+  -F "pin=123456" \
+  --output stego.png
+ +

+ API documentation available at /docs (Swagger) or /redoc when running the API server. +

+ +
+

+ Stegasoo v2.1.0 • + Open Source • + Built with Python, Flask, and cryptography +

+
{% endblock %} diff --git a/frontends/web/templates/base.html b/frontends/web/templates/base.html index 7254369..190c289 100644 --- a/frontends/web/templates/base.html +++ b/frontends/web/templates/base.html @@ -63,7 +63,7 @@
- Stegasoo v1.1 — Hybrid Photo + Day-Phrase + PIN Steganography + Stegasoo v2.1.0 — Hybrid Photo + Day-Phrase + PIN Steganography
diff --git a/frontends/web/templates/decode.html b/frontends/web/templates/decode.html index 226d6fd..3d8c165 100644 --- a/frontends/web/templates/decode.html +++ b/frontends/web/templates/decode.html @@ -7,25 +7,57 @@
-
Decode Secret Message
+
Decode Secret Message or File
{% if decoded_message %} +
Message Decrypted Successfully!
+ + +
+
{{ decoded_message }}
+ +
+ + + Decode Another + -
- -
{{ decoded_message }}
+ {% elif decoded_file %} + +
+
File Decrypted Successfully!
+
+ +
+ +
{{ filename }}
+

{{ file_size }}

+ {% if mime_type %} + Type: {{ mime_type }} + {% endif %} +
+ + + Download File + + +
+ + File expires in 5 minutes. Download now.
- Decode Another Message + Decode Another {% else %} - +
@@ -58,7 +90,7 @@
- The image containing the hidden message + The image containing the hidden message/file
@@ -83,24 +115,47 @@
- - + + +
+ + +
If PIN was used during encoding
+
- + +
+
+ +
+
+ +
PNG, JPG, or other image of QR code
+
+
- If RSA key was used during encoding + If RSA key was used during encoding (file or QR image)
@@ -118,7 +173,7 @@
@@ -126,6 +181,7 @@
+ {% if not decoded_message and not decoded_file %}
Troubleshooting
@@ -153,6 +209,7 @@
+ {% endif %}
{% endblock %} @@ -166,13 +223,32 @@ document.getElementById('decodeForm')?.addEventListener('submit', function() { btn.disabled = true; }); -// Show RSA password field when key is selected +// Show RSA password field when key is selected (only for .pem files, not QR) const rsaKeyInput = document.getElementById('rsaKeyInput'); +const rsaKeyQrInput = document.getElementById('rsaKeyQrInput'); const rsaPasswordGroup = document.getElementById('rsaPasswordGroup'); -rsaKeyInput?.addEventListener('change', function() { - rsaPasswordGroup.classList.toggle('d-none', !this.files.length); -}); +if (rsaKeyInput) { + rsaKeyInput.addEventListener('change', function() { + // Show password field only for .pem files + rsaPasswordGroup.classList.toggle('d-none', !this.files.length); + // Clear QR input if file is selected + if (rsaKeyQrInput && this.files.length) { + rsaKeyQrInput.value = ''; + } + }); +} + +if (rsaKeyQrInput) { + rsaKeyQrInput.addEventListener('change', function() { + // Hide password field for QR codes (they're unencrypted) + rsaPasswordGroup.classList.add('d-none'); + // Clear file input if QR is selected + if (rsaKeyInput && this.files.length) { + rsaKeyInput.value = ''; + } + }); +} // Day names for date detection const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; @@ -197,6 +273,43 @@ function updateDayLabel(dayName) { } } +// PIN Toggle +document.getElementById('togglePin')?.addEventListener('click', function() { + const input = document.getElementById('pinInput'); + const icon = this.querySelector('i'); + if (input.type === 'password') { + input.type = 'text'; + icon.classList.replace('bi-eye', 'bi-eye-slash'); + } else { + input.type = 'password'; + icon.classList.replace('bi-eye-slash', 'bi-eye'); + } +}); + +// Paste from Clipboard +document.addEventListener('paste', function(e) { + if (!document.getElementById('decodeForm')) return; + + const items = e.clipboardData.items; + for (let i = 0; i < items.length; i++) { + if (items[i].type.indexOf("image") !== -1) { + const blob = items[i].getAsFile(); + + const stegoInput = document.querySelector('input[name="stego_image"]'); + const refInput = document.querySelector('input[name="reference_photo"]'); + + const targetInput = (!stegoInput.files.length) ? stegoInput : refInput; + + const container = new DataTransfer(); + container.items.add(blob); + targetInput.files = container.files; + + targetInput.dispatchEvent(new Event('change')); + break; + } + } +}); + // Drag & drop with preview document.querySelectorAll('.drop-zone').forEach(zone => { const input = zone.querySelector('input[type="file"]'); diff --git a/frontends/web/templates/encode.html b/frontends/web/templates/encode.html index 8498c19..acf3e79 100644 --- a/frontends/web/templates/encode.html +++ b/frontends/web/templates/encode.html @@ -7,7 +7,7 @@
-
Encode Secret Message
+
Encode Secret Message or File
@@ -49,15 +49,34 @@
+
+ +
+ + + + + +
+
+ + +
+ placeholder="Enter your secret message here...">
- 0 / 50,000 characters + 0 / 250,000 characters Getting long! @@ -66,6 +85,29 @@
+ +
+ +
+ +
+ + Drop any file or click to browse +
Max {{ max_payload_kb }} KB
+
+
+
+ Supports any file type: PDF, ZIP, documents, etc. +
+
+ + + () +
+
+
@@ -177,13 +236,83 @@ if (dateInput) { dateInput.value = localDate; } -// Show RSA password field when key is selected +// Payload type switching +const payloadTextRadio = document.getElementById('payloadText'); +const payloadFileRadio = document.getElementById('payloadFile'); +const textSection = document.getElementById('textPayloadSection'); +const fileSection = document.getElementById('filePayloadSection'); +const messageInput = document.getElementById('messageInput'); +const payloadFileInput = document.getElementById('payloadFileInput'); + +function updatePayloadSection() { + const isText = payloadTextRadio.checked; + textSection.classList.toggle('d-none', !isText); + fileSection.classList.toggle('d-none', isText); + + // Update required attribute + if (isText) { + messageInput.required = true; + payloadFileInput.required = false; + } else { + messageInput.required = false; + payloadFileInput.required = true; + } +} + +payloadTextRadio.addEventListener('change', updatePayloadSection); +payloadFileRadio.addEventListener('change', updatePayloadSection); + +// File payload info display +const fileInfo = document.getElementById('fileInfo'); +const fileInfoName = document.getElementById('fileInfoName'); +const fileInfoSize = document.getElementById('fileInfoSize'); +const payloadDropLabel = document.getElementById('payloadDropLabel'); + +payloadFileInput.addEventListener('change', function() { + if (this.files && this.files[0]) { + const file = this.files[0]; + fileInfoName.textContent = file.name; + fileInfoSize.textContent = formatFileSize(file.size); + fileInfo.classList.remove('d-none'); + payloadDropLabel.innerHTML = `${file.name}`; + } else { + fileInfo.classList.add('d-none'); + payloadDropLabel.innerHTML = `Drop any file or click to browse
Max {{ max_payload_kb }} KB
`; + } +}); + +function formatFileSize(bytes) { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; +} + +// Show RSA password field when key is selected (only for .pem files, not QR) const rsaKeyInput = document.getElementById('rsaKeyInput'); +const rsaKeyQrInput = document.getElementById('rsaKeyQrInput'); const rsaPasswordGroup = document.getElementById('rsaPasswordGroup'); -rsaKeyInput.addEventListener('change', function() { - rsaPasswordGroup.classList.toggle('d-none', !this.files.length); -}); +if (rsaKeyInput) { + rsaKeyInput.addEventListener('change', function() { + // Show password field only for .pem files + rsaPasswordGroup.classList.toggle('d-none', !this.files.length); + // Clear QR input if file is selected + if (rsaKeyQrInput && this.files.length) { + rsaKeyQrInput.value = ''; + } + }); +} + +if (rsaKeyQrInput) { + rsaKeyQrInput.addEventListener('change', function() { + // Hide password field for QR codes (they're unencrypted) + rsaPasswordGroup.classList.add('d-none'); + // Clear file input if QR is selected + if (rsaKeyInput && this.files.length) { + rsaKeyInput.value = ''; + } + }); +} // Form submit loading state document.getElementById('encodeForm').addEventListener('submit', function(e) { @@ -192,12 +321,11 @@ document.getElementById('encodeForm').addEventListener('submit', function(e) { btn.disabled = true; }); -// Character counter -const messageInput = document.getElementById('messageInput'); +// Character counter for text const charCount = document.getElementById('charCount'); const charWarning = document.getElementById('charWarning'); const charPercent = document.getElementById('charPercent'); -const maxChars = 50000; +const maxChars = 250000; messageInput.addEventListener('input', function() { const len = this.value.length; @@ -210,11 +338,12 @@ messageInput.addEventListener('input', function() { charCount.classList.toggle('text-danger', len > maxChars * 0.95); }); -// Drag & drop with preview +// Drag & drop with preview for images 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 isPayloadZone = zone.id === 'payloadDropZone'; ['dragenter', 'dragover'].forEach(evt => { zone.addEventListener(evt, e => { @@ -233,27 +362,90 @@ document.querySelectorAll('.drop-zone').forEach(zone => { zone.addEventListener('drop', e => { if (e.dataTransfer.files.length) { input.files = e.dataTransfer.files; - showPreview(e.dataTransfer.files[0]); + input.dispatchEvent(new Event('change')); + + if (!isPayloadZone) { + showPreview(e.dataTransfer.files[0]); + } } }); - input.addEventListener('change', function() { - if (this.files && this.files[0]) { - showPreview(this.files[0]); - } - }); + if (!isPayloadZone) { + 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'); + if (preview) { + preview.src = e.target.result; + preview.classList.remove('d-none'); + } label.innerHTML = '' + file.name; }; reader.readAsDataURL(file); } }); + +// PIN Toggle Logic +document.getElementById('togglePin').addEventListener('click', function() { + const input = document.getElementById('pinInput'); + const icon = this.querySelector('i'); + if (input.type === 'password') { + input.type = 'text'; + icon.classList.replace('bi-eye', 'bi-eye-slash'); + } else { + input.type = 'password'; + icon.classList.replace('bi-eye-slash', 'bi-eye'); + } +}); + +// Prevent Same File Selection +function checkDuplicateFiles() { + const refInput = document.querySelector('input[name="reference_photo"]'); + const carInput = document.querySelector('input[name="carrier"]'); + + if (refInput.files[0] && carInput.files[0]) { + if (refInput.files[0].name === carInput.files[0].name && + refInput.files[0].size === carInput.files[0].size) { + alert("Security Warning: You cannot use the same image for both Reference and Carrier!"); + carInput.value = ''; + document.getElementById('carrierPreview').classList.add('d-none'); + document.querySelector('#carrierDropZone .drop-zone-label').innerHTML = + '' + + 'Drop image or click to browse'; + } + } +} +document.querySelector('input[name="reference_photo"]').addEventListener('change', checkDuplicateFiles); +document.querySelector('input[name="carrier"]').addEventListener('change', checkDuplicateFiles); + +// Paste from Clipboard +document.addEventListener('paste', function(e) { + const items = e.clipboardData.items; + for (let i = 0; i < items.length; i++) { + if (items[i].type.indexOf("image") !== -1) { + const blob = items[i].getAsFile(); + + const carrierInput = document.querySelector('input[name="carrier"]'); + const refInput = document.querySelector('input[name="reference_photo"]'); + + const targetInput = (!carrierInput.files.length) ? carrierInput : refInput; + + const container = new DataTransfer(); + container.items.add(blob); + targetInput.files = container.files; + + targetInput.dispatchEvent(new Event('change')); + break; + } + } +}); {% endblock %} diff --git a/frontends/web/templates/encode_result.html b/frontends/web/templates/encode_result.html index c5a268d..6a3036d 100644 --- a/frontends/web/templates/encode_result.html +++ b/frontends/web/templates/encode_result.html @@ -1,61 +1,64 @@ {% extends "base.html" %} -{% block title %}Message Encoded - Stegasoo{% endblock %} +{% block title %}Encode Success - Stegasoo{% endblock %} {% block content %}
-
+
-
-
Message Encoded Successfully!
+
+
Encoding Successful!
-
- -
{{ filename }}
-

Your secret message is hidden in this image

+
+ {% if thumbnail_url %} + +
+ Encoded image thumbnail +
+ Encoded Image Preview +
+
+ {% else %} + + + {% endif %}
-
+

Your secret has been hidden in the image.

+ +
+ {{ filename }} +
+ +
Download Image -
- -
-

Share via:

- -
-
- - File expires in 5 minutes. - Download or share now. The file will be securely deleted after expiry. + + Important: +
    +
  • This file expires in 5 minutes
  • +
  • Do not resize or recompress the image
  • +
  • PNG format preserves your hidden data
  • +
- - Encode Another Message + + Encode Another Message
@@ -65,78 +68,42 @@ {% block scripts %} -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/frontends/web/templates/generate.html b/frontends/web/templates/generate.html index 5d6af8d..fd8c8c2 100644 --- a/frontends/web/templates/generate.html +++ b/frontends/web/templates/generate.html @@ -11,335 +11,479 @@
{% if not generated %} -

- Generate your weekly phrase card and security factors. You must choose at least one: PIN or RSA Key. -

- -
+ +
- - -
More words = more security, harder to memorize
+ + +
+ 3 (33 bits) + 3 words (~33 bits) + 12 (132 bits) +
-
+
-
SECURITY FACTORS (select at least one)
+
SECURITY FACTORS (select at least one)
- -
-
-
- -