Added password-protect PEM support.

This commit is contained in:
Aaron D. Lee
2025-12-27 20:22:25 -05:00
parent 2abb458c57
commit ee937c832f
5 changed files with 687 additions and 225 deletions

379
app.py
View File

@@ -1,34 +1,31 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Stegasoo: Stegonography portal, for security-mided messagin demo. Stegasoo: Steganography portal for security-minded messaging.
Aaron D. Lee (w/ vibes) Aaron D. Lee (w/ vibes)
2025-12-27 2025-12-27
Right Now: It's a stupid bootstrap looking portal that works.
Future: Socials, Slack, and Matrix server channel watcher function to encode/send/decode
messages in real-time from a configurable memes or photos channel.
Built as a learning experience with a few LLMs to see if I can make something decent. Built as a learning experience with a few LLMs to see if I can make something decent.
""" """
import os import os
import io import io
import re
import secrets import secrets
import hashlib import hashlib
import struct import struct
import time import time
import threading
from datetime import datetime from datetime import datetime
from flask import Flask, render_template, request, send_file, jsonify, flash, redirect, url_for from flask import Flask, render_template, request, send_file, jsonify, flash, redirect, url_for
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from PIL import Image from PIL import Image
from secureDeleter import SecureDeleter from secureDeleter import SecureDeleter
#from story_generator import generate_all_stories, HAS_ML
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.serialization import load_pem_private_key
try: try:
from argon2.low_level import hash_secret_raw, Type from argon2.low_level import hash_secret_raw, Type
@@ -38,6 +35,8 @@ except ImportError:
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import hashes
HAS_ML = False # Story generator disabled
# ============================================================================ # ============================================================================
# FLASK APP CONFIGURATION # FLASK APP CONFIGURATION
# ============================================================================ # ============================================================================
@@ -50,6 +49,10 @@ app.config['UPLOAD_FOLDER'] = '/tmp/stego_uploads'
# Limits # Limits
MAX_IMAGE_PIXELS = 4000000 # 4 megapixels max (e.g., 2000x2000) MAX_IMAGE_PIXELS = 4000000 # 4 megapixels max (e.g., 2000x2000)
MAX_MESSAGE_SIZE = 50000 # 50KB message max MAX_MESSAGE_SIZE = 50000 # 50KB message max
MIN_PIN_LENGTH = 6
MAX_PIN_LENGTH = 9
MIN_RSA_BITS = 2048
VALID_RSA_SIZES = [2048, 3072, 4096]
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
@@ -94,7 +97,6 @@ def secure_cleanup_uploads():
deleter = SecureDeleter(filepath) deleter = SecureDeleter(filepath)
deleter.execute() deleter.execute()
except Exception as e: except Exception as e:
# Fallback to regular delete
os.remove(filepath) os.remove(filepath)
@@ -106,18 +108,139 @@ def cleanup_temp_files():
TEMP_FILES.pop(fid, None) TEMP_FILES.pop(fid, None)
# ============================================================================ # ============================================================================
# HELPER FUNCTIONS # VALIDATION FUNCTIONS
# ============================================================================ # ============================================================================
def allowed_file(filename): def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def validate_pin(pin):
"""Validate PIN format: 6-9 digits, no leading zeros."""
if not pin:
return True, "" # Empty PIN is valid (if RSA key provided)
if not pin.isdigit():
return False, "PIN must contain only digits"
if len(pin) < MIN_PIN_LENGTH or len(pin) > MAX_PIN_LENGTH:
return False, f"PIN must be {MIN_PIN_LENGTH}-{MAX_PIN_LENGTH} digits"
if pin[0] == '0':
return False, "PIN cannot start with zero"
return True, ""
def validate_message(message):
"""Validate message size."""
if not message:
return False, "Message is required"
if len(message) > MAX_MESSAGE_SIZE:
return False, f"Message too long. Max {MAX_MESSAGE_SIZE // 1000}KB allowed"
return True, ""
def validate_image(image_data, name="Image"):
"""Validate image data and dimensions."""
try:
img = Image.open(io.BytesIO(image_data))
width, height = img.size
num_pixels = width * height
if num_pixels > MAX_IMAGE_PIXELS:
max_dim = int(MAX_IMAGE_PIXELS ** 0.5)
return False, f"{name} too large ({width}x{height} = {num_pixels:,} pixels). Max ~{MAX_IMAGE_PIXELS:,} pixels ({max_dim}x{max_dim})"
return True, ""
except Exception as e:
return False, f"Could not read {name}: {str(e)}"
def validate_rsa_key(key_data, password=None):
"""
Validate RSA private key.
Returns (is_valid, error_message, key_size_bits)
"""
if not key_data:
return True, "", 0 # Empty key is valid (if PIN provided)
try:
# Try to load the key
if password:
private_key = load_pem_private_key(key_data, password=password.encode(), backend=default_backend())
else:
# Try without password first
try:
private_key = load_pem_private_key(key_data, password=None, backend=default_backend())
except TypeError:
# Key is encrypted but no password provided
return False, "RSA key is password-protected. Please enter the password.", 0
# Check key size
key_size = private_key.key_size
if key_size < MIN_RSA_BITS:
return False, f"RSA key must be at least {MIN_RSA_BITS} bits (got {key_size})", 0
return True, "", key_size
except ValueError as e:
if "password" in str(e).lower() or "encrypted" in str(e).lower():
return False, "Incorrect password for RSA key", 0
return False, f"Invalid RSA key format: {str(e)}", 0
except Exception as e:
return False, f"Could not load RSA key: {str(e)}", 0
def validate_security_factors(pin, rsa_key_data):
"""Ensure at least one security factor is provided."""
has_pin = bool(pin and pin.strip())
has_key = bool(rsa_key_data and len(rsa_key_data) > 0)
if not has_pin and not has_key:
return False, "You must provide at least a PIN or RSA Key"
return True, ""
# ============================================================================
# RSA KEY GENERATION
# ============================================================================
def generate_rsa_key(bits=2048):
"""Generate RSA private key."""
if bits not in VALID_RSA_SIZES:
bits = 2048
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=bits,
backend=default_backend()
)
return private_key
def export_rsa_key_pem(private_key, password=None):
"""Export RSA key to PEM format, optionally encrypted."""
if password:
encryption = serialization.BestAvailableEncryption(password.encode())
else:
encryption = serialization.NoEncryption()
pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=encryption
)
return pem
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
def generate_pin(length=6): def generate_pin(length=6):
"""Generate a random PIN of specified length (6-8 digits).""" """Generate a random PIN of specified length (6-9 digits)."""
length = max(MIN_PIN_LENGTH, min(MAX_PIN_LENGTH, length))
first_digit = str(secrets.randbelow(9) + 1) # 1-9 first_digit = str(secrets.randbelow(9) + 1) # 1-9
rest = ''.join(str(secrets.randbelow(10)) for _ in range(length - 1)) # 0-9 rest = ''.join(str(secrets.randbelow(10)) for _ in range(length - 1))
return first_digit + rest return first_digit + rest
def generate_day_phrases(words_per_phrase=3): def generate_day_phrases(words_per_phrase=3):
phrases = {} phrases = {}
for day in DAY_NAMES: for day in DAY_NAMES:
@@ -136,8 +259,8 @@ def hash_photo(image_data):
return h return h
def derive_hybrid_key(photo_data, day_phrase, date_str, salt, pin=""): def derive_hybrid_key(photo_data, day_phrase, date_str, salt, pin="", rsa_key_data=None):
"""Derive encryption key from photo + phrase + PIN + date + salt.""" """Derive encryption key from photo + phrase + PIN + RSA key + date + salt."""
photo_hash = hash_photo(photo_data) photo_hash = hash_photo(photo_data)
key_material = ( key_material = (
@@ -148,6 +271,10 @@ def derive_hybrid_key(photo_data, day_phrase, date_str, salt, pin=""):
salt salt
) )
# Add RSA key hash if provided
if rsa_key_data:
key_material += hashlib.sha256(rsa_key_data).digest()
if HAS_ARGON2: if HAS_ARGON2:
key = hash_secret_raw( key = hash_secret_raw(
secret=key_material, secret=key_material,
@@ -171,20 +298,23 @@ def derive_hybrid_key(photo_data, day_phrase, date_str, salt, pin=""):
return key return key
def derive_pixel_key(photo_data, day_phrase, date_str, pin=""): def derive_pixel_key(photo_data, day_phrase, date_str, pin="", rsa_key_data=None):
"""Derive key for pixel selection.""" """Derive key for pixel selection."""
photo_hash = hash_photo(photo_data) photo_hash = hash_photo(photo_data)
material = photo_hash + day_phrase.lower().encode() + pin.encode() + date_str.encode() material = photo_hash + day_phrase.lower().encode() + pin.encode() + date_str.encode()
if rsa_key_data:
material += hashlib.sha256(rsa_key_data).digest()
return hashlib.sha256(material + b"pixel_selection").digest() return hashlib.sha256(material + b"pixel_selection").digest()
def encrypt_message(message, photo_data, day_phrase, date_str, pin=""): def encrypt_message(message, photo_data, day_phrase, date_str, pin="", rsa_key_data=None):
"""Encrypt message using hybrid key derivation.""" """Encrypt message using hybrid key derivation."""
salt = secrets.token_bytes(SALT_SIZE) salt = secrets.token_bytes(SALT_SIZE)
key = derive_hybrid_key(photo_data, day_phrase, date_str, salt, pin) key = derive_hybrid_key(photo_data, day_phrase, date_str, salt, pin, rsa_key_data)
iv = secrets.token_bytes(IV_SIZE) iv = secrets.token_bytes(IV_SIZE)
# Random padding
if isinstance(message, str): if isinstance(message, str):
message = message.encode() message = message.encode()
@@ -214,14 +344,8 @@ def encrypt_message(message, photo_data, day_phrase, date_str, pin=""):
def generate_pixel_indices(key, num_pixels, num_needed): def generate_pixel_indices(key, num_pixels, num_needed):
""" """Generate pseudo-random pixel indices."""
Generate pseudo-random pixel indices.
Optimized: Instead of shuffling ALL pixels (slow for large images),
we generate indices directly using PRNG and handle collisions.
"""
if num_needed >= num_pixels // 2: if num_needed >= num_pixels // 2:
# If we need many pixels, fall back to shuffle (rare case)
nonce = b'\x00' * 16 nonce = b'\x00' * 16
cipher = Cipher(algorithms.ChaCha20(key, nonce), mode=None, backend=default_backend()) cipher = Cipher(algorithms.ChaCha20(key, nonce), mode=None, backend=default_backend())
encryptor = cipher.encryptor() encryptor = cipher.encryptor()
@@ -236,18 +360,14 @@ def generate_pixel_indices(key, num_pixels, num_needed):
return indices[:num_needed] return indices[:num_needed]
# Optimized path: generate indices directly
# Use key to seed selection, ensuring deterministic results
selected = [] selected = []
used = set() used = set()
# Generate random bytes for index selection
nonce = b'\x00' * 16 nonce = b'\x00' * 16
cipher = Cipher(algorithms.ChaCha20(key, nonce), mode=None, backend=default_backend()) cipher = Cipher(algorithms.ChaCha20(key, nonce), mode=None, backend=default_backend())
encryptor = cipher.encryptor() encryptor = cipher.encryptor()
# Generate more than needed to handle collisions bytes_needed = (num_needed * 2) * 4
bytes_needed = (num_needed * 2) * 4 # 4 bytes per index, 2x for collisions
random_bytes = encryptor.update(b'\x00' * bytes_needed) random_bytes = encryptor.update(b'\x00' * bytes_needed)
byte_offset = 0 byte_offset = 0
@@ -259,7 +379,6 @@ def generate_pixel_indices(key, num_pixels, num_needed):
used.add(idx) used.add(idx)
selected.append(idx) selected.append(idx)
# If we still need more (very unlikely), generate additional
while len(selected) < num_needed: while len(selected) < num_needed:
extra_bytes = encryptor.update(b'\x00' * 4) extra_bytes = encryptor.update(b'\x00' * 4)
idx = int.from_bytes(extra_bytes, 'big') % num_pixels idx = int.from_bytes(extra_bytes, 'big') % num_pixels
@@ -343,7 +462,6 @@ def extract_from_image(image_data, pixel_key, bits_per_channel=1):
num_pixels = len(pixels) num_pixels = len(pixels)
bits_per_pixel = 3 * bits_per_channel bits_per_pixel = 3 * bits_per_channel
# First extract enough to get length
initial_pixels = (32 + bits_per_pixel - 1) // bits_per_pixel + 10 initial_pixels = (32 + bits_per_pixel - 1) // bits_per_pixel + 10
initial_indices = generate_pixel_indices(pixel_key, num_pixels, initial_pixels) initial_indices = generate_pixel_indices(pixel_key, num_pixels, initial_pixels)
@@ -407,13 +525,13 @@ def parse_header(encrypted_data):
return {'date': date_str, 'salt': salt, 'iv': iv, 'tag': tag, 'ciphertext': ciphertext} return {'date': date_str, 'salt': salt, 'iv': iv, 'tag': tag, 'ciphertext': ciphertext}
def decrypt_message(encrypted_data, photo_data, day_phrase, pin=""): def decrypt_message(encrypted_data, photo_data, day_phrase, pin="", rsa_key_data=None):
"""Decrypt message.""" """Decrypt message."""
header = parse_header(encrypted_data) header = parse_header(encrypted_data)
if not header: if not header:
return None return None
key = derive_hybrid_key(photo_data, day_phrase, header['date'], header['salt'], pin) key = derive_hybrid_key(photo_data, day_phrase, header['date'], header['salt'], pin, rsa_key_data)
cipher = Cipher( cipher = Cipher(
algorithms.AES(key), algorithms.AES(key),
@@ -444,28 +562,42 @@ def index():
def generate(): def generate():
if request.method == 'POST': if request.method == 'POST':
words_per_phrase = int(request.form.get('words_per_phrase', 3)) words_per_phrase = int(request.form.get('words_per_phrase', 3))
pin_length = int(request.form.get('pin_length', 6))
# Disable generate_stories for now (until much better) # Security factor options
#generate_stories = request.form.get('generate_stories') == 'on' use_pin = request.form.get('use_pin') == 'on'
generate_stories = request.form.get('generate_stories') == 'off' use_rsa = request.form.get('use_rsa') == 'on'
# Validate at least one factor selected
if not use_pin and not use_rsa:
flash('You must select at least one security factor (PIN or RSA Key)', 'error')
return render_template('generate.html', generated=False, has_ml=HAS_ML)
pin_length = int(request.form.get('pin_length', 6))
rsa_bits = int(request.form.get('rsa_bits', 2048))
# Clamp values to valid ranges # Clamp values to valid ranges
words_per_phrase = max(3, min(12, words_per_phrase)) words_per_phrase = max(3, min(12, words_per_phrase))
pin_length = max(6, min(8, pin_length)) pin_length = max(MIN_PIN_LENGTH, min(MAX_PIN_LENGTH, pin_length))
if rsa_bits not in VALID_RSA_SIZES:
rsa_bits = 2048
phrases = generate_day_phrases(words_per_phrase) phrases = generate_day_phrases(words_per_phrase)
pin = generate_pin(pin_length)
# Generate PIN if selected
pin = generate_pin(pin_length) if use_pin else None
# Generate RSA key if selected
rsa_key_pem = None
if use_rsa:
private_key = generate_rsa_key(rsa_bits)
rsa_key_pem = export_rsa_key_pem(private_key, password=None).decode('utf-8')
# Calculate entropy # Calculate entropy
phrase_entropy = words_per_phrase * 11 # ~11 bits per BIP-39 word phrase_entropy = words_per_phrase * 11
pin_entropy = int(pin_length * 3.32) # log2(10) ≈ 3.32 bits per digit pin_entropy = int(pin_length * 3.32) if use_pin else 0
total_entropy = phrase_entropy + pin_entropy # RSA key adds significant entropy (conservatively estimate effective security)
rsa_entropy = min(rsa_bits // 16, 128) if use_rsa else 0 # ~128 bits effective for 2048-bit
# Generate memory aid stories if requested total_entropy = phrase_entropy + pin_entropy + rsa_entropy
stories = None
if generate_stories:
stories = generate_all_stories(phrases, use_ml=HAS_ML)
return render_template('generate.html', return render_template('generate.html',
phrases=phrases, phrases=phrases,
@@ -473,15 +605,56 @@ def generate():
days=DAY_NAMES, days=DAY_NAMES,
generated=True, generated=True,
words_per_phrase=words_per_phrase, words_per_phrase=words_per_phrase,
pin_length=pin_length, pin_length=pin_length if use_pin else None,
use_pin=use_pin,
use_rsa=use_rsa,
rsa_bits=rsa_bits,
rsa_key_pem=rsa_key_pem,
phrase_entropy=phrase_entropy, phrase_entropy=phrase_entropy,
pin_entropy=pin_entropy, pin_entropy=pin_entropy,
rsa_entropy=rsa_entropy,
total_entropy=total_entropy, total_entropy=total_entropy,
stories=stories,
has_ml=HAS_ML) has_ml=HAS_ML)
return render_template('generate.html', generated=False, has_ml=HAS_ML) return render_template('generate.html', generated=False, has_ml=HAS_ML)
@app.route('/generate/download-key', methods=['POST'])
def download_key():
"""Download RSA key as password-protected PEM file."""
key_pem = request.form.get('key_pem', '')
password = request.form.get('key_password', '')
if not key_pem:
flash('No key to download', 'error')
return redirect(url_for('generate'))
if not password or len(password) < 8:
flash('Password must be at least 8 characters', 'error')
return redirect(url_for('generate'))
try:
# Load the unencrypted key
private_key = load_pem_private_key(key_pem.encode(), password=None, backend=default_backend())
# Re-export with password protection
encrypted_pem = export_rsa_key_pem(private_key, password=password)
# Generate filename
key_id = secrets.token_hex(4)
filename = f'stegasoo_key_{private_key.key_size}_{key_id}.pem'
return send_file(
io.BytesIO(encrypted_pem),
mimetype='application/x-pem-file',
as_attachment=True,
download_name=filename
)
except Exception as e:
flash(f'Error creating key file: {str(e)}', 'error')
return redirect(url_for('generate'))
@app.route('/encode', methods=['GET', 'POST']) @app.route('/encode', methods=['GET', 'POST'])
def encode(): def encode():
day_of_week = datetime.now().strftime("%A") day_of_week = datetime.now().strftime("%A")
@@ -491,6 +664,7 @@ def encode():
# Get files # Get files
ref_photo = request.files.get('reference_photo') ref_photo = request.files.get('reference_photo')
carrier = request.files.get('carrier') carrier = request.files.get('carrier')
rsa_key_file = request.files.get('rsa_key')
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')
@@ -503,55 +677,72 @@ def encode():
# Get form data # Get form data
message = request.form.get('message', '') message = request.form.get('message', '')
day_phrase = request.form.get('day_phrase', '') day_phrase = request.form.get('day_phrase', '')
pin = request.form.get('pin', '') pin = request.form.get('pin', '').strip()
rsa_password = request.form.get('rsa_password', '')
if not message or not day_phrase: # Validate message
flash('Message and day phrase are required', 'error') valid, error = validate_message(message)
if not valid:
flash(error, 'error')
return render_template('encode.html', day_of_week=day_of_week) return render_template('encode.html', day_of_week=day_of_week)
# Check message size if not day_phrase:
if len(message) > MAX_MESSAGE_SIZE: flash('Day phrase is required', 'error')
flash(f'Message too long. Max {MAX_MESSAGE_SIZE // 1000}KB allowed.', 'error')
return render_template('encode.html', day_of_week=day_of_week) return render_template('encode.html', day_of_week=day_of_week)
# 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
# Validate carrier image dimensions # Validate security factors
try: valid, error = validate_security_factors(pin, rsa_key_data)
carrier_img = Image.open(io.BytesIO(carrier_data)) if not valid:
width, height = carrier_img.size flash(error, 'error')
num_pixels = width * height
if num_pixels > MAX_IMAGE_PIXELS:
max_dim = int(MAX_IMAGE_PIXELS ** 0.5)
flash(f'Carrier image too large ({width}x{height} = {num_pixels:,} pixels). '
f'Max ~{MAX_IMAGE_PIXELS:,} pixels ({max_dim}x{max_dim}). '
f'Please resize your image.', 'error')
return render_template('encode.html', day_of_week=day_of_week)
except Exception as e:
flash(f'Could not read carrier image: {str(e)}', 'error')
return render_template('encode.html', day_of_week=day_of_week) return render_template('encode.html', day_of_week=day_of_week)
# Get date # Validate PIN if provided
date_str = datetime.now().strftime('%Y-%m-%d') if pin:
valid, error = validate_pin(pin)
if not valid:
flash(error, 'error')
return render_template('encode.html', day_of_week=day_of_week)
# Validate RSA key if provided
if rsa_key_data:
valid, error, key_size = validate_rsa_key(rsa_key_data, rsa_password if rsa_password else None)
if not valid:
flash(error, 'error')
return render_template('encode.html', day_of_week=day_of_week)
# Validate carrier image
valid, error = validate_image(carrier_data, "Carrier image")
if not valid:
flash(error, 'error')
return render_template('encode.html', day_of_week=day_of_week)
# Get date - use client's local date if provided
client_date = request.form.get('client_date', '').strip()
if client_date and len(client_date) == 10 and client_date[4] == '-' and client_date[7] == '-':
date_str = client_date
else:
date_str = datetime.now().strftime('%Y-%m-%d')
# Encrypt # Encrypt
encrypted = encrypt_message(message, ref_data, day_phrase, date_str, pin) encrypted = encrypt_message(message, ref_data, day_phrase, date_str, pin, rsa_key_data)
# Get pixel key # Get pixel key
pixel_key = derive_pixel_key(ref_data, day_phrase, date_str, pin) pixel_key = derive_pixel_key(ref_data, day_phrase, date_str, pin, rsa_key_data)
# Embed # Embed
stego_data, stats = embed_in_image(carrier_data, encrypted, pixel_key) stego_data, stats = embed_in_image(carrier_data, encrypted, pixel_key)
# Generate filename and file ID # Generate filename and file ID
filename = f'{secrets.token_hex(4)}_{datetime.now().strftime("%Y%m%d")}.png' filename = f'{secrets.token_hex(4)}_{date_str.replace("-", "")}.png'
file_id = secrets.token_urlsafe(16) file_id = secrets.token_urlsafe(16)
# Store temporarily for download/share # Store temporarily for download/share
cleanup_temp_files() # Clean old files first cleanup_temp_files()
TEMP_FILES[file_id] = { TEMP_FILES[file_id] = {
'data': stego_data, 'data': stego_data,
'filename': filename, 'filename': filename,
@@ -625,6 +816,7 @@ def decode():
# Get files # Get files
ref_photo = request.files.get('reference_photo') ref_photo = request.files.get('reference_photo')
stego_image = request.files.get('stego_image') stego_image = request.files.get('stego_image')
rsa_key_file = request.files.get('rsa_key')
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')
@@ -632,7 +824,8 @@ def decode():
# Get form data # Get form data
day_phrase = request.form.get('day_phrase', '') day_phrase = request.form.get('day_phrase', '')
pin = request.form.get('pin', '') pin = request.form.get('pin', '').strip()
rsa_password = request.form.get('rsa_password', '')
if not day_phrase: if not day_phrase:
flash('Day phrase is required', 'error') flash('Day phrase is required', 'error')
@@ -641,32 +834,50 @@ def decode():
# 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
# Validate security factors
valid, error = validate_security_factors(pin, rsa_key_data)
if not valid:
flash(error, 'error')
return render_template('decode.html')
# Validate PIN if provided
if pin:
valid, error = validate_pin(pin)
if not valid:
flash(error, 'error')
return render_template('decode.html')
# Validate RSA key if provided
if rsa_key_data:
valid, error, key_size = validate_rsa_key(rsa_key_data, rsa_password if rsa_password else None)
if not valid:
flash(error, 'error')
return render_template('decode.html')
# Try to extract and decrypt # Try to extract and decrypt
# We need to try with today's date first for pixel key
date_str = datetime.now().strftime('%Y-%m-%d') date_str = datetime.now().strftime('%Y-%m-%d')
pixel_key = derive_pixel_key(ref_data, day_phrase, date_str, pin) pixel_key = derive_pixel_key(ref_data, day_phrase, date_str, pin, rsa_key_data)
encrypted = extract_from_image(stego_data, pixel_key) encrypted = extract_from_image(stego_data, pixel_key)
if encrypted: if encrypted:
# Parse to get actual date
header = parse_header(encrypted) header = parse_header(encrypted)
if header and header['date'] != date_str: if header and header['date'] != date_str:
# Re-extract with correct date pixel_key = derive_pixel_key(ref_data, day_phrase, header['date'], pin, rsa_key_data)
pixel_key = derive_pixel_key(ref_data, day_phrase, header['date'], pin)
encrypted = extract_from_image(stego_data, pixel_key) encrypted = extract_from_image(stego_data, pixel_key)
if not encrypted: if not encrypted:
flash('Could not extract data. Check your inputs.', 'error') flash('Could not extract data. Check your inputs.', 'error')
return render_template('decode.html') return render_template('decode.html')
message = decrypt_message(encrypted, ref_data, day_phrase, pin) message = decrypt_message(encrypted, ref_data, day_phrase, pin, rsa_key_data)
if message: if message:
return render_template('decode.html', decoded_message=message) return render_template('decode.html', decoded_message=message)
else: else:
flash('Decryption failed. Wrong phrase, PIN, or reference photo.', 'error') flash('Decryption failed. Wrong phrase, PIN, RSA key, or reference photo.', 'error')
return render_template('decode.html') return render_template('decode.html')
except Exception as e: except Exception as e:

View File

@@ -226,6 +226,15 @@ footer {
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.2);
} }
/* ----------------------------------------------------------------------------
Custom Alert Variants
---------------------------------------------------------------------------- */
.alert-success-bright {
background: rgba(34, 197, 94, 0.2);
border-color: #22c55e;
color: #4ade80;
}
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
Utility Classes Utility Classes
---------------------------------------------------------------------------- */ ---------------------------------------------------------------------------- */

View File

@@ -49,7 +49,7 @@
<label class="form-label"> <label class="form-label">
<i class="bi bi-file-earmark-image me-1"></i> Stego Image <i class="bi bi-file-earmark-image me-1"></i> Stego Image
</label> </label>
<div class="drop-zone"> <div class="drop-zone" id="stegoDropZone">
<input type="file" name="stego_image" accept="image/*" required> <input type="file" name="stego_image" accept="image/*" required>
<div class="drop-zone-label"> <div class="drop-zone-label">
<i class="bi bi-cloud-arrow-up fs-3 d-block mb-2 text-muted"></i> <i class="bi bi-cloud-arrow-up fs-3 d-block mb-2 text-muted"></i>
@@ -63,28 +63,58 @@
</div> </div>
</div> </div>
<div class="row"> <div class="mb-3">
<div class="col-md-8 mb-3"> <label class="form-label" id="dayPhraseLabel">
<label class="form-label"> <i class="bi bi-chat-quote me-1"></i> Day Phrase
<i class="bi bi-chat-quote me-1"></i> Day Phrase </label>
</label> <input type="text" name="day_phrase" class="form-control"
<input type="text" name="day_phrase" class="form-control" placeholder="e.g., correct horse battery" required>
placeholder="e.g., correct horse battery" required> <div class="form-text">
<div class="form-text"> The phrase for the day the message was encoded
The phrase for the day the message was encoded
</div>
</div> </div>
</div>
<div class="col-md-4 mb-3"> <hr class="my-4">
<h6 class="text-muted mb-3">
SECURITY FACTORS
<span class="text-warning small">(provide same factors used during encoding)</span>
</h6>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label"> <label class="form-label">
<i class="bi bi-123 me-1"></i> PIN <i class="bi bi-123 me-1"></i> PIN
</label> </label>
<input type="password" name="pin" class="form-control" <input type="password" name="pin" class="form-control" id="pinInput"
placeholder="123456" maxlength="10"> placeholder="6-9 digits" maxlength="9">
<div class="form-text"> <div class="form-text">
Your static 6-digit PIN If PIN was used during encoding
</div> </div>
</div> </div>
<div class="col-md-6 mb-3">
<label class="form-label">
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
</label>
<input type="file" name="rsa_key" class="form-control" id="rsaKeyInput"
accept=".pem,.key">
<div class="form-text">
If RSA key was used during encoding
</div>
</div>
</div>
<!-- RSA Key Password (shown when key selected) -->
<div class="mb-3 d-none" id="rsaPasswordGroup">
<label class="form-label">
<i class="bi bi-key me-1"></i> RSA Key Password
</label>
<input type="password" name="rsa_password" class="form-control"
placeholder="Password for the .pem file (if encrypted)">
<div class="form-text">
Leave blank if your key file is not password-protected
</div>
</div> </div>
<button type="submit" class="btn btn-primary btn-lg w-100" id="decodeBtn"> <button type="submit" class="btn btn-primary btn-lg w-100" id="decodeBtn">
@@ -108,13 +138,17 @@
<i class="bi bi-dot"></i> <i class="bi bi-dot"></i>
Use the phrase for the <strong>day the message was encoded</strong>, not today Use the phrase for the <strong>day the message was encoded</strong>, not today
</li> </li>
<li class="mb-2">
<i class="bi bi-dot"></i>
Provide the <strong>same security factors</strong> (PIN and/or RSA key) used during encoding
</li>
<li class="mb-2"> <li class="mb-2">
<i class="bi bi-dot"></i> <i class="bi bi-dot"></i>
Ensure the stego image hasn't been <strong>resized or recompressed</strong> Ensure the stego image hasn't been <strong>resized or recompressed</strong>
</li> </li>
<li class="mb-0"> <li class="mb-0">
<i class="bi bi-dot"></i> <i class="bi bi-dot"></i>
Double-check your <strong>PIN</strong> is correct If using an RSA key, make sure the <strong>password is correct</strong>
</li> </li>
</ul> </ul>
</div> </div>
@@ -132,13 +166,44 @@ document.getElementById('decodeForm')?.addEventListener('submit', function() {
btn.disabled = true; btn.disabled = true;
}); });
// Show RSA password field when key is selected
const rsaKeyInput = document.getElementById('rsaKeyInput');
const rsaPasswordGroup = document.getElementById('rsaPasswordGroup');
rsaKeyInput?.addEventListener('change', function() {
rsaPasswordGroup.classList.toggle('d-none', !this.files.length);
});
// Day names for date detection
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
// Detect day from filename
function detectDayFromFilename(filename) {
const dateMatch = filename.match(/_(\d{4})[-]?(\d{2})[-]?(\d{2})/);
if (dateMatch) {
const [, year, month, day] = dateMatch;
const date = new Date(year, month - 1, day);
return dayNames[date.getDay()];
}
return null;
}
// Update day phrase label
function updateDayLabel(dayName) {
const label = document.getElementById('dayPhraseLabel');
if (label && dayName) {
label.innerHTML = `<i class="bi bi-chat-quote me-1"></i> ${dayName}'s Phrase`;
}
}
// Drag & drop with preview // Drag & drop with preview
document.querySelectorAll('.drop-zone').forEach(zone => { document.querySelectorAll('.drop-zone').forEach(zone => {
const input = zone.querySelector('input[type="file"]'); const input = zone.querySelector('input[type="file"]');
const label = zone.querySelector('.drop-zone-label'); const label = zone.querySelector('.drop-zone-label');
const preview = zone.querySelector('.drop-zone-preview'); const preview = zone.querySelector('.drop-zone-preview');
const isStegoZone = zone.id === 'stegoDropZone';
// Drag events
['dragenter', 'dragover'].forEach(evt => { ['dragenter', 'dragover'].forEach(evt => {
zone.addEventListener(evt, e => { zone.addEventListener(evt, e => {
e.preventDefault(); e.preventDefault();
@@ -153,18 +218,28 @@ document.querySelectorAll('.drop-zone').forEach(zone => {
}); });
}); });
// Handle drop
zone.addEventListener('drop', e => { zone.addEventListener('drop', e => {
if (e.dataTransfer.files.length) { if (e.dataTransfer.files.length) {
input.files = e.dataTransfer.files; input.files = e.dataTransfer.files;
showPreview(e.dataTransfer.files[0]); const file = e.dataTransfer.files[0];
showPreview(file);
if (isStegoZone) {
const dayName = detectDayFromFilename(file.name);
updateDayLabel(dayName);
}
} }
}); });
// Handle click selection
input.addEventListener('change', function() { input.addEventListener('change', function() {
if (this.files && this.files[0]) { if (this.files && this.files[0]) {
showPreview(this.files[0]); const file = this.files[0];
showPreview(file);
if (isStegoZone) {
const dayName = detectDayFromFilename(file.name);
updateDayLabel(dayName);
}
} }
}); });

View File

@@ -11,6 +11,8 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="POST" enctype="multipart/form-data" id="encodeForm"> <form method="POST" enctype="multipart/form-data" id="encodeForm">
<input type="hidden" name="client_date" id="clientDate" value="">
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label"> <label class="form-label">
@@ -64,28 +66,58 @@
</div> </div>
</div> </div>
<div class="row"> <div class="mb-3">
<div class="col-md-8 mb-3"> <label class="form-label" id="dayPhraseLabel">
<label class="form-label"> <i class="bi bi-chat-quote me-1"></i> {{ day_of_week }}'s Phrase
<i class="bi bi-chat-quote me-1"></i> {{ day_of_week }}'s Phrase </label>
</label> <input type="text" name="day_phrase" class="form-control"
<input type="text" name="day_phrase" class="form-control" placeholder="e.g., correct horse battery" required>
placeholder="e.g., correct horse battery" required> <div class="form-text">
<div class="form-text"> Your phrase for <strong>today</strong> (based on your local timezone)
Your phrase for <strong>today</strong> (based on your local timezone)
</div>
</div> </div>
</div>
<div class="col-md-4 mb-3"> <hr class="my-4">
<h6 class="text-muted mb-3">
SECURITY FACTORS
<span class="text-warning small">(provide at least one: PIN or RSA Key)</span>
</h6>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label"> <label class="form-label">
<i class="bi bi-123 me-1"></i> PIN <i class="bi bi-123 me-1"></i> PIN
</label> </label>
<input type="password" name="pin" class="form-control" <input type="password" name="pin" class="form-control" id="pinInput"
placeholder="123456" maxlength="10"> placeholder="6-9 digits" maxlength="9">
<div class="form-text"> <div class="form-text">
Your static 6-digit PIN Your static 6-9 digit PIN (if configured)
</div> </div>
</div> </div>
<div class="col-md-6 mb-3">
<label class="form-label">
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
</label>
<input type="file" name="rsa_key" class="form-control" id="rsaKeyInput"
accept=".pem,.key">
<div class="form-text">
Your shared .pem key file (if configured)
</div>
</div>
</div>
<!-- RSA Key Password (shown when key selected) -->
<div class="mb-3 d-none" id="rsaPasswordGroup">
<label class="form-label">
<i class="bi bi-key me-1"></i> RSA Key Password
</label>
<input type="password" name="rsa_password" class="form-control"
placeholder="Password for the .pem file (if encrypted)">
<div class="form-text">
Leave blank if your key file is not password-protected
</div>
</div> </div>
<button type="submit" class="btn btn-primary btn-lg w-100" id="encodeBtn"> <button type="submit" class="btn btn-primary btn-lg w-100" id="encodeBtn">
@@ -125,6 +157,34 @@
{% block scripts %} {% block scripts %}
<script> <script>
// Detect client's local date and day
const now = new Date();
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const localDay = dayNames[now.getDay()];
const localDate = now.getFullYear() + '-' +
String(now.getMonth() + 1).padStart(2, '0') + '-' +
String(now.getDate()).padStart(2, '0');
// Update day label to client's local day
const dayLabel = document.getElementById('dayPhraseLabel');
if (dayLabel) {
dayLabel.innerHTML = `<i class="bi bi-chat-quote me-1"></i> ${localDay}'s Phrase`;
}
// Set hidden field with client's local date for server
const dateInput = document.getElementById('clientDate');
if (dateInput) {
dateInput.value = localDate;
}
// Show RSA password field when key is selected
const rsaKeyInput = document.getElementById('rsaKeyInput');
const rsaPasswordGroup = document.getElementById('rsaPasswordGroup');
rsaKeyInput.addEventListener('change', function() {
rsaPasswordGroup.classList.toggle('d-none', !this.files.length);
});
// Form submit loading state // Form submit loading state
document.getElementById('encodeForm').addEventListener('submit', function(e) { document.getElementById('encodeForm').addEventListener('submit', function(e) {
const btn = document.getElementById('encodeBtn'); const btn = document.getElementById('encodeBtn');
@@ -146,10 +206,7 @@ messageInput.addEventListener('input', function() {
const pct = Math.round((len / maxChars) * 100); const pct = Math.round((len / maxChars) * 100);
charPercent.textContent = pct + '%'; charPercent.textContent = pct + '%';
// Warning at 80%
charWarning.classList.toggle('d-none', len < maxChars * 0.8); charWarning.classList.toggle('d-none', len < maxChars * 0.8);
// Red text when near/over limit
charCount.classList.toggle('text-danger', len > maxChars * 0.95); charCount.classList.toggle('text-danger', len > maxChars * 0.95);
}); });
@@ -159,7 +216,6 @@ document.querySelectorAll('.drop-zone').forEach(zone => {
const label = zone.querySelector('.drop-zone-label'); const label = zone.querySelector('.drop-zone-label');
const preview = zone.querySelector('.drop-zone-preview'); const preview = zone.querySelector('.drop-zone-preview');
// Drag events
['dragenter', 'dragover'].forEach(evt => { ['dragenter', 'dragover'].forEach(evt => {
zone.addEventListener(evt, e => { zone.addEventListener(evt, e => {
e.preventDefault(); e.preventDefault();
@@ -174,7 +230,6 @@ document.querySelectorAll('.drop-zone').forEach(zone => {
}); });
}); });
// Handle drop
zone.addEventListener('drop', e => { zone.addEventListener('drop', e => {
if (e.dataTransfer.files.length) { if (e.dataTransfer.files.length) {
input.files = e.dataTransfer.files; input.files = e.dataTransfer.files;
@@ -182,7 +237,6 @@ document.querySelectorAll('.drop-zone').forEach(zone => {
} }
}); });
// Handle click selection
input.addEventListener('change', function() { input.addEventListener('change', function() {
if (this.files && this.files[0]) { if (this.files && this.files[0]) {
showPreview(this.files[0]); showPreview(this.files[0]);

View File

@@ -1,53 +1,88 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Generate Phrase Card - Stegasoo{% endblock %} {% block title %}Generate Credentials - 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-8">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5 class="mb-0"><i class="bi bi-key-fill me-2"></i>Generate Phrase Card + PIN</h5> <h5 class="mb-0"><i class="bi bi-key-fill me-2"></i>Generate Credentials</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
{% if not generated %} {% if not generated %}
<p class="text-muted mb-4"> <p class="text-muted mb-4">
Generate your weekly phrase card and static PIN. Customize your security level: Generate your weekly phrase card and security factors. You must choose at least one: PIN or RSA Key.
</p> </p>
<form method="POST"> <form method="POST" id="generateForm">
<div class="row"> <div class="mb-4">
<div class="col-md-6 mb-3"> <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">
<select name="words_per_phrase" class="form-select" id="wordsSelect"> <option value="3" selected>3 words (~33 bits)</option>
<option value="3" selected>3 words (~33 bits)</option> <option value="4">4 words (~44 bits)</option>
<option value="4">4 words (~44 bits)</option> <option value="5">5 words (~55 bits)</option>
<option value="5">5 words (~55 bits)</option> <option value="6">6 words (~66 bits)</option>
<option value="6">6 words (~66 bits)</option> <option value="7">7 words (~77 bits)</option>
<option value="7">7 words (~77 bits)</option> <option value="8">8 words (~88 bits)</option>
<option value="8">8 words (~88 bits)</option> <option value="9">9 words (~99 bits)</option>
<option value="9">9 words (~99 bits)</option> <option value="10">10 words (~110 bits)</option>
<option value="10">10 words (~110 bits)</option> <option value="11">11 words (~121 bits)</option>
<option value="11">11 words (~121 bits)</option> <option value="12">12 words (~132 bits)</option>
<option value="12">12 words (~132 bits)</option> </select>
</select> <div class="form-text">More words = more security, harder to memorize</div>
<div class="form-text">More words = more security, harder to memorize</div> </div>
</div>
<div class="col-md-6 mb-3"> <hr class="my-4">
<label class="form-label">PIN length</label>
<select name="pin_length" class="form-select" id="pinSelect"> <h6 class="text-muted mb-3">SECURITY FACTORS <span class="text-warning">(select at least one)</span></h6>
<option value="6" selected>6 digits (~20 bits)</option>
<option value="7">7 digits (~23 bits)</option> <!-- PIN Option -->
<option value="8">8 digits (~27 bits)</option> <div class="card mb-3" style="background: rgba(0,0,0,0.2);">
</select> <div class="card-body">
<div class="form-text">Same PIN used every day</div> <div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="use_pin" id="usePin" checked>
<label class="form-check-label fw-bold" for="usePin">
<i class="bi bi-123 me-1"></i> PIN
</label>
</div>
<div id="pinOptions">
<label class="form-label">PIN length</label>
<select name="pin_length" class="form-select" id="pinSelect">
<option value="6" selected>6 digits (~20 bits)</option>
<option value="7">7 digits (~23 bits)</option>
<option value="8">8 digits (~27 bits)</option>
<option value="9">9 digits (~30 bits)</option>
</select>
<div class="form-text">Memorizable, same PIN used every day</div>
</div>
</div>
</div>
<!-- RSA Key Option -->
<div class="card mb-3" style="background: rgba(0,0,0,0.2);">
<div class="card-body">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="use_rsa" id="useRsa">
<label class="form-check-label fw-bold" for="useRsa">
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
</label>
</div>
<div id="rsaOptions" class="d-none">
<label class="form-label">Key size</label>
<select name="rsa_bits" class="form-select" id="rsaSelect">
<option value="2048" selected>2048-bit (~128 bits effective)</option>
<option value="3072">3072-bit (~128 bits effective)</option>
<option value="4096">4096-bit (~128 bits effective)</option>
</select>
<div class="form-text">File-based key, both parties need the same .pem file</div>
</div>
</div> </div>
</div> </div>
<div class="alert alert-info mb-4"> <div class="alert alert-info mb-4">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<span><i class="bi bi-calculator me-2"></i>Estimated phrase+PIN entropy:</span> <span><i class="bi bi-calculator me-2"></i>Estimated entropy:</span>
<strong id="entropyDisplay">~53 bits</strong> <strong id="entropyDisplay">~53 bits</strong>
</div> </div>
<div class="progress mt-2" style="height: 8px;"> <div class="progress mt-2" style="height: 8px;">
@@ -59,33 +94,33 @@
</small> </small>
</div> </div>
<!--<div class="form-check mb-4"> <div class="alert alert-warning d-none" id="noFactorWarning">
<input class="form-check-input" type="checkbox" name="generate_stories" id="generateStories" checked> <i class="bi bi-exclamation-triangle me-2"></i>
<label class="form-check-label" for="generateStories"> You must select at least one security factor (PIN or RSA Key)
<i class="bi bi-book me-2"></i>Generate memory aid stories </div>
{% if has_ml %}<span class="badge bg-success ms-2">AI-powered</span>{% else %}<span class="badge bg-secondary ms-2">Template-based</span>{% endif %}
</label>
<div class="form-text">Creates memorable stories to help you remember each day's phrase</div>
</div>-->
<button type="submit" class="btn btn-primary btn-lg w-100" id="generateBtn"> <button type="submit" class="btn btn-primary btn-lg w-100" id="generateBtn">
<i class="bi bi-shuffle me-2"></i>Generate New Credentials <i class="bi bi-shuffle me-2"></i>Generate Credentials
</button> </button>
</form> </form>
{% else %} {% else %}
<div class="alert alert-info"> <!-- Generated Results -->
<i class="bi bi-exclamation-circle me-2"></i> <div class="alert alert-success-bright alert-dismissible fade show">
<strong>Credentials Generated!</strong> - Refresh to generate new credentials <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>
<div class="alert alert-warning"> <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 the information then close!</strong> - Do not save/screenshot <strong>Memorize phrases, save key securely, then close!</strong> - Do not screenshot
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div> </div>
{% if pin %}
<hr class="my-4"> <hr class="my-4">
<div class="text-center mb-4"> <div class="text-center mb-4">
<h6 class="text-muted mb-2">YOUR STATIC PIN</h6> <h6 class="text-muted mb-2">YOUR STATIC PIN</h6>
<div class="pin-container"> <div class="pin-container">
@@ -95,6 +130,55 @@
<small class="text-muted">Use this {{ pin_length }}-digit PIN every day</small> <small class="text-muted">Use this {{ pin_length }}-digit PIN every day</small>
</div> </div>
</div> </div>
{% endif %}
{% if rsa_key_pem %}
<hr class="my-4">
<div class="mb-4">
<h6 class="text-muted mb-3">
<i class="bi bi-file-earmark-lock me-2"></i>YOUR RSA KEY ({{ rsa_bits }}-bit)
</h6>
<div class="alert alert-danger small">
<i class="bi bi-shield-exclamation me-1"></i>
<strong>Save this key securely!</strong> Share it with your recipient through a secure channel. You cannot recover it later.
</div>
<!-- Key Display -->
<div class="mb-3">
<textarea class="form-control font-monospace" id="rsaKeyText" rows="6" readonly style="font-size: 0.75rem;">{{ rsa_key_pem }}</textarea>
</div>
<!-- Copy to Clipboard -->
<button type="button" class="btn btn-outline-light me-2" id="copyKeyBtn">
<i class="bi bi-clipboard me-1"></i> Copy to Clipboard
</button>
<!-- Download with Password -->
<button type="button" class="btn btn-outline-light" data-bs-toggle="collapse" data-bs-target="#downloadKeyForm">
<i class="bi bi-download me-1"></i> Download as .pem
</button>
<div class="collapse mt-3" id="downloadKeyForm">
<div class="card" style="background: rgba(0,0,0,0.2);">
<div class="card-body">
<form method="POST" action="{{ url_for('download_key') }}">
<input type="hidden" name="key_pem" value="{{ rsa_key_pem }}">
<div class="mb-3">
<label class="form-label">Password to protect key file</label>
<input type="password" name="key_password" class="form-control"
placeholder="Minimum 8 characters" minlength="8" required>
<div class="form-text">You'll need this password when using the key</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-file-earmark-lock me-1"></i> Download Protected Key
</button>
</form>
</div>
</div>
</div>
</div>
{% endif %}
<hr class="my-4"> <hr class="my-4">
@@ -126,15 +210,23 @@
<div class="alert alert-success mt-4"> <div class="alert alert-success mt-4">
<h6><i class="bi bi-shield-check me-2"></i>Security Summary</h6> <h6><i class="bi bi-shield-check me-2"></i>Security Summary</h6>
<div class="row text-center mt-3"> <div class="row text-center mt-3">
<div class="col-4"> <div class="col-3">
<div class="fs-4 fw-bold">{{ phrase_entropy }}</div> <div class="fs-4 fw-bold">{{ phrase_entropy }}</div>
<small class="text-muted">bits/phrase</small> <small class="text-muted">bits/phrase</small>
</div> </div>
<div class="col-4"> {% if pin %}
<div class="col-3">
<div class="fs-4 fw-bold">{{ pin_entropy }}</div> <div class="fs-4 fw-bold">{{ pin_entropy }}</div>
<small class="text-muted">bits/PIN</small> <small class="text-muted">bits/PIN</small>
</div> </div>
<div class="col-4"> {% endif %}
{% if rsa_key_pem %}
<div class="col-3">
<div class="fs-4 fw-bold">{{ rsa_entropy }}</div>
<small class="text-muted">bits/RSA</small>
</div>
{% endif %}
<div class="col-3">
<div class="fs-4 fw-bold text-success">{{ total_entropy }}</div> <div class="fs-4 fw-bold text-success">{{ total_entropy }}</div>
<small class="text-muted">bits total</small> <small class="text-muted">bits total</small>
</div> </div>
@@ -144,36 +236,6 @@
</small> </small>
</div> </div>
<div class="alert alert-info mt-4">
<h6><i class="bi bi-lightbulb me-2"></i>Memorization Tip</h6>
<p class="mb-1">
<strong>Total to memorize:</strong> {{ words_per_phrase * 7 }} words + {{ pin_length }} digits
</p>
<p class="mb-0 small">
Create a story for each day: "On Monday, the <em>[word1]</em> and <em>[word2]</em> went to see <em>[word3]</em>..."
</p>
</div>
{% if stories %}
<hr class="my-4">
<h6 class="text-muted mb-3">
<i class="bi bi-book me-2"></i>MEMORY AID STORIES
{% if has_ml %}<span class="badge bg-success ms-2">AI-generated</span>{% else %}<span class="badge bg-secondary ms-2">Template-based</span>{% endif %}
</h6>
<p class="text-muted small mb-3">
Passphrase words are shown in <span class="story-word">RED CAPS</span>.
Read each story to help memorize your phrases.
</p>
{% for day in days %}
<div class="story-card">
<div class="day-label"><i class="bi bi-calendar3 me-2"></i>{{ day }}</div>
<div>{{ stories[day].story_html|safe }}</div>
</div>
{% endfor %}
{% endif %}
<a href="/generate" class="btn btn-outline-light btn-lg w-100 mt-3"> <a href="/generate" class="btn btn-outline-light btn-lg w-100 mt-3">
<i class="bi bi-arrow-repeat me-2"></i>Generate New Credentials <i class="bi bi-arrow-repeat me-2"></i>Generate New Credentials
</a> </a>
@@ -186,47 +248,98 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
{% if not generated %}
<script> <script>
{% if not generated %}
const usePinCheckbox = document.getElementById('usePin');
const useRsaCheckbox = document.getElementById('useRsa');
const pinOptions = document.getElementById('pinOptions');
const rsaOptions = document.getElementById('rsaOptions');
const noFactorWarning = document.getElementById('noFactorWarning');
const generateBtn = document.getElementById('generateBtn');
// Toggle option visibility
usePinCheckbox.addEventListener('change', function() {
pinOptions.classList.toggle('d-none', !this.checked);
validateFactors();
updateEntropy();
});
useRsaCheckbox.addEventListener('change', function() {
rsaOptions.classList.toggle('d-none', !this.checked);
validateFactors();
updateEntropy();
});
function validateFactors() {
const hasPin = usePinCheckbox.checked;
const hasRsa = useRsaCheckbox.checked;
const valid = hasPin || hasRsa;
noFactorWarning.classList.toggle('d-none', valid);
generateBtn.disabled = !valid;
}
function updateEntropy() { function updateEntropy() {
const words = parseInt(document.getElementById('wordsSelect').value); const words = parseInt(document.getElementById('wordsSelect').value);
const usePin = usePinCheckbox.checked;
const useRsa = useRsaCheckbox.checked;
const pinLen = parseInt(document.getElementById('pinSelect').value); const pinLen = parseInt(document.getElementById('pinSelect').value);
const phraseEntropy = words * 11; const phraseEntropy = words * 11;
const pinEntropy = Math.floor(pinLen * 3.32); const pinEntropy = usePin ? Math.floor(pinLen * 3.32) : 0;
const total = phraseEntropy + pinEntropy; const rsaEntropy = useRsa ? 128 : 0;
const total = phraseEntropy + pinEntropy + rsaEntropy;
document.getElementById('entropyDisplay').textContent = '~' + total + ' bits'; document.getElementById('entropyDisplay').textContent = '~' + total + ' bits';
// Update progress bar (scale: 50 bits = 40%, 150 bits = 100%) // Update progress bar
const pct = Math.min(100, Math.max(10, (total - 30) * 0.7)); const pct = Math.min(100, Math.max(10, (total - 30) * 0.5));
document.getElementById('entropyBar').style.width = pct + '%'; document.getElementById('entropyBar').style.width = pct + '%';
// Update description // Update description
let desc; let desc;
if (total < 50) desc = 'Basic security'; if (total < 50) desc = 'Basic security';
else if (total < 70) desc = 'Good for most use cases'; else if (total < 80) desc = 'Good for most use cases';
else if (total < 100) desc = 'Strong security'; else if (total < 120) desc = 'Strong security';
else if (total < 130) desc = 'Very strong security'; else if (total < 180) desc = 'Very strong security';
else desc = 'Extreme security (hard to memorize!)'; else desc = 'Maximum security';
document.getElementById('entropyDesc').textContent = desc; document.getElementById('entropyDesc').textContent = desc;
} }
document.getElementById('wordsSelect').addEventListener('change', updateEntropy); document.getElementById('wordsSelect').addEventListener('change', updateEntropy);
document.getElementById('pinSelect').addEventListener('change', updateEntropy); document.getElementById('pinSelect').addEventListener('change', updateEntropy);
document.getElementById('rsaSelect').addEventListener('change', updateEntropy);
// Loading state for generate button // Form submit
document.querySelector('form').addEventListener('submit', function() { document.getElementById('generateForm').addEventListener('submit', function(e) {
const btn = document.getElementById('generateBtn'); if (!usePinCheckbox.checked && !useRsaCheckbox.checked) {
const storiesChecked = document.getElementById('generateStories')?.checked; e.preventDefault();
btn.disabled = true; noFactorWarning.classList.remove('d-none');
if (storiesChecked) { return;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Generating stories...';
} else {
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Generating...';
} }
generateBtn.disabled = true;
generateBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Generating...';
}); });
</script>
// 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(() => {
this.innerHTML = '<i class="bi bi-clipboard me-1"></i> Copy to Clipboard';
}, 2000);
});
});
{% endif %} {% endif %}
</script>
{% endblock %} {% endblock %}