QR functionality (sorta).

This commit is contained in:
Aaron D. Lee
2025-12-28 12:04:15 -05:00
parent 5bd49cb581
commit 653de8cbaa
17 changed files with 1677 additions and 599 deletions

3
.gitignore vendored
View File

@@ -51,3 +51,6 @@ htmlcov/
# Distribution
*.manifest
*.spec
# Output test files.
*.png

View File

@@ -37,6 +37,17 @@ from stegasoo.constants import (
VALID_RSA_SIZES,
)
# QR Code utilities
try:
from stegasoo.qr_utils import (
extract_key_from_qr,
has_qr_read,
)
HAS_QR_READ = has_qr_read()
except ImportError:
HAS_QR_READ = False
extract_key_from_qr = None
# ============================================================================
# FASTAPI APP
@@ -132,10 +143,17 @@ class ImageInfoResponse(BaseModel):
class StatusResponse(BaseModel):
version: str
has_argon2: bool
has_qrcode_read: bool
day_names: list[str]
max_payload_kb: int
class QrExtractResponse(BaseModel):
success: bool
key_pem: Optional[str] = None
error: Optional[str] = None
class ErrorResponse(BaseModel):
error: str
detail: Optional[str] = None
@@ -151,11 +169,43 @@ async def root():
return StatusResponse(
version=__version__,
has_argon2=has_argon2(),
has_qrcode_read=HAS_QR_READ,
day_names=list(DAY_NAMES),
max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024
)
@app.post("/extract-key-from-qr", response_model=QrExtractResponse)
async def api_extract_key_from_qr(
qr_image: UploadFile = File(..., description="QR code image containing RSA key")
):
"""
Extract RSA key from a QR code image.
Supports both compressed (STEGASOO-Z: prefix) and uncompressed keys.
Returns the PEM-encoded key if found.
"""
if not HAS_QR_READ:
raise HTTPException(
501,
"QR code reading not available. Install pyzbar and libzbar."
)
try:
image_data = await qr_image.read()
key_pem = extract_key_from_qr(image_data)
if key_pem:
return QrExtractResponse(success=True, key_pem=key_pem)
else:
return QrExtractResponse(
success=False,
error="No valid RSA key found in QR code"
)
except Exception as e:
return QrExtractResponse(success=False, error=str(e))
@app.post("/generate", response_model=GenerateResponse)
async def api_generate(request: GenerateRequest):
"""
@@ -335,6 +385,7 @@ async def api_encode_multipart(
payload_file: Optional[UploadFile] = File(None),
pin: str = Form(""),
rsa_key: Optional[UploadFile] = File(None),
rsa_key_qr: Optional[UploadFile] = File(None),
rsa_password: str = Form(""),
date_str: str = Form("")
):
@@ -342,12 +393,34 @@ async def api_encode_multipart(
Encode using multipart form data (file uploads).
Provide either 'message' (text) or 'payload_file' (binary file).
RSA key can be provided as 'rsa_key' (.pem file) or 'rsa_key_qr' (QR code image).
Returns the stego image directly as PNG with metadata headers.
"""
try:
ref_data = await reference_photo.read()
carrier_data = await carrier.read()
rsa_key_data = await rsa_key.read() if rsa_key else None
# Handle RSA key from .pem file or QR code image
rsa_key_data = None
rsa_key_from_qr = False
if rsa_key and rsa_key.filename:
rsa_key_data = await rsa_key.read()
elif rsa_key_qr and rsa_key_qr.filename:
if not HAS_QR_READ:
raise HTTPException(
501,
"QR code reading not available. Install pyzbar and libzbar."
)
qr_image_data = await rsa_key_qr.read()
key_pem = extract_key_from_qr(qr_image_data)
if not key_pem:
raise HTTPException(400, "Could not extract RSA key from QR code image")
rsa_key_data = key_pem.encode('utf-8')
rsa_key_from_qr = True
# QR code keys are never password-protected
effective_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None)
# Determine payload
if payload_file and payload_file.filename:
@@ -369,7 +442,7 @@ async def api_encode_multipart(
day_phrase=day_phrase,
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=rsa_password if rsa_password else None,
rsa_password=effective_password,
date_str=date_str if date_str else None
)
@@ -401,17 +474,40 @@ async def api_decode_multipart(
stego_image: UploadFile = File(...),
pin: str = Form(""),
rsa_key: Optional[UploadFile] = File(None),
rsa_key_qr: Optional[UploadFile] = File(None),
rsa_password: str = Form("")
):
"""
Decode using multipart form data (file uploads).
RSA key can be provided as 'rsa_key' (.pem file) or 'rsa_key_qr' (QR code image).
Returns JSON with payload_type indicating text or file.
"""
try:
ref_data = await reference_photo.read()
stego_data = await stego_image.read()
rsa_key_data = await rsa_key.read() if rsa_key else None
# Handle RSA key from .pem file or QR code image
rsa_key_data = None
rsa_key_from_qr = False
if rsa_key and rsa_key.filename:
rsa_key_data = await rsa_key.read()
elif rsa_key_qr and rsa_key_qr.filename:
if not HAS_QR_READ:
raise HTTPException(
501,
"QR code reading not available. Install pyzbar and libzbar."
)
qr_image_data = await rsa_key_qr.read()
key_pem = extract_key_from_qr(qr_image_data)
if not key_pem:
raise HTTPException(400, "Could not extract RSA key from QR code image")
rsa_key_data = key_pem.encode('utf-8')
rsa_key_from_qr = True
# QR code keys are never password-protected
effective_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None)
result = decode(
stego_image=stego_data,
@@ -419,7 +515,7 @@ async def api_decode_multipart(
day_phrase=day_phrase,
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=rsa_password if rsa_password else None
rsa_password=effective_password
)
if result.is_file:

View File

@@ -30,6 +30,20 @@ from stegasoo import (
FilePayload,
)
# QR Code utilities
try:
from stegasoo.qr_utils import (
extract_key_from_qr_file,
generate_qr_code,
has_qr_read, has_qr_write,
can_fit_in_qr, needs_compression,
)
HAS_QR = True
except ImportError:
HAS_QR = False
has_qr_read = lambda: False
has_qr_write = lambda: False
# ============================================================================
# CLI SETUP
@@ -177,34 +191,36 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
@click.option('--embed-file', '-e', type=click.Path(exists=True), help='Embed a file (binary)')
@click.option('--phrase', '-p', required=True, help='Day phrase')
@click.option('--pin', help='Static PIN')
@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file')
@click.option('--key-password', help='RSA key password')
@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file (.pem)')
@click.option('--key-qr', type=click.Path(exists=True), help='RSA key from QR code image')
@click.option('--key-password', help='RSA key password (for encrypted .pem files)')
@click.option('--output', '-o', type=click.Path(), help='Output file (default: auto-generated)')
@click.option('--date', 'date_str', help='Date override (YYYY-MM-DD)')
@click.option('--quiet', '-q', is_flag=True, help='Suppress output except errors')
def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key, key_password, output, date_str, quiet):
def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key, key_qr, key_password, output, date_str, quiet):
"""
Encode a secret message or file into an image.
Requires a reference photo, carrier image, and day phrase.
Must provide either --pin or --key (or both).
Must provide either --pin or --key/--key-qr (or both).
For text messages, use -m or -f or pipe via stdin.
For binary files, use -e/--embed-file.
RSA key can be provided as a .pem file (--key) or QR code image (--key-qr).
\b
Examples:
# Text message
# Text message with PIN
stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder" --pin 123456 -m "secret"
# Text from file
stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -f message.txt
# With RSA key file
stegasoo encode -r photo.jpg -c meme.png -p "words" -k mykey.pem -m "secret"
# Embed a binary file (PDF, ZIP, etc.)
# With RSA key from QR code image
stegasoo encode -r photo.jpg -c meme.png -p "words" --key-qr keyqr.png -m "secret"
# Embed a binary file
stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -e secret.pdf
# Pipe text
echo "secret" | stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456
"""
# Determine what to encode
payload = None
@@ -223,14 +239,35 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
else:
raise click.UsageError("Must provide message via -m, -f, -e, or stdin")
# Load key if provided
# Load key if provided (from .pem file or QR code image)
rsa_key_data = None
rsa_key_from_qr = False
if key and key_qr:
raise click.UsageError("Cannot use both --key and --key-qr. Choose one.")
if key:
rsa_key_data = Path(key).read_bytes()
elif key_qr:
if not HAS_QR or not has_qr_read():
raise click.ClickException(
"QR code reading not available. Install: pip install pyzbar\n"
"Also requires system library: sudo apt-get install libzbar0"
)
key_pem = extract_key_from_qr_file(key_qr)
if not key_pem:
raise click.ClickException(f"Could not extract RSA key from QR code: {key_qr}")
rsa_key_data = key_pem.encode('utf-8')
rsa_key_from_qr = True
if not quiet:
click.echo(f"Loaded RSA key from QR code: {key_qr}")
# QR code keys are never password-protected
effective_key_password = None if rsa_key_from_qr else key_password
# Validate security factors
if not pin and not rsa_key_data:
raise click.UsageError("Must provide --pin or --key (or both)")
raise click.UsageError("Must provide --pin or --key/--key-qr (or both)")
try:
ref_photo = Path(ref).read_bytes()
@@ -243,7 +280,7 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
day_phrase=phrase,
pin=pin or "",
rsa_key_data=rsa_key_data,
rsa_password=key_password,
rsa_password=effective_key_password,
date_str=date_str,
)
@@ -278,37 +315,63 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
@click.option('--stego', '-s', required=True, type=click.Path(exists=True), help='Stego image')
@click.option('--phrase', '-p', required=True, help='Day phrase')
@click.option('--pin', help='Static PIN')
@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file')
@click.option('--key-password', help='RSA key password')
@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file (.pem)')
@click.option('--key-qr', type=click.Path(exists=True), help='RSA key from QR code image')
@click.option('--key-password', help='RSA key password (for encrypted .pem files)')
@click.option('--output', '-o', type=click.Path(), help='Save decoded content to file')
@click.option('--quiet', '-q', is_flag=True, help='Output only the content (for text) or suppress messages (for files)')
@click.option('--force', is_flag=True, help='Overwrite existing output file')
def decode_cmd(ref, stego, phrase, pin, key, key_password, output, quiet, force):
def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, quiet, force):
"""
Decode a secret message or file from a stego image.
Must use the same credentials that were used for encoding.
Automatically detects whether content is text or a file.
RSA key can be provided as a .pem file (--key) or QR code image (--key-qr).
\b
Examples:
# Decode and print text
# Decode with PIN
stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder" --pin 123456
# Decode and save (auto-detect type)
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 -o output.txt
# Decode with RSA key file
stegasoo decode -r photo.jpg -s stego.png -p "words" -k mykey.pem
# Quiet mode for piping text
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 -q | less
# Decode with RSA key from QR code image
stegasoo decode -r photo.jpg -s stego.png -p "words" --key-qr keyqr.png
# Save output to file
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 -o output.txt
"""
# Load key if provided
# Load key if provided (from .pem file or QR code image)
rsa_key_data = None
rsa_key_from_qr = False
if key and key_qr:
raise click.UsageError("Cannot use both --key and --key-qr. Choose one.")
if key:
rsa_key_data = Path(key).read_bytes()
elif key_qr:
if not HAS_QR or not has_qr_read():
raise click.ClickException(
"QR code reading not available. Install: pip install pyzbar\n"
"Also requires system library: sudo apt-get install libzbar0"
)
key_pem = extract_key_from_qr_file(key_qr)
if not key_pem:
raise click.ClickException(f"Could not extract RSA key from QR code: {key_qr}")
rsa_key_data = key_pem.encode('utf-8')
rsa_key_from_qr = True
if not quiet:
click.echo(f"Loaded RSA key from QR code: {key_qr}")
# QR code keys are never password-protected
effective_key_password = None if rsa_key_from_qr else key_password
# Validate security factors
if not pin and not rsa_key_data:
raise click.UsageError("Must provide --pin or --key (or both)")
raise click.UsageError("Must provide --pin or --key/--key-qr (or both)")
try:
ref_photo = Path(ref).read_bytes()
@@ -320,7 +383,7 @@ def decode_cmd(ref, stego, phrase, pin, key, key_password, output, quiet, force)
day_phrase=phrase,
pin=pin or "",
rsa_key_data=rsa_key_data,
rsa_password=key_password,
rsa_password=effective_key_password,
)
if result.is_file:

