More tweaks
This commit is contained in:
@@ -15,8 +15,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install Python dependencies
|
# Install Python dependencies
|
||||||
|
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
#COPY requirements-ml.txt .
|
||||||
|
|
||||||
|
RUN pip install --upgrade pip
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt --root-user-action=ignore
|
||||||
|
#RUN pip install --no-cache-dir -r requirements-ml.txt
|
||||||
|
|
||||||
# Copy application
|
# Copy application
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
64
README.md
64
README.md
@@ -1,4 +1,4 @@
|
|||||||
# StegoCrypt Web Service
|
# Stegasoo 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.
|
||||||
|
|
||||||
@@ -12,10 +12,11 @@ A containerized Flask + Bootstrap web UI for hybrid Photo + Day-Phrase + PIN ste
|
|||||||
- 🔐 **AES-256-GCM** authenticated encryption
|
- 🔐 **AES-256-GCM** authenticated encryption
|
||||||
- 🧠 **Argon2id** memory-hard key derivation (256MB)
|
- 🧠 **Argon2id** memory-hard key derivation (256MB)
|
||||||
- 🎲 **Pseudo-random pixel selection** defeats steganalysis
|
- 🎲 **Pseudo-random pixel selection** defeats steganalysis
|
||||||
- 📅 **Daily key rotation** with 3-word phrases
|
- 📅 **Daily key rotation** with customizable phrases (3-12 words)
|
||||||
- 🔢 **Static PIN** for additional entropy
|
- 🔢 **Static PIN** for additional entropy (6-8 digits)
|
||||||
- 🖼️ **Reference photo** as "something you have"
|
- 🖼️ **Reference photo** as "something you have"
|
||||||
- 🌐 **Web UI** with Bootstrap 5 dark theme
|
- 🌐 **Web UI** with Bootstrap 5 dark theme
|
||||||
|
- 📖 **Memory aid stories** to help memorize phrases (template or AI-powered)
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@@ -39,6 +40,9 @@ source venv/bin/activate # Linux/Mac
|
|||||||
# Install dependencies
|
# Install dependencies
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Optional: Enable AI-powered story generation
|
||||||
|
pip install -r requirements-ml.txt
|
||||||
|
|
||||||
# Run development server
|
# Run development server
|
||||||
python app.py
|
python app.py
|
||||||
|
|
||||||
@@ -51,8 +55,9 @@ gunicorn --bind 0.0.0.0:5000 app:app
|
|||||||
### 1. Generate Credentials
|
### 1. Generate Credentials
|
||||||
|
|
||||||
Visit `/generate` to create:
|
Visit `/generate` to create:
|
||||||
- **7 three-word phrases** (one per day of week)
|
- **7 phrases** (one per day of week, 3-12 words each)
|
||||||
- **1 six-digit PIN** (same every day)
|
- **1 PIN** (6-8 digits, same every day)
|
||||||
|
- **Memory aid stories** (optional, helps memorize phrases)
|
||||||
|
|
||||||
Memorize these! Don't save them.
|
Memorize these! Don't save them.
|
||||||
|
|
||||||
@@ -62,8 +67,8 @@ Visit `/encode` and provide:
|
|||||||
- **Reference photo** - A photo both parties have (NOT transmitted)
|
- **Reference photo** - A photo both parties have (NOT transmitted)
|
||||||
- **Carrier image** - The image to hide your message in
|
- **Carrier image** - The image to hide your message in
|
||||||
- **Message** - Your secret text
|
- **Message** - Your secret text
|
||||||
- **Day phrase** - Today's 3-word phrase
|
- **Day phrase** - Today's phrase
|
||||||
- **PIN** - Your static 6-digit PIN
|
- **PIN** - Your static PIN
|
||||||
|
|
||||||
Download the stego image and share it through any channel.
|
Download the stego image and share it through any channel.
|
||||||
|
|
||||||
@@ -80,28 +85,30 @@ Visit `/decode` and provide:
|
|||||||
| Component | Entropy | Purpose |
|
| Component | Entropy | Purpose |
|
||||||
|-----------|---------|---------|
|
|-----------|---------|---------|
|
||||||
| Reference Photo | ~80-256 bits | Something you have |
|
| Reference Photo | ~80-256 bits | Something you have |
|
||||||
| 3-Word Phrase | ~33 bits | Something you know (rotates daily) |
|
| Day Phrase | ~33-132 bits | Something you know (rotates daily) |
|
||||||
| 6-Digit PIN | ~20 bits | Something you know (static) |
|
| PIN | ~20-27 bits | Something you know (static) |
|
||||||
| **Combined** | **133+ bits** | **Beyond brute force** |
|
| **Combined** | **133-415+ bits** | **Beyond brute force** |
|
||||||
|
|
||||||
### Attack Resistance
|
### Attack Resistance
|
||||||
|
|
||||||
| Attack | Result |
|
| Attack | Result |
|
||||||
|--------|--------|
|
|--------|--------|
|
||||||
| Brute force | 2^133 combinations = impossible |
|
| Brute force | 2^133+ combinations = impossible |
|
||||||
| Rainbow tables | Random salt per message |
|
| Rainbow tables | Random salt per message |
|
||||||
| Steganalysis | Random pixel selection defeats detection |
|
| Steganalysis | Random pixel selection defeats detection |
|
||||||
| GPU cracking | Argon2 requires 256MB RAM per attempt |
|
| GPU cracking | Argon2 requires 256MB RAM per attempt |
|
||||||
|
|
||||||
## API Endpoints
|
## Memory Aid Stories
|
||||||
|
|
||||||
| Endpoint | Method | Description |
|
The generate page can create stories to help you memorize your phrases:
|
||||||
|----------|--------|-------------|
|
|
||||||
| `/` | GET | Home page |
|
**Template-based** (default):
|
||||||
| `/generate` | GET/POST | Generate phrase card + PIN |
|
> Monday morning began when I discovered a **APPLE** near the **FOREST**. I had to **THUNDER** quickly, then grab the **CRYSTAL** before reaching the **BRAVE**.
|
||||||
| `/encode` | GET/POST | Encode message in image |
|
|
||||||
| `/decode` | GET/POST | Decode message from image |
|
**AI-powered** (with `requirements-ml.txt`):
|
||||||
| `/about` | GET | Security information |
|
- Uses DistilGPT-2 (~350MB model)
|
||||||
|
- Generates more coherent, natural stories
|
||||||
|
- Words highlighted in RED CAPS
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@@ -121,25 +128,6 @@ For production, consider:
|
|||||||
3. **Logging** - Monitor for security events
|
3. **Logging** - Monitor for security events
|
||||||
4. **Memory** - Allocate at least 512MB (Argon2 needs 256MB)
|
4. **Memory** - Allocate at least 512MB (Argon2 needs 256MB)
|
||||||
|
|
||||||
Example nginx config:
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name stegocrypt.example.com;
|
|
||||||
|
|
||||||
ssl_certificate /path/to/cert.pem;
|
|
||||||
ssl_certificate_key /path/to/key.pem;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://stegocrypt:5000;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
client_max_body_size 50M;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT License - Use responsibly.
|
MIT License - Use responsibly.
|
||||||
|
|||||||
16
app.py
16
app.py
@@ -25,6 +25,7 @@ from flask import Flask, render_template, request, send_file, jsonify, flash, re
|
|||||||
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
|
||||||
@@ -445,6 +446,10 @@ def generate():
|
|||||||
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))
|
pin_length = int(request.form.get('pin_length', 6))
|
||||||
|
|
||||||
|
# Disable generate_stories for now (until much better)
|
||||||
|
#generate_stories = request.form.get('generate_stories') == 'on'
|
||||||
|
generate_stories = request.form.get('generate_stories') == 'off'
|
||||||
|
|
||||||
# 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(6, min(8, pin_length))
|
||||||
@@ -457,6 +462,11 @@ def generate():
|
|||||||
pin_entropy = int(pin_length * 3.32) # log2(10) ≈ 3.32 bits per digit
|
pin_entropy = int(pin_length * 3.32) # log2(10) ≈ 3.32 bits per digit
|
||||||
total_entropy = phrase_entropy + pin_entropy
|
total_entropy = phrase_entropy + pin_entropy
|
||||||
|
|
||||||
|
# Generate memory aid stories if requested
|
||||||
|
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,
|
||||||
pin=pin,
|
pin=pin,
|
||||||
@@ -466,8 +476,10 @@ def generate():
|
|||||||
pin_length=pin_length,
|
pin_length=pin_length,
|
||||||
phrase_entropy=phrase_entropy,
|
phrase_entropy=phrase_entropy,
|
||||||
pin_entropy=pin_entropy,
|
pin_entropy=pin_entropy,
|
||||||
total_entropy=total_entropy)
|
total_entropy=total_entropy,
|
||||||
return render_template('generate.html', generated=False)
|
stories=stories,
|
||||||
|
has_ml=HAS_ML)
|
||||||
|
return render_template('generate.html', generated=False, has_ml=HAS_ML)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/encode', methods=['GET', 'POST'])
|
@app.route('/encode', methods=['GET', 'POST'])
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
stegocrypt:
|
stegasoo:
|
||||||
build: .
|
build: .
|
||||||
container_name: stegocrypt
|
container_name: stegasoo
|
||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
environment:
|
environment:
|
||||||
@@ -29,5 +29,5 @@ services:
|
|||||||
# - ./nginx.conf:/etc/nginx/nginx.conf:ro
|
# - ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
# - ./certs:/etc/nginx/certs:ro
|
# - ./certs:/etc/nginx/certs:ro
|
||||||
# depends_on:
|
# depends_on:
|
||||||
# - stegocrypt
|
# - stegasoo
|
||||||
# restart: unless-stopped
|
# restart: unless-stopped
|
||||||
|
|||||||
8
requirements-ml.txt
Normal file
8
requirements-ml.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# ML dependencies for AI-powered story generation
|
||||||
|
# Install with: pip install -r requirements-ml.txt
|
||||||
|
#
|
||||||
|
# Note: These add ~1-2GB disk space for model downloads
|
||||||
|
# The app works without these (falls back to template-based stories)
|
||||||
|
|
||||||
|
transformers>=4.35.0
|
||||||
|
torch>=2.0.0
|
||||||
@@ -7,5 +7,10 @@ cryptography>=41.0.0
|
|||||||
# Memory-hard key derivation (highly recommended)
|
# Memory-hard key derivation (highly recommended)
|
||||||
argon2-cffi>=23.0.0
|
argon2-cffi>=23.0.0
|
||||||
|
|
||||||
|
# Optional: ML story generation (adds ~1GB disk space)
|
||||||
|
# Uncomment for AI-powered memory aid stories
|
||||||
|
# transformers>=4.35.0
|
||||||
|
# torch>=2.0.0
|
||||||
|
|
||||||
# Optional: For production deployment
|
# Optional: For production deployment
|
||||||
# gevent>=23.0.0
|
# gevent>=23.0.0
|
||||||
|
|||||||
244
story_generator.py
Normal file
244
story_generator.py
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
"""
|
||||||
|
Story Generator for Passphrase Memorization
|
||||||
|
Uses lightweight ML (DistilGPT-2) for coherent stories, with template fallback.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# Try to import ML libraries
|
||||||
|
try:
|
||||||
|
from transformers import pipeline, set_seed
|
||||||
|
import torch
|
||||||
|
HAS_ML = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_ML = False
|
||||||
|
|
||||||
|
# Global generator (lazy loaded)
|
||||||
|
_generator = None
|
||||||
|
_model_loaded = False
|
||||||
|
|
||||||
|
|
||||||
|
def get_generator():
|
||||||
|
"""Lazy load the text generation model."""
|
||||||
|
global _generator, _model_loaded
|
||||||
|
|
||||||
|
if not HAS_ML:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not _model_loaded:
|
||||||
|
try:
|
||||||
|
# Use distilgpt2 - small (~350MB) and fast
|
||||||
|
device = 0 if torch.cuda.is_available() else -1
|
||||||
|
_generator = pipeline(
|
||||||
|
'text-generation',
|
||||||
|
model='distilgpt2',
|
||||||
|
device=device,
|
||||||
|
torch_dtype=torch.float32
|
||||||
|
)
|
||||||
|
_model_loaded = True
|
||||||
|
print("ML story generator loaded successfully")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Could not load ML model: {e}. Using templates.")
|
||||||
|
_generator = None
|
||||||
|
_model_loaded = True # Don't retry
|
||||||
|
|
||||||
|
return _generator
|
||||||
|
|
||||||
|
|
||||||
|
def generate_story_ml(day: str, words: list[str], max_attempts: int = 3) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Generate a story using ML that incorporates all passphrase words.
|
||||||
|
|
||||||
|
Returns None if ML is unavailable or generation fails.
|
||||||
|
"""
|
||||||
|
generator = get_generator()
|
||||||
|
if generator is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Create a compelling prompt
|
||||||
|
words_str = ', '.join(words[:-1]) + f', and {words[-1]}' if len(words) > 1 else words[0]
|
||||||
|
|
||||||
|
prompts = [
|
||||||
|
f"{day}, something memorable happened including: {words_str}.",
|
||||||
|
]
|
||||||
|
|
||||||
|
prompt = random.choice(prompts)
|
||||||
|
|
||||||
|
try:
|
||||||
|
set_seed(random.randint(0, 10000))
|
||||||
|
|
||||||
|
# Generate text
|
||||||
|
result = generator(
|
||||||
|
prompt,
|
||||||
|
max_new_tokens=80,
|
||||||
|
num_return_sequences=1,
|
||||||
|
temperature=0.8,
|
||||||
|
top_p=0.9,
|
||||||
|
do_sample=True,
|
||||||
|
pad_token_id=50256, # eos token for gpt2
|
||||||
|
)
|
||||||
|
|
||||||
|
story = result[0]['generated_text']
|
||||||
|
|
||||||
|
# Clean up - get just a few sentences
|
||||||
|
story = story.strip()
|
||||||
|
|
||||||
|
# Try to end at a sentence boundary
|
||||||
|
for end_char in ['. ', '! ', '? ']:
|
||||||
|
last_end = story.rfind(end_char)
|
||||||
|
if last_end > len(prompt) + 20:
|
||||||
|
story = story[:last_end + 1]
|
||||||
|
break
|
||||||
|
|
||||||
|
# Verify most words are present (ML doesn't always include all)
|
||||||
|
story_lower = story.lower()
|
||||||
|
words_found = sum(1 for w in words if w.lower() in story_lower)
|
||||||
|
|
||||||
|
if words_found < len(words) * 0.5: # At least 50% of words
|
||||||
|
# Append missing words naturally
|
||||||
|
missing = [w for w in words if w.lower() not in story_lower]
|
||||||
|
if missing:
|
||||||
|
story += f" Don't forget: {', '.join(missing)}."
|
||||||
|
|
||||||
|
return story
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ML generation error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# TEMPLATE FALLBACK (always available)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
STORY_TEMPLATES = {
|
||||||
|
'Monday': [
|
||||||
|
"Monday morning began when I discovered a {0} near the {1}. I had to {2} quickly, then grab the {3} before reaching the {4}.",
|
||||||
|
"The week started with a {0} appearing at the {1}. My plan was to {2}, secure the {3}, and head toward the {4}.",
|
||||||
|
"On Monday, the {0} and the {1} crossed paths. We decided to {2}, bring the {3}, and meet at the {4}.",
|
||||||
|
],
|
||||||
|
'Tuesday': [
|
||||||
|
"Tuesday brought a {0} to the {1}. Everyone wanted to {2}, especially with the {3} near the {4}.",
|
||||||
|
"The {0} arrived Tuesday carrying a {1}. Together we would {2}, protect the {3}, and explore the {4}.",
|
||||||
|
"On Tuesday, my {0} transformed into a {1}. I needed to {2}, find the {3}, and unlock the {4}.",
|
||||||
|
],
|
||||||
|
'Wednesday': [
|
||||||
|
"By Wednesday, the {0} had found a {1}. The mission: {2}, retrieve the {3}, and guard the {4}.",
|
||||||
|
"Midweek magic: a {0} emerged from the {1}. We had to {2}, grab the {3}, and escape to the {4}.",
|
||||||
|
"Wednesday's {0} was hiding near the {1}. To {2} successfully, we needed the {3} and the {4}.",
|
||||||
|
],
|
||||||
|
'Thursday': [
|
||||||
|
"Thursday's {0} came with a {1}. Our plan: {2}, then move the {3} inside the {4}.",
|
||||||
|
"On Thursday, the {0} met the {1} unexpectedly. They decided to {2}, share the {3}, and visit the {4}.",
|
||||||
|
"The {0} adventure on Thursday led us to a {1}. We chose to {2}, carry the {3}, and discover the {4}.",
|
||||||
|
],
|
||||||
|
'Friday': [
|
||||||
|
"Friday arrived with a {0} and a {1}. Time to {2}, celebrate with the {3}, and toast the {4}!",
|
||||||
|
"TGIF! The {0} party featured a {1}. We would {2}, enjoy the {3}, and dance around the {4}.",
|
||||||
|
"Friday's surprise was a {0} inside a {1}. Everyone wanted to {2}, taste the {3}, and admire the {4}.",
|
||||||
|
],
|
||||||
|
'Saturday': [
|
||||||
|
"Saturday morning, the {0} journeyed to the {1}. Goals: {2}, collect the {3}, and protect the {4}.",
|
||||||
|
"Weekend mode: a {0} relaxing near a {1}. I chose to {2}, photograph the {3}, and sketch the {4}.",
|
||||||
|
"On Saturday, the legendary {0} appeared at the {1}. Heroes must {2}, wield the {3}, and defeat the {4}.",
|
||||||
|
],
|
||||||
|
'Sunday': [
|
||||||
|
"Sunday peace was broken by a {0} and a {1}. We needed to {2}, fix the {3}, and restore the {4}.",
|
||||||
|
"A quiet Sunday with my {0} near the {1}. Plans: {2} later, maybe find the {3}, or visit the {4}.",
|
||||||
|
"Sunday sunset revealed a {0} beside a {1}. Time to {2}, remember the {3}, and dream of the {4}.",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extensions for 6+ word phrases
|
||||||
|
EXTENSIONS = [
|
||||||
|
[" Suddenly, a {5} appeared!"],
|
||||||
|
[" The {6} changed everything."],
|
||||||
|
[" Behind it was a {7}."],
|
||||||
|
[" Plus a mysterious {8}."],
|
||||||
|
[" The {9} completed the quest."],
|
||||||
|
[" A {10} watched from afar."],
|
||||||
|
[" And finally, the legendary {11}."],
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def generate_story_template(day: str, words: list[str]) -> str:
|
||||||
|
"""Generate story using templates (fallback method)."""
|
||||||
|
templates = STORY_TEMPLATES.get(day, STORY_TEMPLATES['Monday'])
|
||||||
|
template = random.choice(templates)
|
||||||
|
|
||||||
|
# Add extensions for longer phrases
|
||||||
|
for i, ext_list in enumerate(EXTENSIONS):
|
||||||
|
word_idx = i + 5
|
||||||
|
if len(words) > word_idx:
|
||||||
|
template += random.choice(ext_list)
|
||||||
|
|
||||||
|
# Pad words list to ensure we have enough for any template
|
||||||
|
padded_words = words + [''] * (12 - len(words))
|
||||||
|
|
||||||
|
return template.format(*padded_words)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# MAIN API
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def generate_story(day: str, words: list[str], use_ml: bool = True) -> dict:
|
||||||
|
"""
|
||||||
|
Generate a memorable story incorporating the passphrase words.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
day: Day of the week (e.g., 'Monday')
|
||||||
|
words: List of passphrase words
|
||||||
|
use_ml: Whether to try ML generation first
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict with 'story' (plain text) and 'story_html' (with highlighted words)
|
||||||
|
"""
|
||||||
|
story = None
|
||||||
|
used_ml = False
|
||||||
|
|
||||||
|
# Try ML first if requested
|
||||||
|
if use_ml and HAS_ML:
|
||||||
|
story = generate_story_ml(day, words)
|
||||||
|
if story:
|
||||||
|
used_ml = True
|
||||||
|
|
||||||
|
# Fall back to templates
|
||||||
|
if story is None:
|
||||||
|
story = generate_story_template(day, words)
|
||||||
|
|
||||||
|
# Generate HTML version with highlighted words (RED and CAPS)
|
||||||
|
html_story = story
|
||||||
|
for word in words:
|
||||||
|
# Case-insensitive replacement with highlighted version
|
||||||
|
pattern = re.compile(re.escape(word), re.IGNORECASE)
|
||||||
|
html_story = pattern.sub(
|
||||||
|
f'<span class="story-word">{word.upper()}</span>',
|
||||||
|
html_story
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'story': story,
|
||||||
|
'story_html': html_story,
|
||||||
|
'used_ml': used_ml
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def generate_all_stories(phrases: dict[str, str], use_ml: bool = True) -> dict[str, dict]:
|
||||||
|
"""
|
||||||
|
Generate stories for all days.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
phrases: Dict mapping day names to phrase strings
|
||||||
|
use_ml: Whether to use ML generation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict mapping day names to story dicts
|
||||||
|
"""
|
||||||
|
stories = {}
|
||||||
|
for day, phrase in phrases.items():
|
||||||
|
words = phrase.split()
|
||||||
|
stories[day] = generate_story(day, words, use_ml=use_ml)
|
||||||
|
return stories
|
||||||
@@ -97,10 +97,33 @@
|
|||||||
word-spacing: 0.3rem;
|
word-spacing: 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.story-word {
|
||||||
|
color: #ff6b6b;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-card {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-left: 3px solid var(--gradient-start);
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-card .day-label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--gradient-start);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.pin-display {
|
.pin-display {
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
|
color: #ff9900;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
letter-spacing: 0.5rem;
|
letter-spacing: 0.3rem;
|
||||||
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
|
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
|
|||||||
@@ -59,18 +59,33 @@
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary btn-lg w-100">
|
<!--<div class="form-check mb-4">
|
||||||
|
<input class="form-check-input" type="checkbox" name="generate_stories" id="generateStories" checked>
|
||||||
|
<label class="form-check-label" for="generateStories">
|
||||||
|
<i class="bi bi-book me-2"></i>Generate memory aid stories
|
||||||
|
{% 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">
|
||||||
<i class="bi bi-shuffle me-2"></i>Generate New Credentials
|
<i class="bi bi-shuffle me-2"></i>Generate New Credentials
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-exclamation-circle me-2"></i>
|
||||||
|
<strong>Credentials Generated!</strong> - Refresh to generate new credentials
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||||
<strong>Memorize this information, then close this page!</strong>
|
<strong>Memorize the information then close!</strong> - Do not save/screenshot
|
||||||
Do not save or screenshot. Refresh to generate new credentials.
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<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-display">{{ pin }}</div>
|
<div class="pin-display">{{ pin }}</div>
|
||||||
@@ -135,6 +150,26 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
@@ -176,6 +211,18 @@ function updateEntropy() {
|
|||||||
|
|
||||||
document.getElementById('wordsSelect').addEventListener('change', updateEntropy);
|
document.getElementById('wordsSelect').addEventListener('change', updateEntropy);
|
||||||
document.getElementById('pinSelect').addEventListener('change', updateEntropy);
|
document.getElementById('pinSelect').addEventListener('change', updateEntropy);
|
||||||
|
|
||||||
|
// Loading state for generate button
|
||||||
|
document.querySelector('form').addEventListener('submit', function() {
|
||||||
|
const btn = document.getElementById('generateBtn');
|
||||||
|
const storiesChecked = document.getElementById('generateStories').checked;
|
||||||
|
btn.disabled = true;
|
||||||
|
if (storiesChecked) {
|
||||||
|
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...';
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user