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 # Distribution
*.manifest *.manifest
*.spec *.spec
# Output test files.
*.png

View File

@@ -37,6 +37,17 @@ from stegasoo.constants import (
VALID_RSA_SIZES, 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 # FASTAPI APP
@@ -132,10 +143,17 @@ class ImageInfoResponse(BaseModel):
class StatusResponse(BaseModel): class StatusResponse(BaseModel):
version: str version: str
has_argon2: bool has_argon2: bool
has_qrcode_read: bool
day_names: list[str] day_names: list[str]
max_payload_kb: int max_payload_kb: int
class QrExtractResponse(BaseModel):
success: bool
key_pem: Optional[str] = None
error: Optional[str] = None
class ErrorResponse(BaseModel): class ErrorResponse(BaseModel):
error: str error: str
detail: Optional[str] = None detail: Optional[str] = None
@@ -151,11 +169,43 @@ async def root():
return StatusResponse( return StatusResponse(
version=__version__, version=__version__,
has_argon2=has_argon2(), has_argon2=has_argon2(),
has_qrcode_read=HAS_QR_READ,
day_names=list(DAY_NAMES), day_names=list(DAY_NAMES),
max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024 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) @app.post("/generate", response_model=GenerateResponse)
async def api_generate(request: GenerateRequest): async def api_generate(request: GenerateRequest):
""" """
@@ -335,6 +385,7 @@ async def api_encode_multipart(
payload_file: Optional[UploadFile] = File(None), payload_file: Optional[UploadFile] = File(None),
pin: str = Form(""), pin: str = Form(""),
rsa_key: Optional[UploadFile] = File(None), rsa_key: Optional[UploadFile] = File(None),
rsa_key_qr: Optional[UploadFile] = File(None),
rsa_password: str = Form(""), rsa_password: str = Form(""),
date_str: str = Form("") date_str: str = Form("")
): ):
@@ -342,12 +393,34 @@ async def api_encode_multipart(
Encode using multipart form data (file uploads). Encode using multipart form data (file uploads).
Provide either 'message' (text) or 'payload_file' (binary file). 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. Returns the stego image directly as PNG with metadata headers.
""" """
try: try:
ref_data = await reference_photo.read() ref_data = await reference_photo.read()
carrier_data = await carrier.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 # Determine payload
if payload_file and payload_file.filename: if payload_file and payload_file.filename:
@@ -369,7 +442,7 @@ async def api_encode_multipart(
day_phrase=day_phrase, day_phrase=day_phrase,
pin=pin, pin=pin,
rsa_key_data=rsa_key_data, 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 date_str=date_str if date_str else None
) )
@@ -401,17 +474,40 @@ async def api_decode_multipart(
stego_image: UploadFile = File(...), stego_image: UploadFile = File(...),
pin: str = Form(""), pin: str = Form(""),
rsa_key: Optional[UploadFile] = File(None), rsa_key: Optional[UploadFile] = File(None),
rsa_key_qr: Optional[UploadFile] = File(None),
rsa_password: str = Form("") rsa_password: str = Form("")
): ):
""" """
Decode using multipart form data (file uploads). 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. Returns JSON with payload_type indicating text or file.
""" """
try: try:
ref_data = await reference_photo.read() ref_data = await reference_photo.read()
stego_data = await stego_image.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( result = decode(
stego_image=stego_data, stego_image=stego_data,
@@ -419,7 +515,7 @@ async def api_decode_multipart(
day_phrase=day_phrase, day_phrase=day_phrase,
pin=pin, pin=pin,
rsa_key_data=rsa_key_data, rsa_key_data=rsa_key_data,
rsa_password=rsa_password if rsa_password else None rsa_password=effective_password
) )
if result.is_file: if result.is_file:

View File

@@ -30,6 +30,20 @@ from stegasoo import (
FilePayload, 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 # 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('--embed-file', '-e', type=click.Path(exists=True), help='Embed a file (binary)')
@click.option('--phrase', '-p', required=True, help='Day phrase') @click.option('--phrase', '-p', required=True, help='Day phrase')
@click.option('--pin', help='Static PIN') @click.option('--pin', help='Static PIN')
@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file') @click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file (.pem)')
@click.option('--key-password', help='RSA key password') @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('--output', '-o', type=click.Path(), help='Output file (default: auto-generated)')
@click.option('--date', 'date_str', help='Date override (YYYY-MM-DD)') @click.option('--date', 'date_str', help='Date override (YYYY-MM-DD)')
@click.option('--quiet', '-q', is_flag=True, help='Suppress output except errors') @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. Encode a secret message or file into an image.
Requires a reference photo, carrier image, and day phrase. 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 text messages, use -m or -f or pipe via stdin.
For binary files, use -e/--embed-file. For binary files, use -e/--embed-file.
RSA key can be provided as a .pem file (--key) or QR code image (--key-qr).
\b \b
Examples: Examples:
# Text message # Text message with PIN
stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder" --pin 123456 -m "secret" stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder" --pin 123456 -m "secret"
# Text from file # With RSA key file
stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -f message.txt 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 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 # Determine what to encode
payload = None payload = None
@@ -223,14 +239,35 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
else: else:
raise click.UsageError("Must provide message via -m, -f, -e, or stdin") 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_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: if key:
rsa_key_data = Path(key).read_bytes() 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 # Validate security factors
if not pin and not rsa_key_data: 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: try:
ref_photo = Path(ref).read_bytes() 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, day_phrase=phrase,
pin=pin or "", pin=pin or "",
rsa_key_data=rsa_key_data, rsa_key_data=rsa_key_data,
rsa_password=key_password, rsa_password=effective_key_password,
date_str=date_str, 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('--stego', '-s', required=True, type=click.Path(exists=True), help='Stego image')
@click.option('--phrase', '-p', required=True, help='Day phrase') @click.option('--phrase', '-p', required=True, help='Day phrase')
@click.option('--pin', help='Static PIN') @click.option('--pin', help='Static PIN')
@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file') @click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file (.pem)')
@click.option('--key-password', help='RSA key password') @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('--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('--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') @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. Decode a secret message or file from a stego image.
Must use the same credentials that were used for encoding. Must use the same credentials that were used for encoding.
Automatically detects whether content is text or a file. 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 \b
Examples: Examples:
# Decode and print text # Decode with PIN
stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder" --pin 123456 stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder" --pin 123456
# Decode and save (auto-detect type) # Decode with RSA key file
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 -o output.txt stegasoo decode -r photo.jpg -s stego.png -p "words" -k mykey.pem
# Quiet mode for piping text # Decode with RSA key from QR code image
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 -q | less 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_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: if key:
rsa_key_data = Path(key).read_bytes() 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 # Validate security factors
if not pin and not rsa_key_data: 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: try:
ref_photo = Path(ref).read_bytes() 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, day_phrase=phrase,
pin=pin or "", pin=pin or "",
rsa_key_data=rsa_key_data, rsa_key_data=rsa_key_data,
rsa_password=key_password, rsa_password=effective_key_password,
) )
if result.is_file: if result.is_file:

