A whoooole lotta 4.0.x fixes.
This commit is contained in:
62
frontends/web/README_subprocess.md
Normal file
62
frontends/web/README_subprocess.md
Normal 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.
|
||||
@@ -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
|
||||
# ============================================================================
|
||||
|
||||
191
frontends/web/stego_worker.py
Normal file
191
frontends/web/stego_worker.py
Normal 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()
|
||||
425
frontends/web/subprocess_stego.py
Normal file
425
frontends/web/subprocess_stego.py
Normal 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
|
||||
@@ -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 & 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 & 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 }} •
|
||||
<i class="bi bi-github me-1"></i>Open Source •
|
||||
Built with Python, Flask/FastAPI, and cryptography
|
||||
Built with Python, Flask, and cryptography
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
90
frontends/web/test_routes.py
Normal file
90
frontends/web/test_routes.py
Normal 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
|
||||
Reference in New Issue
Block a user