A whoooole lotta 4.0.x fixes.

This commit is contained in:
Aaron D. Lee
2026-01-01 22:18:13 -05:00
parent 12929bf326
commit ef7478b30a
40 changed files with 6003 additions and 1830 deletions

View File

@@ -1,10 +1,15 @@
#!/usr/bin/env python3
"""
Stegasoo REST API (v3.2.0)
Stegasoo REST API (v4.0.0)
FastAPI-based REST API for steganography operations.
Supports both text messages and file embedding.
CHANGES in v4.0.0:
- Updated from v3.2.0 with no functional API changes
- Internal: JPEG normalization for jpegio compatibility
- Internal: Python 3.12 recommended
CHANGES in v3.2.0:
- Removed date dependency from all operations
- Renamed day_phrase → passphrase
@@ -32,7 +37,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'src'))
import stegasoo
from stegasoo import (
encode, decode, generate_credentials,
validate_image, calculate_capacity,
validate_image,
__version__,
StegasooError, DecryptionError, CapacityError,
has_argon2,
@@ -75,6 +80,11 @@ app = FastAPI(
description="""
Secure steganography with hybrid authentication. Supports text messages and file embedding.
## Version 4.0.0 Changes
- **Python 3.12 recommended** - jpegio compatibility improvements
- **JPEG normalization** - Handles quality=100 images automatically
## Version 3.2.0 Changes
- **No date parameters needed** - Encode and decode anytime without tracking dates
@@ -1052,7 +1062,7 @@ async def api_image_info(
if not result.is_valid:
raise HTTPException(400, result.error_message)
capacity = calculate_capacity(image_data)
capacity = calculate_capacity_by_mode(image_data, 'lsb')
response = ImageInfoResponse(
width=result.details['width'],

View File

@@ -64,15 +64,37 @@ from stegasoo import (
# Models
FilePayload,
# Constants
EMBED_MODE_LSB,
EMBED_MODE_DCT,
EMBED_MODE_AUTO,
DEFAULT_PASSPHRASE_WORDS,
DEFAULT_PIN_LENGTH,
)
# Import constants - try main module first, then constants submodule
try:
from stegasoo import (
EMBED_MODE_LSB,
EMBED_MODE_DCT,
EMBED_MODE_AUTO,
)
except ImportError:
from stegasoo.constants import (
EMBED_MODE_LSB,
EMBED_MODE_DCT,
EMBED_MODE_AUTO,
)
# Import constants that may not be in main __init__
try:
from stegasoo.constants import (
DEFAULT_PASSPHRASE_WORDS,
DEFAULT_PIN_LENGTH,
MIN_PIN_LENGTH,
MAX_PIN_LENGTH,
)
except ImportError:
# Fallback defaults if constants not available
DEFAULT_PASSPHRASE_WORDS = 4
DEFAULT_PIN_LENGTH = 6
MIN_PIN_LENGTH = 6
MAX_PIN_LENGTH = 9
# Optional: strip_image_metadata from utils
try:
from stegasoo.utils import strip_image_metadata

View File

@@ -0,0 +1,62 @@
# Subprocess Isolation for Stegasoo WebUI
This update runs encode/decode/compare operations in isolated subprocesses
to prevent jpegio/scipy crashes from taking down the Flask server.
## Files
- **app.py** - Updated Flask app using subprocess isolation
- **subprocess_stego.py** - Flask-side wrapper with clean API
- **stego_worker.py** - Subprocess script that does actual stegasoo operations
## Setup
1. Place all three files in your `webui/` directory (same level as templates/)
2. Make sure stego_worker.py is executable (optional):
```bash
chmod +x stego_worker.py
```
3. Run the Flask app:
```bash
python app.py
```
## How It Works
Instead of calling stegasoo functions directly in the Flask process:
```python
# OLD (crashes could kill Flask)
result = encode(...)
```
We now run them in subprocesses:
```python
# NEW (crashes only kill the subprocess)
result = subprocess_stego.encode(...)
```
If jpegio or scipy crashes due to memory corruption, only the subprocess
dies. Flask logs the error and continues running. The next request spawns
a fresh subprocess.
## Configuration
In `app.py`, you can adjust the timeout:
```python
subprocess_stego = SubprocessStego(timeout=180) # 3 minutes
```
Larger images may need longer timeouts.
## Troubleshooting
If you see "Worker script not found" errors, make sure `stego_worker.py`
is in the same directory as `app.py`.
If subprocess operations fail, check the Flask logs for error details.
The subprocess wrapper captures both stdout and stderr from the worker.

View File

@@ -29,12 +29,16 @@ from flask import (
jsonify, flash, redirect, url_for
)
import os
os.environ['NUMPY_MADVISE_HUGEPAGE'] = '0'
os.environ['OMP_NUM_THREADS'] = '1'
# 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,
generate_credentials,
export_rsa_key_pem, load_rsa_key,
validate_pin, validate_message, validate_image,
validate_rsa_key, validate_security_factors,
@@ -48,8 +52,7 @@ from stegasoo import (
EMBED_MODE_DCT,
EMBED_MODE_AUTO,
has_dct_support,
compare_modes,
will_fit_by_mode,
# NOTE: encode, decode, compare_modes, will_fit_by_mode now use subprocess isolation
)
from stegasoo.constants import (
__version__,
@@ -90,6 +93,17 @@ from stegasoo.qr_utils import (
QR_MAX_BINARY, COMPRESSION_PREFIX
)
# ============================================================================
# SUBPROCESS ISOLATION FOR STEGASOO OPERATIONS
# ============================================================================
# Runs encode/decode/compare in subprocesses to prevent jpegio/scipy crashes
# from taking down the Flask server.
from subprocess_stego import SubprocessStego
# Initialize subprocess wrapper (worker script must be in same directory)
subprocess_stego = SubprocessStego(timeout=180) # 3 minute timeout for large images
# ============================================================================
# FLASK APP CONFIGURATION
@@ -436,6 +450,7 @@ def api_compare_capacity():
"""
Compare LSB and DCT capacity for an uploaded carrier image.
Returns JSON with capacity info for both modes.
Uses subprocess isolation to prevent crashes.
"""
carrier = request.files.get('carrier')
if not carrier:
@@ -443,23 +458,28 @@ def api_compare_capacity():
try:
carrier_data = carrier.read()
comparison = compare_modes(carrier_data)
# Use subprocess-isolated compare_modes
result = subprocess_stego.compare_modes(carrier_data)
if not result.success:
return jsonify({'error': result.error or 'Comparison failed'}), 500
return jsonify({
'success': True,
'width': comparison['width'],
'height': comparison['height'],
'width': result.width,
'height': result.height,
'lsb': {
'capacity_bytes': comparison['lsb']['capacity_bytes'],
'capacity_kb': round(comparison['lsb']['capacity_kb'], 1),
'output': comparison['lsb']['output'],
'capacity_bytes': result.lsb['capacity_bytes'],
'capacity_kb': round(result.lsb['capacity_kb'], 1),
'output': result.lsb.get('output', 'PNG'),
},
'dct': {
'capacity_bytes': comparison['dct']['capacity_bytes'],
'capacity_kb': round(comparison['dct']['capacity_kb'], 1),
'output': comparison['dct']['output'],
'available': comparison['dct']['available'],
'ratio': round(comparison['dct']['ratio_vs_lsb'], 1),
'capacity_bytes': result.dct['capacity_bytes'],
'capacity_kb': round(result.dct['capacity_kb'], 1),
'output': result.dct.get('output', 'JPEG'),
'available': result.dct.get('available', True),
'ratio': round(result.dct.get('ratio_vs_lsb', 0), 1),
}
})
except Exception as e:
@@ -471,6 +491,7 @@ def api_check_fit():
"""
Check if a payload will fit in the carrier with selected mode.
Returns JSON with fit status and details.
Uses subprocess isolation to prevent crashes.
"""
carrier = request.files.get('carrier')
payload_size = request.form.get('payload_size', type=int)
@@ -487,16 +508,25 @@ def api_check_fit():
try:
carrier_data = carrier.read()
result = will_fit_by_mode(payload_size, carrier_data, embed_mode=embed_mode)
# Use subprocess-isolated capacity check
result = subprocess_stego.check_capacity(
carrier_data=carrier_data,
payload_size=payload_size,
embed_mode=embed_mode,
)
if not result.success:
return jsonify({'error': result.error or 'Capacity check failed'}), 500
return jsonify({
'success': True,
'fits': result['fits'],
'payload_size': result['payload_size'],
'capacity': result['capacity'],
'usage_percent': round(result['usage_percent'], 1),
'headroom': result['headroom'],
'mode': embed_mode,
'fits': result.fits,
'payload_size': result.payload_size,
'capacity': result.capacity,
'usage_percent': round(result.usage_percent, 1),
'headroom': result.headroom,
'mode': result.mode,
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@@ -641,37 +671,63 @@ def encode_page():
return render_template('encode.html', has_qrcode_read=HAS_QRCODE_READ)
# v3.2.0: No date parameter needed
encode_result = encode(
message=payload,
reference_photo=ref_data,
carrier_image=carrier_data,
passphrase=passphrase, # v3.2.0: Renamed from day_phrase
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=key_password,
# date_str removed in v3.2.0
embed_mode=embed_mode,
dct_output_format=dct_output_format if embed_mode == 'dct' else None,
dct_color_mode=dct_color_mode if embed_mode == 'dct' else None,
)
# Use subprocess-isolated encode to prevent crashes
if payload_type == 'file' and payload_file and payload_file.filename:
encode_result = subprocess_stego.encode(
carrier_data=carrier_data,
reference_data=ref_data,
file_data=payload.data,
file_name=payload.filename,
file_mime=payload.mime_type,
passphrase=passphrase,
pin=pin if pin else None,
rsa_key_data=rsa_key_data,
rsa_password=key_password,
embed_mode=embed_mode,
dct_output_format=dct_output_format if embed_mode == 'dct' else 'png',
dct_color_mode=dct_color_mode if embed_mode == 'dct' else 'color',
)
else:
encode_result = subprocess_stego.encode(
carrier_data=carrier_data,
reference_data=ref_data,
message=payload,
passphrase=passphrase,
pin=pin if pin else None,
rsa_key_data=rsa_key_data,
rsa_password=key_password,
embed_mode=embed_mode,
dct_output_format=dct_output_format if embed_mode == 'dct' else 'png',
dct_color_mode=dct_color_mode if embed_mode == 'dct' else 'color',
)
# Check for subprocess errors
if not encode_result.success:
error_msg = encode_result.error or 'Encoding failed'
if 'capacity' in error_msg.lower():
raise CapacityError(error_msg)
raise StegasooError(error_msg)
# Determine actual output format for filename and storage
if embed_mode == 'dct' and dct_output_format == 'jpeg':
output_ext = '.jpg'
output_mime = 'image/jpeg'
filename = encode_result.filename
if filename.endswith('.png'):
filename = filename[:-4] + '.jpg'
else:
output_ext = '.png'
output_mime = 'image/png'
filename = encode_result.filename
# Use filename from result or generate one
filename = encode_result.filename
if not filename:
filename = generate_filename('stego', output_ext)
elif embed_mode == 'dct' and dct_output_format == 'jpeg' and filename.endswith('.png'):
filename = filename[:-4] + '.jpg'
# Store temporarily
file_id = secrets.token_urlsafe(16)
cleanup_temp_files()
TEMP_FILES[file_id] = {
'data': encode_result.stego_image,
'data': encode_result.stego_data,
'filename': filename,
'timestamp': time.time(),
'embed_mode': embed_mode,
@@ -864,17 +920,24 @@ def decode_page():
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# v3.2.0: No date_str parameter needed
decode_result = decode(
stego_image=stego_data,
reference_photo=ref_data,
passphrase=passphrase, # v3.2.0: Renamed from day_phrase
pin=pin,
# Use subprocess-isolated decode to prevent crashes
decode_result = subprocess_stego.decode(
stego_data=stego_data,
reference_data=ref_data,
passphrase=passphrase,
pin=pin if pin else None,
rsa_key_data=rsa_key_data,
rsa_password=key_password,
# date_str removed in v3.2.0
embed_mode=embed_mode,
)
# Check for subprocess errors
if not decode_result.success:
error_msg = decode_result.error or 'Decoding failed'
if 'decrypt' in error_msg.lower() or decode_result.error_type == 'DecryptionError':
raise DecryptionError(error_msg)
raise StegasooError(error_msg)
if decode_result.is_file:
# File content - store temporarily for download
file_id = secrets.token_urlsafe(16)
@@ -942,6 +1005,54 @@ def about():
)
# Add these two test routes anywhere in app.py after the app = Flask(...) line:
@app.route('/test-capacity', methods=['POST'])
def test_capacity():
"""Minimal capacity test - no stegasoo code, just PIL."""
carrier = request.files.get('carrier')
if not carrier:
return jsonify({'error': 'No carrier image provided'}), 400
try:
carrier_data = carrier.read()
buffer = io.BytesIO(carrier_data)
img = Image.open(buffer)
width, height = img.size
fmt = img.format
img.close()
buffer.close()
pixels = width * height
lsb_bytes = (pixels * 3) // 8
dct_bytes = ((width // 8) * (height // 8) * 16) // 8 - 10
return jsonify({
'success': True,
'width': width,
'height': height,
'format': fmt,
'lsb_kb': round(lsb_bytes / 1024, 1),
'dct_kb': round(dct_bytes / 1024, 1),
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/test-capacity-nopil', methods=['POST'])
def test_capacity_nopil():
"""Ultra-minimal test - no PIL, no stegasoo."""
carrier = request.files.get('carrier')
if not carrier:
return jsonify({'error': 'No carrier image provided'}), 400
carrier_data = carrier.read()
return jsonify({
'success': True,
'data_size': len(carrier_data),
})
# ============================================================================
# MAIN
# ============================================================================

View File

@@ -0,0 +1,191 @@
#!/usr/bin/env python3
"""
Stegasoo Subprocess Worker
This script runs in a subprocess and handles encode/decode operations.
If it crashes due to jpegio/scipy issues, the parent Flask process survives.
Communication is via JSON over stdin/stdout:
- Input: JSON object with operation parameters
- Output: JSON object with results or error
Usage:
echo '{"operation": "encode", ...}' | python stego_worker.py
"""
import sys
import json
import base64
import traceback
from pathlib import Path
# Ensure stegasoo is importable
sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'src'))
sys.path.insert(0, str(Path(__file__).parent))
def encode_operation(params: dict) -> dict:
"""Handle encode operation."""
from stegasoo import encode, FilePayload
# Decode base64 inputs
carrier_data = base64.b64decode(params['carrier_b64'])
reference_data = base64.b64decode(params['reference_b64'])
# Optional RSA key
rsa_key_data = None
if params.get('rsa_key_b64'):
rsa_key_data = base64.b64decode(params['rsa_key_b64'])
# Determine payload type
if params.get('file_b64'):
file_data = base64.b64decode(params['file_b64'])
payload = FilePayload(
data=file_data,
filename=params.get('file_name', 'file'),
mime_type=params.get('file_mime', 'application/octet-stream'),
)
else:
payload = params.get('message', '')
# Call encode with correct parameter names
result = encode(
message=payload,
reference_photo=reference_data,
carrier_image=carrier_data,
passphrase=params.get('passphrase', ''),
pin=params.get('pin'),
rsa_key_data=rsa_key_data,
rsa_password=params.get('rsa_password'),
embed_mode=params.get('embed_mode', 'lsb'),
dct_output_format=params.get('dct_output_format', 'png'),
dct_color_mode=params.get('dct_color_mode', 'color'),
)
# Build stats dict if available
stats = None
if hasattr(result, 'stats') and result.stats:
stats = {
'pixels_modified': getattr(result.stats, 'pixels_modified', 0),
'capacity_used': getattr(result.stats, 'capacity_used', 0),
'bytes_embedded': getattr(result.stats, 'bytes_embedded', 0),
}
return {
'success': True,
'stego_b64': base64.b64encode(result.stego_image).decode('ascii'),
'filename': getattr(result, 'filename', None),
'stats': stats,
}
def decode_operation(params: dict) -> dict:
"""Handle decode operation."""
from stegasoo import decode
# Decode base64 inputs
stego_data = base64.b64decode(params['stego_b64'])
reference_data = base64.b64decode(params['reference_b64'])
# Optional RSA key
rsa_key_data = None
if params.get('rsa_key_b64'):
rsa_key_data = base64.b64decode(params['rsa_key_b64'])
# Call decode with correct parameter names
result = decode(
stego_image=stego_data,
reference_photo=reference_data,
passphrase=params.get('passphrase', ''),
pin=params.get('pin'),
rsa_key_data=rsa_key_data,
rsa_password=params.get('rsa_password'),
embed_mode=params.get('embed_mode', 'auto'),
)
if result.is_file:
return {
'success': True,
'is_file': True,
'file_b64': base64.b64encode(result.file_data).decode('ascii'),
'filename': result.filename,
'mime_type': result.mime_type,
}
else:
return {
'success': True,
'is_file': False,
'message': result.message,
}
def compare_operation(params: dict) -> dict:
"""Handle compare_modes operation."""
from stegasoo import compare_modes
carrier_data = base64.b64decode(params['carrier_b64'])
result = compare_modes(carrier_data)
return {
'success': True,
'comparison': result,
}
def capacity_check_operation(params: dict) -> dict:
"""Handle will_fit_by_mode operation."""
from stegasoo import will_fit_by_mode
carrier_data = base64.b64decode(params['carrier_b64'])
result = will_fit_by_mode(
payload=params['payload_size'],
carrier_image=carrier_data,
embed_mode=params.get('embed_mode', 'lsb'),
)
return {
'success': True,
'result': result,
}
def main():
"""Main entry point - read JSON from stdin, write JSON to stdout."""
try:
# Read all input
input_text = sys.stdin.read()
if not input_text.strip():
output = {'success': False, 'error': 'No input provided'}
else:
params = json.loads(input_text)
operation = params.get('operation')
if operation == 'encode':
output = encode_operation(params)
elif operation == 'decode':
output = decode_operation(params)
elif operation == 'compare':
output = compare_operation(params)
elif operation == 'capacity':
output = capacity_check_operation(params)
else:
output = {'success': False, 'error': f'Unknown operation: {operation}'}
except json.JSONDecodeError as e:
output = {'success': False, 'error': f'Invalid JSON: {e}'}
except Exception as e:
output = {
'success': False,
'error': str(e),
'error_type': type(e).__name__,
'traceback': traceback.format_exc(),
}
# Write output as JSON
print(json.dumps(output), flush=True)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,425 @@
"""
Subprocess Steganography Wrapper
Runs stegasoo operations in isolated subprocesses to prevent crashes
from taking down the Flask server.
Usage:
from subprocess_stego import SubprocessStego
stego = SubprocessStego()
# Encode
result = stego.encode(
carrier_data=carrier_bytes,
reference_data=ref_bytes,
message="secret message",
passphrase="my passphrase",
pin="123456",
embed_mode="dct",
)
if result.success:
stego_bytes = result.stego_data
extension = result.extension
else:
error_message = result.error
# Decode
result = stego.decode(
stego_data=stego_bytes,
reference_data=ref_bytes,
passphrase="my passphrase",
pin="123456",
)
# Compare modes (capacity)
result = stego.compare_modes(carrier_bytes)
"""
import json
import base64
import subprocess
import sys
from pathlib import Path
from dataclasses import dataclass
from typing import Optional, Dict, Any, Union
# Default timeout for operations (seconds)
DEFAULT_TIMEOUT = 120
# Path to worker script - adjust if needed
WORKER_SCRIPT = Path(__file__).parent / 'stego_worker.py'
@dataclass
class EncodeResult:
"""Result from encode operation."""
success: bool
stego_data: Optional[bytes] = None
filename: Optional[str] = None
stats: Optional[Dict[str, Any]] = None
error: Optional[str] = None
error_type: Optional[str] = None
@dataclass
class DecodeResult:
"""Result from decode operation."""
success: bool
is_file: bool = False
message: Optional[str] = None
file_data: Optional[bytes] = None
filename: Optional[str] = None
mime_type: Optional[str] = None
error: Optional[str] = None
error_type: Optional[str] = None
@dataclass
class CompareResult:
"""Result from compare_modes operation."""
success: bool
width: int = 0
height: int = 0
lsb: Optional[Dict[str, Any]] = None
dct: Optional[Dict[str, Any]] = None
error: Optional[str] = None
@dataclass
class CapacityResult:
"""Result from capacity check operation."""
success: bool
fits: bool = False
payload_size: int = 0
capacity: int = 0
usage_percent: float = 0.0
headroom: int = 0
mode: str = ""
error: Optional[str] = None
class SubprocessStego:
"""
Subprocess-isolated steganography operations.
All operations run in a separate Python process. If jpegio or scipy
crashes, only the subprocess dies - Flask keeps running.
"""
def __init__(
self,
worker_path: Optional[Path] = None,
python_executable: Optional[str] = None,
timeout: int = DEFAULT_TIMEOUT,
):
"""
Initialize subprocess wrapper.
Args:
worker_path: Path to stego_worker.py (default: same directory)
python_executable: Python interpreter to use (default: same as current)
timeout: Default timeout in seconds
"""
self.worker_path = worker_path or WORKER_SCRIPT
self.python = python_executable or sys.executable
self.timeout = timeout
if not self.worker_path.exists():
raise FileNotFoundError(f"Worker script not found: {self.worker_path}")
def _run_worker(self, params: Dict[str, Any], timeout: Optional[int] = None) -> Dict[str, Any]:
"""
Run the worker subprocess with given parameters.
Args:
params: Dictionary of parameters (will be JSON-encoded)
timeout: Operation timeout in seconds
Returns:
Dictionary with results from worker
"""
timeout = timeout or self.timeout
input_json = json.dumps(params)
try:
result = subprocess.run(
[self.python, str(self.worker_path)],
input=input_json,
capture_output=True,
text=True,
timeout=timeout,
cwd=str(self.worker_path.parent),
)
if result.returncode != 0:
# Worker crashed
return {
'success': False,
'error': f'Worker crashed (exit code {result.returncode})',
'stderr': result.stderr,
}
if not result.stdout.strip():
return {
'success': False,
'error': 'Worker returned empty output',
'stderr': result.stderr,
}
return json.loads(result.stdout)
except subprocess.TimeoutExpired:
return {
'success': False,
'error': f'Operation timed out after {timeout} seconds',
'error_type': 'TimeoutError',
}
except json.JSONDecodeError as e:
return {
'success': False,
'error': f'Invalid JSON from worker: {e}',
'raw_output': result.stdout if 'result' in dir() else None,
}
except Exception as e:
return {
'success': False,
'error': str(e),
'error_type': type(e).__name__,
}
def encode(
self,
carrier_data: bytes,
reference_data: bytes,
message: Optional[str] = None,
file_data: Optional[bytes] = None,
file_name: Optional[str] = None,
file_mime: Optional[str] = None,
passphrase: str = "",
pin: Optional[str] = None,
rsa_key_data: Optional[bytes] = None,
rsa_password: Optional[str] = None,
embed_mode: str = "lsb",
dct_output_format: str = "png",
dct_color_mode: str = "color",
timeout: Optional[int] = None,
) -> EncodeResult:
"""
Encode a message or file into an image.
Args:
carrier_data: Carrier image bytes
reference_data: Reference photo bytes
message: Text message to encode (if not file)
file_data: File bytes to encode (if not message)
file_name: Original filename (for file payload)
file_mime: MIME type (for file payload)
passphrase: Encryption passphrase
pin: Optional PIN
rsa_key_data: Optional RSA key PEM bytes
rsa_password: RSA key password if encrypted
embed_mode: 'lsb' or 'dct'
dct_output_format: 'png' or 'jpeg' (for DCT mode)
dct_color_mode: 'grayscale' or 'color' (for DCT mode)
timeout: Operation timeout in seconds
Returns:
EncodeResult with stego_data and extension on success
"""
params = {
'operation': 'encode',
'carrier_b64': base64.b64encode(carrier_data).decode('ascii'),
'reference_b64': base64.b64encode(reference_data).decode('ascii'),
'message': message,
'passphrase': passphrase,
'pin': pin,
'embed_mode': embed_mode,
'dct_output_format': dct_output_format,
'dct_color_mode': dct_color_mode,
}
if file_data:
params['file_b64'] = base64.b64encode(file_data).decode('ascii')
params['file_name'] = file_name
params['file_mime'] = file_mime
if rsa_key_data:
params['rsa_key_b64'] = base64.b64encode(rsa_key_data).decode('ascii')
params['rsa_password'] = rsa_password
result = self._run_worker(params, timeout)
if result.get('success'):
return EncodeResult(
success=True,
stego_data=base64.b64decode(result['stego_b64']),
filename=result.get('filename'),
stats=result.get('stats'),
)
else:
return EncodeResult(
success=False,
error=result.get('error', 'Unknown error'),
error_type=result.get('error_type'),
)
def decode(
self,
stego_data: bytes,
reference_data: bytes,
passphrase: str = "",
pin: Optional[str] = None,
rsa_key_data: Optional[bytes] = None,
rsa_password: Optional[str] = None,
embed_mode: str = "auto",
timeout: Optional[int] = None,
) -> DecodeResult:
"""
Decode a message or file from a stego image.
Args:
stego_data: Stego image bytes
reference_data: Reference photo bytes
passphrase: Decryption passphrase
pin: Optional PIN
rsa_key_data: Optional RSA key PEM bytes
rsa_password: RSA key password if encrypted
embed_mode: 'auto', 'lsb', or 'dct'
timeout: Operation timeout in seconds
Returns:
DecodeResult with message or file_data on success
"""
params = {
'operation': 'decode',
'stego_b64': base64.b64encode(stego_data).decode('ascii'),
'reference_b64': base64.b64encode(reference_data).decode('ascii'),
'passphrase': passphrase,
'pin': pin,
'embed_mode': embed_mode,
}
if rsa_key_data:
params['rsa_key_b64'] = base64.b64encode(rsa_key_data).decode('ascii')
params['rsa_password'] = rsa_password
result = self._run_worker(params, timeout)
if result.get('success'):
if result.get('is_file'):
return DecodeResult(
success=True,
is_file=True,
file_data=base64.b64decode(result['file_b64']),
filename=result.get('filename'),
mime_type=result.get('mime_type'),
)
else:
return DecodeResult(
success=True,
is_file=False,
message=result.get('message'),
)
else:
return DecodeResult(
success=False,
error=result.get('error', 'Unknown error'),
error_type=result.get('error_type'),
)
def compare_modes(
self,
carrier_data: bytes,
timeout: Optional[int] = None,
) -> CompareResult:
"""
Compare LSB and DCT capacity for a carrier image.
Args:
carrier_data: Carrier image bytes
timeout: Operation timeout in seconds
Returns:
CompareResult with capacity information
"""
params = {
'operation': 'compare',
'carrier_b64': base64.b64encode(carrier_data).decode('ascii'),
}
result = self._run_worker(params, timeout)
if result.get('success'):
comparison = result.get('comparison', {})
return CompareResult(
success=True,
width=comparison.get('width', 0),
height=comparison.get('height', 0),
lsb=comparison.get('lsb'),
dct=comparison.get('dct'),
)
else:
return CompareResult(
success=False,
error=result.get('error', 'Unknown error'),
)
def check_capacity(
self,
carrier_data: bytes,
payload_size: int,
embed_mode: str = "lsb",
timeout: Optional[int] = None,
) -> CapacityResult:
"""
Check if a payload will fit in the carrier.
Args:
carrier_data: Carrier image bytes
payload_size: Size of payload in bytes
embed_mode: 'lsb' or 'dct'
timeout: Operation timeout in seconds
Returns:
CapacityResult with fit information
"""
params = {
'operation': 'capacity',
'carrier_b64': base64.b64encode(carrier_data).decode('ascii'),
'payload_size': payload_size,
'embed_mode': embed_mode,
}
result = self._run_worker(params, timeout)
if result.get('success'):
r = result.get('result', {})
return CapacityResult(
success=True,
fits=r.get('fits', False),
payload_size=r.get('payload_size', 0),
capacity=r.get('capacity', 0),
usage_percent=r.get('usage_percent', 0.0),
headroom=r.get('headroom', 0),
mode=r.get('mode', embed_mode),
)
else:
return CapacityResult(
success=False,
error=result.get('error', 'Unknown error'),
)
# Convenience function for quick usage
_default_stego: Optional[SubprocessStego] = None
def get_subprocess_stego() -> SubprocessStego:
"""Get or create default SubprocessStego instance."""
global _default_stego
if _default_stego is None:
_default_stego = SubprocessStego()
return _default_stego

View File

@@ -11,11 +11,11 @@
</div>
<div class="card-body">
<p class="lead">
Stegasoo is a secure steganography tool that hides encrypted messages and files
Stegasoo is a steganography tool that hides encrypted messages and files
inside ordinary images using multi-factor authentication.
</p>
<h6 class="text-primary mt-4 mb-3"><i class="bi bi-stars me-2"></i>Key Features</h6>
<h6 class="text-primary mt-4 mb-3">Features</h6>
<div class="row">
<div class="col-md-6">
<ul class="list-unstyled">
@@ -32,39 +32,37 @@
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>AES-256-GCM Encryption</strong>
<br><small class="text-muted">Military-grade authenticated encryption</small>
<br><small class="text-muted">Authenticated encryption with integrity verification</small>
</li>
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>Single Passphrase</strong>
<span class="badge bg-success ms-1">v3.2.0</span>
<br><small class="text-muted">Stronger default security</small>
<strong>LSB &amp; DCT Modes</strong>
<br><small class="text-muted">Choose capacity (LSB) or JPEG resilience (DCT)</small>
</li>
</ul>
</div>
<div class="col-md-6">
<ul class="list-unstyled">
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>DCT Mode</strong>
<span class="badge bg-warning text-dark ms-1">v3.0</span>
<br><small class="text-muted">Survives JPEG recompression for social media</small>
</li>
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>Random Pixel Embedding</strong>
<br><small class="text-muted">Defeats statistical steganalysis</small>
<br><small class="text-muted">Key-derived selection defeats statistical analysis</small>
</li>
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>Large Capacity</strong>
<br><small class="text-muted">Up to {{ max_payload_kb }} KB payload, 24MP images</small>
<strong>Large Image Support</strong>
<br><small class="text-muted">Up to {{ max_payload_kb }} KB payload, tested with 14MB+ images</small>
</li>
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>Zero Server Storage</strong>
<br><small class="text-muted">Nothing saved, files auto-expire</small>
</li>
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>QR Code Keys</strong>
<br><small class="text-muted">Import/export RSA keys via QR codes</small>
</li>
</ul>
</div>
</div>
@@ -78,8 +76,7 @@
</div>
<div class="card-body">
<p>
<span class="badge bg-warning text-dark me-1">New in v3.0</span>
Stegasoo now supports two embedding modes, each optimized for different use cases.
Stegasoo supports two embedding modes, each optimized for different use cases.
</p>
<div class="row mt-4">
@@ -120,7 +117,6 @@
<div class="card-header">
<i class="bi bi-soundwave text-warning me-2"></i>
<strong>DCT Mode</strong>
<span class="badge bg-warning text-dark ms-2">v3.0</span>
</div>
<div class="card-body">
<p class="small">
@@ -200,7 +196,7 @@
<h5 class="mb-0"><i class="bi bi-shield-lock me-2"></i>How Security Works</h5>
</div>
<div class="card-body">
<p>Stegasoo uses <strong>hybrid multi-factor authentication</strong> to derive encryption keys:</p>
<p>Stegasoo uses <strong>multi-factor authentication</strong> to derive encryption keys:</p>
<div class="row text-center my-4">
<div class="col-md-3 mb-3">
@@ -215,7 +211,6 @@
<div class="p-3 bg-dark rounded">
<i class="bi bi-chat-quote text-warning fs-2 d-block mb-2"></i>
<strong>Passphrase</strong>
<span class="badge bg-success ms-1">v3.2.0</span>
<div class="small text-muted mt-1">Something you know</div>
<div class="small text-success">~44 bits (4 words)</div>
</div>
@@ -224,7 +219,7 @@
<div class="p-3 bg-dark rounded">
<i class="bi bi-123 text-danger fs-2 d-block mb-2"></i>
<strong>Static PIN</strong>
<div class="small text-muted mt-1">Something you know (fixed)</div>
<div class="small text-muted mt-1">Something you know</div>
<div class="small text-success">~20 bits (6 digits)</div>
</div>
</div>
@@ -233,7 +228,7 @@
<i class="bi bi-key text-primary fs-2 d-block mb-2"></i>
<strong>RSA Key</strong>
<div class="small text-muted mt-1">Something you have (optional)</div>
<div class="small text-success">~128 bits (2048-bit)</div>
<div class="small text-success">~128 bits</div>
</div>
</div>
</div>
@@ -247,148 +242,77 @@
<h6 class="mt-4">Key Derivation</h6>
<p>
{% if has_argon2 %}
<span class="badge bg-success me-1"><i class="bi bi-check"></i> Argon2id Available</span>
Using <strong>Argon2id</strong> with 256MB memory cost — the winner of the Password Hashing Competition
and current best practice for key derivation. This makes GPU/ASIC attacks infeasible.
<span class="badge bg-success me-1"><i class="bi bi-check"></i> Argon2id</span>
Using <strong>Argon2id</strong> with 256MB memory cost — memory-hard KDF that
makes GPU/ASIC attacks infeasible.
{% else %}
<span class="badge bg-warning text-dark me-1"><i class="bi bi-exclamation-triangle"></i> Argon2 Not Available</span>
Falling back to <strong>PBKDF2-SHA512</strong> with 600,000 iterations.
Install <code>argon2-cffi</code> for stronger security.
{% endif %}
</p>
<h6 class="mt-4">Steganography Techniques</h6>
<p>
<strong>LSB Mode:</strong> Uses 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.
</p>
<p>
<strong>DCT Mode:</strong> Uses Discrete Cosine Transform embedding with Quantization Index Modulation (QIM).
Data is hidden in mid-frequency coefficients of 8×8 blocks, making it resilient to JPEG recompression.
{% if has_dct %}
<span class="badge bg-success"><i class="bi bi-check"></i> DCT Available</span>
{% else %}
<span class="badge bg-secondary">DCT Requires scipy</span>
{% endif %}
</p>
</div>
</div>
<!-- Version History -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-file-earmark-binary me-2"></i>File Embedding</h5>
<h5 class="mb-0"><i class="bi bi-clock-history me-2"></i>Version History</h5>
</div>
<div class="card-body">
<p>
Stegasoo supports embedding <strong>any file type</strong>, not just text messages.
</p>
<div class="row">
<div class="col-md-6">
<h6><i class="bi bi-check2-square text-success me-2"></i>Supported</h6>
<ul class="small">
<li>PDF documents</li>
<li>ZIP/RAR archives</li>
<li>Office documents (DOCX, XLSX, PPTX)</li>
<li>Source code files</li>
<li>Any binary file up to {{ max_payload_kb }} KB</li>
</ul>
</div>
<div class="col-md-6">
<h6><i class="bi bi-info-circle text-info me-2"></i>How It Works</h6>
<ul class="small">
<li>Original filename is preserved</li>
<li>MIME type is stored for proper handling</li>
<li>File is encrypted identically to text</li>
<li>Decoding auto-detects text vs. file</li>
</ul>
</div>
<div class="table-responsive">
<table class="table table-dark table-sm small">
<thead>
<tr>
<th>Version</th>
<th>Changes</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>4.0.0</strong></td>
<td>
Simplified auth (no date dependency), passphrase replaces day_phrase,
4-word default, JPEG normalization fix, large image support (14MB+ tested),
subprocess isolation for stability, Python 3.10-3.12 required
</td>
</tr>
<tr>
<td>3.2.0</td>
<td>Single passphrase (removed day-of-week rotation), increased default words</td>
</tr>
<tr>
<td>3.0.0</td>
<td>DCT steganography mode, JPEG output, color preservation option</td>
</tr>
<tr>
<td>2.2.0</td>
<td>QR code RSA key import/export</td>
</tr>
<tr>
<td>2.1.0</td>
<td>File embedding, compression support</td>
</tr>
<tr>
<td>2.0.0</td>
<td>Web UI, REST API, RSA key support</td>
</tr>
<tr>
<td>1.0.0</td>
<td>Initial release, CLI only, LSB mode</td>
</tr>
</tbody>
</table>
</div>
<div class="alert alert-info small mt-3">
<i class="bi bi-lightbulb me-2"></i>
<strong>Tip:</strong> For larger files, compress them first (ZIP) to maximize capacity.
Note that DCT mode has ~10× less capacity than LSB mode.
<div class="alert alert-warning small mt-3 mb-0">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Compatibility:</strong> v4.0 cannot decode messages from v3.1 or earlier (different format).
Messages encoded with v3.2 should decode correctly.
</div>
</div>
</div>
<!-- REST API Card -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-braces me-2"></i>REST API</h5>
</div>
<div class="card-body">
<p>
<span class="badge bg-success me-1"><i class="bi bi-check-circle"></i> FastAPI</span>
Stegasoo includes a complete REST API with automatic documentation and type validation.
</p>
<h6 class="mt-4"><i class="bi bi-layers me-2"></i>Endpoints</h6>
<div class="row">
<div class="col-md-6">
<ul class="small">
<li><code>POST /generate</code> Generate credentials</li>
<li><code>POST /encode</code> Encode text (JSON)</li>
<li><code>POST /encode/multipart</code> Encode with uploads</li>
<li><code>POST /decode</code> Decode message (JSON)</li>
<li><code>POST /decode/multipart</code> Decode with uploads</li>
</ul>
</div>
<div class="col-md-6">
<ul class="small">
<li><code>POST /image/info</code> Get image capacity</li>
<li><code>POST /extract-key-from-qr</code> Extract RSA from QR</li>
<li><code>GET /</code> API status and capabilities</li>
<li><code>GET /docs</code> Swagger documentation</li>
<li><code>GET /redoc</code> ReDoc documentation</li>
</ul>
</div>
</div>
<h6 class="mt-4"><i class="bi bi-code-slash me-2"></i>Example: DCT Encode</h6>
<pre class="bg-dark p-3 rounded small"><code># Encode with DCT mode for social media
curl -X POST "http://localhost:8000/encode/multipart" \
-F "passphrase=apple forest thunder mountain" \
-F "pin=123456" \
-F "embed_mode=dct" \
-F "dct_output_format=jpeg" \
-F "reference_photo=@photo.jpg" \
-F "carrier=@meme.png" \
-F "message=secret message" \
--output stego.jpg</code></pre>
<h6 class="mt-4"><i class="bi bi-terminal me-2"></i>Command Line</h6>
<pre class="bg-dark p-3 rounded small"><code># Generate credentials
stegasoo generate --pin --words 4
# Encode with LSB (default)
stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder mountain" \
--pin 123456 -m "secret"
# Encode with DCT for social media
stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder mountain" \
--pin 123456 -m "secret" --mode dct --dct-format jpeg
# Decode (auto-detects mode)
stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder mountain" \
--pin 123456</code></pre>
<p class="small text-muted mt-3 mb-0">
<span class="badge bg-{% if has_argon2 %}success{% else %}warning{% endif %} me-1">
{% if has_argon2 %}Argon2{% else %}PBKDF2{% endif %}
</span>
<span class="badge bg-{% if has_dct %}success{% else %}secondary{% endif %} me-1">
{% if has_dct %}DCT Available{% else %}DCT Unavailable{% endif %}
</span>
<span class="badge bg-{% if has_qrcode_read %}success{% else %}secondary{% endif %}">
{% if has_qrcode_read %}QR Reading{% else %}No QR Reading{% endif %}
</span>
</p>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-question-circle me-2"></i>Usage Guide</h5>
@@ -470,7 +394,7 @@ stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder mountain" \
</div>
</div>
<div class="card">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-speedometer2 me-2"></i>Limits &amp; Specifications</h5>
</div>
@@ -514,11 +438,13 @@ stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder mountain" \
<td><strong>2048, 3072, 4096 bits</strong></td>
</tr>
<tr>
<td><i class="bi bi-chat-quote me-2"></i>Passphrase length
<span class="badge bg-success ms-1">v3.2.0</span>
</td>
<td><i class="bi bi-chat-quote me-2"></i>Passphrase length</td>
<td><strong>3-12 words</strong> (BIP-39, recommended: 4+ words)</td>
</tr>
<tr>
<td><i class="bi bi-code me-2"></i>Python version</td>
<td><strong>3.10-3.12</strong> (3.13 not supported)</td>
</tr>
</tbody>
</table>
</div>
@@ -528,7 +454,7 @@ stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder mountain" \
<p>
Stegasoo v{{ version }} &bull;
<i class="bi bi-github me-1"></i>Open Source &bull;
Built with Python, Flask/FastAPI, and cryptography
Built with Python, Flask, and cryptography
</p>
</div>
</div>

View File

@@ -3,6 +3,53 @@
{% block title %}Decode Message - Stegasoo{% endblock %}
{% block content %}
<style>
/* Glowing passphrase input */
.passphrase-input {
background: rgba(30, 40, 50, 0.8) !important;
border: 2px solid rgba(99, 179, 237, 0.3) !important;
color: #63b3ed !important;
font-family: 'Courier New', monospace;
font-size: 1.1rem;
letter-spacing: 0.5px;
padding: 12px 16px;
transition: border-color 0.3s ease, box-shadow 0.3s ease, background 0.3s ease;
}
.passphrase-input:focus {
border-color: rgba(99, 179, 237, 0.8) !important;
box-shadow: 0 0 20px rgba(99, 179, 237, 0.4), 0 0 40px rgba(99, 179, 237, 0.2) !important;
background: rgba(30, 40, 50, 0.95) !important;
}
.passphrase-input::placeholder {
color: rgba(99, 179, 237, 0.4);
}
/* Glowing PIN input */
.pin-input-container .form-control {
background: rgba(30, 40, 50, 0.8) !important;
border: 2px solid rgba(246, 173, 85, 0.3) !important;
color: #f6ad55 !important;
font-family: 'Courier New', monospace;
font-size: 1.2rem;
letter-spacing: 3px;
text-align: center;
transition: all 0.3s ease;
}
.pin-input-container .form-control:focus {
border-color: rgba(246, 173, 85, 0.8) !important;
box-shadow: 0 0 20px rgba(246, 173, 85, 0.4), 0 0 40px rgba(246, 173, 85, 0.2) !important;
background: rgba(30, 40, 50, 0.95) !important;
}
.pin-input-container .form-control::placeholder {
color: rgba(246, 173, 85, 0.4);
letter-spacing: 1px;
}
</style>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
@@ -17,14 +64,14 @@
</div>
<label class="form-label text-muted">Decoded Message:</label>
<div class="position-relative">
<div class="alert-message p-3 rounded bg-dark border border-secondary" id="decodedContent" style="white-space: pre-wrap;">{{ decoded_message }}</div>
<button class="btn btn-sm btn-outline-light position-absolute top-0 end-0 m-2" onclick="navigator.clipboard.writeText(document.getElementById('decodedContent').innerText).then(() => this.innerHTML = '<i class=\'bi bi-check\'></i>').catch(() => alert('Failed to copy'))">
<i class="bi bi-clipboard"></i> Copy
</button>
<div class="alert-message p-3 rounded bg-dark border border-secondary mb-2" id="decodedContent" style="white-space: pre-wrap;">{{ decoded_message }}</div>
<div class="d-flex justify-content-end mb-3">
<button class="btn btn-sm btn-outline-light" onclick="navigator.clipboard.writeText(document.getElementById('decodedContent').innerText).then(() => { this.innerHTML = '<i class=\'bi bi-check\'></i> Copied!'; setTimeout(() => this.innerHTML = '<i class=\'bi bi-clipboard\'></i> Copy', 2000); }).catch(() => alert('Failed to copy'))">
<i class="bi bi-clipboard"></i> Copy
</button>
</div>
<a href="/decode" class="btn btn-outline-light w-100 mt-3">
<a href="/decode" class="btn btn-outline-light w-100">
<i class="bi bi-arrow-repeat me-2"></i>Decode Another
</a>
@@ -99,7 +146,7 @@
<label class="form-label">
<i class="bi bi-chat-quote me-1"></i> Passphrase
</label>
<input type="text" name="passphrase" class="form-control"
<input type="text" name="passphrase" id="passphraseInput" class="form-control passphrase-input"
placeholder="e.g., correct horse battery staple" required>
<div class="form-text">
The passphrase used during encoding (typically 4 words)
@@ -114,64 +161,62 @@
</h6>
<div class="row">
<div class="col-md-6 mb-3">
<div class="col-md-4 mb-3">
<label class="form-label"><i class="bi bi-123 me-1"></i> PIN</label>
<div class="input-group">
<input type="password" name="pin" class="form-control" id="pinInput" placeholder="6-9 digits" maxlength="9">
<div class="input-group pin-input-container">
<input type="password" name="pin" class="form-control" id="pinInput" placeholder="••••••" maxlength="9" style="max-width: 180px;">
<button class="btn btn-outline-secondary" type="button" id="togglePin">
<i class="bi bi-eye"></i>
</button>
</div>
<div class="form-text">
If PIN was used during encoding
</div>
<div class="form-text">If PIN was used during encoding</div>
</div>
<div class="col-md-6 mb-3">
<div class="col-md-8 mb-3">
<label class="form-label">
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
</label>
<ul class="nav nav-tabs nav-tabs-sm mb-2" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active py-1 px-2 small" data-bs-toggle="tab" data-bs-target="#rsaFileTabDec" type="button">
<i class="bi bi-file-earmark me-1"></i>.pem File
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link py-1 px-2 small" data-bs-toggle="tab" data-bs-target="#rsaQrTabDec" type="button">
<i class="bi bi-qr-code me-1"></i>QR Code
</button>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="rsaFileTabDec" role="tabpanel">
<input type="file" name="rsa_key" class="form-control form-control-sm" id="rsaKeyInput" accept=".pem,.key,application/x-pem-file">
</div>
<div class="tab-pane fade" id="rsaQrTabDec" role="tabpanel">
<input type="file" name="rsa_key_qr" class="form-control form-control-sm" id="rsaKeyQrInput" accept="image/*">
<div class="form-text small">PNG, JPG, or other image of QR code</div>
<!-- RSA Input Method Toggle -->
<div class="btn-group w-100 mb-2" role="group">
<input type="radio" class="btn-check" name="rsa_input_method" id="rsaMethodFile" value="file" checked>
<label class="btn btn-outline-secondary btn-sm" for="rsaMethodFile">
<i class="bi bi-file-earmark me-1"></i>.pem File
</label>
<input type="radio" class="btn-check" name="rsa_input_method" id="rsaMethodQr" value="qr">
<label class="btn btn-outline-secondary btn-sm" for="rsaMethodQr">
<i class="bi bi-qr-code me-1"></i>QR Code
</label>
</div>
<!-- .pem File Input -->
<div id="rsaFileSection">
<input type="file" name="rsa_key" class="form-control form-control-sm" id="rsaKeyInput" accept=".pem,.key,application/x-pem-file">
</div>
<!-- QR Code Input -->
<div id="rsaQrSection" class="d-none">
<div class="drop-zone p-3" id="qrDropZone">
<input type="file" name="rsa_key_qr" accept="image/*" id="rsaKeyQrInput">
<div class="drop-zone-label text-center">
<i class="bi bi-qr-code-scan fs-4 d-block text-muted mb-1"></i>
<span class="text-muted small">Drop QR image or click to browse</span>
</div>
<img class="drop-zone-preview d-none" id="qrPreview" style="max-height: 80px;">
</div>
</div>
<div class="form-text">
If RSA key was used during encoding (file or QR image)
<!-- Key Password (always visible) -->
<div class="input-group input-group-sm mt-2">
<input type="password" name="rsa_password" class="form-control" id="rsaPasswordInput" placeholder="Key password (if encrypted)">
<button class="btn btn-outline-secondary" type="button" id="toggleRsaPassword">
<i class="bi bi-eye"></i>
</button>
</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>
<!-- ================================================================
ADVANCED OPTIONS (v3.0) - Extraction Mode
================================================================ -->
@@ -276,7 +321,7 @@
</li>
<li class="mb-2">
<i class="bi bi-exclamation-triangle-fill text-warning me-1"></i>
<strong>Format compatibility:</strong> v3.2.0 cannot decode messages from v3.1.0 (different format)
<strong>Format compatibility:</strong> v4.0 cannot decode messages from v3.1 or earlier (different format)
</li>
<li class="mb-2">
<i class="bi bi-info-circle-fill text-info me-1"></i>
@@ -304,22 +349,6 @@ document.getElementById('decodeForm')?.addEventListener('submit', function() {
btn.disabled = true;
});
// Show RSA password field when key is selected
const rsaKeyInput = document.getElementById('rsaKeyInput');
const rsaKeyQrInput = document.getElementById('rsaKeyQrInput');
const rsaPasswordGroup = document.getElementById('rsaPasswordGroup');
function checkRsaKeySelected() {
const hasFile = (rsaKeyInput && rsaKeyInput.files.length > 0) ||
(rsaKeyQrInput && rsaKeyQrInput.files.length > 0);
if (rsaPasswordGroup) {
rsaPasswordGroup.classList.toggle('d-none', !hasFile);
}
}
rsaKeyInput?.addEventListener('change', checkRsaKeySelected);
rsaKeyQrInput?.addEventListener('change', checkRsaKeySelected);
// PIN Toggle
document.getElementById('togglePin')?.addEventListener('click', function() {
const input = document.getElementById('pinInput');
@@ -333,6 +362,35 @@ document.getElementById('togglePin')?.addEventListener('click', function() {
}
});
// RSA Password Toggle
document.getElementById('toggleRsaPassword')?.addEventListener('click', function() {
const input = document.getElementById('rsaPasswordInput');
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');
}
});
// RSA Input Method Toggle (File vs QR)
const rsaMethodFile = document.getElementById('rsaMethodFile');
const rsaMethodQr = document.getElementById('rsaMethodQr');
const rsaFileSection = document.getElementById('rsaFileSection');
const rsaQrSection = document.getElementById('rsaQrSection');
function updateRsaInputMethod() {
if (!rsaMethodFile || !rsaFileSection || !rsaQrSection) return;
const isFile = rsaMethodFile.checked;
rsaFileSection.classList.toggle('d-none', !isFile);
rsaQrSection.classList.toggle('d-none', isFile);
}
rsaMethodFile?.addEventListener('change', updateRsaInputMethod);
rsaMethodQr?.addEventListener('change', updateRsaInputMethod);
// Mode card highlighting
const autoModeCard = document.getElementById('autoModeCard');
const lsbModeCardDec = document.getElementById('lsbModeCardDec');
@@ -428,12 +486,85 @@ document.querySelectorAll('.drop-zone').forEach(zone => {
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 = '<i class="bi bi-check-circle text-success me-1"></i>' + file.name;
};
reader.readAsDataURL(file);
}
});
// QR Code RSA Key scanning
const rsaKeyQrInput = document.getElementById('rsaKeyQrInput');
const qrPreview = document.getElementById('qrPreview');
if (rsaKeyQrInput) {
rsaKeyQrInput.addEventListener('change', function() {
if (this.files && this.files[0]) {
const file = this.files[0];
// Show image preview
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = e => {
if (qrPreview) {
qrPreview.src = e.target.result;
qrPreview.classList.remove('d-none');
}
};
reader.readAsDataURL(file);
}
// Extract key from QR
const formData = new FormData();
formData.append('qr_image', file);
fetch('/extract-key-from-qr', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (!data.success) {
alert('QR decode failed: ' + data.error);
return;
}
// Visual feedback
document.querySelector('#qrDropZone .drop-zone-label').innerHTML =
'<i class="bi bi-check-circle text-success me-1"></i>RSA Key loaded from QR';
})
.catch(err => {
alert('QR decode failed: ' + err);
});
}
});
}
// Auto-resize passphrase input font to fit long passphrases
const passphraseInput = document.getElementById('passphraseInput');
if (passphraseInput) {
// Stepped font sizes (characters -> rem)
const fontSizeSteps = [
{ maxChars: 30, size: 1.1 },
{ maxChars: 45, size: 1.0 },
{ maxChars: 60, size: 0.95 },
{ maxChars: Infinity, size: 0.9 }
];
function adjustPassphraseFontSize() {
const len = passphraseInput.value.length;
for (const step of fontSizeSteps) {
if (len <= step.maxChars) {
passphraseInput.style.fontSize = step.size + 'rem';
break;
}
}
}
passphraseInput.addEventListener('input', adjustPassphraseFontSize);
adjustPassphraseFontSize(); // Initial call in case of pre-filled value
}
</script>
{% endblock %}

View File

@@ -3,6 +3,57 @@
{% block title %}Encode Message - Stegasoo{% endblock %}
{% block content %}
<style>
/* Glowing passphrase input */
.passphrase-input-container {
position: relative;
}
.passphrase-input {
background: rgba(30, 40, 50, 0.8) !important;
border: 2px solid rgba(99, 179, 237, 0.3) !important;
color: #63b3ed !important;
font-family: 'Courier New', monospace;
font-size: 1.1rem;
letter-spacing: 0.5px;
padding: 12px 16px;
transition: border-color 0.3s ease, box-shadow 0.3s ease, background 0.3s ease;
}
.passphrase-input:focus {
border-color: rgba(99, 179, 237, 0.8) !important;
box-shadow: 0 0 20px rgba(99, 179, 237, 0.4), 0 0 40px rgba(99, 179, 237, 0.2) !important;
background: rgba(30, 40, 50, 0.95) !important;
}
.passphrase-input::placeholder {
color: rgba(99, 179, 237, 0.4);
}
/* Glowing PIN input */
.pin-input-container .form-control {
background: rgba(30, 40, 50, 0.8) !important;
border: 2px solid rgba(246, 173, 85, 0.3) !important;
color: #f6ad55 !important;
font-family: 'Courier New', monospace;
font-size: 1.2rem;
letter-spacing: 3px;
text-align: center;
transition: all 0.3s ease;
}
.pin-input-container .form-control:focus {
border-color: rgba(246, 173, 85, 0.8) !important;
box-shadow: 0 0 20px rgba(246, 173, 85, 0.4), 0 0 40px rgba(246, 173, 85, 0.2) !important;
background: rgba(30, 40, 50, 0.95) !important;
}
.pin-input-container .form-control::placeholder {
color: rgba(246, 173, 85, 0.4);
letter-spacing: 1px;
}
</style>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
@@ -11,7 +62,7 @@
</div>
<div class="card-body">
<form method="POST" enctype="multipart/form-data" id="encodeForm">
<!-- v3.2.0: Removed client_date hidden field -->
<!-- Removed client_date hidden field -->
<!-- Embedding Mode Selection -->
<div class="mb-4">
@@ -182,15 +233,16 @@
</div>
</div>
<!-- v3.2.0: Renamed from day_phrase to passphrase, removed date selection -->
<!-- Passphrase input with glow styling -->
<div class="mb-3">
<label class="form-label" id="passphraseLabel">
<i class="bi bi-chat-quote me-1"></i> Passphrase
<span class="badge bg-success ms-2">v3.2.0</span>
</label>
<input type="text" name="passphrase" class="form-control"
placeholder="e.g., apple forest thunder mountain" required
id="passphraseInput">
<div class="passphrase-input-container">
<input type="text" name="passphrase" class="form-control passphrase-input"
placeholder="e.g., apple forest thunder mountain" required
id="passphraseInput">
</div>
<div class="form-text">
Your passphrase for this message
</div>
@@ -210,8 +262,8 @@
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label"><i class="bi bi-123 me-1"></i> PIN</label>
<div class="input-group">
<input type="password" name="pin" class="form-control" id="pinInput" placeholder="6-9 digits" maxlength="9" style="max-width: 140px;">
<div class="input-group pin-input-container">
<input type="password" name="pin" class="form-control" id="pinInput" placeholder="••••••" maxlength="9" style="max-width: 180px;">
<button class="btn btn-outline-secondary" type="button" id="togglePin">
<i class="bi bi-eye"></i>
</button>
@@ -250,6 +302,7 @@
<i class="bi bi-qr-code-scan fs-4 d-block text-muted mb-1"></i>
<span class="text-muted small">Drop QR image or click to browse</span>
</div>
<img class="drop-zone-preview d-none" id="qrPreview" style="max-height: 80px;">
</div>
</div>
@@ -406,12 +459,33 @@ function updatePayloadSection() {
payloadTextRadio.addEventListener('change', updatePayloadSection);
payloadFileRadio.addEventListener('change', updatePayloadSection);
// Passphrase validation (v3.2.0)
// Passphrase validation and auto-resize font
const passphraseInput = document.getElementById('passphraseInput');
const passphraseWarning = document.getElementById('passphraseWarning');
// Stepped font sizes (characters -> rem)
const fontSizeSteps = [
{ maxChars: 30, size: 1.1 },
{ maxChars: 45, size: 1.0 },
{ maxChars: 60, size: 0.95 },
{ maxChars: Infinity, size: 0.9 }
];
function adjustPassphraseFontSize() {
if (!passphraseInput) return;
const len = passphraseInput.value.length;
for (const step of fontSizeSteps) {
if (len <= step.maxChars) {
passphraseInput.style.fontSize = step.size + 'rem';
break;
}
}
}
if (passphraseInput) {
passphraseInput.addEventListener('input', function() {
// Word count warning
const words = this.value.trim().split(/\s+/).filter(w => w.length > 0);
const recommendedWords = {{ recommended_passphrase_words }};
@@ -420,7 +494,13 @@ if (passphraseInput) {
} else {
passphraseWarning.style.display = 'none';
}
// Auto-resize font
adjustPassphraseFontSize();
});
// Initial font size adjustment
adjustPassphraseFontSize();
}
// Payload file info display
@@ -728,11 +808,27 @@ document.addEventListener('paste', function(e) {
// QR Code RSA Key scanning
const rsaQrInput = document.getElementById('rsaQrInput');
const qrPreview = document.getElementById('qrPreview');
if (rsaQrInput) {
rsaQrInput.addEventListener('change', function() {
if (this.files && this.files[0]) {
const file = this.files[0];
// Show image preview
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = e => {
if (qrPreview) {
qrPreview.src = e.target.result;
qrPreview.classList.remove('d-none');
}
};
reader.readAsDataURL(file);
}
// Extract key from QR
const formData = new FormData();
formData.append('qr_image', this.files[0]);
formData.append('qr_image', file);
fetch('/extract-key-from-qr', {
method: 'POST',

View File

@@ -120,7 +120,7 @@
{% if embed_mode == 'dct' %}
<li>Recipient needs <strong>DCT mode</strong> or <strong>Auto</strong> detection to decode</li>
{% if color_mode == 'color' %}
<li><span class="badge bg-success">v3.0.1</span> Color preserved - extraction works on both color and grayscale</li>
<li>Color preserved - extraction works on both color and grayscale</li>
{% endif %}
{% endif %}
</ul>

View File

@@ -112,7 +112,6 @@
<div class="mb-4">
<h6 class="text-muted">
<i class="bi bi-chat-quote me-2"></i>PASSPHRASE
<span class="badge bg-success ms-2">v3.2.0</span>
</h6>
<div class="passphrase-container">

View File

@@ -11,7 +11,7 @@
<div style="margin-bottom: 40px;">
<h1 class="display-4 fw-bold mb-2">
Stegasoo
<span class="badge bg-success fs-6 ms-2">v3.2.0</span>
<span class="badge bg-success fs-6 ms-2">v4.0</span>
</h1>
<p class="lead text-muted mb-0">Hide encrypted data in plain sight.</p>
</div>
@@ -94,7 +94,6 @@
<div class="p-3 bg-dark rounded h-100">
<i class="bi bi-soundwave text-warning fs-2 d-block mb-2"></i>
<strong>DCT Mode</strong>
<span class="badge bg-warning text-dark ms-1">v3.0</span>
<div class="small text-muted mt-2">
Survives JPEG recompression<br>
Best for social media
@@ -108,7 +107,7 @@
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-diagram-3 me-2"></i>How It Works</h5>
<a href="/about" class="btn btn-sm btn-outline-secondary">Learn More</a>
<a href="/about" class="btn btn-sm btn-outline-light">Learn More</a>
</div>
<div class="card-body">
<div class="row">
@@ -117,21 +116,20 @@
<ul class="list-unstyled small">
<li class="mb-1">
<i class="bi bi-image text-info me-2"></i>
<strong>Reference Photo</strong> shared secret image
<strong>Reference Photo</strong> shared secret image
</li>
<li class="mb-1">
<i class="bi bi-chat-quote text-info me-2"></i>
<strong>Passphrase</strong> 4+ words
<span class="badge bg-success ms-1">v3.2.0</span>
<strong>Passphrase</strong> 4+ words
</li>
<li class="mb-1">
<i class="bi bi-123 text-info me-2"></i>
<strong>PIN</strong> 6-9 digits (and/or RSA key)
<strong>PIN</strong> 6-9 digits (and/or RSA key)
</li>
</ul>
</div>
<div class="col-md-6">
<h6 class="text-primary"><i class="bi bi-shield-check me-2"></i>We Provide</h6>
<h6 class="text-primary"><i class="bi bi-shield-check me-2"></i>Security</h6>
<ul class="list-unstyled small">
<li class="mb-1">
<i class="bi bi-lock text-success me-2"></i>

View File

@@ -0,0 +1,90 @@
"""
Minimal test to isolate the memory corruption crash.
Add this route to your app.py temporarily to test if the crash
is in Flask/Pillow or in stegasoo code.
Usage:
1. Add this code to app.py
2. Restart the server
3. Use the /test-capacity endpoint instead of /api/compare-capacity
4. If it crashes: Flask or Pillow issue
5. If it works: Stegasoo code issue
"""
# Add these imports at the top of app.py if not present:
# from PIL import Image
# import io
# Add this route to app.py:
@app.route('/test-capacity', methods=['POST'])
def test_capacity():
"""
Minimal capacity test - no stegasoo code, just PIL.
"""
carrier = request.files.get('carrier')
if not carrier:
return jsonify({'error': 'No carrier image provided'}), 400
try:
# Read the file data
carrier_data = carrier.read()
# Method 1: Just get size from PIL
buffer = io.BytesIO(carrier_data)
img = Image.open(buffer)
width, height = img.size
fmt = img.format
mode = img.mode
img.close()
buffer.close()
# Simple capacity calculation (no scipy, no numpy)
pixels = width * height
lsb_bytes = (pixels * 3) // 8
blocks = (width // 8) * (height // 8)
dct_bytes = (blocks * 16) // 8 - 10
return jsonify({
'success': True,
'width': width,
'height': height,
'format': fmt,
'mode': mode,
'lsb': {
'capacity_bytes': lsb_bytes,
'capacity_kb': round(lsb_bytes / 1024, 1),
},
'dct': {
'capacity_bytes': dct_bytes,
'capacity_kb': round(dct_bytes / 1024, 1),
}
})
except Exception as e:
import traceback
return jsonify({'error': str(e), 'trace': traceback.format_exc()}), 500
# Alternative: completely bypass PIL too
@app.route('/test-capacity-nopil', methods=['POST'])
def test_capacity_nopil():
"""
Ultra-minimal test - no PIL, no stegasoo.
"""
carrier = request.files.get('carrier')
if not carrier:
return jsonify({'error': 'No carrier image provided'}), 400
try:
carrier_data = carrier.read()
# Just return size info, no image processing at all
return jsonify({
'success': True,
'data_size': len(carrier_data),
'first_bytes': carrier_data[:20].hex() if len(carrier_data) >= 20 else carrier_data.hex(),
})
except Exception as e:
import traceback
return jsonify({'error': str(e), 'trace': traceback.format_exc()}), 500