View File

@@ -41,6 +41,33 @@ from stegasoo.constants import (
VALID_RSA_SIZES, MAX_FILE_SIZE,
)
# QR Code support
try:
import qrcode
from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_M
HAS_QRCODE = True
except ImportError:
HAS_QRCODE = False
# QR Code reading
try:
from pyzbar.pyzbar import decode as pyzbar_decode
HAS_QRCODE_READ = True
except ImportError:
HAS_QRCODE_READ = False
import zlib
import base64
# Import QR utilities
from stegasoo.qr_utils import (
compress_data, decompress_data, auto_decompress,
is_compressed, can_fit_in_qr, needs_compression,
generate_qr_code, read_qr_code, extract_key_from_qr,
has_qr_write, has_qr_read,
QR_MAX_BINARY, COMPRESSION_PREFIX
)
# ============================================================================
# FLASK APP CONFIGURATION
@@ -81,6 +108,7 @@ def format_size(size_bytes: int) -> str:
return f"{size_bytes / (1024 * 1024):.1f} MB"
# ============================================================================
# ROUTES
# ============================================================================
@@ -99,7 +127,7 @@ def generate():
if not use_pin and not use_rsa:
flash('You must select at least one security factor (PIN or RSA Key)', 'error')
return render_template('generate.html', generated=False, has_ml=False)
return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE)
pin_length = int(request.form.get('pin_length', 6))
rsa_bits = int(request.form.get('rsa_bits', 2048))
@@ -119,6 +147,31 @@ def generate():
words_per_phrase=words_per_phrase
)
# Store RSA key temporarily for QR generation
qr_token = None
qr_needs_compression = False
qr_too_large = False
if creds.rsa_key_pem and HAS_QRCODE:
# Check if key fits in QR code
if can_fit_in_qr(creds.rsa_key_pem, compress=False):
qr_needs_compression = False
elif can_fit_in_qr(creds.rsa_key_pem, compress=True):
qr_needs_compression = True
else:
qr_too_large = True
if not qr_too_large:
qr_token = secrets.token_urlsafe(16)
cleanup_temp_files()
TEMP_FILES[qr_token] = {
'data': creds.rsa_key_pem.encode(),
'filename': 'rsa_key.pem',
'timestamp': time.time(),
'type': 'rsa_key',
'compress': qr_needs_compression
}
return render_template('generate.html',
phrases=creds.phrases,
pin=creds.pin,
@@ -134,19 +187,118 @@ def generate():
pin_entropy=creds.pin_entropy,
rsa_entropy=creds.rsa_entropy,
total_entropy=creds.total_entropy,
has_ml=False
has_qrcode=HAS_QRCODE,
qr_token=qr_token,
qr_needs_compression=qr_needs_compression,
qr_too_large=qr_too_large
)
except Exception as e:
flash(f'Error generating credentials: {e}', 'error')
return render_template('generate.html', generated=False, has_ml=False)
return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE)
return render_template('generate.html', generated=False, has_ml=False)
return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE)
@app.route('/generate/qr/<token>')
def generate_qr(token):
"""Generate QR code for RSA key."""
if not HAS_QRCODE:
return "QR code support not available", 501
if token not in TEMP_FILES:
return "Token expired or invalid", 404
file_info = TEMP_FILES[token]
if file_info.get('type') != 'rsa_key':
return "Invalid token type", 400
try:
key_pem = file_info['data'].decode('utf-8')
compress = file_info.get('compress', False)
qr_png = generate_qr_code(key_pem, compress=compress)
return send_file(
io.BytesIO(qr_png),
mimetype='image/png',
as_attachment=False
)
except Exception as e:
return f"Error generating QR code: {e}", 500
@app.route('/generate/qr-download/<token>')
def generate_qr_download(token):
"""Download QR code as PNG file."""
if not HAS_QRCODE:
return "QR code support not available", 501
if token not in TEMP_FILES:
return "Token expired or invalid", 404
file_info = TEMP_FILES[token]
if file_info.get('type') != 'rsa_key':
return "Invalid token type", 400
try:
key_pem = file_info['data'].decode('utf-8')
compress = file_info.get('compress', False)
qr_png = generate_qr_code(key_pem, compress=compress)
return send_file(
io.BytesIO(qr_png),
mimetype='image/png',
as_attachment=True,
download_name='stegasoo_rsa_key_qr.png'
)
except Exception as e:
return f"Error generating QR code: {e}", 500
@app.route('/generate/download-key', methods=['POST'])
def download_key():
"""Download RSA key as password-protected PEM file."""
key_pem = request.form.get('key_pem', '')
@app.route('/extract-key-from-qr', methods=['POST'])
def extract_key_from_qr_route():
"""
Extract RSA key from uploaded QR code image.
Returns JSON with the extracted key or error.
"""
if not HAS_QRCODE_READ:
return jsonify({
'success': False,
'error': 'QR code reading not available. Install pyzbar and libzbar.'
}), 501
qr_image = request.files.get('qr_image')
if not qr_image:
return jsonify({
'success': False,
'error': 'No QR image provided'
}), 400
try:
image_data = qr_image.read()
key_pem = extract_key_from_qr(image_data)
if key_pem:
return jsonify({
'success': True,
'key_pem': key_pem
})
else:
return jsonify({
'success': False,
'error': 'No valid RSA key found in QR code'
}), 400
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
password = request.form.get('key_password', '')
if not key_pem:
@@ -190,11 +342,11 @@ def encode_page():
if not ref_photo or not carrier:
flash('Both reference photo and carrier image are required', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
if not allowed_image(ref_photo.filename) or not allowed_image(carrier.filename):
flash('Invalid file type. Use PNG, JPG, or BMP', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Get form data
message = request.form.get('message', '')
@@ -211,7 +363,7 @@ def encode_page():
result = validate_file_payload(file_data, payload_file.filename)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
mime_type, _ = mimetypes.guess_type(payload_file.filename)
payload = FilePayload(
@@ -224,43 +376,64 @@ def encode_page():
result = validate_message(message)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
payload = message
if not day_phrase:
flash('Day phrase is required', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Read files
ref_data = ref_photo.read()
carrier_data = carrier.read()
rsa_key_data = rsa_key_file.read() if rsa_key_file and rsa_key_file.filename else None
# Handle RSA key - can come from .pem file or QR code image
rsa_key_data = None
rsa_key_qr = request.files.get('rsa_key_qr')
rsa_key_from_qr = False # Track source for password handling
if rsa_key_file and rsa_key_file.filename:
# RSA key from .pem file
rsa_key_data = rsa_key_file.read()
elif rsa_key_qr and rsa_key_qr.filename and HAS_QRCODE_READ:
# RSA key from QR code image
qr_image_data = rsa_key_qr.read()
key_pem = extract_key_from_qr(qr_image_data)
if key_pem:
rsa_key_data = key_pem.encode('utf-8')
rsa_key_from_qr = True # QR keys are never password-protected
else:
flash('Could not extract RSA key from QR code image. Make sure the image contains a valid QR code.', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Validate security factors
result = validate_security_factors(pin, rsa_key_data)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Validate PIN if provided
if pin:
result = validate_pin(pin)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Determine key password - QR code keys are never password-protected
key_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None)
# Validate RSA key if provided
if rsa_key_data:
result = validate_rsa_key(rsa_key_data, rsa_password if rsa_password else None)
result = validate_rsa_key(rsa_key_data, key_password)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Validate carrier image
result = validate_image(carrier_data, "Carrier image")
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Get date
client_date = request.form.get('client_date', '').strip()
@@ -277,7 +450,7 @@ def encode_page():
day_phrase=day_phrase,
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=rsa_password if rsa_password else None,
rsa_password=key_password,
date_str=date_str
)
@@ -294,15 +467,15 @@ def encode_page():
except CapacityError as e:
flash(str(e), 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
except StegasooError as e:
flash(str(e), 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
except Exception as e:
flash(f'Error: {e}', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
@app.route('/encode/result/<file_id>')
@@ -366,7 +539,7 @@ def decode_page():
if not ref_photo or not stego_image:
flash('Both reference photo and stego image are required', 'error')
return render_template('decode.html')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Get form data
day_phrase = request.form.get('day_phrase', '')
@@ -375,32 +548,53 @@ def decode_page():
if not day_phrase:
flash('Day phrase is required', 'error')
return render_template('decode.html')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Read files
ref_data = ref_photo.read()
stego_data = stego_image.read()
rsa_key_data = rsa_key_file.read() if rsa_key_file and rsa_key_file.filename else None
# Handle RSA key - can come from .pem file or QR code image
rsa_key_data = None
rsa_key_qr = request.files.get('rsa_key_qr')
rsa_key_from_qr = False # Track source for password handling
if rsa_key_file and rsa_key_file.filename:
# RSA key from .pem file
rsa_key_data = rsa_key_file.read()
elif rsa_key_qr and rsa_key_qr.filename and HAS_QRCODE_READ:
# RSA key from QR code image
qr_image_data = rsa_key_qr.read()
key_pem = extract_key_from_qr(qr_image_data)
if key_pem:
rsa_key_data = key_pem.encode('utf-8')
rsa_key_from_qr = True # QR keys are never password-protected
else:
flash('Could not extract RSA key from QR code image. Make sure the image contains a valid QR code.', 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Validate security factors
result = validate_security_factors(pin, rsa_key_data)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('decode.html')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Validate PIN if provided
if pin:
result = validate_pin(pin)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('decode.html')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Determine key password - QR code keys are never password-protected
key_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None)
# Validate RSA key if provided
if rsa_key_data:
result = validate_rsa_key(rsa_key_data, rsa_password if rsa_password else None)
result = validate_rsa_key(rsa_key_data, key_password)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('decode.html')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Decode
decode_result = decode(
@@ -409,7 +603,7 @@ def decode_page():
day_phrase=day_phrase,
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=rsa_password if rsa_password else None
rsa_password=key_password
)
if decode_result.is_file:
@@ -438,15 +632,15 @@ def decode_page():
except DecryptionError:
flash('Decryption failed. Check your phrase, PIN, RSA key, and reference photo.', 'error')
return render_template('decode.html')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
except StegasooError as e:
flash(str(e), 'error')
return render_template('decode.html')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
except Exception as e:
flash(f'Error: {e}', 'error')
return render_template('decode.html')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
return render_template('decode.html')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
@app.route('/decode/download/<file_id>')
@@ -471,6 +665,7 @@ def decode_download(file_id):
def about():
return render_template('about.html',
has_argon2=has_argon2(),
has_qrcode_read=HAS_QRCODE_READ,
max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024
)

View File

@@ -10,37 +10,239 @@
<h5 class="mb-0"><i class="bi bi-info-circle me-2"></i>About Stegasoo</h5>
</div>
<div class="card-body">
<p>
Stegasoo is a hybrid steganography system that hides encrypted messages inside
ordinary images. It combines multiple security layers to create a system that is
both highly secure and practical to use.
<p class="lead">
Stegasoo is a secure steganography tool that hides encrypted messages and files
inside ordinary images using multi-factor authentication.
</p>
<h6 class="mt-4 mb-3">System Status</h6>
<div class="row g-3">
<h6 class="text-primary mt-4 mb-3"><i class="bi bi-stars me-2"></i>Key Features</h6>
<div class="row">
<div class="col-md-6">
<div class="d-flex align-items-center p-3 rounded status-box">
{% if has_argon2 %}
<i class="bi bi-check-circle-fill text-success fs-4 me-3"></i>
<div>
<strong>Argon2id Available</strong>
<div class="small text-muted">Memory-hard key derivation (256MB)</div>
</div>
{% else %}
<i class="bi bi-exclamation-triangle-fill text-warning fs-4 me-3"></i>
<div>
<strong>Using PBKDF2 Fallback</strong>
<div class="small text-muted">Install argon2-cffi for better security</div>
</div>
{% endif %}
</div>
<ul class="list-unstyled">
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>Text &amp; File Embedding</strong> — Hide messages or any file type (PDF, ZIP, documents)
</li>
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>Multi-Factor Security</strong> — Combines photo + phrase + PIN/RSA key
</li>
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>AES-256-GCM Encryption</strong> — Military-grade authenticated encryption
</li>
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>Daily Rotating Phrases</strong> — Different passphrase each day of the week
</li>
</ul>
</div>
<div class="col-md-6">
<div class="d-flex align-items-center p-3 rounded status-box">
<i class="bi bi-shield-fill-check text-success fs-4 me-3"></i>
<div>
<strong>AES-256-GCM</strong>
<div class="small text-muted">Authenticated encryption enabled</div>
<ul class="list-unstyled">
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>Random Pixel Embedding</strong> — Defeats statistical steganalysis
</li>
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>Format Preservation</strong> — Maintains PNG/BMP lossless formats
</li>
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>Large Capacity</strong> — Up to {{ max_payload_kb }} KB payload, 16MP images
</li>
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>Zero Server Storage</strong> — Nothing saved, files auto-expire
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-shield-lock me-2"></i>How Security Works</h5>
</div>
<div class="card-body">
<p>Stegasoo uses <strong>hybrid multi-factor authentication</strong> to derive encryption keys:</p>
<div class="row text-center my-4">
<div class="col-md-3 mb-3">
<div class="p-3 bg-dark rounded">
<i class="bi bi-image text-info fs-2 d-block mb-2"></i>
<strong>Reference Photo</strong>
<div class="small text-muted mt-1">Something you have</div>
<div class="small text-success">~80-256 bits</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="p-3 bg-dark rounded">
<i class="bi bi-chat-quote text-warning fs-2 d-block mb-2"></i>
<strong>Daily Phrase</strong>
<div class="small text-muted mt-1">Something you know (rotates)</div>
<div class="small text-success">~33 bits (3 words)</div>
</div>
</div>
<div class="col-md-3 mb-3">
<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-success">~20 bits (6 digits)</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="p-3 bg-dark rounded">
<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>
</div>
</div>
<div class="alert alert-secondary">
<i class="bi bi-calculator me-2"></i>
<strong>Combined entropy:</strong> 130-400+ bits depending on configuration.
For reference, 128 bits is considered computationally infeasible to brute force.
</div>
<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.
{% 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 Technique</h6>
<p>
Uses <strong>LSB (Least Significant Bit)</strong> 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>
</div>
</div>
<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>
</div>
<div class="card-body">
<p>
<span class="badge bg-info me-1">New in v2.1</span>
Stegasoo now 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>
<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.
A 16MP carrier image can hold approximately 6MB of raw data, but we limit payloads
to {{ max_payload_kb }} KB for reasonable processing times.
</div>
</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>
</div>
<div class="card-body">
<div class="accordion" id="usageAccordion">
<div class="accordion-item bg-dark">
<h2 class="accordion-header">
<button class="accordion-button bg-dark text-light" type="button"
data-bs-toggle="collapse" data-bs-target="#setup">
<i class="bi bi-1-circle me-2"></i>Initial Setup
</button>
</h2>
<div id="setup" class="accordion-collapse collapse show" data-bs-parent="#usageAccordion">
<div class="accordion-body">
<ol>
<li>Both parties agree on a <strong>reference photo</strong> (shared secretly, never transmitted)</li>
<li>Go to <a href="/generate">Generate</a> and create credentials</li>
<li><strong>Memorize</strong> the 7 daily phrases and PIN</li>
<li>If using RSA, download and securely store the key file</li>
<li>Share credentials with your contact through a secure channel</li>
</ol>
</div>
</div>
</div>
<div class="accordion-item bg-dark">
<h2 class="accordion-header">
<button class="accordion-button collapsed bg-dark text-light" type="button"
data-bs-toggle="collapse" data-bs-target="#encoding">
<i class="bi bi-2-circle me-2"></i>Encoding a Message or File
</button>
</h2>
<div id="encoding" class="accordion-collapse collapse" data-bs-parent="#usageAccordion">
<div class="accordion-body">
<ol>
<li>Go to <a href="/encode">Encode</a></li>
<li>Upload your <strong>reference photo</strong></li>
<li>Upload a <strong>carrier image</strong> (the image to hide data in)</li>
<li>Choose <strong>Text</strong> or <strong>File</strong> mode</li>
<li>Enter your message or select a file to embed</li>
<li>Enter <strong>today's phrase</strong> and your PIN/key</li>
<li>Download the resulting stego image</li>
<li>Send the stego image through any channel (email, social media, etc.)</li>
</ol>
</div>
</div>
</div>
<div class="accordion-item bg-dark">
<h2 class="accordion-header">
<button class="accordion-button collapsed bg-dark text-light" type="button"
data-bs-toggle="collapse" data-bs-target="#decoding">
<i class="bi bi-3-circle me-2"></i>Decoding a Message or File
</button>
</h2>
<div id="decoding" class="accordion-collapse collapse" data-bs-parent="#usageAccordion">
<div class="accordion-body">
<ol>
<li>Go to <a href="/decode">Decode</a></li>
<li>Upload your <strong>reference photo</strong> (same one used for encoding)</li>
<li>Upload the <strong>stego image</strong> you received</li>
<li>Enter the phrase for <strong>the day it was encoded</strong> (check the filename for date)</li>
<li>Enter your PIN and/or RSA key</li>
<li>View the decoded message or download the extracted file</li>
</ol>
<div class="alert alert-warning small mt-3 mb-0">
<i class="bi bi-exclamation-triangle me-2"></i>
The stego image filename contains the encoding date (e.g., <code>abc123_20251228.png</code>).
Use this to determine which day's phrase to use!
</div>
</div>
</div>
</div>
@@ -50,129 +252,100 @@
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-shield-lock me-2"></i>Security Model</h5>
<h5 class="mb-0"><i class="bi bi-speedometer2 me-2"></i>Limits &amp; Specifications</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-dark">
<thead>
<tr>
<th>Component</th>
<th>Entropy</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><i class="bi bi-image text-info me-2"></i>Reference Photo</td>
<td>~80-256 bits</td>
<td>Something you have (plausible deniability)</td>
</tr>
<tr>
<td><i class="bi bi-chat-quote text-info me-2"></i>3-Word Phrase</td>
<td>~33 bits</td>
<td>Something you know (changes daily)</td>
</tr>
<tr>
<td><i class="bi bi-123 text-info me-2"></i>6-Digit PIN</td>
<td>~20 bits</td>
<td>Something you know (static)</td>
</tr>
<tr>
<td><i class="bi bi-calendar text-info me-2"></i>Date</td>
<td>N/A</td>
<td>Automatic key rotation</td>
</tr>
<tr class="table-active">
<td><strong>Combined</strong></td>
<td><strong>133+ bits</strong></td>
<td><strong>Beyond brute force</strong></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-cpu me-2"></i>Attack Resistance</h5>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-6">
<h6 class="text-danger"><i class="bi bi-x-circle me-2"></i>What Attackers Can't Do</h6>
<ul class="list-unstyled">
<li class="mb-2">
<i class="bi bi-shield-x text-muted me-2"></i>
Brute force the passphrase (2<sup>133</sup> combinations)
</li>
<li class="mb-2">
<i class="bi bi-shield-x text-muted me-2"></i>
Use rainbow tables (random salt per message)
</li>
<li class="mb-2">
<i class="bi bi-shield-x text-muted me-2"></i>
Detect hidden data (random pixel selection)
</li>
<li class="mb-2">
<i class="bi bi-shield-x text-muted me-2"></i>
Use GPU farms (Argon2 requires 256MB RAM per attempt)
</li>
</ul>
</div>
<div class="col-md-6">
<h6 class="text-warning"><i class="bi bi-exclamation-triangle me-2"></i>Real Threats</h6>
<ul class="list-unstyled">
<li class="mb-2">
<i class="bi bi-person-x text-muted me-2"></i>
Social engineering (someone tricks you)
</li>
<li class="mb-2">
<i class="bi bi-door-open text-muted me-2"></i>
Physical access to your devices
</li>
<li class="mb-2">
<i class="bi bi-bug text-muted me-2"></i>
Malware/keyloggers on your system
</li>
<li class="mb-2">
<i class="bi bi-camera-video text-muted me-2"></i>
Shoulder surfing while you type
</li>
</ul>
</div>
</div>
<table class="table table-dark table-striped">
<tbody>
<tr>
<td><i class="bi bi-file-text me-2"></i>Max text message</td>
<td><strong>250,000 characters</strong> (~250 KB)</td>
</tr>
<tr>
<td><i class="bi bi-file-earmark me-2"></i>Max file payload</td>
<td><strong>{{ max_payload_kb }} KB</strong></td>
</tr>
<tr>
<td><i class="bi bi-image me-2"></i>Max carrier image</td>
<td><strong>16 megapixels</strong> (~4000×4000)</td>
</tr>
<tr>
<td><i class="bi bi-upload me-2"></i>Max upload size</td>
<td><strong>10 MB</strong></td>
</tr>
<tr>
<td><i class="bi bi-clock me-2"></i>Temp file expiry</td>
<td><strong>5 minutes</strong></td>
</tr>
<tr>
<td><i class="bi bi-key me-2"></i>PIN length</td>
<td><strong>6-9 digits</strong></td>
</tr>
<tr>
<td><i class="bi bi-shield me-2"></i>RSA key sizes</td>
<td><strong>2048, 3072, 4096 bits</strong></td>
</tr>
<tr>
<td><i class="bi bi-chat-quote me-2"></i>Phrase length</td>
<td><strong>3-12 words</strong> (BIP-39 wordlist)</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-book me-2"></i>Best Practices</h5>
<h5 class="mb-0"><i class="bi bi-terminal me-2"></i>CLI &amp; API</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6 class="text-success"><i class="bi bi-check-lg me-2"></i>Do</h6>
<ul>
<li>Memorize your phrases and PIN, never write them down</li>
<li>Use a reference photo that both parties already have</li>
<li>Use different carrier images for each message</li>
<li>Share stego images through normal channels (looks innocent)</li>
</ul>
</div>
<div class="col-md-6">
<h6 class="text-danger"><i class="bi bi-x-lg me-2"></i>Don't</h6>
<ul>
<li>Don't transmit the reference photo</li>
<li>Don't reuse the same carrier image</li>
<li>Don't store phrases or PIN digitally</li>
<li>Don't resize or recompress stego images</li>
</ul>
</div>
</div>
<p>Stegasoo is also available as a command-line tool and REST API:</p>
<h6 class="mt-3">Command Line</h6>
<pre class="bg-dark p-3 rounded"><code># Generate credentials
stegasoo generate --pin --rsa
# Encode a text message
stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder" --pin 123456 -m "secret"
# Encode a file
stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder" --pin 123456 -e document.pdf
# Decode (auto-detects text vs file)
stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder" --pin 123456</code></pre>
<h6 class="mt-4">REST API</h6>
<pre class="bg-dark p-3 rounded"><code># Encode with multipart upload
curl -X POST http://localhost:8000/encode/multipart \
-F "reference_photo=@photo.jpg" \
-F "carrier=@meme.png" \
-F "message=secret" \
-F "day_phrase=apple forest thunder" \
-F "pin=123456" \
--output stego.png
# Encode a file
curl -X POST http://localhost:8000/encode/multipart \
-F "reference_photo=@photo.jpg" \
-F "carrier=@meme.png" \
-F "payload_file=@document.pdf" \
-F "day_phrase=apple forest thunder" \
-F "pin=123456" \
--output stego.png</code></pre>
<p class="small text-muted mt-3 mb-0">
API documentation available at <code>/docs</code> (Swagger) or <code>/redoc</code> when running the API server.
</p>
</div>
</div>
<div class="text-center mt-4 text-muted small">
<p>
Stegasoo v2.1.0 &bull;
<i class="bi bi-github me-1"></i>Open Source &bull;
Built with Python, Flask, and cryptography
</p>
</div>
</div>
</div>
{% endblock %}

View File

@@ -63,7 +63,7 @@
<div class="container text-center text-muted">
<small>
<img src="{{ url_for('static', filename='favicon.svg') }}" alt="" height="16" class="me-1" style="vertical-align: text-bottom;">
Stegasoo v1.1 — Hybrid Photo + Day-Phrase + PIN Steganography
Stegasoo v2.1.0 — Hybrid Photo + Day-Phrase + PIN Steganography
</small>
</div>
</footer>

View File

@@ -133,10 +133,33 @@
<label class="form-label">
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
</label>
<input type="file" name="rsa_key" class="form-control" id="rsaKeyInput"
accept=".pem,.key">
{% if has_qrcode_read %}
<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/png,image/jpeg,image/gif,image/webp,.png,.jpg,.jpeg,.gif,.webp">
<div class="form-text small">PNG, JPG, or other image of QR code</div>
</div>
</div>
{% else %}
<input type="file" name="rsa_key" class="form-control" id="rsaKeyInput" accept=".pem,.key">
{% endif %}
<div class="form-text">
If RSA key was used during encoding
If RSA key was used during encoding (file or QR image)
</div>
</div>
</div>
@@ -204,13 +227,32 @@ document.getElementById('decodeForm')?.addEventListener('submit', function() {
btn.disabled = true;
});
// Show RSA password field when key is selected
// Show RSA password field when key is selected (only for .pem files, not QR)
const rsaKeyInput = document.getElementById('rsaKeyInput');
const rsaKeyQrInput = document.getElementById('rsaKeyQrInput');
const rsaPasswordGroup = document.getElementById('rsaPasswordGroup');
rsaKeyInput?.addEventListener('change', function() {
rsaPasswordGroup.classList.toggle('d-none', !this.files.length);
});
if (rsaKeyInput) {
rsaKeyInput.addEventListener('change', function() {
// Show password field only for .pem files
rsaPasswordGroup.classList.toggle('d-none', !this.files.length);
// Clear QR input if file is selected
if (rsaKeyQrInput && this.files.length) {
rsaKeyQrInput.value = '';
}
});
}
if (rsaKeyQrInput) {
rsaKeyQrInput.addEventListener('change', function() {
// Hide password field for QR codes (they're unencrypted)
rsaPasswordGroup.classList.add('d-none');
// Clear file input if QR is selected
if (rsaKeyInput && this.files.length) {
rsaKeyInput.value = '';
}
});
}
// Day names for date detection
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];

View File

@@ -142,10 +142,33 @@
<label class="form-label">
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
</label>
<input type="file" name="rsa_key" class="form-control" id="rsaKeyInput"
accept=".pem,.key">
{% if has_qrcode_read %}
<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="#rsaFileTab" 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="#rsaQrTab" 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="rsaFileTab" 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="rsaQrTab" role="tabpanel">
<input type="file" name="rsa_key_qr" class="form-control form-control-sm" id="rsaKeyQrInput" accept="image/png,image/jpeg,image/gif,image/webp,.png,.jpg,.jpeg,.gif,.webp">
<div class="form-text small">PNG, JPG, or other image of QR code</div>
</div>
</div>
{% else %}
<input type="file" name="rsa_key" class="form-control" id="rsaKeyInput" accept=".pem,.key">
{% endif %}
<div class="form-text">
Your shared .pem key file (if configured)
Your shared .pem key file or QR code image (if configured)
</div>
</div>
</div>
@@ -158,7 +181,7 @@
<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
Leave blank if your key file is not password-protected (not needed for QR codes)
</div>
</div>
@@ -270,13 +293,32 @@ function formatFileSize(bytes) {
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
// Show RSA password field when key is selected
// Show RSA password field when key is selected (only for .pem files, not QR)
const rsaKeyInput = document.getElementById('rsaKeyInput');
const rsaKeyQrInput = document.getElementById('rsaKeyQrInput');
const rsaPasswordGroup = document.getElementById('rsaPasswordGroup');
rsaKeyInput.addEventListener('change', function() {
rsaPasswordGroup.classList.toggle('d-none', !this.files.length);
});
if (rsaKeyInput) {
rsaKeyInput.addEventListener('change', function() {
// Show password field only for .pem files
rsaPasswordGroup.classList.toggle('d-none', !this.files.length);
// Clear QR input if file is selected
if (rsaKeyQrInput && this.files.length) {
rsaKeyQrInput.value = '';
}
});
}
if (rsaKeyQrInput) {
rsaKeyQrInput.addEventListener('change', function() {
// Hide password field for QR codes (they're unencrypted)
rsaPasswordGroup.classList.add('d-none');
// Clear file input if QR is selected
if (rsaKeyInput && this.files.length) {
rsaKeyInput.value = '';
}
});
}
// Form submit loading state
document.getElementById('encodeForm').addEventListener('submit', function(e) {

View File

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

View File

@@ -3,18 +3,6 @@
{% block title %}Generate Credentials - Stegasoo{% endblock %}
{% block content %}
<style>
@media print {
body * { visibility: hidden; }
.card-body, .card-body * { visibility: visible; color: black !important; }
.card-body { position: absolute; left: 0; top: 0; width: 100%; margin: 0; border: none; }
.btn, .alert, form, .card-header { display: none !important; }
.table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #000 !important; padding: 8px; }
/* Hide the inputs, show the results */
#generateForm { display: none; }
}
</style>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
@@ -23,335 +11,479 @@
</div>
<div class="card-body">
{% if not generated %}
<p class="text-muted mb-4">
Generate your weekly phrase card and security factors. You must choose at least one: PIN or RSA Key.
</p>
<form method="POST" id="generateForm">
<!-- Generation Form -->
<form method="POST">
<div class="mb-4">
<label class="form-label">Words per phrase</label>
<select name="words_per_phrase" class="form-select" id="wordsSelect">
<option value="3" selected>3 words (~33 bits)</option>
<option value="4">4 words (~44 bits)</option>
<option value="5">5 words (~55 bits)</option>
<option value="6">6 words (~66 bits)</option>
<option value="7">7 words (~77 bits)</option>
<option value="8">8 words (~88 bits)</option>
<option value="9">9 words (~99 bits)</option>
<option value="10">10 words (~110 bits)</option>
<option value="11">11 words (~121 bits)</option>
<option value="12">12 words (~132 bits)</option>
</select>
<div class="form-text">More words = more security, harder to memorize</div>
<label class="form-label">Words per Phrase</label>
<input type="range" class="form-range" name="words_per_phrase"
min="3" max="12" value="3" id="wordsRange">
<div class="d-flex justify-content-between small text-muted">
<span>3 (33 bits)</span>
<span id="wordsValue" class="text-primary fw-bold">3 words (~33 bits)</span>
<span>12 (132 bits)</span>
</div>
</div>
<hr class="my-4">
<hr>
<h6 class="text-muted mb-3">SECURITY FACTORS <span class="text-warning">(select at least one)</span></h6>
<h6 class="text-muted mb-3">SECURITY FACTORS <span class="text-warning small">(select at least one)</span></h6>
<!-- PIN Option -->
<div class="card mb-3" style="background: rgba(0,0,0,0.2);">
<div class="card-body">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="use_pin" id="usePin" checked>
<label class="form-check-label fw-bold" for="usePin">
<i class="bi bi-123 me-1"></i> PIN
<div class="row">
<div class="col-md-6 mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="use_pin"
id="usePinCheck" checked>
<label class="form-check-label" for="usePinCheck">
<i class="bi bi-123 me-1"></i> Generate PIN
</label>
</div>
<div id="pinOptions">
<label class="form-label">PIN length</label>
<select name="pin_length" class="form-select" id="pinSelect">
<div class="mt-2" id="pinOptions">
<label class="form-label small">PIN Length</label>
<select name="pin_length" class="form-select form-select-sm">
<option value="6" selected>6 digits (~20 bits)</option>
<option value="7">7 digits (~23 bits)</option>
<option value="8">8 digits (~27 bits)</option>
<option value="8">8 digits (~26 bits)</option>
<option value="9">9 digits (~30 bits)</option>
</select>
<div class="form-text">Memorizable, same PIN used every day</div>
</div>
</div>
</div>
<!-- RSA Key Option -->
<div class="card mb-3" style="background: rgba(0,0,0,0.2);">
<div class="card-body">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="use_rsa" id="useRsa">
<label class="form-check-label fw-bold" for="useRsa">
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
<div class="col-md-6 mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="use_rsa"
id="useRsaCheck">
<label class="form-check-label" for="useRsaCheck">
<i class="bi bi-file-earmark-lock me-1"></i> Generate RSA Key
</label>
</div>
<div id="rsaOptions" class="d-none">
<label class="form-label">Key size</label>
<select name="rsa_bits" class="form-select" id="rsaSelect">
<option value="2048" selected>2048-bit (~128 bits effective)</option>
<option value="3072">3072-bit (~128 bits effective)</option>
<option value="4096">4096-bit (~128 bits effective)</option>
<div class="mt-2 d-none" id="rsaOptions">
<label class="form-label small">Key Size</label>
<select name="rsa_bits" class="form-select form-select-sm">
<option value="2048" selected>2048 bits (~128 bits entropy)</option>
<option value="3072">3072 bits (~128 bits entropy)</option>
<option value="4096">4096 bits (~128 bits entropy)</option>
</select>
<div class="form-text">File-based key, both parties need the same .pem file</div>
</div>
</div>
</div>
<div class="alert alert-info mb-4">
<div class="d-flex justify-content-between align-items-center">
<span><i class="bi bi-calculator me-2"></i>Estimated entropy:</span>
<strong id="entropyDisplay">~53 bits</strong>
</div>
<div class="progress mt-2" style="height: 8px;">
<div class="progress-bar bg-success" id="entropyBar" style="width: 40%"></div>
</div>
<small class="text-muted mt-1 d-block">
<span id="entropyDesc">Good for most use cases</span>
• Reference photo adds ~80-256 bits more
</small>
</div>
<div class="alert alert-warning d-none" id="noFactorWarning">
<i class="bi bi-exclamation-triangle me-2"></i>
You must select at least one security factor (PIN or RSA Key)
</div>
<button type="submit" class="btn btn-primary btn-lg w-100" id="generateBtn">
<button type="submit" class="btn btn-primary btn-lg w-100 mt-3">
<i class="bi bi-shuffle me-2"></i>Generate Credentials
</button>
</form>
{% else %}
<!-- Generated Results -->
<div class="alert alert-success-bright alert-dismissible fade show">
<i class="bi bi-check-circle me-2"></i>
<strong>Credentials Generated!</strong>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<div class="alert alert-warning alert-dismissible fade show">
<!-- Generated Credentials Display -->
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Memorize phrases, save key securely, then close!</strong> - Do not screenshot
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
<strong>Memorize these credentials!</strong> They will not be shown again.
<br><small>Do not screenshot or save to an unencrypted file.</small>
</div>
{% if pin %}
<div class="text-center mb-4">
<h6 class="text-muted mb-2">YOUR STATIC PIN</h6>
<div class="pin-container p-3 bg-dark border rounded fs-1 font-monospace text-white">
{{ pin }}
</div>
<div class="mt-2 d-print-none"> <small class="text-muted">Use this {{ pin_length }}-digit PIN every day</small>
</div>
</div>
{% endif %}
{% if rsa_key_pem %}
<hr class="my-4">
<div class="mb-4">
<h6 class="text-muted mb-3">
<i class="bi bi-file-earmark-lock me-2"></i>YOUR RSA KEY ({{ rsa_bits }}-bit)
</h6>
<div class="alert alert-danger small">
<i class="bi bi-shield-exclamation me-1"></i>
<strong>Save this key securely!</strong> Share it with your recipient through a secure channel. You cannot recover it later.
</div>
<!-- Key Display -->
<div class="mb-3">
<textarea class="form-control font-monospace" id="rsaKeyText" rows="6" readonly style="font-size: 0.75rem;">{{ rsa_key_pem }}</textarea>
</div>
<!-- Copy to Clipboard -->
<button type="button" class="btn btn-outline-light me-2" id="copyKeyBtn">
<i class="bi bi-clipboard me-1"></i> Copy to Clipboard
</button>
<!-- Download with Password -->
<button type="button" class="btn btn-outline-light" data-bs-toggle="collapse" data-bs-target="#downloadKeyForm">
<i class="bi bi-download me-1"></i> Download as .pem
</button>
<div class="collapse mt-3" id="downloadKeyForm">
<div class="card" style="background: rgba(0,0,0,0.2);">
<div class="card-body">
<form method="POST" action="{{ url_for('download_key') }}">
<input type="hidden" name="key_pem" value="{{ rsa_key_pem }}">
<div class="mb-3">
<label class="form-label">Password to protect key file</label>
<input type="password" name="key_password" class="form-control"
placeholder="Minimum 8 characters" minlength="8" required>
<div class="form-text">You'll need this password when using the key</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-file-earmark-lock me-1"></i> Download Protected Key
</button>
</form>
<h6 class="text-muted"><i class="bi bi-123 me-2"></i>STATIC PIN</h6>
<div class="text-center">
<div class="pin-container d-inline-block">
<div class="pin-digits-row" id="pinDigits">
{% for digit in pin %}
<span class="pin-digit-box">{{ digit }}</span>
{% endfor %}
</div>
<div class="pin-buttons mt-3">
<button type="button" class="btn btn-sm btn-outline-secondary me-2" onclick="togglePinVisibility()">
<i class="bi bi-eye-slash" id="pinToggleIcon"></i>
<span id="pinToggleText">Hide</span>
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="copyPin()">
<i class="bi bi-clipboard" id="pinCopyIcon"></i>
<span id="pinCopyText">Copy</span>
</button>
</div>
</div>
</div>
</div>
{% endif %}
<hr class="my-4">
<h6 class="text-muted mb-3">DAILY PHRASES ({{ words_per_phrase }} words each)</h6>
<div class="table-responsive">
<table class="table table-dark table-hover">
<thead>
<tr>
<th style="width: 140px;">Day</th>
<th>Phrase</th>
</tr>
</thead>
<tbody>
{% for day in days %}
<tr>
<td class="text-nowrap">
<i class="bi bi-calendar3 me-2"></i>{{ day }}
</td>
<td>
<span class="phrase-display">{{ phrases[day] }}</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="mb-4">
<h6 class="text-muted"><i class="bi bi-chat-quote me-2"></i>DAILY PHRASES</h6>
<div class="table-responsive">
<table class="table table-dark table-striped mb-0">
<tbody>
{% for day in days %}
<tr>
<td class="text-muted" style="width: 100px;">{{ day }}</td>
<td>
<span class="font-monospace phrase-display" id="phrase{{ loop.index }}">{{ phrases[day] }}</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="text-end mt-2">
<button class="btn btn-sm btn-outline-secondary" onclick="toggleAllPhrases()">
<i class="bi bi-eye-slash me-1"></i>Toggle Visibility
</button>
</div>
</div>
<div class="alert alert-success mt-4">
<h6><i class="bi bi-shield-check me-2"></i>Security Summary</h6>
<div class="row text-center mt-3">
<div class="col-3">
<div class="fs-4 fw-bold">{{ phrase_entropy }}</div>
<small class="text-muted">bits/phrase</small>
{% if rsa_key_pem %}
<div class="mb-4">
<h6 class="text-muted"><i class="bi bi-file-earmark-lock me-2"></i>RSA PRIVATE KEY ({{ rsa_bits }} bits)</h6>
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#keyTextTab" type="button">
<i class="bi bi-file-text me-1"></i>PEM Text
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#keyDownloadTab" type="button">
<i class="bi bi-download me-1"></i>Download
</button>
</li>
{% if has_qrcode and qr_token %}
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#keyQrTab" type="button">
<i class="bi bi-qr-code me-1"></i>QR Code
</button>
</li>
{% endif %}
</ul>
<div class="tab-content border border-top-0 rounded-bottom p-3 bg-dark">
<!-- PEM Text Tab -->
<div class="tab-pane fade show active" id="keyTextTab" role="tabpanel">
<pre class="bg-black p-2 rounded small mb-2" style="max-height: 200px; overflow-y: auto;"><code id="rsaKeyDisplay">{{ rsa_key_pem }}</code></pre>
<button class="btn btn-sm btn-outline-light"
onclick="navigator.clipboard.writeText(document.getElementById('rsaKeyDisplay').textContent)">
<i class="bi bi-clipboard me-1"></i>Copy to Clipboard
</button>
</div>
{% if pin %}
<div class="col-3">
<div class="fs-4 fw-bold">{{ pin_entropy }}</div>
<small class="text-muted">bits/PIN</small>
<!-- Download Tab -->
<div class="tab-pane fade" id="keyDownloadTab" role="tabpanel">
<form action="{{ url_for('download_key') }}" method="POST" class="row g-2 align-items-end">
<input type="hidden" name="key_pem" value="{{ rsa_key_pem }}">
<div class="col-md-8">
<label class="form-label small">Password to encrypt the key file</label>
<input type="password" name="key_password" class="form-control"
placeholder="Min 8 characters" minlength="8" required>
</div>
<div class="col-md-4">
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-download me-1"></i>Download .pem
</button>
</div>
</form>
<div class="form-text mt-2">
The downloaded file will be password-protected (AES-256 encrypted).
</div>
</div>
{% if has_qrcode and qr_token %}
<!-- QR Code Tab -->
<div class="tab-pane fade" id="keyQrTab" role="tabpanel">
<div class="text-center">
<p class="small text-muted mb-3">
Scan this QR code to transfer the RSA key to another device.
<br><strong>Warning:</strong> This is the unencrypted private key!
</p>
<div class="qr-container d-inline-block p-3 bg-white rounded mb-3">
<img src="{{ url_for('generate_qr', token=qr_token) }}"
alt="RSA Key QR Code"
class="img-fluid"
style="max-width: 300px;"
id="qrCodeImage">
</div>
<div>
<a href="{{ url_for('generate_qr_download', token=qr_token) }}"
class="btn btn-outline-primary">
<i class="bi bi-download me-1"></i>Download QR Code
</a>
<button class="btn btn-outline-secondary ms-2" onclick="printQrCode()">
<i class="bi bi-printer me-1"></i>Print
</button>
</div>
<div class="alert alert-warning small mt-3 mb-0 text-start">
<i class="bi bi-shield-exclamation me-1"></i>
<strong>Security note:</strong> The QR code contains your unencrypted private key.
Only scan in a secure environment. Consider using the password-protected download instead.
{% if rsa_bits >= 4096 %}
<br><br>
<i class="bi bi-exclamation-triangle me-1"></i>
<strong>4096-bit keys</strong> produce very dense QR codes. If scanning fails,
use the PEM text or download options instead.
{% endif %}
</div>
</div>
</div>
{% endif %}
{% if rsa_key_pem %}
<div class="col-3">
<div class="fs-4 fw-bold">{{ rsa_entropy }}</div>
<small class="text-muted">bits/RSA</small>
</div>
</div>
{% endif %}
<div class="mb-4">
<h6 class="text-muted"><i class="bi bi-shield-check me-2"></i>SECURITY SUMMARY</h6>
<div class="row text-center">
<div class="col">
<div class="p-2 bg-dark rounded">
<div class="small text-muted">Phrase</div>
<div class="fs-5 text-info">{{ phrase_entropy }} bits</div>
</div>
</div>
{% if pin_entropy %}
<div class="col">
<div class="p-2 bg-dark rounded">
<div class="small text-muted">PIN</div>
<div class="fs-5 text-warning">{{ pin_entropy }} bits</div>
</div>
</div>
{% endif %}
<div class="col-3">
<div class="fs-4 fw-bold text-success">{{ total_entropy }}</div>
<small class="text-muted">bits total</small>
{% if rsa_entropy %}
<div class="col">
<div class="p-2 bg-dark rounded">
<div class="small text-muted">RSA</div>
<div class="fs-5 text-primary">{{ rsa_entropy }} bits</div>
</div>
</div>
{% endif %}
<div class="col">
<div class="p-2 bg-dark rounded">
<div class="small text-muted">Total</div>
<div class="fs-5 text-success">{{ total_entropy }} bits</div>
</div>
</div>
</div>
<small class="d-block mt-2 text-center text-muted">
+ reference photo (~80-256 bits) = <strong>{{ total_entropy + 80 }}+ bits combined</strong>
</small>
<div class="form-text text-center mt-2">
+ reference photo entropy (~80-256 bits)
</div>
</div>
<a href="/generate" class="btn btn-outline-light btn-lg w-100 mt-3">
<i class="bi bi-arrow-repeat me-2"></i>Generate New Credentials
</a>
<div class="d-grid gap-2">
<a href="{{ url_for('generate') }}" class="btn btn-outline-primary">
<i class="bi bi-arrow-repeat me-2"></i>Generate New Credentials
</a>
<a href="{{ url_for('encode_page') }}" class="btn btn-success">
<i class="bi bi-lock me-2"></i>Start Encoding
</a>
</div>
{% endif %}
</div>
</div>
{% if not generated %}
<div class="card mt-4">
<div class="card-body">
<h6 class="text-muted mb-3"><i class="bi bi-info-circle me-2"></i>About Credentials</h6>
<ul class="small text-muted mb-0">
<li class="mb-2">
<strong>Daily phrases</strong> rotate each day of the week for forward secrecy
</li>
<li class="mb-2">
<strong>PIN</strong> is static and adds another factor both parties must know
</li>
<li class="mb-2">
<strong>RSA key</strong> adds asymmetric cryptography for additional security
</li>
<li class="mb-0">
You need <strong>at least one</strong> of PIN or RSA key (or both)
</li>
</ul>
</div>
</div>
{% endif %}
</div>
</div>
<style>
.pin-container {
background: linear-gradient(145deg, #1e1e2e 0%, #2d2d44 100%);
border: 1px solid #ffc107;
border-radius: 16px;
padding: 1.5rem 2rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3), 0 0 40px rgba(255, 193, 7, 0.1);
}
.pin-digits-row {
display: flex;
justify-content: center;
gap: 0.5rem;
}
.pin-digit-box {
display: inline-flex;
align-items: center;
justify-content: center;
width: 3rem;
height: 3.5rem;
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 193, 7, 0.3);
border-radius: 8px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 1.75rem;
font-weight: bold;
color: #ffc107;
text-shadow: 0 0 10px rgba(255, 193, 7, 0.5);
transition: filter 0.3s ease, transform 0.2s ease;
}
.pin-digit-box:hover {
transform: translateY(-2px);
border-color: rgba(255, 193, 7, 0.6);
}
.pin-digits-row.blurred .pin-digit-box {
filter: blur(8px);
user-select: none;
}
.pin-buttons .btn {
min-width: 80px;
}
/* Responsive */
@media (max-width: 576px) {
.pin-container {
padding: 1rem 1.25rem;
}
.pin-digit-box {
width: 2.25rem;
height: 2.75rem;
font-size: 1.25rem;
}
.pin-digits-row {
gap: 0.35rem;
}
}
</style>
{% endblock %}
{% block scripts %}
<script>
{% if not generated %}
const usePinCheckbox = document.getElementById('usePin');
const useRsaCheckbox = document.getElementById('useRsa');
// Words range slider
const wordsRange = document.getElementById('wordsRange');
const wordsValue = document.getElementById('wordsValue');
if (wordsRange) {
wordsRange.addEventListener('input', function() {
const bits = this.value * 11;
wordsValue.textContent = `${this.value} words (~${bits} bits)`;
});
}
// Toggle PIN/RSA options
const usePinCheck = document.getElementById('usePinCheck');
const useRsaCheck = document.getElementById('useRsaCheck');
const pinOptions = document.getElementById('pinOptions');
const rsaOptions = document.getElementById('rsaOptions');
const noFactorWarning = document.getElementById('noFactorWarning');
const generateBtn = document.getElementById('generateBtn');
// Toggle option visibility
usePinCheckbox.addEventListener('change', function() {
pinOptions.classList.toggle('d-none', !this.checked);
validateFactors();
updateEntropy();
});
useRsaCheckbox.addEventListener('change', function() {
rsaOptions.classList.toggle('d-none', !this.checked);
validateFactors();
updateEntropy();
});
function validateFactors() {
const hasPin = usePinCheckbox.checked;
const hasRsa = useRsaCheckbox.checked;
const valid = hasPin || hasRsa;
noFactorWarning.classList.toggle('d-none', valid);
generateBtn.disabled = !valid;
if (usePinCheck) {
usePinCheck.addEventListener('change', function() {
pinOptions.classList.toggle('d-none', !this.checked);
});
}
function updateEntropy() {
const words = parseInt(document.getElementById('wordsSelect').value);
const usePin = usePinCheckbox.checked;
const useRsa = useRsaCheckbox.checked;
const pinLen = parseInt(document.getElementById('pinSelect').value);
const phraseEntropy = words * 11;
const pinEntropy = usePin ? Math.floor(pinLen * 3.32) : 0;
const rsaEntropy = useRsa ? 128 : 0;
const total = phraseEntropy + pinEntropy + rsaEntropy;
document.getElementById('entropyDisplay').textContent = '~' + total + ' bits';
// Update progress bar
const pct = Math.min(100, Math.max(10, (total - 30) * 0.5));
document.getElementById('entropyBar').style.width = pct + '%';
// Update description
let desc;
if (total < 50) desc = 'Basic security';
else if (total < 80) desc = 'Good for most use cases';
else if (total < 120) desc = 'Strong security';
else if (total < 180) desc = 'Very strong security';
else desc = 'Maximum security';
document.getElementById('entropyDesc').textContent = desc;
if (useRsaCheck) {
useRsaCheck.addEventListener('change', function() {
rsaOptions.classList.toggle('d-none', !this.checked);
});
}
document.getElementById('wordsSelect').addEventListener('change', updateEntropy);
document.getElementById('pinSelect').addEventListener('change', updateEntropy);
document.getElementById('rsaSelect').addEventListener('change', updateEntropy);
// Form submit
document.getElementById('generateForm').addEventListener('submit', function(e) {
if (!usePinCheckbox.checked && !useRsaCheckbox.checked) {
e.preventDefault();
noFactorWarning.classList.remove('d-none');
return;
// PIN visibility toggle
let pinHidden = false;
function togglePinVisibility() {
const pinDigits = document.getElementById('pinDigits');
const icon = document.getElementById('pinToggleIcon');
const text = document.getElementById('pinToggleText');
pinHidden = !pinHidden;
if (pinHidden) {
pinDigits.classList.add('blurred');
icon.className = 'bi bi-eye';
text.textContent = 'Show';
} else {
pinDigits.classList.remove('blurred');
icon.className = 'bi bi-eye-slash';
text.textContent = 'Hide';
}
}
// Copy PIN
function copyPin() {
const pin = '{{ pin|default("", true) }}';
const icon = document.getElementById('pinCopyIcon');
const text = document.getElementById('pinCopyText');
generateBtn.disabled = true;
generateBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Generating...';
});
// Initial state
validateFactors();
updateEntropy();
{% else %}
// Copy RSA key to clipboard
document.getElementById('copyKeyBtn')?.addEventListener('click', function() {
const keyText = document.getElementById('rsaKeyText');
navigator.clipboard.writeText(keyText.value).then(() => {
this.innerHTML = '<i class="bi bi-check me-1"></i> Copied!';
navigator.clipboard.writeText(pin).then(() => {
icon.className = 'bi bi-check';
text.textContent = 'Copied!';
setTimeout(() => {
this.innerHTML = '<i class="bi bi-clipboard me-1"></i> Copy to Clipboard';
icon.className = 'bi bi-clipboard';
text.textContent = 'Copy';
}, 2000);
});
});
}
{% endif %}
// Toggle all phrases visibility
let phrasesHidden = false;
function toggleAllPhrases() {
phrasesHidden = !phrasesHidden;
document.querySelectorAll('.phrase-display').forEach(el => {
el.style.filter = phrasesHidden ? 'blur(8px)' : 'none';
});
}
// Print QR code
function printQrCode() {
const qrImg = document.getElementById('qrCodeImage');
if (!qrImg) return;
const printWindow = window.open('', '_blank');
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>Stegasoo RSA Key QR Code</title>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
font-family: sans-serif;
}
img { max-width: 400px; }
.warning {
margin-top: 20px;
padding: 10px;
border: 2px solid #ff9800;
background: #fff3e0;
max-width: 400px;
text-align: center;
font-size: 12px;
}
</style>
</head>
<body>
<h2>Stegasoo RSA Private Key</h2>
<img src="${qrImg.src}" alt="RSA Key QR Code">
<div class="warning">
<strong>⚠️ SECURITY WARNING</strong><br>
This QR code contains your unencrypted RSA private key.<br>
Store securely and destroy after use.
</div>
<script>window.onload = function() { window.print(); }<\/script>
</body>
</html>
`);
printWindow.document.close();
}
</script>
{% endblock %}

View File

@@ -1,16 +1,36 @@
# Core dependencies
pillow>=10.0.0
# Stegasoo Requirements
# ====================
# Core Dependencies
cryptography>=41.0.0
argon2-cffi>=23.0.0
Pillow>=10.0.0
# CLI (optional)
click>=8.0.0
# Key Derivation (recommended for stronger security)
argon2-cffi>=23.1.0
# Web UI (optional)
flask>=3.0.0
gunicorn>=21.0.0
# QR Code Generation & Reading
qrcode[pil]>=7.4.0
pyzbar>=0.1.9
# REST API (optional)
# fastapi>=0.100.0
# uvicorn[standard]>=0.20.0
# python-multipart>=0.0.6
# Web Frontend (Flask)
Flask>=3.0.0
# API Frontend (FastAPI)
fastapi>=0.109.0
uvicorn[standard]>=0.27.0
python-multipart>=0.0.6
# CLI Frontend
click>=8.1.0
# Development & Testing
pytest>=7.4.0
pytest-cov>=4.1.0
# Optional: Better performance for Pillow
# pillow-simd>=9.0.0 # Uncomment if available for your platform
# Note: pyzbar requires system library:
# Ubuntu/Debian: sudo apt-get install libzbar0
# macOS: brew install zbar
# Windows: Included in pyzbar wheel

View File

@@ -149,6 +149,29 @@ from .utils import (
format_file_size,
)
# QR Code utilities (optional, depends on qrcode and pyzbar)
try:
from .qr_utils import (
generate_qr_code,
read_qr_code,
read_qr_code_from_file,
extract_key_from_qr,
extract_key_from_qr_file,
compress_data,
decompress_data,
auto_decompress,
normalize_pem,
is_compressed,
can_fit_in_qr,
needs_compression,
has_qr_read,
has_qr_write,
has_qr_support,
)
HAS_QR_UTILS = True
except ImportError:
HAS_QR_UTILS = False
from datetime import date
from pathlib import Path
from typing import Optional, Union

336
src/stegasoo/qr_utils.py Normal file
View File

@@ -0,0 +1,336 @@
"""
Stegasoo QR Code Utilities
Functions for generating and reading QR codes containing RSA keys.
Supports automatic compression for large keys.
"""
import io
import zlib
import base64
from typing import Optional, Tuple
from PIL import Image
# QR code generation
try:
import qrcode
from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_M
HAS_QRCODE_WRITE = True
except ImportError:
HAS_QRCODE_WRITE = False
# QR code reading
try:
from pyzbar.pyzbar import decode as pyzbar_decode
from pyzbar.pyzbar import ZBarSymbol
HAS_QRCODE_READ = True
except ImportError:
HAS_QRCODE_READ = False
# Constants
COMPRESSION_PREFIX = "STEGASOO-Z:"
QR_MAX_BINARY = 2900 # Safe limit for binary data in QR
def compress_data(data: str) -> str:
"""
Compress string data for QR code storage.
Args:
data: String to compress
Returns:
Compressed string with STEGASOO-Z: prefix
"""
compressed = zlib.compress(data.encode('utf-8'), level=9)
encoded = base64.b64encode(compressed).decode('ascii')
return COMPRESSION_PREFIX + encoded
def decompress_data(data: str) -> str:
"""
Decompress data from QR code.
Args:
data: Compressed string with STEGASOO-Z: prefix
Returns:
Original uncompressed string
Raises:
ValueError: If data is not valid compressed format
"""
if not data.startswith(COMPRESSION_PREFIX):
raise ValueError("Data is not in compressed format")
encoded = data[len(COMPRESSION_PREFIX):]
compressed = base64.b64decode(encoded)
return zlib.decompress(compressed).decode('utf-8')
def normalize_pem(pem_data: str) -> str:
"""
Normalize PEM data to ensure proper formatting.
Fixes common issues:
- Inconsistent line endings
- Missing newlines after header/before footer
- Extra whitespace
Args:
pem_data: Raw PEM string
Returns:
Properly formatted PEM string
"""
import re
# Normalize line endings
pem_data = pem_data.replace('\r\n', '\n').replace('\r', '\n')
# Remove any leading/trailing whitespace
pem_data = pem_data.strip()
# Extract header, content, and footer using regex
# Match patterns like -----BEGIN RSA PRIVATE KEY----- or -----BEGIN PRIVATE KEY-----
pattern = r'(-----BEGIN [^-]+-----)(.*?)(-----END [^-]+-----)'
match = re.search(pattern, pem_data, re.DOTALL)
if not match:
return pem_data # Return as-is if not recognized
header = match.group(1)
content = match.group(2)
footer = match.group(3)
# Clean up the base64 content
# Remove all whitespace and rejoin with proper 64-char lines
content_clean = ''.join(content.split())
# Split into 64-character lines (PEM standard)
lines = [content_clean[i:i+64] for i in range(0, len(content_clean), 64)]
# Reconstruct PEM
return header + '\n' + '\n'.join(lines) + '\n' + footer + '\n'
def is_compressed(data: str) -> bool:
"""Check if data has compression prefix."""
return data.startswith(COMPRESSION_PREFIX)
def auto_decompress(data: str) -> str:
"""
Automatically decompress data if compressed, otherwise return as-is.
Args:
data: Possibly compressed string
Returns:
Decompressed string
"""
if is_compressed(data):
return decompress_data(data)
return data
def get_compressed_size(data: str) -> int:
"""Get size of data after compression (including prefix)."""
return len(compress_data(data))
def can_fit_in_qr(data: str, compress: bool = False) -> bool:
"""
Check if data can fit in a QR code.
Args:
data: String data
compress: Whether compression will be used
Returns:
True if data fits
"""
if compress:
size = get_compressed_size(data)
else:
size = len(data.encode('utf-8'))
return size <= QR_MAX_BINARY
def needs_compression(data: str) -> bool:
"""Check if data needs compression to fit in QR code."""
return not can_fit_in_qr(data, compress=False) and can_fit_in_qr(data, compress=True)
def generate_qr_code(
data: str,
compress: bool = False,
error_correction=None
) -> bytes:
"""
Generate a QR code PNG from string data.
Args:
data: String data to encode
compress: Whether to compress data first
error_correction: QR error correction level (default: auto)
Returns:
PNG image bytes
Raises:
RuntimeError: If qrcode library not available
ValueError: If data too large for QR code
"""
if not HAS_QRCODE_WRITE:
raise RuntimeError("qrcode library not installed. Run: pip install qrcode[pil]")
qr_data = data
# Compress if requested
if compress:
qr_data = compress_data(data)
# Check size
if len(qr_data.encode('utf-8')) > QR_MAX_BINARY:
raise ValueError(
f"Data too large for QR code ({len(qr_data)} bytes). "
f"Maximum: {QR_MAX_BINARY} bytes"
)
# Use lower error correction for larger data
if error_correction is None:
error_correction = ERROR_CORRECT_L if len(qr_data) > 1000 else ERROR_CORRECT_M
qr = qrcode.QRCode(
version=None,
error_correction=error_correction,
box_size=10,
border=4,
)
qr.add_data(qr_data)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
buf = io.BytesIO()
img.save(buf, format='PNG')
buf.seek(0)
return buf.getvalue()
def read_qr_code(image_data: bytes) -> Optional[str]:
"""
Read QR code from image data.
Args:
image_data: Image bytes (PNG, JPG, etc.)
Returns:
Decoded string, or None if no QR code found
Raises:
RuntimeError: If pyzbar library not available
"""
if not HAS_QRCODE_READ:
raise RuntimeError(
"pyzbar library not installed. Run: pip install pyzbar\n"
"Also requires system library: sudo apt-get install libzbar0"
)
try:
img = Image.open(io.BytesIO(image_data))
# Convert to RGB if necessary (pyzbar works best with RGB/grayscale)
if img.mode not in ('RGB', 'L'):
img = img.convert('RGB')
# Decode QR codes
decoded = pyzbar_decode(img, symbols=[ZBarSymbol.QRCODE])
if not decoded:
return None
# Return first QR code found
return decoded[0].data.decode('utf-8')
except Exception:
return None
def read_qr_code_from_file(filepath: str) -> Optional[str]:
"""
Read QR code from image file.
Args:
filepath: Path to image file
Returns:
Decoded string, or None if no QR code found
"""
with open(filepath, 'rb') as f:
return read_qr_code(f.read())
def extract_key_from_qr(image_data: bytes) -> Optional[str]:
"""
Extract RSA key from QR code image, auto-decompressing if needed.
Args:
image_data: Image bytes containing QR code
Returns:
PEM-encoded RSA key string, or None if not found/invalid
"""
qr_data = read_qr_code(image_data)
if not qr_data:
return None
# Auto-decompress if needed
try:
if is_compressed(qr_data):
key_pem = decompress_data(qr_data)
else:
key_pem = qr_data
except Exception:
key_pem = qr_data
# Validate it looks like a PEM key
if '-----BEGIN' in key_pem and '-----END' in key_pem:
# Normalize PEM format to fix potential line ending issues
key_pem = normalize_pem(key_pem)
return key_pem
return None
def extract_key_from_qr_file(filepath: str) -> Optional[str]:
"""
Extract RSA key from QR code image file.
Args:
filepath: Path to image file containing QR code
Returns:
PEM-encoded RSA key string, or None if not found/invalid
"""
with open(filepath, 'rb') as f:
return extract_key_from_qr(f.read())
def has_qr_write() -> bool:
"""Check if QR code writing is available."""
return HAS_QRCODE_WRITE
def has_qr_read() -> bool:
"""Check if QR code reading is available."""
return HAS_QRCODE_READ
def has_qr_support() -> bool:
"""Check if full QR code support is available."""
return HAS_QRCODE_WRITE and HAS_QRCODE_READ

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB