Basic quility of like improvements.
This commit is contained in:
@@ -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
104
app.py
@@ -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
24
static/favicon.svg
Normal 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
99
static/logo.svg
Normal 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 |
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
142
templates/encode_result.html
Normal file
142
templates/encode_result.html
Normal 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 %}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user