diff --git a/Dockerfile b/Dockerfile
index bf41094..9b67308 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -15,8 +15,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
+
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 . .
diff --git a/README.md b/README.md
index 39f9f74..c2113ad 100644
--- a/README.md
+++ b/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.
@@ -12,10 +12,11 @@ A containerized Flask + Bootstrap web UI for hybrid Photo + Day-Phrase + PIN ste
- 🔐 **AES-256-GCM** authenticated encryption
- 🧠 **Argon2id** memory-hard key derivation (256MB)
- 🎲 **Pseudo-random pixel selection** defeats steganalysis
-- 📅 **Daily key rotation** with 3-word phrases
-- 🔢 **Static PIN** for additional entropy
+- 📅 **Daily key rotation** with customizable phrases (3-12 words)
+- 🔢 **Static PIN** for additional entropy (6-8 digits)
- 🖼️ **Reference photo** as "something you have"
- 🌐 **Web UI** with Bootstrap 5 dark theme
+- 📖 **Memory aid stories** to help memorize phrases (template or AI-powered)
## Quick Start
@@ -39,6 +40,9 @@ source venv/bin/activate # Linux/Mac
# Install dependencies
pip install -r requirements.txt
+# Optional: Enable AI-powered story generation
+pip install -r requirements-ml.txt
+
# Run development server
python app.py
@@ -51,8 +55,9 @@ gunicorn --bind 0.0.0.0:5000 app:app
### 1. Generate Credentials
Visit `/generate` to create:
-- **7 three-word phrases** (one per day of week)
-- **1 six-digit PIN** (same every day)
+- **7 phrases** (one per day of week, 3-12 words each)
+- **1 PIN** (6-8 digits, same every day)
+- **Memory aid stories** (optional, helps memorize phrases)
Memorize these! Don't save them.
@@ -62,8 +67,8 @@ Visit `/encode` and provide:
- **Reference photo** - A photo both parties have (NOT transmitted)
- **Carrier image** - The image to hide your message in
- **Message** - Your secret text
-- **Day phrase** - Today's 3-word phrase
-- **PIN** - Your static 6-digit PIN
+- **Day phrase** - Today's phrase
+- **PIN** - Your static PIN
Download the stego image and share it through any channel.
@@ -80,28 +85,30 @@ Visit `/decode` and provide:
| Component | Entropy | Purpose |
|-----------|---------|---------|
| Reference Photo | ~80-256 bits | Something you have |
-| 3-Word Phrase | ~33 bits | Something you know (rotates daily) |
-| 6-Digit PIN | ~20 bits | Something you know (static) |
-| **Combined** | **133+ bits** | **Beyond brute force** |
+| Day Phrase | ~33-132 bits | Something you know (rotates daily) |
+| PIN | ~20-27 bits | Something you know (static) |
+| **Combined** | **133-415+ bits** | **Beyond brute force** |
### Attack Resistance
| Attack | Result |
|--------|--------|
-| Brute force | 2^133 combinations = impossible |
+| Brute force | 2^133+ combinations = impossible |
| Rainbow tables | Random salt per message |
| Steganalysis | Random pixel selection defeats detection |
| GPU cracking | Argon2 requires 256MB RAM per attempt |
-## API Endpoints
+## Memory Aid Stories
-| Endpoint | Method | Description |
-|----------|--------|-------------|
-| `/` | GET | Home page |
-| `/generate` | GET/POST | Generate phrase card + PIN |
-| `/encode` | GET/POST | Encode message in image |
-| `/decode` | GET/POST | Decode message from image |
-| `/about` | GET | Security information |
+The generate page can create stories to help you memorize your phrases:
+
+**Template-based** (default):
+> Monday morning began when I discovered a **APPLE** near the **FOREST**. I had to **THUNDER** quickly, then grab the **CRYSTAL** before reaching the **BRAVE**.
+
+**AI-powered** (with `requirements-ml.txt`):
+- Uses DistilGPT-2 (~350MB model)
+- Generates more coherent, natural stories
+- Words highlighted in RED CAPS
## Configuration
@@ -121,25 +128,6 @@ For production, consider:
3. **Logging** - Monitor for security events
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
MIT License - Use responsibly.
diff --git a/app.py b/app.py
index e0bba00..4cd36a5 100644
--- a/app.py
+++ b/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 PIL import Image
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.backends import default_backend
@@ -445,6 +446,10 @@ def generate():
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)
+ #generate_stories = request.form.get('generate_stories') == 'on'
+ generate_stories = request.form.get('generate_stories') == 'off'
+
# Clamp values to valid ranges
words_per_phrase = max(3, min(12, words_per_phrase))
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
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',
phrases=phrases,
pin=pin,
@@ -466,8 +476,10 @@ def generate():
pin_length=pin_length,
phrase_entropy=phrase_entropy,
pin_entropy=pin_entropy,
- total_entropy=total_entropy)
- return render_template('generate.html', generated=False)
+ total_entropy=total_entropy,
+ stories=stories,
+ has_ml=HAS_ML)
+ return render_template('generate.html', generated=False, has_ml=HAS_ML)
@app.route('/encode', methods=['GET', 'POST'])
diff --git a/docker-compose.yml b/docker-compose.yml
index 3f3a0dd..900ff3b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,9 +1,9 @@
version: '3.8'
services:
- stegocrypt:
+ stegasoo:
build: .
- container_name: stegocrypt
+ container_name: stegasoo
ports:
- "5000:5000"
environment:
@@ -29,5 +29,5 @@ services:
# - ./nginx.conf:/etc/nginx/nginx.conf:ro
# - ./certs:/etc/nginx/certs:ro
# depends_on:
- # - stegocrypt
+ # - stegasoo
# restart: unless-stopped
diff --git a/requirements-ml.txt b/requirements-ml.txt
new file mode 100644
index 0000000..0933ed8
--- /dev/null
+++ b/requirements-ml.txt
@@ -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
diff --git a/requirements.txt b/requirements.txt
index 982a0d7..9605ef7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,5 +7,10 @@ cryptography>=41.0.0
# Memory-hard key derivation (highly recommended)
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
# gevent>=23.0.0
diff --git a/story_generator.py b/story_generator.py
new file mode 100644
index 0000000..a7a10b6
--- /dev/null
+++ b/story_generator.py
@@ -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'{word.upper()}',
+ 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
diff --git a/templates/base.html b/templates/base.html
index d19a188..12e7efe 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -97,10 +97,33 @@
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 {
font-family: 'Courier New', monospace;
+ color: #ff9900;
font-size: 2rem;
- letter-spacing: 0.5rem;
+ letter-spacing: 0.3rem;
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
diff --git a/templates/generate.html b/templates/generate.html
index cb1acd9..87b7cf9 100644
--- a/templates/generate.html
+++ b/templates/generate.html
@@ -59,18 +59,33 @@
-