View File

@@ -41,6 +41,33 @@ from stegasoo.constants import (
VALID_RSA_SIZES, MAX_FILE_SIZE, 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 # FLASK APP CONFIGURATION
@@ -81,6 +108,7 @@ def format_size(size_bytes: int) -> str:
return f"{size_bytes / (1024 * 1024):.1f} MB" return f"{size_bytes / (1024 * 1024):.1f} MB"
# ============================================================================ # ============================================================================
# ROUTES # ROUTES
# ============================================================================ # ============================================================================
@@ -99,7 +127,7 @@ def generate():
if not use_pin and not use_rsa: if not use_pin and not use_rsa:
flash('You must select at least one security factor (PIN or RSA Key)', 'error') 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)) pin_length = int(request.form.get('pin_length', 6))
rsa_bits = int(request.form.get('rsa_bits', 2048)) rsa_bits = int(request.form.get('rsa_bits', 2048))
@@ -119,6 +147,31 @@ def generate():
words_per_phrase=words_per_phrase 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', return render_template('generate.html',
phrases=creds.phrases, phrases=creds.phrases,
pin=creds.pin, pin=creds.pin,
@@ -134,19 +187,118 @@ def generate():
pin_entropy=creds.pin_entropy, pin_entropy=creds.pin_entropy,
rsa_entropy=creds.rsa_entropy, rsa_entropy=creds.rsa_entropy,
total_entropy=creds.total_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: except Exception as e:
flash(f'Error generating credentials: {e}', 'error') 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']) @app.route('/generate/download-key', methods=['POST'])
def download_key(): def download_key():
"""Download RSA key as password-protected PEM file.""" """Download RSA key as password-protected PEM file."""
key_pem = request.form.get('key_pem', '') 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', '') password = request.form.get('key_password', '')
if not key_pem: if not key_pem:
@@ -190,11 +342,11 @@ def encode_page():
if not ref_photo or not carrier: if not ref_photo or not carrier:
flash('Both reference photo and carrier image are required', 'error') 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): if not allowed_image(ref_photo.filename) or not allowed_image(carrier.filename):
flash('Invalid file type. Use PNG, JPG, or BMP', 'error') 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 # Get form data
message = request.form.get('message', '') message = request.form.get('message', '')
@@ -211,7 +363,7 @@ def encode_page():
result = validate_file_payload(file_data, payload_file.filename) result = validate_file_payload(file_data, payload_file.filename)
if not result.is_valid: if not result.is_valid:
flash(result.error_message, 'error') 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) mime_type, _ = mimetypes.guess_type(payload_file.filename)
payload = FilePayload( payload = FilePayload(
@@ -224,43 +376,64 @@ def encode_page():
result = validate_message(message) result = validate_message(message)
if not result.is_valid: if not result.is_valid:
flash(result.error_message, 'error') 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 payload = message
if not day_phrase: if not day_phrase:
flash('Day phrase is required', 'error') 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 # Read files
ref_data = ref_photo.read() ref_data = ref_photo.read()
carrier_data = carrier.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 # Validate security factors
result = validate_security_factors(pin, rsa_key_data) result = validate_security_factors(pin, rsa_key_data)
if not result.is_valid: if not result.is_valid:
flash(result.error_message, 'error') 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 # Validate PIN if provided
if pin: if pin:
result = validate_pin(pin) result = validate_pin(pin)
if not result.is_valid: if not result.is_valid:
flash(result.error_message, 'error') 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 # Validate RSA key if provided
if rsa_key_data: 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: if not result.is_valid:
flash(result.error_message, 'error') 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 # Validate carrier image
result = validate_image(carrier_data, "Carrier image") result = validate_image(carrier_data, "Carrier image")
if not result.is_valid: if not result.is_valid:
flash(result.error_message, 'error') 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 # Get date
client_date = request.form.get('client_date', '').strip() client_date = request.form.get('client_date', '').strip()
@@ -277,7 +450,7 @@ def encode_page():
day_phrase=day_phrase, day_phrase=day_phrase,
pin=pin, pin=pin,
rsa_key_data=rsa_key_data, rsa_key_data=rsa_key_data,
rsa_password=rsa_password if rsa_password else None, rsa_password=key_password,
date_str=date_str date_str=date_str
) )
@@ -294,15 +467,15 @@ def encode_page():
except CapacityError as e: except CapacityError as e:
flash(str(e), 'error') 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: except StegasooError as e:
flash(str(e), 'error') 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: except Exception as e:
flash(f'Error: {e}', 'error') 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>') @app.route('/encode/result/<file_id>')
@@ -366,7 +539,7 @@ def decode_page():
if not ref_photo or not stego_image: if not ref_photo or not stego_image:
flash('Both reference photo and stego image are required', 'error') 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 # Get form data
day_phrase = request.form.get('day_phrase', '') day_phrase = request.form.get('day_phrase', '')
@@ -375,32 +548,53 @@ def decode_page():
if not day_phrase: if not day_phrase:
flash('Day phrase is required', 'error') flash('Day phrase is required', 'error')
return render_template('decode.html') return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Read files # Read files
ref_data = ref_photo.read() ref_data = ref_photo.read()
stego_data = stego_image.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 # Validate security factors
result = validate_security_factors(pin, rsa_key_data) result = validate_security_factors(pin, rsa_key_data)
if not result.is_valid: if not result.is_valid:
flash(result.error_message, 'error') 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 # Validate PIN if provided
if pin: if pin:
result = validate_pin(pin) result = validate_pin(pin)
if not result.is_valid: if not result.is_valid:
flash(result.error_message, 'error') 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 # Validate RSA key if provided
if rsa_key_data: 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: if not result.is_valid:
flash(result.error_message, 'error') flash(result.error_message, 'error')
return render_template('decode.html') return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Decode # Decode
decode_result = decode( decode_result = decode(
@@ -409,7 +603,7 @@ def decode_page():
day_phrase=day_phrase, day_phrase=day_phrase,
pin=pin, pin=pin,
rsa_key_data=rsa_key_data, rsa_key_data=rsa_key_data,
rsa_password=rsa_password if rsa_password else None rsa_password=key_password
) )
if decode_result.is_file: if decode_result.is_file:
@@ -438,15 +632,15 @@ def decode_page():
except DecryptionError: except DecryptionError:
flash('Decryption failed. Check your phrase, PIN, RSA key, and reference photo.', 'error') 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: except StegasooError as e:
flash(str(e), 'error') flash(str(e), 'error')
return render_template('decode.html') return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
except Exception as e: except Exception as e:
flash(f'Error: {e}', 'error') 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>') @app.route('/decode/download/<file_id>')
@@ -471,6 +665,7 @@ def decode_download(file_id):
def about(): def about():
return render_template('about.html', return render_template('about.html',
has_argon2=has_argon2(), has_argon2=has_argon2(),
has_qrcode_read=HAS_QRCODE_READ,
max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024 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> <h5 class="mb-0"><i class="bi bi-info-circle me-2"></i>About Stegasoo</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<p> <p class="lead">
Stegasoo is a hybrid steganography system that hides encrypted messages inside Stegasoo is a secure steganography tool that hides encrypted messages and files
ordinary images. It combines multiple security layers to create a system that is inside ordinary images using multi-factor authentication.
both highly secure and practical to use.
</p> </p>
<h6 class="mt-4 mb-3">System Status</h6> <h6 class="text-primary mt-4 mb-3"><i class="bi bi-stars me-2"></i>Key Features</h6>
<div class="row g-3"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="d-flex align-items-center p-3 rounded status-box"> <ul class="list-unstyled">
{% if has_argon2 %} <li class="mb-2">
<i class="bi bi-check-circle-fill text-success fs-4 me-3"></i> <i class="bi bi-check-circle text-success me-2"></i>
<div> <strong>Text &amp; File Embedding</strong> — Hide messages or any file type (PDF, ZIP, documents)
<strong>Argon2id Available</strong> </li>
<div class="small text-muted">Memory-hard key derivation (256MB)</div> <li class="mb-2">
</div> <i class="bi bi-check-circle text-success me-2"></i>
{% else %} <strong>Multi-Factor Security</strong> — Combines photo + phrase + PIN/RSA key
<i class="bi bi-exclamation-triangle-fill text-warning fs-4 me-3"></i> </li>
<div> <li class="mb-2">
<strong>Using PBKDF2 Fallback</strong> <i class="bi bi-check-circle text-success me-2"></i>
<div class="small text-muted">Install argon2-cffi for better security</div> <strong>AES-256-GCM Encryption</strong> — Military-grade authenticated encryption
</div> </li>
{% endif %} <li class="mb-2">
</div> <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>
<div class="col-md-6"> <div class="col-md-6">
<div class="d-flex align-items-center p-3 rounded status-box"> <ul class="list-unstyled">
<i class="bi bi-shield-fill-check text-success fs-4 me-3"></i> <li class="mb-2">
<div> <i class="bi bi-check-circle text-success me-2"></i>
<strong>AES-256-GCM</strong> <strong>Random Pixel Embedding</strong> — Defeats statistical steganalysis
<div class="small text-muted">Authenticated encryption enabled</div> </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> </div>
</div> </div>
@@ -50,129 +252,100 @@
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header"> <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>
<div class="card-body"> <div class="card-body">
<div class="table-responsive"> <table class="table table-dark table-striped">
<table class="table table-dark"> <tbody>
<thead> <tr>
<tr> <td><i class="bi bi-file-text me-2"></i>Max text message</td>
<th>Component</th> <td><strong>250,000 characters</strong> (~250 KB)</td>
<th>Entropy</th> </tr>
<th>Purpose</th> <tr>
</tr> <td><i class="bi bi-file-earmark me-2"></i>Max file payload</td>
</thead> <td><strong>{{ max_payload_kb }} KB</strong></td>
<tbody> </tr>
<tr> <tr>
<td><i class="bi bi-image text-info me-2"></i>Reference Photo</td> <td><i class="bi bi-image me-2"></i>Max carrier image</td>
<td>~80-256 bits</td> <td><strong>16 megapixels</strong> (~4000×4000)</td>
<td>Something you have (plausible deniability)</td> </tr>
</tr> <tr>
<tr> <td><i class="bi bi-upload me-2"></i>Max upload size</td>
<td><i class="bi bi-chat-quote text-info me-2"></i>3-Word Phrase</td> <td><strong>10 MB</strong></td>
<td>~33 bits</td> </tr>
<td>Something you know (changes daily)</td> <tr>
</tr> <td><i class="bi bi-clock me-2"></i>Temp file expiry</td>
<tr> <td><strong>5 minutes</strong></td>
<td><i class="bi bi-123 text-info me-2"></i>6-Digit PIN</td> </tr>
<td>~20 bits</td> <tr>
<td>Something you know (static)</td> <td><i class="bi bi-key me-2"></i>PIN length</td>
</tr> <td><strong>6-9 digits</strong></td>
<tr> </tr>
<td><i class="bi bi-calendar text-info me-2"></i>Date</td> <tr>
<td>N/A</td> <td><i class="bi bi-shield me-2"></i>RSA key sizes</td>
<td>Automatic key rotation</td> <td><strong>2048, 3072, 4096 bits</strong></td>
</tr> </tr>
<tr class="table-active"> <tr>
<td><strong>Combined</strong></td> <td><i class="bi bi-chat-quote me-2"></i>Phrase length</td>
<td><strong>133+ bits</strong></td> <td><strong>3-12 words</strong> (BIP-39 wordlist)</td>
<td><strong>Beyond brute force</strong></td> </tr>
</tr> </tbody>
</tbody> </table>
</table>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-cpu me-2"></i>Attack Resistance</h5>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-6">
<h6 class="text-danger"><i class="bi bi-x-circle me-2"></i>What Attackers Can't Do</h6>
<ul class="list-unstyled">
<li class="mb-2">
<i class="bi bi-shield-x text-muted me-2"></i>
Brute force the passphrase (2<sup>133</sup> combinations)
</li>
<li class="mb-2">
<i class="bi bi-shield-x text-muted me-2"></i>
Use rainbow tables (random salt per message)
</li>
<li class="mb-2">
<i class="bi bi-shield-x text-muted me-2"></i>
Detect hidden data (random pixel selection)
</li>
<li class="mb-2">
<i class="bi bi-shield-x text-muted me-2"></i>
Use GPU farms (Argon2 requires 256MB RAM per attempt)
</li>
</ul>
</div>
<div class="col-md-6">
<h6 class="text-warning"><i class="bi bi-exclamation-triangle me-2"></i>Real Threats</h6>
<ul class="list-unstyled">
<li class="mb-2">
<i class="bi bi-person-x text-muted me-2"></i>
Social engineering (someone tricks you)
</li>
<li class="mb-2">
<i class="bi bi-door-open text-muted me-2"></i>
Physical access to your devices
</li>
<li class="mb-2">
<i class="bi bi-bug text-muted me-2"></i>
Malware/keyloggers on your system
</li>
<li class="mb-2">
<i class="bi bi-camera-video text-muted me-2"></i>
Shoulder surfing while you type
</li>
</ul>
</div>
</div>
</div> </div>
</div> </div>
<div class="card"> <div class="card">
<div class="card-header"> <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>
<div class="card-body"> <div class="card-body">
<div class="row"> <p>Stegasoo is also available as a command-line tool and REST API:</p>
<div class="col-md-6">
<h6 class="text-success"><i class="bi bi-check-lg me-2"></i>Do</h6> <h6 class="mt-3">Command Line</h6>
<ul> <pre class="bg-dark p-3 rounded"><code># Generate credentials
<li>Memorize your phrases and PIN, never write them down</li> stegasoo generate --pin --rsa
<li>Use a reference photo that both parties already have</li>
<li>Use different carrier images for each message</li> # Encode a text message
<li>Share stego images through normal channels (looks innocent)</li> stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder" --pin 123456 -m "secret"
</ul>
</div> # Encode a file
<div class="col-md-6"> stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder" --pin 123456 -e document.pdf
<h6 class="text-danger"><i class="bi bi-x-lg me-2"></i>Don't</h6>
<ul> # Decode (auto-detects text vs file)
<li>Don't transmit the reference photo</li> stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder" --pin 123456</code></pre>
<li>Don't reuse the same carrier image</li>
<li>Don't store phrases or PIN digitally</li> <h6 class="mt-4">REST API</h6>
<li>Don't resize or recompress stego images</li> <pre class="bg-dark p-3 rounded"><code># Encode with multipart upload
</ul> curl -X POST http://localhost:8000/encode/multipart \
</div> -F "reference_photo=@photo.jpg" \
</div> -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> </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>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -63,7 +63,7 @@
<div class="container text-center text-muted"> <div class="container text-center text-muted">
<small> <small>
<img src="{{ url_for('static', filename='favicon.svg') }}" alt="" height="16" class="me-1" style="vertical-align: text-bottom;"> <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> </small>
</div> </div>
</footer> </footer>

View File

@@ -133,10 +133,33 @@
<label class="form-label"> <label class="form-label">
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key <i class="bi bi-file-earmark-lock me-1"></i> RSA Key
</label> </label>
<input type="file" name="rsa_key" class="form-control" id="rsaKeyInput" {% if has_qrcode_read %}
accept=".pem,.key"> <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"> <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> </div>
</div> </div>
@@ -204,13 +227,32 @@ document.getElementById('decodeForm')?.addEventListener('submit', function() {
btn.disabled = true; 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 rsaKeyInput = document.getElementById('rsaKeyInput');
const rsaKeyQrInput = document.getElementById('rsaKeyQrInput');
const rsaPasswordGroup = document.getElementById('rsaPasswordGroup'); const rsaPasswordGroup = document.getElementById('rsaPasswordGroup');
rsaKeyInput?.addEventListener('change', function() { if (rsaKeyInput) {
rsaPasswordGroup.classList.toggle('d-none', !this.files.length); 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 // Day names for date detection
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];

View File

@@ -142,10 +142,33 @@
<label class="form-label"> <label class="form-label">
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key <i class="bi bi-file-earmark-lock me-1"></i> RSA Key
</label> </label>
<input type="file" name="rsa_key" class="form-control" id="rsaKeyInput" {% if has_qrcode_read %}
accept=".pem,.key"> <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"> <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> </div>
</div> </div>
@@ -158,7 +181,7 @@
<input type="password" name="rsa_password" class="form-control" <input type="password" name="rsa_password" class="form-control"
placeholder="Password for the .pem file (if encrypted)"> placeholder="Password for the .pem file (if encrypted)">
<div class="form-text"> <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>
</div> </div>
@@ -270,13 +293,32 @@ function formatFileSize(bytes) {
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; 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 rsaKeyInput = document.getElementById('rsaKeyInput');
const rsaKeyQrInput = document.getElementById('rsaKeyQrInput');
const rsaPasswordGroup = document.getElementById('rsaPasswordGroup'); const rsaPasswordGroup = document.getElementById('rsaPasswordGroup');
rsaKeyInput.addEventListener('change', function() { if (rsaKeyInput) {
rsaPasswordGroup.classList.toggle('d-none', !this.files.length); 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 // Form submit loading state
document.getElementById('encodeForm').addEventListener('submit', function(e) { document.getElementById('encodeForm').addEventListener('submit', function(e) {

View File

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

View File

@@ -3,18 +3,6 @@
{% block title %}Generate Credentials - Stegasoo{% endblock %} {% block title %}Generate Credentials - Stegasoo{% endblock %}
{% block content %} {% 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="row justify-content-center">
<div class="col-lg-8"> <div class="col-lg-8">
<div class="card"> <div class="card">
@@ -23,335 +11,479 @@
</div> </div>
<div class="card-body"> <div class="card-body">
{% if not generated %} {% if not generated %}
<p class="text-muted mb-4"> <!-- Generation Form -->
Generate your weekly phrase card and security factors. You must choose at least one: PIN or RSA Key. <form method="POST">
</p>
<form method="POST" id="generateForm">
<div class="mb-4"> <div class="mb-4">
<label class="form-label">Words per phrase</label> <label class="form-label">Words per Phrase</label>
<select name="words_per_phrase" class="form-select" id="wordsSelect"> <input type="range" class="form-range" name="words_per_phrase"
<option value="3" selected>3 words (~33 bits)</option> min="3" max="12" value="3" id="wordsRange">
<option value="4">4 words (~44 bits)</option> <div class="d-flex justify-content-between small text-muted">
<option value="5">5 words (~55 bits)</option> <span>3 (33 bits)</span>
<option value="6">6 words (~66 bits)</option> <span id="wordsValue" class="text-primary fw-bold">3 words (~33 bits)</span>
<option value="7">7 words (~77 bits)</option> <span>12 (132 bits)</span>
<option value="8">8 words (~88 bits)</option> </div>
<option value="9">9 words (~99 bits)</option>
<option value="10">10 words (~110 bits)</option>
<option value="11">11 words (~121 bits)</option>
<option value="12">12 words (~132 bits)</option>
</select>
<div class="form-text">More words = more security, harder to memorize</div>
</div> </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="row">
<div class="card mb-3" style="background: rgba(0,0,0,0.2);"> <div class="col-md-6 mb-3">
<div class="card-body"> <div class="form-check form-switch">
<div class="form-check mb-3"> <input class="form-check-input" type="checkbox" name="use_pin"
<input class="form-check-input" type="checkbox" name="use_pin" id="usePin" checked> id="usePinCheck" checked>
<label class="form-check-label fw-bold" for="usePin"> <label class="form-check-label" for="usePinCheck">
<i class="bi bi-123 me-1"></i> PIN <i class="bi bi-123 me-1"></i> Generate PIN
</label> </label>
</div> </div>
<div id="pinOptions"> <div class="mt-2" id="pinOptions">
<label class="form-label">PIN length</label> <label class="form-label small">PIN Length</label>
<select name="pin_length" class="form-select" id="pinSelect"> <select name="pin_length" class="form-select form-select-sm">
<option value="6" selected>6 digits (~20 bits)</option> <option value="6" selected>6 digits (~20 bits)</option>
<option value="7">7 digits (~23 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> <option value="9">9 digits (~30 bits)</option>
</select> </select>
<div class="form-text">Memorizable, same PIN used every day</div>
</div> </div>
</div> </div>
</div>
<div class="col-md-6 mb-3">
<!-- RSA Key Option --> <div class="form-check form-switch">
<div class="card mb-3" style="background: rgba(0,0,0,0.2);"> <input class="form-check-input" type="checkbox" name="use_rsa"
<div class="card-body"> id="useRsaCheck">
<div class="form-check mb-3"> <label class="form-check-label" for="useRsaCheck">
<input class="form-check-input" type="checkbox" name="use_rsa" id="useRsa"> <i class="bi bi-file-earmark-lock me-1"></i> Generate RSA Key
<label class="form-check-label fw-bold" for="useRsa">
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
</label> </label>
</div> </div>
<div id="rsaOptions" class="d-none"> <div class="mt-2 d-none" id="rsaOptions">
<label class="form-label">Key size</label> <label class="form-label small">Key Size</label>
<select name="rsa_bits" class="form-select" id="rsaSelect"> <select name="rsa_bits" class="form-select form-select-sm">
<option value="2048" selected>2048-bit (~128 bits effective)</option> <option value="2048" selected>2048 bits (~128 bits entropy)</option>
<option value="3072">3072-bit (~128 bits effective)</option> <option value="3072">3072 bits (~128 bits entropy)</option>
<option value="4096">4096-bit (~128 bits effective)</option> <option value="4096">4096 bits (~128 bits entropy)</option>
</select> </select>
<div class="form-text">File-based key, both parties need the same .pem file</div>
</div> </div>
</div> </div>
</div> </div>
<div class="alert alert-info mb-4"> <button type="submit" class="btn btn-primary btn-lg w-100 mt-3">
<div class="d-flex justify-content-between align-items-center">
<span><i class="bi bi-calculator me-2"></i>Estimated entropy:</span>
<strong id="entropyDisplay">~53 bits</strong>
</div>
<div class="progress mt-2" style="height: 8px;">
<div class="progress-bar bg-success" id="entropyBar" style="width: 40%"></div>
</div>
<small class="text-muted mt-1 d-block">
<span id="entropyDesc">Good for most use cases</span>
• Reference photo adds ~80-256 bits more
</small>
</div>
<div class="alert alert-warning d-none" id="noFactorWarning">
<i class="bi bi-exclamation-triangle me-2"></i>
You must select at least one security factor (PIN or RSA Key)
</div>
<button type="submit" class="btn btn-primary btn-lg w-100" id="generateBtn">
<i class="bi bi-shuffle me-2"></i>Generate Credentials <i class="bi bi-shuffle me-2"></i>Generate Credentials
</button> </button>
</form> </form>
{% else %} {% else %}
<!-- Generated Credentials Display -->
<!-- Generated Results --> <div class="alert alert-warning">
<div class="alert alert-success-bright alert-dismissible fade show">
<i class="bi bi-check-circle me-2"></i>
<strong>Credentials Generated!</strong>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<div class="alert alert-warning alert-dismissible fade show">
<i class="bi bi-exclamation-triangle me-2"></i> <i class="bi bi-exclamation-triangle me-2"></i>
<strong>Memorize phrases, save key securely, then close!</strong> - Do not screenshot <strong>Memorize these credentials!</strong> They will not be shown again.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button> <br><small>Do not screenshot or save to an unencrypted file.</small>
</div> </div>
{% if pin %} {% 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"> <div class="mb-4">
<h6 class="text-muted mb-3"> <h6 class="text-muted"><i class="bi bi-123 me-2"></i>STATIC PIN</h6>
<i class="bi bi-file-earmark-lock me-2"></i>YOUR RSA KEY ({{ rsa_bits }}-bit) <div class="text-center">
</h6> <div class="pin-container d-inline-block">
<div class="pin-digits-row" id="pinDigits">
<div class="alert alert-danger small"> {% for digit in pin %}
<i class="bi bi-shield-exclamation me-1"></i> <span class="pin-digit-box">{{ digit }}</span>
<strong>Save this key securely!</strong> Share it with your recipient through a secure channel. You cannot recover it later. {% endfor %}
</div> </div>
<div class="pin-buttons mt-3">
<!-- Key Display --> <button type="button" class="btn btn-sm btn-outline-secondary me-2" onclick="togglePinVisibility()">
<div class="mb-3"> <i class="bi bi-eye-slash" id="pinToggleIcon"></i>
<textarea class="form-control font-monospace" id="rsaKeyText" rows="6" readonly style="font-size: 0.75rem;">{{ rsa_key_pem }}</textarea> <span id="pinToggleText">Hide</span>
</div> </button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="copyPin()">
<!-- Copy to Clipboard --> <i class="bi bi-clipboard" id="pinCopyIcon"></i>
<button type="button" class="btn btn-outline-light me-2" id="copyKeyBtn"> <span id="pinCopyText">Copy</span>
<i class="bi bi-clipboard me-1"></i> Copy to Clipboard </button>
</button>
<!-- Download with Password -->
<button type="button" class="btn btn-outline-light" data-bs-toggle="collapse" data-bs-target="#downloadKeyForm">
<i class="bi bi-download me-1"></i> Download as .pem
</button>
<div class="collapse mt-3" id="downloadKeyForm">
<div class="card" style="background: rgba(0,0,0,0.2);">
<div class="card-body">
<form method="POST" action="{{ url_for('download_key') }}">
<input type="hidden" name="key_pem" value="{{ rsa_key_pem }}">
<div class="mb-3">
<label class="form-label">Password to protect key file</label>
<input type="password" name="key_password" class="form-control"
placeholder="Minimum 8 characters" minlength="8" required>
<div class="form-text">You'll need this password when using the key</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-file-earmark-lock me-1"></i> Download Protected Key
</button>
</form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
<hr class="my-4"> <div class="mb-4">
<h6 class="text-muted"><i class="bi bi-chat-quote me-2"></i>DAILY PHRASES</h6>
<h6 class="text-muted mb-3">DAILY PHRASES ({{ words_per_phrase }} words each)</h6> <div class="table-responsive">
<table class="table table-dark table-striped mb-0">
<div class="table-responsive"> <tbody>
<table class="table table-dark table-hover"> {% for day in days %}
<thead> <tr>
<tr> <td class="text-muted" style="width: 100px;">{{ day }}</td>
<th style="width: 140px;">Day</th> <td>
<th>Phrase</th> <span class="font-monospace phrase-display" id="phrase{{ loop.index }}">{{ phrases[day] }}</span>
</tr> </td>
</thead> </tr>
<tbody> {% endfor %}
{% for day in days %} </tbody>
<tr> </table>
<td class="text-nowrap"> </div>
<i class="bi bi-calendar3 me-2"></i>{{ day }} <div class="text-end mt-2">
</td> <button class="btn btn-sm btn-outline-secondary" onclick="toggleAllPhrases()">
<td> <i class="bi bi-eye-slash me-1"></i>Toggle Visibility
<span class="phrase-display">{{ phrases[day] }}</span> </button>
</td> </div>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
<div class="alert alert-success mt-4"> {% if rsa_key_pem %}
<h6><i class="bi bi-shield-check me-2"></i>Security Summary</h6> <div class="mb-4">
<div class="row text-center mt-3"> <h6 class="text-muted"><i class="bi bi-file-earmark-lock me-2"></i>RSA PRIVATE KEY ({{ rsa_bits }} bits)</h6>
<div class="col-3">
<div class="fs-4 fw-bold">{{ phrase_entropy }}</div> <ul class="nav nav-tabs" role="tablist">
<small class="text-muted">bits/phrase</small> <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> </div>
{% if pin %}
<div class="col-3"> <!-- Download Tab -->
<div class="fs-4 fw-bold">{{ pin_entropy }}</div> <div class="tab-pane fade" id="keyDownloadTab" role="tabpanel">
<small class="text-muted">bits/PIN</small> <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> </div>
{% endif %} {% endif %}
{% if rsa_key_pem %} </div>
<div class="col-3"> </div>
<div class="fs-4 fw-bold">{{ rsa_entropy }}</div> {% endif %}
<small class="text-muted">bits/RSA</small>
<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> </div>
{% endif %} {% endif %}
<div class="col-3"> {% if rsa_entropy %}
<div class="fs-4 fw-bold text-success">{{ total_entropy }}</div> <div class="col">
<small class="text-muted">bits total</small> <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>
</div> </div>
<small class="d-block mt-2 text-center text-muted"> <div class="form-text text-center mt-2">
+ reference photo (~80-256 bits) = <strong>{{ total_entropy + 80 }}+ bits combined</strong> + reference photo entropy (~80-256 bits)
</small> </div>
</div> </div>
<a href="/generate" class="btn btn-outline-light btn-lg w-100 mt-3"> <div class="d-grid gap-2">
<i class="bi bi-arrow-repeat me-2"></i>Generate New Credentials <a href="{{ url_for('generate') }}" class="btn btn-outline-primary">
</a> <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 %} {% endif %}
</div> </div>
</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>
</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 %} {% endblock %}
{% block scripts %} {% block scripts %}
<script> <script>
{% if not generated %} // Words range slider
const usePinCheckbox = document.getElementById('usePin'); const wordsRange = document.getElementById('wordsRange');
const useRsaCheckbox = document.getElementById('useRsa'); 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 pinOptions = document.getElementById('pinOptions');
const rsaOptions = document.getElementById('rsaOptions'); const rsaOptions = document.getElementById('rsaOptions');
const noFactorWarning = document.getElementById('noFactorWarning');
const generateBtn = document.getElementById('generateBtn');
// Toggle option visibility if (usePinCheck) {
usePinCheckbox.addEventListener('change', function() { usePinCheck.addEventListener('change', function() {
pinOptions.classList.toggle('d-none', !this.checked); pinOptions.classList.toggle('d-none', !this.checked);
validateFactors(); });
updateEntropy();
});
useRsaCheckbox.addEventListener('change', function() {
rsaOptions.classList.toggle('d-none', !this.checked);
validateFactors();
updateEntropy();
});
function validateFactors() {
const hasPin = usePinCheckbox.checked;
const hasRsa = useRsaCheckbox.checked;
const valid = hasPin || hasRsa;
noFactorWarning.classList.toggle('d-none', valid);
generateBtn.disabled = !valid;
} }
function updateEntropy() { if (useRsaCheck) {
const words = parseInt(document.getElementById('wordsSelect').value); useRsaCheck.addEventListener('change', function() {
const usePin = usePinCheckbox.checked; rsaOptions.classList.toggle('d-none', !this.checked);
const useRsa = useRsaCheckbox.checked; });
const pinLen = parseInt(document.getElementById('pinSelect').value);
const phraseEntropy = words * 11;
const pinEntropy = usePin ? Math.floor(pinLen * 3.32) : 0;
const rsaEntropy = useRsa ? 128 : 0;
const total = phraseEntropy + pinEntropy + rsaEntropy;
document.getElementById('entropyDisplay').textContent = '~' + total + ' bits';
// Update progress bar
const pct = Math.min(100, Math.max(10, (total - 30) * 0.5));
document.getElementById('entropyBar').style.width = pct + '%';
// Update description
let desc;
if (total < 50) desc = 'Basic security';
else if (total < 80) desc = 'Good for most use cases';
else if (total < 120) desc = 'Strong security';
else if (total < 180) desc = 'Very strong security';
else desc = 'Maximum security';
document.getElementById('entropyDesc').textContent = desc;
} }
document.getElementById('wordsSelect').addEventListener('change', updateEntropy); // PIN visibility toggle
document.getElementById('pinSelect').addEventListener('change', updateEntropy); let pinHidden = false;
document.getElementById('rsaSelect').addEventListener('change', updateEntropy); function togglePinVisibility() {
const pinDigits = document.getElementById('pinDigits');
// Form submit const icon = document.getElementById('pinToggleIcon');
document.getElementById('generateForm').addEventListener('submit', function(e) { const text = document.getElementById('pinToggleText');
if (!usePinCheckbox.checked && !useRsaCheckbox.checked) {
e.preventDefault(); pinHidden = !pinHidden;
noFactorWarning.classList.remove('d-none');
return; 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; navigator.clipboard.writeText(pin).then(() => {
generateBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Generating...'; icon.className = 'bi bi-check';
}); text.textContent = 'Copied!';
// Initial state
validateFactors();
updateEntropy();
{% else %}
// Copy RSA key to clipboard
document.getElementById('copyKeyBtn')?.addEventListener('click', function() {
const keyText = document.getElementById('rsaKeyText');
navigator.clipboard.writeText(keyText.value).then(() => {
this.innerHTML = '<i class="bi bi-check me-1"></i> Copied!';
setTimeout(() => { setTimeout(() => {
this.innerHTML = '<i class="bi bi-clipboard me-1"></i> Copy to Clipboard'; icon.className = 'bi bi-clipboard';
text.textContent = 'Copy';
}, 2000); }, 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> </script>
{% endblock %} {% endblock %}

View File

@@ -1,16 +1,36 @@
# Core dependencies # Stegasoo Requirements
pillow>=10.0.0 # ====================
# Core Dependencies
cryptography>=41.0.0 cryptography>=41.0.0
argon2-cffi>=23.0.0 Pillow>=10.0.0
# CLI (optional) # Key Derivation (recommended for stronger security)
click>=8.0.0 argon2-cffi>=23.1.0
# Web UI (optional) # QR Code Generation & Reading
flask>=3.0.0 qrcode[pil]>=7.4.0
gunicorn>=21.0.0 pyzbar>=0.1.9
# REST API (optional) # Web Frontend (Flask)
# fastapi>=0.100.0 Flask>=3.0.0
# uvicorn[standard]>=0.20.0
# python-multipart>=0.0.6 # 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, 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 datetime import date
from pathlib import Path from pathlib import Path
from typing import Optional, Union 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