Basic quility of like improvements.

This commit is contained in:
Aaron D. Lee
2025-12-27 13:25:17 -05:00
parent 02c9466102
commit 18fb5059c3
10 changed files with 363 additions and 48 deletions

View File

@@ -1,4 +1,4 @@
# Stegasoo Web Service # StegoCrypt Web Service
A containerized Flask + Bootstrap web UI for hybrid Photo + Day-Phrase + PIN steganography. A containerized Flask + Bootstrap web UI for hybrid Photo + Day-Phrase + PIN steganography.

104
app.py
View File

@@ -18,6 +18,8 @@ import io
import secrets import secrets
import hashlib import hashlib
import struct import struct
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
@@ -52,6 +54,10 @@ os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'bmp', 'gif'} ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'bmp', 'gif'}
# Temporary file storage for sharing (file_id -> {data, timestamp, filename})
TEMP_FILES = {}
TEMP_FILE_EXPIRY = 300 # 5 minutes
# ============================================================================ # ============================================================================
# CRYPTO CONFIGURATION # CRYPTO CONFIGURATION
# ============================================================================ # ============================================================================
@@ -65,7 +71,7 @@ ARGON2_TIME_COST = 4
ARGON2_MEMORY_COST = 256 * 1024 ARGON2_MEMORY_COST = 256 * 1024
ARGON2_PARALLELISM = 4 ARGON2_PARALLELISM = 4
DAY_NAMES = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] DAY_NAMES = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
# BIP-39 wordlist (loaded from file) # BIP-39 wordlist (loaded from file)
BIP39_FILE = os.path.join(os.path.dirname(__file__), 'bip39-words.txt') BIP39_FILE = os.path.join(os.path.dirname(__file__), 'bip39-words.txt')
@@ -90,6 +96,14 @@ def secure_cleanup_uploads():
# Fallback to regular delete # Fallback to regular delete
os.remove(filepath) os.remove(filepath)
def cleanup_temp_files():
"""Remove expired temporary files."""
now = time.time()
expired = [fid for fid, info in TEMP_FILES.items() if now - info['timestamp'] > TEMP_FILE_EXPIRY]
for fid in expired:
TEMP_FILES.pop(fid, None)
# ============================================================================ # ============================================================================
# HELPER FUNCTIONS # HELPER FUNCTIONS
# ============================================================================ # ============================================================================
@@ -458,25 +472,21 @@ def generate():
@app.route('/encode', methods=['GET', 'POST']) @app.route('/encode', methods=['GET', 'POST'])
def encode(): def encode():
# Get day of week
day_of_week = datetime.now().strftime("%A") day_of_week = datetime.now().strftime("%A")
if request.method == 'POST': if request.method == 'POST':
try: try:
# 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')
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') return render_template('encode.html', day_of_week=day_of_week)
if not allowed_file(ref_photo.filename) or not allowed_file(carrier.filename): if not allowed_file(ref_photo.filename) or not allowed_file(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') return render_template('encode.html', day_of_week=day_of_week)
# Get form data # Get form data
message = request.form.get('message', '') message = request.form.get('message', '')
@@ -485,12 +495,12 @@ def encode():
if not message or not day_phrase: if not message or not day_phrase:
flash('Message and day phrase are required', 'error') flash('Message and day phrase are required', 'error')
return render_template('encode.html') return render_template('encode.html', day_of_week=day_of_week)
# Check message size # Check message size
if len(message) > MAX_MESSAGE_SIZE: if len(message) > MAX_MESSAGE_SIZE:
flash(f'Message too long. Max {MAX_MESSAGE_SIZE // 1000}KB allowed.', 'error') flash(f'Message too long. Max {MAX_MESSAGE_SIZE // 1000}KB allowed.', 'error')
return render_template('encode.html') 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()
@@ -507,10 +517,10 @@ def encode():
flash(f'Carrier image too large ({width}x{height} = {num_pixels:,} pixels). ' flash(f'Carrier image too large ({width}x{height} = {num_pixels:,} pixels). '
f'Max ~{MAX_IMAGE_PIXELS:,} pixels ({max_dim}x{max_dim}). ' f'Max ~{MAX_IMAGE_PIXELS:,} pixels ({max_dim}x{max_dim}). '
f'Please resize your image.', 'error') f'Please resize your image.', 'error')
return render_template('encode.html') return render_template('encode.html', day_of_week=day_of_week)
except Exception as e: except Exception as e:
flash(f'Could not read carrier image: {str(e)}', 'error') flash(f'Could not read carrier image: {str(e)}', 'error')
return render_template('encode.html') return render_template('encode.html', day_of_week=day_of_week)
# Get date # Get date
date_str = datetime.now().strftime('%Y-%m-%d') date_str = datetime.now().strftime('%Y-%m-%d')
@@ -524,14 +534,19 @@ def encode():
# Embed # Embed
stego_data, stats = embed_in_image(carrier_data, encrypted, pixel_key) stego_data, stats = embed_in_image(carrier_data, encrypted, pixel_key)
# Return as download # Generate filename and file ID
return send_file( filename = f'{secrets.token_hex(4)}_{datetime.now().strftime("%Y%m%d")}.png'
io.BytesIO(stego_data), file_id = secrets.token_urlsafe(16)
mimetype='image/png',
as_attachment=True, # Store temporarily for download/share
#download_name='stego_image.png' cleanup_temp_files() # Clean old files first
download_name=f'{secrets.token_hex(4)}_{datetime.now().strftime("%Y%m%d")}.png' TEMP_FILES[file_id] = {
) 'data': stego_data,
'filename': filename,
'timestamp': time.time()
}
return redirect(url_for('encode_result', file_id=file_id))
except Exception as e: except Exception as e:
flash(f'Error: {str(e)}', 'error') flash(f'Error: {str(e)}', 'error')
@@ -540,6 +555,57 @@ def encode():
return render_template('encode.html', day_of_week=day_of_week) return render_template('encode.html', day_of_week=day_of_week)
@app.route('/encode/result/<file_id>')
def encode_result(file_id):
if file_id not in TEMP_FILES:
flash('File expired or not found. Please encode again.', 'error')
return redirect(url_for('encode'))
file_info = TEMP_FILES[file_id]
return render_template('encode_result.html',
file_id=file_id,
filename=file_info['filename'])
@app.route('/encode/download/<file_id>')
def encode_download(file_id):
if file_id not in TEMP_FILES:
flash('File expired or not found.', 'error')
return redirect(url_for('encode'))
file_info = TEMP_FILES[file_id]
return send_file(
io.BytesIO(file_info['data']),
mimetype='image/png',
as_attachment=True,
download_name=file_info['filename']
)
@app.route('/encode/file/<file_id>')
def encode_file(file_id):
"""Serve file for Web Share API (inline, not attachment)."""
if file_id not in TEMP_FILES:
return "Not found", 404
file_info = TEMP_FILES[file_id]
return send_file(
io.BytesIO(file_info['data']),
mimetype='image/png',
as_attachment=False,
download_name=file_info['filename']
)
@app.route('/encode/cleanup/<file_id>', methods=['POST'])
def encode_cleanup(file_id):
"""Manually cleanup a file after sharing."""
TEMP_FILES.pop(file_id, None)
return jsonify({'status': 'ok'})
@app.route('/decode', methods=['GET', 'POST']) @app.route('/decode', methods=['GET', 'POST'])
def decode(): def decode():
if request.method == 'POST': if request.method == 'POST':

24
static/favicon.svg Normal file
View File

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#667eea"/>
<stop offset="100%" style="stop-color:#764ba2"/>
</linearGradient>
</defs>
<!-- Shield -->
<path d="M32 4 L56 14 L56 32 C56 48 44 58 32 62 C20 58 8 48 8 32 L8 14 Z"
fill="url(#grad)"/>
<!-- Photo frame -->
<rect x="16" y="18" width="32" height="24" rx="2" fill="#1a1a2e" stroke="#fff" stroke-width="1.5"/>
<!-- Mountain -->
<polygon points="16,42 26,30 34,36 48,22 48,42" fill="#667eea" opacity="0.5"/>
<!-- Lock -->
<rect x="24" y="30" width="16" height="12" rx="2" fill="#fff"/>
<path d="M27 30 L27 25 C27 20 37 20 37 25 L37 30" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
<circle cx="32" cy="35" r="2.5" fill="url(#grad)"/>
<rect x="31" y="35" width="2" height="4" fill="url(#grad)"/>
</svg>

After

Width:  |  Height:  |  Size: 964 B

99
static/logo.svg Normal file
View File

@@ -0,0 +1,99 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<defs>
<!-- Gradient for the shield/frame -->
<linearGradient id="shieldGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#667eea"/>
<stop offset="100%" style="stop-color:#764ba2"/>
</linearGradient>
<!-- Gradient for the photo -->
<linearGradient id="photoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#1a1a2e"/>
<stop offset="100%" style="stop-color:#16213e"/>
</linearGradient>
<!-- Glow effect -->
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<!-- Drop shadow -->
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
<feDropShadow dx="2" dy="4" stdDeviation="4" flood-color="#000" flood-opacity="0.3"/>
</filter>
</defs>
<!-- Background circle (optional, for icon use) -->
<circle cx="100" cy="100" r="95" fill="none" stroke="url(#shieldGrad)" stroke-width="2" opacity="0.3"/>
<!-- Shield shape (security) that also looks like a photo frame -->
<path d="M100 20
L170 45
L170 100
C170 145 140 175 100 185
C60 175 30 145 30 100
L30 45 Z"
fill="url(#shieldGrad)"
filter="url(#shadow)"
opacity="0.95"/>
<!-- Inner photo frame area -->
<rect x="50" y="55" width="100" height="75" rx="4" ry="4"
fill="url(#photoGrad)"
stroke="#fff"
stroke-width="2"
opacity="0.9"/>
<!-- Stylized mountain/landscape (photo content hint) -->
<polygon points="50,130 75,95 95,115 130,75 150,130"
fill="#667eea"
opacity="0.6"/>
<!-- Sun in photo -->
<circle cx="125" cy="75" r="12" fill="#ffd700" opacity="0.8"/>
<!-- Lock symbol overlay (centered on photo) -->
<g transform="translate(100, 105)" filter="url(#glow)">
<!-- Lock body -->
<rect x="-18" y="-5" width="36" height="28" rx="4" ry="4"
fill="#fff" opacity="0.95"/>
<!-- Lock shackle -->
<path d="M-10 -5 L-10 -18
C-10 -30 10 -30 10 -18
L10 -5"
fill="none"
stroke="#fff"
stroke-width="6"
stroke-linecap="round"
opacity="0.95"/>
<!-- Keyhole -->
<circle cx="0" cy="8" r="5" fill="url(#shieldGrad)"/>
<rect x="-2.5" y="8" width="5" height="10" rx="1" fill="url(#shieldGrad)"/>
</g>
<!-- Binary/pixel dots (steganography hint) - scattered around -->
<g opacity="0.4" fill="#fff">
<circle cx="40" cy="85" r="2"/>
<circle cx="45" cy="140" r="1.5"/>
<circle cx="160" cy="80" r="2"/>
<circle cx="155" cy="135" r="1.5"/>
<circle cx="38" cy="110" r="1"/>
<circle cx="162" cy="110" r="1"/>
<circle cx="65" cy="150" r="1.5"/>
<circle cx="135" cy="150" r="1.5"/>
</g>
<!-- "Hidden" pixels effect on edges -->
<g opacity="0.3">
<rect x="52" y="57" width="3" height="3" fill="#667eea"/>
<rect x="58" y="57" width="3" height="3" fill="#764ba2"/>
<rect x="145" y="57" width="3" height="3" fill="#667eea"/>
<rect x="139" y="57" width="3" height="3" fill="#764ba2"/>
<rect x="52" y="124" width="3" height="3" fill="#764ba2"/>
<rect x="145" y="124" width="3" height="3" fill="#764ba2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Stegasoo{% endblock %}</title> <title>{% block title %}Stegasoo{% endblock %}</title>
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
<style> <style>
@@ -121,7 +122,7 @@
<nav class="navbar navbar-expand-lg navbar-dark"> <nav class="navbar navbar-expand-lg navbar-dark">
<div class="container"> <div class="container">
<a class="navbar-brand d-flex align-items-center" href="/"> <a class="navbar-brand d-flex align-items-center" href="/">
<i class="bi bi-shield-lock-fill me-2"></i> <img src="{{ url_for('static', filename='logo.svg') }}" alt="Stegasoo" height="36" class="me-2">
<span class="fw-bold">Stegasoo</span> <span class="fw-bold">Stegasoo</span>
</a> </a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
@@ -168,7 +169,7 @@
<footer class="py-4 mt-5"> <footer class="py-4 mt-5">
<div class="container text-center text-muted"> <div class="container text-center text-muted">
<small> <small>
<i class="bi bi-shield-check me-1"></i> <img src="{{ url_for('static', filename='favicon.svg') }}" alt="" height="16" class="me-1" style="vertical-align: text-bottom;">
Stegasoo v3.1 — Hybrid Photo + Day-Phrase + PIN Steganography Stegasoo v3.1 — Hybrid Photo + Day-Phrase + PIN Steganography
</small> </small>
</div> </div>

View File

@@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Decode Message - Stegasoo{% endblock %} {% block title %}Decode Message - StegoCrypt{% endblock %}
{% block content %} {% block content %}
<div class="row justify-content-center"> <div class="row justify-content-center">

View File

@@ -51,7 +51,7 @@
<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 3-word phrase for today (from your phrase card) Your phrase for <strong>today</strong> (based on your local timezone)
</div> </div>
</div> </div>
@@ -68,7 +68,7 @@
</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">
<i class="bi bi-lock me-2"></i>Encode & Download <i class="bi bi-lock me-2"></i>Encode Message
</button> </button>
</form> </form>
@@ -106,25 +106,8 @@
<script> <script>
document.getElementById('encodeForm').addEventListener('submit', function(e) { document.getElementById('encodeForm').addEventListener('submit', function(e) {
const btn = document.getElementById('encodeBtn'); const btn = document.getElementById('encodeBtn');
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Encoding...'; btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Encoding...';
btn.disabled = true; btn.disabled = true;
// Argon2 key derivation can take a few seconds
// Reset button after encoding completes (file downloads don't navigate)
setTimeout(function() {
btn.innerHTML = '<i class="bi bi-check-circle me-2"></i>Done! Encode Another?';
btn.disabled = false;
btn.classList.remove('btn-primary');
btn.classList.add('btn-success');
// Reset to original state after a moment
setTimeout(function() {
btn.innerHTML = '<i class="bi bi-lock me-2"></i>Encode & Download';
btn.classList.remove('btn-success');
btn.classList.add('btn-primary');
}, 2000);
}, 4000); // 4 seconds for Argon2 + embedding
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,142 @@
{% extends "base.html" %}
{% block title %}Message Encoded - Stegasoo{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-check-circle-fill me-2"></i>Message Encoded Successfully!</h5>
</div>
<div class="card-body text-center">
<div class="mb-4">
<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 class="d-grid gap-3 mb-4">
<a href="{{ url_for('encode_download', file_id=file_id) }}"
class="btn btn-primary btn-lg" id="downloadBtn">
<i class="bi bi-download me-2"></i>Download Image
</a>
<button type="button" class="btn btn-outline-light btn-lg" id="shareBtn">
<i class="bi bi-share me-2"></i>Share Image
</button>
</div>
<!-- Fallback share options (shown if Web Share API unavailable) -->
<div id="shareFallback" class="d-none">
<p class="text-muted mb-3">Share via:</p>
<div class="d-flex justify-content-center gap-2 flex-wrap">
<a href="#" id="shareEmail" class="btn btn-outline-secondary">
<i class="bi bi-envelope me-1"></i>Email
</a>
<a href="#" id="shareTelegram" class="btn btn-outline-secondary">
<i class="bi bi-telegram me-1"></i>Telegram
</a>
<a href="#" id="shareWhatsapp" class="btn btn-outline-secondary">
<i class="bi bi-whatsapp me-1"></i>WhatsApp
</a>
<button type="button" id="copyLink" class="btn btn-outline-secondary">
<i class="bi bi-link-45deg me-1"></i>Copy Link
</button>
</div>
</div>
<hr class="my-4">
<div class="alert alert-warning small text-start">
<i class="bi bi-clock me-1"></i>
<strong>File expires in 5 minutes.</strong>
Download or share now. The file will be securely deleted after expiry.
</div>
<a href="{{ url_for('encode') }}" class="btn btn-outline-light">
<i class="bi bi-plus-circle me-2"></i>Encode Another Message
</a>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const fileId = "{{ file_id }}";
const filename = "{{ filename }}";
const fileUrl = "{{ url_for('encode_file', file_id=file_id, _external=True) }}";
const downloadUrl = "{{ url_for('encode_download', file_id=file_id, _external=True) }}";
const shareBtn = document.getElementById('shareBtn');
const shareFallback = document.getElementById('shareFallback');
// Check if Web Share API with files is supported
async function canShareFiles() {
if (!navigator.canShare) return false;
// Create a test file to check
const testFile = new File(['test'], 'test.png', { type: 'image/png' });
return navigator.canShare({ files: [testFile] });
}
shareBtn.addEventListener('click', async function() {
const canShare = await canShareFiles();
if (canShare) {
try {
// Fetch the image as a blob
const response = await fetch(fileUrl);
const blob = await response.blob();
const file = new File([blob], filename, { type: 'image/png' });
await navigator.share({
files: [file],
title: 'Shared Image',
});
// Cleanup after successful share
fetch("{{ url_for('encode_cleanup', file_id=file_id) }}", { method: 'POST' });
} catch (err) {
if (err.name !== 'AbortError') {
console.error('Share failed:', err);
shareFallback.classList.remove('d-none');
}
}
} else {
// Show fallback options
shareFallback.classList.remove('d-none');
}
});
// Fallback share links
document.getElementById('shareEmail').href =
`mailto:?subject=Shared Image&body=Check out this image: ${downloadUrl}`;
document.getElementById('shareTelegram').href =
`https://t.me/share/url?url=${encodeURIComponent(downloadUrl)}`;
document.getElementById('shareWhatsapp').href =
`https://wa.me/?text=${encodeURIComponent('Check this out: ' + downloadUrl)}`;
document.getElementById('copyLink').addEventListener('click', function() {
navigator.clipboard.writeText(downloadUrl).then(() => {
this.innerHTML = '<i class="bi bi-check me-1"></i>Copied!';
setTimeout(() => {
this.innerHTML = '<i class="bi bi-link-45deg me-1"></i>Copy Link';
}, 2000);
});
});
// Cleanup after download
document.getElementById('downloadBtn').addEventListener('click', function() {
// Give time for download to start, then cleanup
setTimeout(() => {
fetch("{{ url_for('encode_cleanup', file_id=file_id) }}", { method: 'POST' });
}, 3000);
});
</script>
{% endblock %}

View File

@@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Generate Phrase Card - Stegasoo{% endblock %} {% block title %}Generate Phrase Card - StegoCrypt{% endblock %}
{% block content %} {% block content %}
<div class="row justify-content-center"> <div class="row justify-content-center">
@@ -85,15 +85,15 @@
<table class="table table-dark table-hover"> <table class="table table-dark table-hover">
<thead> <thead>
<tr> <tr>
<th style="width: 120px;">Day</th> <th style="width: 140px;">Day</th>
<th>Phrase</th> <th>Phrase</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for day in days %} {% for day in days %}
<tr> <tr>
<td> <td class="text-nowrap">
<i class="bi bi-calendar-day me-2"></i>{{ day }} <i class="bi bi-calendar3 me-2"></i>{{ day }}
</td> </td>
<td> <td>
<span class="phrase-display">{{ phrases[day] }}</span> <span class="phrase-display">{{ phrases[day] }}</span>

View File

@@ -4,8 +4,8 @@
{% block content %} {% block content %}
<div class="text-center mb-5"> <div class="text-center mb-5">
<i class="bi bi-shield-lock-fill hero-icon"></i> <img src="{{ url_for('static', filename='logo.svg') }}" alt="Stegasoo" height="120" class="mb-3">
<h1 class="display-4 fw-bold mt-3">Stegasoo</h1> <h1 class="display-4 fw-bold">Stegasoo</h1>
<p class="lead text-muted">Hide encrypted messages in plain sight using advanced steganography</p> <p class="lead text-muted">Hide encrypted messages in plain sight using advanced steganography</p>
</div> </div>