Commit ·
136c0f7
0
Parent(s):
Initial Release: WebPass V2 with Steganography, Crypto Vault, and Cloud Toggle
Browse files- .gitignore +7 -0
- Dockerfile +20 -0
- requirements.txt +0 -0
- test.py +25 -0
- webpass/__init__.py +125 -0
- webpass/config.py +40 -0
- webpass/crypto_utils.py +209 -0
- webpass/models.py +38 -0
- webpass/network_monitor.py +57 -0
- webpass/routes/__init__.py +0 -0
- webpass/routes/_decorators.py +34 -0
- webpass/routes/api.py +56 -0
- webpass/routes/auth.py +67 -0
- webpass/routes/bio_auth.py +166 -0
- webpass/routes/dashboard.py +269 -0
- webpass/routes/otp.py +94 -0
- webpass/routes/share.py +101 -0
- webpass/routes/stego.py +114 -0
- webpass/routes/tools.py +184 -0
- webpass/security_utils.py +61 -0
- webpass/static/css/login.css +115 -0
- webpass/static/css/modern.css +130 -0
- webpass/static/js/dashboard.js +176 -0
- webpass/static/js/file_tools.js +81 -0
- webpass/static/js/flash_modal.js +27 -0
- webpass/static/js/network.js +157 -0
- webpass/static/js/watchtower.js +77 -0
- webpass/static/js/zk_crypto.js +91 -0
- webpass/stego_utils.py +165 -0
- webpass/templates/base.html +128 -0
- webpass/templates/bio_lock.html +59 -0
- webpass/templates/bio_mobile.html +211 -0
- webpass/templates/breach.html +91 -0
- webpass/templates/dashboard.html +191 -0
- webpass/templates/index.html +26 -0
- webpass/templates/login.html +24 -0
- webpass/templates/metadata.html +143 -0
- webpass/templates/network.html +96 -0
- webpass/templates/network_demo.html +27 -0
- webpass/templates/profile.html +69 -0
- webpass/templates/share.html +109 -0
- webpass/templates/share_error.html +34 -0
- webpass/templates/share_view.html +129 -0
- webpass/templates/stego.html +99 -0
- webpass/templates/vault.html +114 -0
- webpass/templates/verify_otp.html +43 -0
- wsgi.py +44 -0
.gitignore
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
venv/
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
instance/
|
| 5 |
+
*.db
|
| 6 |
+
.env
|
| 7 |
+
uploads/
|
Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use lightweight Python 3.12 image
|
| 2 |
+
FROM python:3.12-slim
|
| 3 |
+
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
# Install Nmap just so the Python library imports don't crash the server
|
| 7 |
+
RUN apt-get update && apt-get install -y nmap && rm -rf /var/lib/apt/lists/*
|
| 8 |
+
|
| 9 |
+
# Copy requirements and install
|
| 10 |
+
COPY requirements.txt .
|
| 11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 12 |
+
|
| 13 |
+
# Copy all your code
|
| 14 |
+
COPY . .
|
| 15 |
+
|
| 16 |
+
# Hugging Face exposes port 7860
|
| 17 |
+
EXPOSE 7860
|
| 18 |
+
|
| 19 |
+
# Run the app with Gunicorn
|
| 20 |
+
CMD ["gunicorn", "-b", "0.0.0.0:7860", "wsgi:app"]
|
requirements.txt
ADDED
|
Binary file (378 Bytes). View file
|
|
|
test.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import text
|
| 2 |
+
from webpass import create_app, db
|
| 3 |
+
|
| 4 |
+
app = create_app()
|
| 5 |
+
|
| 6 |
+
with app.app_context():
|
| 7 |
+
print("[-] Disabling Foreign Key Checks...")
|
| 8 |
+
# This tells MySQL: "Don't complain about connections between tables, just delete them."
|
| 9 |
+
db.session.execute(text("SET FOREIGN_KEY_CHECKS = 0"))
|
| 10 |
+
|
| 11 |
+
print("[-] Manually dropping ghost 'credential' table...")
|
| 12 |
+
# Explicitly delete the table that is causing the error
|
| 13 |
+
db.session.execute(text("DROP TABLE IF EXISTS credential"))
|
| 14 |
+
|
| 15 |
+
print("[-] Dropping all remaining tables...")
|
| 16 |
+
db.drop_all()
|
| 17 |
+
|
| 18 |
+
print("[-] Creating new schema (User, BiometricDevice, DeadDrop)...")
|
| 19 |
+
db.create_all()
|
| 20 |
+
|
| 21 |
+
print("[-] Re-enabling Foreign Key Checks...")
|
| 22 |
+
db.session.execute(text("SET FOREIGN_KEY_CHECKS = 1"))
|
| 23 |
+
db.session.commit()
|
| 24 |
+
|
| 25 |
+
print("[+] Database reset successful!")
|
webpass/__init__.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from flask import Flask, session, redirect, url_for, request
|
| 3 |
+
from flask_sqlalchemy import SQLAlchemy
|
| 4 |
+
from flask_login import LoginManager, user_logged_in, current_user
|
| 5 |
+
from flask_dance.contrib.google import make_google_blueprint
|
| 6 |
+
from flask_socketio import SocketIO
|
| 7 |
+
from flask_wtf.csrf import CSRFProtect
|
| 8 |
+
from flask_mail import Mail
|
| 9 |
+
from flask_migrate import Migrate
|
| 10 |
+
from flask_limiter import Limiter
|
| 11 |
+
from flask_limiter.util import get_remote_address
|
| 12 |
+
from flask_cors import CORS
|
| 13 |
+
|
| 14 |
+
# Import the Config CLASS
|
| 15 |
+
from .config import Config
|
| 16 |
+
|
| 17 |
+
# Initialize Extensions
|
| 18 |
+
db = SQLAlchemy()
|
| 19 |
+
socketio = SocketIO()
|
| 20 |
+
csrf = CSRFProtect()
|
| 21 |
+
login_manager = LoginManager()
|
| 22 |
+
mail = Mail()
|
| 23 |
+
migrate = Migrate()
|
| 24 |
+
limiter = Limiter(key_func=get_remote_address, default_limits=["200 per day", "50 per hour"])
|
| 25 |
+
|
| 26 |
+
def create_app():
|
| 27 |
+
app = Flask(__name__)
|
| 28 |
+
|
| 29 |
+
# LOAD CONFIGURATION
|
| 30 |
+
app.config.from_object(Config)
|
| 31 |
+
|
| 32 |
+
# --- ENVIRONMENT DETECTION (Cloud vs Local) ---
|
| 33 |
+
IS_CLOUD = os.environ.get('SPACE_ID') is not None
|
| 34 |
+
if IS_CLOUD:
|
| 35 |
+
# Override with SQLite for Hugging Face to prevent MySQL crashes
|
| 36 |
+
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///cloud_demo.db'
|
| 37 |
+
print("[-] Running in CLOUD MODE (Hugging Face) - Using SQLite")
|
| 38 |
+
else:
|
| 39 |
+
print("[-] Running in LOCAL MODE - Using Configured Database (MySQL)")
|
| 40 |
+
|
| 41 |
+
# Initialize Plugins
|
| 42 |
+
db.init_app(app)
|
| 43 |
+
socketio.init_app(app, async_mode='threading', cors_allowed_origins="*")
|
| 44 |
+
csrf.init_app(app)
|
| 45 |
+
login_manager.init_app(app)
|
| 46 |
+
mail.init_app(app)
|
| 47 |
+
migrate.init_app(app, db)
|
| 48 |
+
|
| 49 |
+
CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True)
|
| 50 |
+
|
| 51 |
+
login_manager.login_view = 'auth.login'
|
| 52 |
+
|
| 53 |
+
from webpass.models import User
|
| 54 |
+
@login_manager.user_loader
|
| 55 |
+
def load_user(user_id):
|
| 56 |
+
return User.query.get(int(user_id))
|
| 57 |
+
|
| 58 |
+
# 1. SECURITY: RESET BIO
|
| 59 |
+
@user_logged_in.connect_via(app)
|
| 60 |
+
def on_user_logged_in(sender, user, **extra):
|
| 61 |
+
session['bio_verified'] = False
|
| 62 |
+
session.permanent = True
|
| 63 |
+
|
| 64 |
+
# 2. REGISTER BLUEPRINTS
|
| 65 |
+
from webpass.routes.auth import auth_bp
|
| 66 |
+
from webpass.routes.dashboard import dashboard_bp
|
| 67 |
+
from webpass.routes.bio_auth import bio_bp
|
| 68 |
+
from webpass.routes.api import api_bp
|
| 69 |
+
from webpass.routes.otp import otp_bp
|
| 70 |
+
from webpass.routes.share import share_bp
|
| 71 |
+
from webpass.routes.stego import stego_bp
|
| 72 |
+
from webpass.routes.tools import tools_bp
|
| 73 |
+
|
| 74 |
+
app.register_blueprint(auth_bp)
|
| 75 |
+
app.register_blueprint(dashboard_bp, url_prefix='/dashboard')
|
| 76 |
+
app.register_blueprint(bio_bp)
|
| 77 |
+
app.register_blueprint(api_bp, url_prefix='/api')
|
| 78 |
+
app.register_blueprint(otp_bp)
|
| 79 |
+
app.register_blueprint(share_bp)
|
| 80 |
+
app.register_blueprint(stego_bp)
|
| 81 |
+
app.register_blueprint(tools_bp)
|
| 82 |
+
|
| 83 |
+
# 3. GOOGLE OAUTH
|
| 84 |
+
google_bp = make_google_blueprint(
|
| 85 |
+
client_id = app.config["GOOGLE_OAUTH_CLIENT_ID"],
|
| 86 |
+
client_secret = app.config["GOOGLE_OAUTH_CLIENT_SECRET"],
|
| 87 |
+
scope = ["openid", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"],
|
| 88 |
+
redirect_to = "auth.authorize"
|
| 89 |
+
)
|
| 90 |
+
google_bp.authorization_url_params["prompt"] = "select_account"
|
| 91 |
+
app.register_blueprint(google_bp, url_prefix='/login')
|
| 92 |
+
|
| 93 |
+
# 4. GLOBAL GATEKEEPER
|
| 94 |
+
@app.before_request
|
| 95 |
+
def require_biometric_auth():
|
| 96 |
+
session.permanent = True
|
| 97 |
+
allowed_endpoints = [
|
| 98 |
+
'google.login', 'google.authorized', 'auth.login', 'auth.authorize', 'auth.logout',
|
| 99 |
+
'static', 'bio.lock_screen', 'bio.mobile_authenticate', 'bio.finalize_login',
|
| 100 |
+
'bio.register_begin', 'bio.register_complete', 'bio.auth_begin', 'bio.auth_complete',
|
| 101 |
+
'share.view_drop_page', 'share.reveal_drop_api', 'share.create_share', 'share.share_ui'
|
| 102 |
+
]
|
| 103 |
+
if request.endpoint and request.endpoint not in allowed_endpoints:
|
| 104 |
+
if current_user.is_authenticated:
|
| 105 |
+
if not session.get('bio_verified'):
|
| 106 |
+
return redirect(url_for('bio.lock_screen'))
|
| 107 |
+
|
| 108 |
+
# 5. SECURITY HEADERS
|
| 109 |
+
@app.after_request
|
| 110 |
+
def add_security_headers(response):
|
| 111 |
+
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
|
| 112 |
+
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
|
| 113 |
+
return response
|
| 114 |
+
|
| 115 |
+
from webpass.models import BiometricDevice
|
| 116 |
+
@app.context_processor
|
| 117 |
+
def inject_credential_status():
|
| 118 |
+
if current_user.is_authenticated:
|
| 119 |
+
try:
|
| 120 |
+
device = BiometricDevice.query.filter_by(user_id=current_user.id).first()
|
| 121 |
+
return dict(has_credentials=bool(device))
|
| 122 |
+
except: pass
|
| 123 |
+
return dict(has_credentials=False)
|
| 124 |
+
|
| 125 |
+
return app
|
webpass/config.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from datetime import timedelta
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
|
| 5 |
+
# Load variables from .env file if it exists (for local testing)
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
class Config:
|
| 9 |
+
# 1. BASIC CONFIG
|
| 10 |
+
SECRET_KEY = os.environ.get("FLASK_SECRET", "fallback-dev-key-123")
|
| 11 |
+
# Will use SQLite in cloud via __init__.py, but defaults to local MySQL here
|
| 12 |
+
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", "sqlite:///fallback.db")
|
| 13 |
+
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
| 14 |
+
|
| 15 |
+
# 2. EMAIL CONFIG
|
| 16 |
+
MAIL_SERVER = "smtp.gmail.com"
|
| 17 |
+
MAIL_PORT = 587
|
| 18 |
+
MAIL_USE_TLS = True
|
| 19 |
+
MAIL_USERNAME = os.environ.get("MAIL_USERNAME")
|
| 20 |
+
MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD")
|
| 21 |
+
MAIL_DEFAULT_SENDER = os.environ.get("MAIL_USERNAME")
|
| 22 |
+
|
| 23 |
+
# 3. GOOGLE OAUTH CONFIG
|
| 24 |
+
OAUTHLIB_INSECURE_TRANSPORT = True
|
| 25 |
+
GOOGLE_OAUTH_CLIENT_ID = os.environ.get("GOOGLE_OAUTH_CLIENT_ID")
|
| 26 |
+
GOOGLE_OAUTH_CLIENT_SECRET = os.environ.get("GOOGLE_OAUTH_CLIENT_SECRET")
|
| 27 |
+
|
| 28 |
+
# 4. COOKIE & SESSION SECURITY
|
| 29 |
+
SESSION_COOKIE_SECURE = False
|
| 30 |
+
SESSION_COOKIE_HTTPONLY = True
|
| 31 |
+
SESSION_COOKIE_SAMESITE = 'Lax'
|
| 32 |
+
PERMANENT_SESSION_LIFETIME = timedelta(minutes=5)
|
| 33 |
+
SESSION_REFRESH_EACH_REQUEST = True
|
| 34 |
+
REMEMBER_COOKIE_DURATION = timedelta(minutes=5)
|
| 35 |
+
REMEMBER_COOKIE_SECURE = False
|
| 36 |
+
REMEMBER_COOKIE_HTTPONLY = True
|
| 37 |
+
|
| 38 |
+
# 5. UNIVERSAL ACCESS (NGROK)
|
| 39 |
+
NGROK_URL = os.environ.get("NGROK_URL")
|
| 40 |
+
OVERWRITE_REDIRECT_URI = os.environ.get("OVERWRITE_REDIRECT_URI")
|
webpass/crypto_utils.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from cryptography.hazmat.primitives import hashes, padding
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
import base64
|
| 5 |
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
| 6 |
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
| 7 |
+
from cryptography.hazmat.primitives import hashes, padding
|
| 8 |
+
|
| 9 |
+
# --- Helper: Generate AES Key ---
|
| 10 |
+
def generate_key(password: str, salt: bytes) -> bytes:
|
| 11 |
+
kdf = PBKDF2HMAC(
|
| 12 |
+
algorithm=hashes.SHA256(),
|
| 13 |
+
length=32,
|
| 14 |
+
salt=salt,
|
| 15 |
+
iterations=100000,
|
| 16 |
+
)
|
| 17 |
+
return kdf.derive(password.encode())
|
| 18 |
+
|
| 19 |
+
# --- Encrypt password with key ---
|
| 20 |
+
def encrypt_password(password: str, key: bytes):
|
| 21 |
+
iv = os.urandom(16)
|
| 22 |
+
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
|
| 23 |
+
encryptor = cipher.encryptor()
|
| 24 |
+
padder = padding.PKCS7(128).padder()
|
| 25 |
+
padded = padder.update(password.encode()) + padder.finalize()
|
| 26 |
+
encrypted = encryptor.update(padded) + encryptor.finalize()
|
| 27 |
+
return base64.b64encode(iv + encrypted).decode()
|
| 28 |
+
|
| 29 |
+
# --- Decrypt password with key ---
|
| 30 |
+
def decrypt_password(encrypted_password: str, key: bytes):
|
| 31 |
+
raw = base64.b64decode(encrypted_password)
|
| 32 |
+
iv, encrypted = raw[:16], raw[16:]
|
| 33 |
+
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
|
| 34 |
+
decryptor = cipher.decryptor()
|
| 35 |
+
padded = decryptor.update(encrypted) + decryptor.finalize()
|
| 36 |
+
unpadder = padding.PKCS7(128).unpadder()
|
| 37 |
+
return (unpadder.update(padded) + unpadder.finalize()).decode()
|
| 38 |
+
|
| 39 |
+
# --- Store password ---
|
| 40 |
+
def store_password(account: str, username: str, password: str, master_password: str):
|
| 41 |
+
salt = os.urandom(16)
|
| 42 |
+
key = generate_key(master_password, salt)
|
| 43 |
+
encrypted_pw = encrypt_password(password, key)
|
| 44 |
+
data = {"account": account, "username": username, "password": encrypted_pw, "salt": base64.b64encode(salt).decode()}
|
| 45 |
+
|
| 46 |
+
try:
|
| 47 |
+
with open("passwords.json", "r") as f:
|
| 48 |
+
records = json.load(f)
|
| 49 |
+
except FileNotFoundError:
|
| 50 |
+
records = []
|
| 51 |
+
|
| 52 |
+
records = [r for r in records if r["account"] != account]
|
| 53 |
+
records.append(data)
|
| 54 |
+
|
| 55 |
+
with open("passwords.json", "w") as f:
|
| 56 |
+
json.dump(records, f, indent=4)
|
| 57 |
+
print(f"[+] Password stored for account: {account}")
|
| 58 |
+
|
| 59 |
+
# --- Retrieve password ---
|
| 60 |
+
def retrieve_password(account: str, master_password: str):
|
| 61 |
+
try:
|
| 62 |
+
with open("passwords.json", "r") as f:
|
| 63 |
+
records = json.load(f)
|
| 64 |
+
except FileNotFoundError:
|
| 65 |
+
print("[-] No stored passwords.")
|
| 66 |
+
return None
|
| 67 |
+
|
| 68 |
+
for r in records:
|
| 69 |
+
if r["account"] == account:
|
| 70 |
+
salt = base64.b64decode(r["salt"])
|
| 71 |
+
key = generate_key(master_password, salt)
|
| 72 |
+
try:
|
| 73 |
+
return decrypt_password(r["password"], key)
|
| 74 |
+
except:
|
| 75 |
+
print("[-] Master password incorrect.")
|
| 76 |
+
return None
|
| 77 |
+
print("[-] Account not found.")
|
| 78 |
+
return None
|
| 79 |
+
|
| 80 |
+
# --- Encrypt file ---
|
| 81 |
+
def encrypt_file(file_path: str, account: str, master_password: str):
|
| 82 |
+
password = retrieve_password(account, master_password)
|
| 83 |
+
if not password:
|
| 84 |
+
print("[-] Password retrieval failed.")
|
| 85 |
+
return
|
| 86 |
+
|
| 87 |
+
try:
|
| 88 |
+
with open(file_path, "rb") as f:
|
| 89 |
+
content = f.read()
|
| 90 |
+
except FileNotFoundError:
|
| 91 |
+
print("[-] File not found.")
|
| 92 |
+
return
|
| 93 |
+
|
| 94 |
+
salt = os.urandom(16)
|
| 95 |
+
iv = os.urandom(16)
|
| 96 |
+
key = generate_key(password, salt)
|
| 97 |
+
|
| 98 |
+
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
|
| 99 |
+
encryptor = cipher.encryptor()
|
| 100 |
+
padder = padding.PKCS7(128).padder()
|
| 101 |
+
padded = padder.update(content) + padder.finalize()
|
| 102 |
+
encrypted = encryptor.update(padded) + encryptor.finalize()
|
| 103 |
+
|
| 104 |
+
enc_file = file_path + ".enc"
|
| 105 |
+
with open(enc_file, "wb") as f:
|
| 106 |
+
f.write(encrypted)
|
| 107 |
+
|
| 108 |
+
meta = {
|
| 109 |
+
"salt": base64.b64encode(salt).decode(),
|
| 110 |
+
"iv": base64.b64encode(iv).decode(),
|
| 111 |
+
"account": account
|
| 112 |
+
}
|
| 113 |
+
with open(enc_file + ".meta", "w") as f:
|
| 114 |
+
json.dump(meta, f)
|
| 115 |
+
|
| 116 |
+
print(f"[+] File encrypted as: {enc_file}")
|
| 117 |
+
|
| 118 |
+
# --- Decrypt file ---
|
| 119 |
+
def decrypt_file(enc_file: str, master_password: str):
|
| 120 |
+
meta_file = enc_file + ".meta"
|
| 121 |
+
if not os.path.exists(meta_file):
|
| 122 |
+
print("[-] Metadata file not found.")
|
| 123 |
+
return
|
| 124 |
+
|
| 125 |
+
try:
|
| 126 |
+
with open(meta_file, "r") as f:
|
| 127 |
+
meta = json.load(f)
|
| 128 |
+
except Exception as e:
|
| 129 |
+
print("[-] Failed to read metadata:", e)
|
| 130 |
+
return
|
| 131 |
+
|
| 132 |
+
account = meta["account"]
|
| 133 |
+
salt = base64.b64decode(meta["salt"])
|
| 134 |
+
iv = base64.b64decode(meta["iv"])
|
| 135 |
+
|
| 136 |
+
password = retrieve_password(account, master_password)
|
| 137 |
+
if not password:
|
| 138 |
+
print("[-] Failed to retrieve password.")
|
| 139 |
+
return
|
| 140 |
+
|
| 141 |
+
key = generate_key(password, salt)
|
| 142 |
+
|
| 143 |
+
try:
|
| 144 |
+
with open(enc_file, "rb") as f:
|
| 145 |
+
encrypted = f.read()
|
| 146 |
+
except FileNotFoundError:
|
| 147 |
+
print("[-] Encrypted file not found.")
|
| 148 |
+
return
|
| 149 |
+
|
| 150 |
+
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
|
| 151 |
+
decryptor = cipher.decryptor()
|
| 152 |
+
padded = decryptor.update(encrypted) + decryptor.finalize()
|
| 153 |
+
|
| 154 |
+
unpadder = padding.PKCS7(128).unpadder()
|
| 155 |
+
try:
|
| 156 |
+
data = unpadder.update(padded) + unpadder.finalize()
|
| 157 |
+
except Exception as e:
|
| 158 |
+
print("[-] Decryption failed:", e)
|
| 159 |
+
return
|
| 160 |
+
|
| 161 |
+
dec_file = enc_file.replace(".enc", "_decrypted")
|
| 162 |
+
with open(dec_file, "wb") as f:
|
| 163 |
+
f.write(data)
|
| 164 |
+
|
| 165 |
+
print(f"[+] File decrypted to: {dec_file}")
|
| 166 |
+
|
| 167 |
+
# --- Main Loop ---
|
| 168 |
+
if __name__ == "__main__":
|
| 169 |
+
while True:
|
| 170 |
+
print("\nChoose Action:")
|
| 171 |
+
print("1. Store password")
|
| 172 |
+
print("2. Encrypt file")
|
| 173 |
+
print("3. Decrypt file")
|
| 174 |
+
print("4. Retrieve password")
|
| 175 |
+
print("Type 'exit' to quit.")
|
| 176 |
+
|
| 177 |
+
choice = input("\nEnter choice (1-4 or 'exit'): ").strip().lower()
|
| 178 |
+
|
| 179 |
+
if choice == "1":
|
| 180 |
+
acc = input("Account: ")
|
| 181 |
+
user = input("Username/Email: ")
|
| 182 |
+
pw = input("Password: ")
|
| 183 |
+
m_pw = input("Master Password: ")
|
| 184 |
+
store_password(acc, user, pw, m_pw)
|
| 185 |
+
|
| 186 |
+
elif choice == "2":
|
| 187 |
+
path = input("File path to encrypt: ")
|
| 188 |
+
acc = input("Account to use: ")
|
| 189 |
+
m_pw = input("Master Password: ")
|
| 190 |
+
encrypt_file(path, acc, m_pw)
|
| 191 |
+
|
| 192 |
+
elif choice == "3":
|
| 193 |
+
path = input("Encrypted file path: ")
|
| 194 |
+
m_pw = input("Master Password: ")
|
| 195 |
+
decrypt_file(path, m_pw)
|
| 196 |
+
|
| 197 |
+
elif choice == "4":
|
| 198 |
+
acc = input("Account: ")
|
| 199 |
+
m_pw = input("Master Password: ")
|
| 200 |
+
pw = retrieve_password(acc, m_pw)
|
| 201 |
+
if pw:
|
| 202 |
+
print(f"[+] Retrieved Password: {pw}")
|
| 203 |
+
|
| 204 |
+
elif choice == "exit":
|
| 205 |
+
print("Exiting program. Goodbye!")
|
| 206 |
+
break
|
| 207 |
+
|
| 208 |
+
else:
|
| 209 |
+
print("Invalid choice. Please enter 1, 2, 3, 4, or 'exit'.")
|
webpass/models.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# webpass/models.py
|
| 2 |
+
from flask_login import UserMixin
|
| 3 |
+
from . import db
|
| 4 |
+
|
| 5 |
+
class User(UserMixin, db.Model):
|
| 6 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 7 |
+
email = db.Column(db.String(150), unique=True)
|
| 8 |
+
password = db.Column(db.String(150))
|
| 9 |
+
first_name = db.Column(db.String(150))
|
| 10 |
+
google_id = db.Column(db.String(150))
|
| 11 |
+
|
| 12 |
+
# --- THE FIX: Change these to db.Text ---
|
| 13 |
+
profile_image = db.Column(db.Text) # <--- Was likely db.String(something)
|
| 14 |
+
orig_profile_image = db.Column(db.Text) # <--- Was likely db.String(something)
|
| 15 |
+
|
| 16 |
+
# Relationship to biometric keys
|
| 17 |
+
biometric_devices = db.relationship("BiometricDevice", backref="user", lazy=True)
|
| 18 |
+
|
| 19 |
+
class BiometricDevice(db.Model):
|
| 20 |
+
"""
|
| 21 |
+
Stores the Public Key for WebAuthn/FIDO2 Authentication.
|
| 22 |
+
"""
|
| 23 |
+
__tablename__ = "biometric_device"
|
| 24 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 25 |
+
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
| 26 |
+
credential_id = db.Column(db.String(255), unique=True, nullable=False) # The ID of the key on the phone
|
| 27 |
+
public_key = db.Column(db.LargeBinary, nullable=False) # The actual public key
|
| 28 |
+
sign_count = db.Column(db.Integer, default=0) # Replay protection
|
| 29 |
+
|
| 30 |
+
class DeadDrop(db.Model):
|
| 31 |
+
__tablename__ = "dead_drop"
|
| 32 |
+
id = db.Column(db.String(36), primary_key=True)
|
| 33 |
+
ciphertext = db.Column(db.Text, nullable=False)
|
| 34 |
+
iv = db.Column(db.String(64), nullable=False)
|
| 35 |
+
salt = db.Column(db.String(64), nullable=False)
|
| 36 |
+
expires_at = db.Column(db.DateTime, nullable=False)
|
| 37 |
+
created_at = db.Column(db.DateTime)
|
| 38 |
+
view_time = db.Column(db.Integer, default=30) # NEW: Time in seconds
|
webpass/network_monitor.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# network_monitor.py
|
| 2 |
+
import time
|
| 3 |
+
from scapy.sendrecv import AsyncSniffer
|
| 4 |
+
from scapy.all import IP, TCP, UDP, DNS
|
| 5 |
+
|
| 6 |
+
def process_packet(pkt):
|
| 7 |
+
"""Processes a Scapy packet into a simple dictionary."""
|
| 8 |
+
d = {"timestamp": pkt.time}
|
| 9 |
+
if pkt.haslayer(IP):
|
| 10 |
+
ip = pkt[IP]
|
| 11 |
+
d.update(src=ip.src, dst=ip.dst, ttl=ip.ttl, length=len(pkt))
|
| 12 |
+
if pkt.haslayer(TCP):
|
| 13 |
+
t = pkt[TCP]
|
| 14 |
+
d.update(
|
| 15 |
+
proto=6, src_port=t.sport, dst_port=t.dport, flags=str(t.flags),
|
| 16 |
+
info=f"TCP {ip.src}:{t.sport} → {ip.dst}:{t.dport}"
|
| 17 |
+
)
|
| 18 |
+
elif pkt.haslayer(UDP):
|
| 19 |
+
u = pkt[UDP]
|
| 20 |
+
d.update(proto=17, src_port=u.sport, dst_port=u.dport, flags="-")
|
| 21 |
+
if pkt.haslayer(DNS) and hasattr(pkt[DNS], 'qd') and pkt[DNS].qd is not None:
|
| 22 |
+
qname = pkt[DNS].qd.qname
|
| 23 |
+
# qname can be bytes, decode safely
|
| 24 |
+
qname_str = qname.decode(errors='ignore') if isinstance(qname, bytes) else str(qname)
|
| 25 |
+
d.update(proto="DNS", info=f"DNS Query for {qname_str}")
|
| 26 |
+
else:
|
| 27 |
+
d.update(info=f"UDP {ip.src}:{u.sport} → {ip.dst}:{u.dport}")
|
| 28 |
+
else:
|
| 29 |
+
d.setdefault("proto", ip.proto)
|
| 30 |
+
else:
|
| 31 |
+
d.update(src="N/A", dst="N/A", proto="Other", length=len(pkt))
|
| 32 |
+
return d
|
| 33 |
+
|
| 34 |
+
def start_packet_capture(app, socketio, interface=None, filter_expr="ip"):
|
| 35 |
+
"""
|
| 36 |
+
Sniffs packets in a background thread, adds them to the app's deque,
|
| 37 |
+
and emits every packet to connected clients.
|
| 38 |
+
"""
|
| 39 |
+
def _handle(pkt):
|
| 40 |
+
try:
|
| 41 |
+
data = process_packet(pkt)
|
| 42 |
+
except Exception:
|
| 43 |
+
return # Ignore packets that cause processing errors
|
| 44 |
+
|
| 45 |
+
# 1. Store the packet in the historical backlog
|
| 46 |
+
app.captured_packets.append(data)
|
| 47 |
+
|
| 48 |
+
# 2. Emit EVERY packet to the live feed (filter removed)
|
| 49 |
+
socketio.emit("new_packet", data)
|
| 50 |
+
|
| 51 |
+
sniffer = AsyncSniffer(
|
| 52 |
+
iface=interface,
|
| 53 |
+
filter=filter_expr,
|
| 54 |
+
prn=_handle,
|
| 55 |
+
store=False
|
| 56 |
+
)
|
| 57 |
+
sniffer.start()
|
webpass/routes/__init__.py
ADDED
|
File without changes
|
webpass/routes/_decorators.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from functools import wraps
|
| 2 |
+
from flask import session, redirect, url_for, request
|
| 3 |
+
|
| 4 |
+
def otp_required(f):
|
| 5 |
+
"""
|
| 6 |
+
Strict OTP Gatekeeper.
|
| 7 |
+
1. Checks if 'otp_verified' is True.
|
| 8 |
+
2. If True, consumes the flag and lets user pass.
|
| 9 |
+
3. If False, redirects to '/otp/send' to trigger the email.
|
| 10 |
+
"""
|
| 11 |
+
@wraps(f)
|
| 12 |
+
def decorated(*args, **kwargs):
|
| 13 |
+
# Check if user has passed OTP verification
|
| 14 |
+
if session.get("otp_verified"):
|
| 15 |
+
# CONSUME THE FLAG (Strict Mode)
|
| 16 |
+
# This ensures they must verify again if they leave and come back
|
| 17 |
+
session.pop("otp_verified", None)
|
| 18 |
+
return f(*args, **kwargs)
|
| 19 |
+
|
| 20 |
+
# FIX: Redirect to 'otp.send_otp' first (to send email), not 'verify_otp'
|
| 21 |
+
next_url = request.path
|
| 22 |
+
return redirect(url_for("otp.send_otp", next=next_url))
|
| 23 |
+
return decorated
|
| 24 |
+
|
| 25 |
+
def biometric_required(f):
|
| 26 |
+
"""
|
| 27 |
+
Blocks access unless the user has passed the Biometric (QR/Fingerprint) check.
|
| 28 |
+
"""
|
| 29 |
+
@wraps(f)
|
| 30 |
+
def decorated_function(*args, **kwargs):
|
| 31 |
+
if not session.get('bio_verified'):
|
| 32 |
+
return redirect(url_for('bio.lock_screen'))
|
| 33 |
+
return f(*args, **kwargs)
|
| 34 |
+
return decorated_function
|
webpass/routes/api.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
import requests
|
| 3 |
+
from flask import Blueprint, jsonify, current_app
|
| 4 |
+
from flask_login import login_required
|
| 5 |
+
|
| 6 |
+
# Note: We removed 'Credential' and 'db' imports because
|
| 7 |
+
# the old Master Password system is replaced by Biometrics.
|
| 8 |
+
|
| 9 |
+
api_bp = Blueprint("api", __name__, url_prefix="/api")
|
| 10 |
+
|
| 11 |
+
# ==========================================
|
| 12 |
+
# 1. NETWORK MONITOR API
|
| 13 |
+
# ==========================================
|
| 14 |
+
@api_bp.route("/packets", methods=["GET"])
|
| 15 |
+
@login_required
|
| 16 |
+
def get_packets():
|
| 17 |
+
"""
|
| 18 |
+
Returns the complete, unfiltered list of captured packets.
|
| 19 |
+
Used by the Network Monitor page (network.js).
|
| 20 |
+
"""
|
| 21 |
+
# current_app.captured_packets is populated by wsgi.py
|
| 22 |
+
return jsonify(list(current_app.captured_packets))
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# ==========================================
|
| 26 |
+
# 2. WATCHTOWER (HIBP) PROXY
|
| 27 |
+
# ==========================================
|
| 28 |
+
@api_bp.route("/watchtower/pwned-check/<prefix>", methods=["GET"])
|
| 29 |
+
def check_pwned_proxy(prefix):
|
| 30 |
+
"""
|
| 31 |
+
Proxies the HaveIBeenPwned request to protect user privacy (k-Anonymity).
|
| 32 |
+
Expects a 5-character SHA-1 hash prefix.
|
| 33 |
+
"""
|
| 34 |
+
# Validate prefix (must be 5 hex chars)
|
| 35 |
+
if not re.fullmatch(r"[A-F0-9]{5}", prefix.upper()):
|
| 36 |
+
return jsonify({"error": "Invalid prefix format"}), 400
|
| 37 |
+
|
| 38 |
+
# Custom User-Agent is required by HIBP API
|
| 39 |
+
headers = {
|
| 40 |
+
"User-Agent": "WebPass-FinalYearProject"
|
| 41 |
+
}
|
| 42 |
+
url = f"https://api.pwnedpasswords.com/range/{prefix}"
|
| 43 |
+
|
| 44 |
+
try:
|
| 45 |
+
# Short timeout to prevent server hanging
|
| 46 |
+
resp = requests.get(url, headers=headers, timeout=3)
|
| 47 |
+
|
| 48 |
+
# If API is down or limits reached, fail open (return empty)
|
| 49 |
+
if resp.status_code != 200:
|
| 50 |
+
return jsonify([]), 200
|
| 51 |
+
|
| 52 |
+
# Return the raw text body (list of suffixes:count)
|
| 53 |
+
return resp.text, 200, {'Content-Type': 'text/plain'}
|
| 54 |
+
except Exception as e:
|
| 55 |
+
print(f"HIBP Error: {e}")
|
| 56 |
+
return jsonify([]), 200
|
webpass/routes/auth.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# webpass/routes/auth.py
|
| 2 |
+
|
| 3 |
+
from flask import Blueprint, render_template, redirect, url_for, flash
|
| 4 |
+
from flask_login import login_user, logout_user
|
| 5 |
+
from flask_dance.contrib.google import google
|
| 6 |
+
from oauthlib.oauth2.rfc6749.errors import TokenExpiredError
|
| 7 |
+
|
| 8 |
+
from .. import db
|
| 9 |
+
from ..models import User
|
| 10 |
+
|
| 11 |
+
auth_bp = Blueprint("auth", __name__)
|
| 12 |
+
|
| 13 |
+
@auth_bp.route("/")
|
| 14 |
+
def index():
|
| 15 |
+
return redirect(url_for("auth.login"))
|
| 16 |
+
|
| 17 |
+
@auth_bp.route("/login")
|
| 18 |
+
def login():
|
| 19 |
+
return render_template("login.html")
|
| 20 |
+
|
| 21 |
+
@auth_bp.route("/authorize")
|
| 22 |
+
def authorize():
|
| 23 |
+
# Kick off OAuth if we have no token
|
| 24 |
+
if not google.authorized:
|
| 25 |
+
return redirect(url_for("google.login"))
|
| 26 |
+
|
| 27 |
+
# Try fetching the Google profile, handle expired tokens
|
| 28 |
+
try:
|
| 29 |
+
resp = google.get("/oauth2/v2/userinfo")
|
| 30 |
+
except TokenExpiredError:
|
| 31 |
+
flash("Session expired, please sign in again.", "warning")
|
| 32 |
+
return redirect(url_for("google.login"))
|
| 33 |
+
|
| 34 |
+
if not resp.ok:
|
| 35 |
+
flash("Could not fetch your Google profile.", "danger")
|
| 36 |
+
return redirect(url_for("auth.login"))
|
| 37 |
+
|
| 38 |
+
info = resp.json()
|
| 39 |
+
email = info["email"]
|
| 40 |
+
gid = info["id"]
|
| 41 |
+
pic = info.get("picture")
|
| 42 |
+
|
| 43 |
+
# Find or create local user
|
| 44 |
+
user = User.query.filter_by(email=email).first()
|
| 45 |
+
if not user:
|
| 46 |
+
user = User(
|
| 47 |
+
email = email,
|
| 48 |
+
google_id = gid,
|
| 49 |
+
profile_image = pic,
|
| 50 |
+
orig_profile_image = pic
|
| 51 |
+
)
|
| 52 |
+
db.session.add(user)
|
| 53 |
+
else:
|
| 54 |
+
if not user.orig_profile_image:
|
| 55 |
+
user.orig_profile_image = pic
|
| 56 |
+
|
| 57 |
+
db.session.commit()
|
| 58 |
+
login_user(user)
|
| 59 |
+
|
| 60 |
+
# Redirect directly to /dashboard
|
| 61 |
+
return redirect("/dashboard")
|
| 62 |
+
|
| 63 |
+
@auth_bp.route("/logout")
|
| 64 |
+
def logout():
|
| 65 |
+
logout_user()
|
| 66 |
+
flash("Logged out successfully.", "info")
|
| 67 |
+
return redirect(url_for("auth.login"))
|
webpass/routes/bio_auth.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import base64
|
| 4 |
+
from flask import Blueprint, render_template, request, session, jsonify, url_for, current_app, redirect
|
| 5 |
+
from flask_login import login_required, current_user
|
| 6 |
+
from fido2.server import Fido2Server
|
| 7 |
+
from fido2.webauthn import PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, AttestedCredentialData
|
| 8 |
+
from fido2.utils import websafe_decode, websafe_encode
|
| 9 |
+
from fido2 import cbor
|
| 10 |
+
from fido2.cose import CoseKey
|
| 11 |
+
from webpass import db, socketio, csrf
|
| 12 |
+
from webpass.models import BiometricDevice, User
|
| 13 |
+
|
| 14 |
+
bio_bp = Blueprint('bio', __name__)
|
| 15 |
+
|
| 16 |
+
VERIFIED_CHANNELS = {}
|
| 17 |
+
|
| 18 |
+
def get_server():
|
| 19 |
+
# Dynamically grab the host (works for localhost and ngrok)
|
| 20 |
+
host = request.host.split(':')[0]
|
| 21 |
+
rp = PublicKeyCredentialRpEntity(id=host, name="WebPass Secure")
|
| 22 |
+
return Fido2Server(rp)
|
| 23 |
+
|
| 24 |
+
def make_serializable(data):
|
| 25 |
+
if isinstance(data, bytes): return websafe_encode(data)
|
| 26 |
+
elif isinstance(data, dict): return {k: make_serializable(v) for k, v in data.items()}
|
| 27 |
+
elif isinstance(data, list): return [make_serializable(i) for i in data]
|
| 28 |
+
return data
|
| 29 |
+
|
| 30 |
+
def get_user_from_channel(channel_str):
|
| 31 |
+
if not channel_str or '.' not in channel_str: return None
|
| 32 |
+
try:
|
| 33 |
+
user_id = int(channel_str.split('.')[-1])
|
| 34 |
+
return User.query.get(user_id)
|
| 35 |
+
except: return None
|
| 36 |
+
|
| 37 |
+
# --- ROUTES ---
|
| 38 |
+
|
| 39 |
+
@bio_bp.route('/biometric-lock')
|
| 40 |
+
@login_required
|
| 41 |
+
def lock_screen():
|
| 42 |
+
if session.get('bio_verified'): return redirect('/dashboard/')
|
| 43 |
+
|
| 44 |
+
rand_str = base64.urlsafe_b64encode(os.urandom(10)).decode().rstrip('=')
|
| 45 |
+
channel_id = f"{rand_str}.{current_user.id}"
|
| 46 |
+
|
| 47 |
+
# --- NGROK INTEGRATION ---
|
| 48 |
+
ngrok_base = current_app.config.get('NGROK_URL')
|
| 49 |
+
|
| 50 |
+
if ngrok_base:
|
| 51 |
+
# Strip trailing slash if present
|
| 52 |
+
if ngrok_base.endswith('/'): ngrok_base = ngrok_base[:-1]
|
| 53 |
+
mobile_path = url_for('bio.mobile_authenticate', channel=channel_id)
|
| 54 |
+
mobile_url = f"{ngrok_base}{mobile_path}"
|
| 55 |
+
else:
|
| 56 |
+
mobile_url = url_for('bio.mobile_authenticate', channel=channel_id, _external=True)
|
| 57 |
+
|
| 58 |
+
return render_template('bio_lock.html', mobile_url=mobile_url, channel_id=channel_id)
|
| 59 |
+
|
| 60 |
+
@bio_bp.route('/api/bio/finalize-login/<channel>')
|
| 61 |
+
@login_required
|
| 62 |
+
def finalize_login(channel):
|
| 63 |
+
if channel in VERIFIED_CHANNELS:
|
| 64 |
+
session['bio_verified'] = True
|
| 65 |
+
del VERIFIED_CHANNELS[channel]
|
| 66 |
+
return jsonify({"status": "success", "redirect": "/dashboard/"})
|
| 67 |
+
return jsonify({"status": "pending"}), 400
|
| 68 |
+
|
| 69 |
+
@bio_bp.route('/mobile-auth/<channel>')
|
| 70 |
+
def mobile_authenticate(channel):
|
| 71 |
+
user = get_user_from_channel(channel)
|
| 72 |
+
if not user: return "Invalid Link", 404
|
| 73 |
+
has_reg = len(user.biometric_devices) > 0
|
| 74 |
+
return render_template('bio_mobile.html', channel=channel, has_registered=has_reg, user_email=user.email)
|
| 75 |
+
|
| 76 |
+
# --- API (Standard Logic) ---
|
| 77 |
+
|
| 78 |
+
@bio_bp.route('/api/bio/register/begin', methods=['POST'])
|
| 79 |
+
@csrf.exempt
|
| 80 |
+
def register_begin():
|
| 81 |
+
data = request.get_json() or {}
|
| 82 |
+
user = get_user_from_channel(data.get('channel'))
|
| 83 |
+
if not user and current_user.is_authenticated: user = current_user
|
| 84 |
+
if not user: return jsonify({"error": "User lost"}), 400
|
| 85 |
+
|
| 86 |
+
user_entity = PublicKeyCredentialUserEntity(id=str(user.id).encode(), name=user.email, display_name=user.email)
|
| 87 |
+
server = get_server()
|
| 88 |
+
registration_data, state = server.register_begin(
|
| 89 |
+
user_entity,
|
| 90 |
+
[{"id": d.credential_id, "type": "public-key"} for d in user.biometric_devices],
|
| 91 |
+
user_verification="preferred"
|
| 92 |
+
)
|
| 93 |
+
session['fido_state'] = state
|
| 94 |
+
resp = dict(registration_data)
|
| 95 |
+
if 'publicKey' in resp: resp = resp['publicKey']
|
| 96 |
+
return jsonify(make_serializable(resp))
|
| 97 |
+
|
| 98 |
+
@bio_bp.route('/api/bio/register/complete', methods=['POST'])
|
| 99 |
+
@csrf.exempt
|
| 100 |
+
def register_complete():
|
| 101 |
+
try:
|
| 102 |
+
data = request.json
|
| 103 |
+
user = get_user_from_channel(data.get('channel'))
|
| 104 |
+
if not user: return jsonify({"error": "User not found"}), 400
|
| 105 |
+
|
| 106 |
+
server = get_server()
|
| 107 |
+
auth_data = server.register_complete(session.pop('fido_state'), data['response'])
|
| 108 |
+
|
| 109 |
+
cred = BiometricDevice(
|
| 110 |
+
user_id=user.id,
|
| 111 |
+
credential_id=websafe_encode(auth_data.credential_data.credential_id),
|
| 112 |
+
public_key=cbor.encode(auth_data.credential_data.public_key),
|
| 113 |
+
# --- THE FIX: Removed '.authenticator_data' ---
|
| 114 |
+
sign_count=auth_data.counter
|
| 115 |
+
)
|
| 116 |
+
db.session.add(cred)
|
| 117 |
+
db.session.commit()
|
| 118 |
+
return jsonify({"status": "ok"})
|
| 119 |
+
except Exception as e:
|
| 120 |
+
print(f"REG ERROR: {e}")
|
| 121 |
+
return jsonify({"error": str(e)}), 400
|
| 122 |
+
|
| 123 |
+
@bio_bp.route('/api/bio/auth/begin', methods=['POST'])
|
| 124 |
+
@csrf.exempt
|
| 125 |
+
def auth_begin():
|
| 126 |
+
data = request.get_json() or {}
|
| 127 |
+
user = get_user_from_channel(data.get('channel'))
|
| 128 |
+
creds = [{"type": "public-key", "id": websafe_decode(d.credential_id)} for d in user.biometric_devices] if user else []
|
| 129 |
+
|
| 130 |
+
server = get_server()
|
| 131 |
+
auth_data, state = server.authenticate_begin(creds)
|
| 132 |
+
session['fido_state'] = state
|
| 133 |
+
|
| 134 |
+
resp = dict(auth_data)
|
| 135 |
+
if 'publicKey' in resp: resp = resp['publicKey']
|
| 136 |
+
return jsonify(make_serializable(resp))
|
| 137 |
+
|
| 138 |
+
@bio_bp.route('/api/bio/auth/complete', methods=['POST'])
|
| 139 |
+
@csrf.exempt
|
| 140 |
+
def auth_complete():
|
| 141 |
+
try:
|
| 142 |
+
data = request.json
|
| 143 |
+
device = BiometricDevice.query.filter_by(credential_id=data['credentialId']).first()
|
| 144 |
+
if not device: return jsonify({"error": "Unknown device"}), 400
|
| 145 |
+
|
| 146 |
+
server = get_server()
|
| 147 |
+
cred_data = AttestedCredentialData.create(
|
| 148 |
+
b'\x00'*16, websafe_decode(device.credential_id), CoseKey.parse(cbor.decode(device.public_key))
|
| 149 |
+
)
|
| 150 |
+
server.authenticate_complete(session.pop('fido_state'), [cred_data], data['response'])
|
| 151 |
+
|
| 152 |
+
device.sign_count += 1
|
| 153 |
+
db.session.commit()
|
| 154 |
+
|
| 155 |
+
if data.get('channel'):
|
| 156 |
+
VERIFIED_CHANNELS[data['channel']] = True
|
| 157 |
+
socketio.emit('unlock_command', {'status': 'success'}, to=data['channel'])
|
| 158 |
+
|
| 159 |
+
return jsonify({"status": "ok"})
|
| 160 |
+
except Exception as e:
|
| 161 |
+
return jsonify({"error": str(e)}), 500
|
| 162 |
+
|
| 163 |
+
@socketio.on('join_channel')
|
| 164 |
+
def on_join(data):
|
| 165 |
+
from flask_socketio import join_room
|
| 166 |
+
if data.get('channel'): join_room(data['channel'])
|
webpass/routes/dashboard.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import base64
|
| 3 |
+
import json
|
| 4 |
+
from io import BytesIO
|
| 5 |
+
from uuid import uuid4
|
| 6 |
+
|
| 7 |
+
from flask import (
|
| 8 |
+
Blueprint,
|
| 9 |
+
render_template,
|
| 10 |
+
request,
|
| 11 |
+
redirect,
|
| 12 |
+
url_for,
|
| 13 |
+
flash,
|
| 14 |
+
current_app,
|
| 15 |
+
send_file,
|
| 16 |
+
session
|
| 17 |
+
)
|
| 18 |
+
from flask_login import login_required, current_user, logout_user
|
| 19 |
+
from werkzeug.utils import secure_filename
|
| 20 |
+
from PIL import Image
|
| 21 |
+
from cryptography.fernet import Fernet, InvalidToken
|
| 22 |
+
|
| 23 |
+
from webpass import db
|
| 24 |
+
from webpass.crypto_utils import generate_key
|
| 25 |
+
from webpass.routes._decorators import otp_required, biometric_required
|
| 26 |
+
|
| 27 |
+
dashboard_bp = Blueprint('dashboard', __name__, url_prefix='/dashboard')
|
| 28 |
+
|
| 29 |
+
UPLOAD_FOLDER = 'uploads'
|
| 30 |
+
ALLOWED_EXT = {'png', 'jpg', 'jpeg', 'gif'}
|
| 31 |
+
|
| 32 |
+
# Detect Cloud Environment
|
| 33 |
+
IS_CLOUD = os.environ.get('SPACE_ID') is not None
|
| 34 |
+
|
| 35 |
+
def allowed_file(filename):
|
| 36 |
+
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXT
|
| 37 |
+
|
| 38 |
+
@dashboard_bp.before_request
|
| 39 |
+
def block_direct_access():
|
| 40 |
+
if not current_user.is_authenticated:
|
| 41 |
+
return
|
| 42 |
+
ref = request.headers.get('Referer', '')
|
| 43 |
+
host = request.host_url
|
| 44 |
+
if not ref.startswith(host):
|
| 45 |
+
logout_user()
|
| 46 |
+
return redirect(url_for('auth.login'))
|
| 47 |
+
|
| 48 |
+
# --- DASHBOARD & PROFILE ---
|
| 49 |
+
|
| 50 |
+
@dashboard_bp.route('/', methods=['GET'])
|
| 51 |
+
@login_required
|
| 52 |
+
@biometric_required
|
| 53 |
+
def dashboard():
|
| 54 |
+
return render_template(
|
| 55 |
+
'dashboard.html',
|
| 56 |
+
has_credentials=True,
|
| 57 |
+
show_change_modal=False,
|
| 58 |
+
show_encrypt_modal=(request.args.get('modal')=='encrypt'),
|
| 59 |
+
show_decrypt_modal=(request.args.get('modal')=='decrypt')
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
@dashboard_bp.route('/secure-tools')
|
| 63 |
+
@login_required
|
| 64 |
+
@biometric_required
|
| 65 |
+
@otp_required
|
| 66 |
+
def secure_tools():
|
| 67 |
+
return render_template('vault.html')
|
| 68 |
+
|
| 69 |
+
@dashboard_bp.route('/network')
|
| 70 |
+
@login_required
|
| 71 |
+
@biometric_required
|
| 72 |
+
@otp_required
|
| 73 |
+
def network_monitor():
|
| 74 |
+
# CLOUD SAFETY TOGGLE
|
| 75 |
+
if IS_CLOUD:
|
| 76 |
+
flash("Cloud Deployment Detected: Active hardware network sniffing is disabled for security. Clone from GitHub and run locally to unlock!", "info")
|
| 77 |
+
return render_template("network_demo.html")
|
| 78 |
+
|
| 79 |
+
return render_template('network.html')
|
| 80 |
+
|
| 81 |
+
@dashboard_bp.route('/profile', methods=['GET', 'POST'])
|
| 82 |
+
@login_required
|
| 83 |
+
def profile():
|
| 84 |
+
if request.method == 'POST':
|
| 85 |
+
file = request.files.get('avatar')
|
| 86 |
+
if file and allowed_file(file.filename):
|
| 87 |
+
ext = os.path.splitext(secure_filename(file.filename))[1]
|
| 88 |
+
filename = f"{uuid4().hex}{ext}"
|
| 89 |
+
upload_dir = os.path.join(current_app.root_path, 'static', UPLOAD_FOLDER)
|
| 90 |
+
os.makedirs(upload_dir, exist_ok=True)
|
| 91 |
+
save_path = os.path.join(upload_dir, filename)
|
| 92 |
+
file.save(save_path)
|
| 93 |
+
|
| 94 |
+
img = Image.open(save_path)
|
| 95 |
+
w, h = img.size
|
| 96 |
+
side = min(w, h)
|
| 97 |
+
left = (w - side) // 2
|
| 98 |
+
top = (h - side) // 2
|
| 99 |
+
img = img.crop((left, top, left+side, top+side))
|
| 100 |
+
img = img.resize((128, 128), Image.LANCZOS)
|
| 101 |
+
img.save(save_path)
|
| 102 |
+
|
| 103 |
+
current_user.profile_image = url_for(
|
| 104 |
+
'static', filename=f"{UPLOAD_FOLDER}/{filename}"
|
| 105 |
+
)
|
| 106 |
+
db.session.commit()
|
| 107 |
+
flash('Avatar updated!', 'success')
|
| 108 |
+
return redirect(url_for('dashboard.profile'))
|
| 109 |
+
|
| 110 |
+
flash('Please upload a valid image file.', 'warning')
|
| 111 |
+
|
| 112 |
+
return render_template('profile.html')
|
| 113 |
+
|
| 114 |
+
@dashboard_bp.route('/profile/remove_avatar', methods=['POST'])
|
| 115 |
+
@login_required
|
| 116 |
+
def remove_avatar():
|
| 117 |
+
user = current_user._get_current_object()
|
| 118 |
+
if user.orig_profile_image:
|
| 119 |
+
user.profile_image = user.orig_profile_image
|
| 120 |
+
db.session.commit()
|
| 121 |
+
flash('Custom avatar removed.', 'success')
|
| 122 |
+
else:
|
| 123 |
+
flash('No custom avatar to remove.', 'info')
|
| 124 |
+
return redirect(url_for('dashboard.profile'))
|
| 125 |
+
|
| 126 |
+
# --- FILE ENCRYPTION (Legacy Server-Side Routes Kept Intact) ---
|
| 127 |
+
|
| 128 |
+
@dashboard_bp.route('/encrypt', methods=['GET', 'POST'])
|
| 129 |
+
@login_required
|
| 130 |
+
def encrypt_file():
|
| 131 |
+
if request.method == 'POST':
|
| 132 |
+
uploaded = request.files.get('file')
|
| 133 |
+
custom_pw = request.form.get('master_password','')
|
| 134 |
+
|
| 135 |
+
if not uploaded or not custom_pw:
|
| 136 |
+
flash('File and Password are required.', 'warning')
|
| 137 |
+
return redirect(url_for('dashboard.dashboard', modal='encrypt'))
|
| 138 |
+
|
| 139 |
+
tmpdir = os.path.join(current_app.instance_path, 'tmp')
|
| 140 |
+
os.makedirs(tmpdir, exist_ok=True)
|
| 141 |
+
tmpname = f"{uuid4().hex}.encpending"
|
| 142 |
+
tmppath = os.path.join(tmpdir, tmpname)
|
| 143 |
+
uploaded.save(tmppath)
|
| 144 |
+
|
| 145 |
+
file_salt = os.urandom(16)
|
| 146 |
+
|
| 147 |
+
session['encrypt_pending'] = {
|
| 148 |
+
'filepath': tmppath,
|
| 149 |
+
'orig_name': secure_filename(uploaded.filename),
|
| 150 |
+
'password': custom_pw,
|
| 151 |
+
'salt': base64.b64encode(file_salt).decode()
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
return redirect(url_for('otp.send_otp',
|
| 155 |
+
next=url_for('dashboard.encrypt_confirm'),
|
| 156 |
+
feature='File Encryption'))
|
| 157 |
+
|
| 158 |
+
return redirect(url_for('dashboard.dashboard', modal='encrypt'))
|
| 159 |
+
|
| 160 |
+
@dashboard_bp.route('/encrypt/confirm', methods=['GET'])
|
| 161 |
+
@login_required
|
| 162 |
+
@otp_required
|
| 163 |
+
def encrypt_confirm():
|
| 164 |
+
data = session.pop('encrypt_pending', None)
|
| 165 |
+
if not data:
|
| 166 |
+
flash('No encryption in progress.', 'warning')
|
| 167 |
+
return redirect(url_for('dashboard.dashboard'))
|
| 168 |
+
|
| 169 |
+
salt = base64.b64decode(data['salt'])
|
| 170 |
+
raw_key = generate_key(data['password'], salt)
|
| 171 |
+
fernet_key = base64.urlsafe_b64encode(raw_key)
|
| 172 |
+
f = Fernet(fernet_key)
|
| 173 |
+
|
| 174 |
+
with open(data['filepath'], 'rb') as f_in:
|
| 175 |
+
ciphertext = f.encrypt(f_in.read())
|
| 176 |
+
os.remove(data['filepath'])
|
| 177 |
+
|
| 178 |
+
final_data = salt + ciphertext
|
| 179 |
+
|
| 180 |
+
buf = BytesIO(final_data)
|
| 181 |
+
buf.seek(0)
|
| 182 |
+
download_name = data['orig_name'] + '.enc'
|
| 183 |
+
|
| 184 |
+
return send_file(
|
| 185 |
+
buf,
|
| 186 |
+
as_attachment=True,
|
| 187 |
+
download_name=download_name,
|
| 188 |
+
mimetype='application/octet-stream'
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
# --- FILE DECRYPTION (Legacy Server-Side Routes Kept Intact) ---
|
| 192 |
+
|
| 193 |
+
@dashboard_bp.route('/decrypt', methods=['GET', 'POST'])
|
| 194 |
+
@login_required
|
| 195 |
+
def decrypt_file():
|
| 196 |
+
if request.method == 'POST':
|
| 197 |
+
uploaded = request.files.get('file')
|
| 198 |
+
custom_pw = request.form.get('master_password','')
|
| 199 |
+
|
| 200 |
+
if not uploaded or not custom_pw:
|
| 201 |
+
flash('File and Password are required.', 'warning')
|
| 202 |
+
return redirect(url_for('dashboard.dashboard', modal='decrypt'))
|
| 203 |
+
|
| 204 |
+
tmpdir = os.path.join(current_app.instance_path, 'tmp')
|
| 205 |
+
os.makedirs(tmpdir, exist_ok=True)
|
| 206 |
+
tmpname = f"{uuid4().hex}.decpending"
|
| 207 |
+
tmppath = os.path.join(tmpdir, tmpname)
|
| 208 |
+
uploaded.save(tmppath)
|
| 209 |
+
|
| 210 |
+
session['decrypt_pending'] = {
|
| 211 |
+
'filepath': tmppath,
|
| 212 |
+
'orig_name': secure_filename(uploaded.filename),
|
| 213 |
+
'password': custom_pw
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
return redirect(url_for('otp.send_otp',
|
| 217 |
+
next=url_for('dashboard.decrypt_confirm'),
|
| 218 |
+
feature='File Decryption'))
|
| 219 |
+
|
| 220 |
+
return redirect(url_for('dashboard.dashboard', modal='decrypt'))
|
| 221 |
+
|
| 222 |
+
@dashboard_bp.route('/decrypt/confirm', methods=['GET'])
|
| 223 |
+
@login_required
|
| 224 |
+
@otp_required
|
| 225 |
+
def decrypt_confirm():
|
| 226 |
+
data = session.pop('decrypt_pending', None)
|
| 227 |
+
if not data:
|
| 228 |
+
flash('No decryption in progress.', 'warning')
|
| 229 |
+
return redirect(url_for('dashboard.dashboard'))
|
| 230 |
+
|
| 231 |
+
try:
|
| 232 |
+
with open(data['filepath'], 'rb') as f_in:
|
| 233 |
+
file_content = f_in.read()
|
| 234 |
+
|
| 235 |
+
if len(file_content) < 16:
|
| 236 |
+
raise InvalidToken
|
| 237 |
+
|
| 238 |
+
salt = file_content[:16]
|
| 239 |
+
ciphertext = file_content[16:]
|
| 240 |
+
|
| 241 |
+
raw_key = generate_key(data['password'], salt)
|
| 242 |
+
fernet_key = base64.urlsafe_b64encode(raw_key)
|
| 243 |
+
f = Fernet(fernet_key)
|
| 244 |
+
|
| 245 |
+
plaintext = f.decrypt(ciphertext)
|
| 246 |
+
|
| 247 |
+
except InvalidToken:
|
| 248 |
+
if os.path.exists(data['filepath']): os.remove(data['filepath'])
|
| 249 |
+
flash('Decryption failed: Wrong password or corrupted file.', 'danger')
|
| 250 |
+
return redirect(url_for('dashboard.dashboard', modal='decrypt'))
|
| 251 |
+
except Exception as e:
|
| 252 |
+
if os.path.exists(data['filepath']): os.remove(data['filepath'])
|
| 253 |
+
flash(f'Error: {str(e)}', 'danger')
|
| 254 |
+
return redirect(url_for('dashboard.dashboard', modal='decrypt'))
|
| 255 |
+
|
| 256 |
+
os.remove(data['filepath'])
|
| 257 |
+
|
| 258 |
+
buf = BytesIO(plaintext)
|
| 259 |
+
buf.seek(0)
|
| 260 |
+
filename = data['orig_name']
|
| 261 |
+
if filename.lower().endswith('.enc'):
|
| 262 |
+
filename = filename[:-4]
|
| 263 |
+
|
| 264 |
+
return send_file(
|
| 265 |
+
buf,
|
| 266 |
+
as_attachment=True,
|
| 267 |
+
download_name=filename,
|
| 268 |
+
mimetype='application/octet-stream'
|
| 269 |
+
)
|
webpass/routes/otp.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
import time
|
| 3 |
+
|
| 4 |
+
from flask import (
|
| 5 |
+
Blueprint,
|
| 6 |
+
render_template,
|
| 7 |
+
request,
|
| 8 |
+
session,
|
| 9 |
+
redirect,
|
| 10 |
+
url_for,
|
| 11 |
+
flash
|
| 12 |
+
)
|
| 13 |
+
from flask_login import login_required, current_user
|
| 14 |
+
from flask_mail import Message
|
| 15 |
+
|
| 16 |
+
# Ensure these are imported from your main app package
|
| 17 |
+
from webpass import mail, csrf, limiter
|
| 18 |
+
|
| 19 |
+
otp_bp = Blueprint("otp", __name__)
|
| 20 |
+
|
| 21 |
+
@otp_bp.route("/otp/send")
|
| 22 |
+
@login_required
|
| 23 |
+
@csrf.exempt
|
| 24 |
+
@limiter.limit("3 per minute", key_func=lambda: str(current_user.id))
|
| 25 |
+
def send_otp():
|
| 26 |
+
"""
|
| 27 |
+
Generates OTP, sends email, and redirects to verification page.
|
| 28 |
+
"""
|
| 29 |
+
# 1. Capture where the user was trying to go
|
| 30 |
+
next_url = request.args.get("next") or url_for("dashboard.dashboard")
|
| 31 |
+
feature = request.args.get("feature", "Sensitive Action")
|
| 32 |
+
|
| 33 |
+
# 2. Generate Code
|
| 34 |
+
code = f"{random.randint(0, 999999):06d}"
|
| 35 |
+
|
| 36 |
+
# 3. Store in Session
|
| 37 |
+
session["otp_code"] = code
|
| 38 |
+
session["otp_expiry"] = time.time() + 5 * 60 # 5 minutes
|
| 39 |
+
session["otp_next"] = next_url
|
| 40 |
+
session["otp_feature"] = feature
|
| 41 |
+
|
| 42 |
+
# 4. Send Email
|
| 43 |
+
try:
|
| 44 |
+
msg = Message(
|
| 45 |
+
subject=f"Your OTP for {feature}",
|
| 46 |
+
recipients=[current_user.email],
|
| 47 |
+
body=(
|
| 48 |
+
f"Your one-time code for accessing {feature} is:\n\n"
|
| 49 |
+
f" {code}\n\n"
|
| 50 |
+
"It expires in 5 minutes."
|
| 51 |
+
)
|
| 52 |
+
)
|
| 53 |
+
mail.send(msg)
|
| 54 |
+
flash(f"An OTP has been sent to {current_user.email}", "info")
|
| 55 |
+
except Exception as e:
|
| 56 |
+
print(f"Error sending email: {e}")
|
| 57 |
+
flash("Failed to send OTP email. Please checks logs.", "danger")
|
| 58 |
+
|
| 59 |
+
# 5. Redirect to Enter Code
|
| 60 |
+
return redirect(url_for("otp.verify_otp"))
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
@otp_bp.route("/otp/verify", methods=["GET", "POST"])
|
| 64 |
+
@login_required
|
| 65 |
+
@csrf.exempt
|
| 66 |
+
def verify_otp():
|
| 67 |
+
"""
|
| 68 |
+
Checks the code entered by the user.
|
| 69 |
+
"""
|
| 70 |
+
feature = session.get("otp_feature", "Sensitive Action")
|
| 71 |
+
|
| 72 |
+
if request.method == "POST":
|
| 73 |
+
entered = request.form.get("otp", "").strip()
|
| 74 |
+
code = session.get("otp_code")
|
| 75 |
+
expiry = session.get("otp_expiry", 0)
|
| 76 |
+
|
| 77 |
+
# 1. Check Validity
|
| 78 |
+
if code and time.time() < expiry and entered == code:
|
| 79 |
+
# Clear sensitive session data
|
| 80 |
+
session.pop("otp_code", None)
|
| 81 |
+
session.pop("otp_expiry", None)
|
| 82 |
+
session.pop("otp_feature", None)
|
| 83 |
+
|
| 84 |
+
# 2. Set the 'Verified' Flag
|
| 85 |
+
session["otp_verified"] = True
|
| 86 |
+
|
| 87 |
+
# 3. Redirect back to the tool (Dead Drop, Stego, etc.)
|
| 88 |
+
next_url = session.pop("otp_next", url_for("dashboard.dashboard"))
|
| 89 |
+
return redirect(next_url)
|
| 90 |
+
|
| 91 |
+
flash("Invalid or expired OTP, please try again.", "danger")
|
| 92 |
+
return redirect(url_for("otp.verify_otp"))
|
| 93 |
+
|
| 94 |
+
return render_template("verify_otp.html", feature=feature)
|
webpass/routes/share.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
import datetime
|
| 3 |
+
from flask import Blueprint, render_template, request, jsonify, url_for, current_app
|
| 4 |
+
from flask_login import login_required
|
| 5 |
+
from webpass import db, limiter, csrf
|
| 6 |
+
from webpass.models import DeadDrop
|
| 7 |
+
from webpass.routes._decorators import biometric_required, otp_required
|
| 8 |
+
|
| 9 |
+
share_bp = Blueprint("share", __name__)
|
| 10 |
+
|
| 11 |
+
@share_bp.route("/share/create", methods=["GET"])
|
| 12 |
+
@login_required
|
| 13 |
+
@biometric_required
|
| 14 |
+
@otp_required
|
| 15 |
+
def share_ui():
|
| 16 |
+
return render_template("share.html")
|
| 17 |
+
|
| 18 |
+
@share_bp.route("/share/create", methods=["POST"])
|
| 19 |
+
@limiter.limit("5 per minute")
|
| 20 |
+
@csrf.exempt
|
| 21 |
+
def create_share():
|
| 22 |
+
data = request.get_json()
|
| 23 |
+
if not data: return jsonify({"error": "No JSON payload"}), 400
|
| 24 |
+
|
| 25 |
+
ciphertext = data.get("ciphertext")
|
| 26 |
+
iv = data.get("iv")
|
| 27 |
+
salt = data.get("salt")
|
| 28 |
+
ttl = data.get("ttl", 60) # Link expiry
|
| 29 |
+
view_time = data.get("view_time", 30) # Message view time
|
| 30 |
+
|
| 31 |
+
if not (ciphertext and iv and salt):
|
| 32 |
+
return jsonify({"error": "Missing crypto fields"}), 400
|
| 33 |
+
|
| 34 |
+
drop_id = str(uuid.uuid4())
|
| 35 |
+
|
| 36 |
+
try:
|
| 37 |
+
minutes = int(ttl)
|
| 38 |
+
view_time_sec = int(view_time)
|
| 39 |
+
if view_time_sec <= 0: view_time_sec = 30
|
| 40 |
+
except ValueError:
|
| 41 |
+
minutes = 60
|
| 42 |
+
view_time_sec = 30
|
| 43 |
+
|
| 44 |
+
expires_at = datetime.datetime.utcnow() + datetime.timedelta(minutes=minutes)
|
| 45 |
+
|
| 46 |
+
new_drop = DeadDrop(
|
| 47 |
+
id=drop_id, ciphertext=ciphertext, iv=iv, salt=salt,
|
| 48 |
+
expires_at=expires_at, created_at=datetime.datetime.utcnow(),
|
| 49 |
+
view_time=view_time_sec
|
| 50 |
+
)
|
| 51 |
+
db.session.add(new_drop)
|
| 52 |
+
db.session.commit()
|
| 53 |
+
|
| 54 |
+
relative_path = url_for('share.view_drop_page', drop_id=drop_id)
|
| 55 |
+
ngrok_base = current_app.config.get('NGROK_URL')
|
| 56 |
+
|
| 57 |
+
if ngrok_base:
|
| 58 |
+
if ngrok_base.endswith('/'): ngrok_base = ngrok_base[:-1]
|
| 59 |
+
full_link = f"{ngrok_base}{relative_path}"
|
| 60 |
+
else:
|
| 61 |
+
full_link = url_for('share.view_drop_page', drop_id=drop_id, _external=True)
|
| 62 |
+
|
| 63 |
+
return jsonify({"id": drop_id, "link": full_link})
|
| 64 |
+
|
| 65 |
+
@share_bp.route("/share/v/<drop_id>", methods=["GET"])
|
| 66 |
+
def view_drop_page(drop_id):
|
| 67 |
+
drop = DeadDrop.query.get(drop_id)
|
| 68 |
+
if not drop:
|
| 69 |
+
return render_template("share_error.html", message="This link has expired or never existed.")
|
| 70 |
+
|
| 71 |
+
if datetime.datetime.utcnow() > drop.expires_at:
|
| 72 |
+
db.session.delete(drop)
|
| 73 |
+
db.session.commit()
|
| 74 |
+
return render_template("share_error.html", message="This link has expired.")
|
| 75 |
+
|
| 76 |
+
return render_template("share_view.html", drop_id=drop_id)
|
| 77 |
+
|
| 78 |
+
@share_bp.route("/api/share/<drop_id>", methods=["POST"])
|
| 79 |
+
@limiter.limit("10 per minute")
|
| 80 |
+
@csrf.exempt
|
| 81 |
+
def reveal_drop_api(drop_id):
|
| 82 |
+
drop = DeadDrop.query.get(drop_id)
|
| 83 |
+
if not drop: return jsonify({"error": "Not found"}), 410
|
| 84 |
+
|
| 85 |
+
if datetime.datetime.utcnow() > drop.expires_at:
|
| 86 |
+
db.session.delete(drop)
|
| 87 |
+
db.session.commit()
|
| 88 |
+
return jsonify({"error": "Expired"}), 410
|
| 89 |
+
|
| 90 |
+
payload = {
|
| 91 |
+
"ciphertext": drop.ciphertext,
|
| 92 |
+
"iv": drop.iv,
|
| 93 |
+
"salt": drop.salt,
|
| 94 |
+
"view_time": drop.view_time # SENDING TIMER TO FRONTEND
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
# BURN IT
|
| 98 |
+
db.session.delete(drop)
|
| 99 |
+
db.session.commit()
|
| 100 |
+
|
| 101 |
+
return jsonify(payload)
|
webpass/routes/stego.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, render_template, request, send_file, flash, redirect, url_for
|
| 2 |
+
from flask_login import login_required
|
| 3 |
+
from webpass.routes._decorators import biometric_required, otp_required
|
| 4 |
+
# Import the new universal functions
|
| 5 |
+
from webpass.stego_utils import encode_data, decode_data
|
| 6 |
+
|
| 7 |
+
stego_bp = Blueprint('stego', __name__)
|
| 8 |
+
|
| 9 |
+
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg'}
|
| 10 |
+
|
| 11 |
+
def allowed_file(filename):
|
| 12 |
+
return '.' in filename and \
|
| 13 |
+
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
| 14 |
+
|
| 15 |
+
@stego_bp.route('/steganography', methods=['GET'])
|
| 16 |
+
@login_required
|
| 17 |
+
@biometric_required
|
| 18 |
+
@otp_required
|
| 19 |
+
def ui():
|
| 20 |
+
return render_template('stego.html')
|
| 21 |
+
|
| 22 |
+
@stego_bp.route('/steganography/hide', methods=['POST'])
|
| 23 |
+
@login_required
|
| 24 |
+
def hide():
|
| 25 |
+
if 'cover_image' not in request.files:
|
| 26 |
+
flash('Missing cover image.', 'warning')
|
| 27 |
+
return redirect(url_for('stego.ui'))
|
| 28 |
+
|
| 29 |
+
cover = request.files['cover_image']
|
| 30 |
+
password = request.form.get('stego_password', '')
|
| 31 |
+
mode = request.form.get('mode', 'text') # 'text' or 'file'
|
| 32 |
+
|
| 33 |
+
if cover.filename == '' or not allowed_file(cover.filename):
|
| 34 |
+
flash('Invalid cover image.', 'warning')
|
| 35 |
+
return redirect(url_for('stego.ui'))
|
| 36 |
+
|
| 37 |
+
if not password:
|
| 38 |
+
flash('Password is required.', 'warning')
|
| 39 |
+
return redirect(url_for('stego.ui'))
|
| 40 |
+
|
| 41 |
+
try:
|
| 42 |
+
if mode == 'text':
|
| 43 |
+
secret_text = request.form.get('secret_text', '')
|
| 44 |
+
if not secret_text:
|
| 45 |
+
flash("Secret text is empty.", "warning")
|
| 46 |
+
return redirect(url_for('stego.ui'))
|
| 47 |
+
|
| 48 |
+
output = encode_data(cover, secret_text, password)
|
| 49 |
+
|
| 50 |
+
elif mode == 'file':
|
| 51 |
+
if 'secret_file' not in request.files:
|
| 52 |
+
flash("No secret file uploaded.", "warning")
|
| 53 |
+
return redirect(url_for('stego.ui'))
|
| 54 |
+
|
| 55 |
+
secret_file = request.files['secret_file']
|
| 56 |
+
if secret_file.filename == '':
|
| 57 |
+
flash("No secret file selected.", "warning")
|
| 58 |
+
return redirect(url_for('stego.ui'))
|
| 59 |
+
|
| 60 |
+
output = encode_data(cover, secret_file, password, filename=secret_file.filename)
|
| 61 |
+
|
| 62 |
+
return send_file(
|
| 63 |
+
output,
|
| 64 |
+
mimetype='image/png',
|
| 65 |
+
as_attachment=True,
|
| 66 |
+
download_name='secure_stego_image.png'
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
except ValueError as e:
|
| 70 |
+
flash(f'Error: {str(e)}', 'danger')
|
| 71 |
+
except Exception as e:
|
| 72 |
+
flash(f'System Error: {str(e)}', 'danger')
|
| 73 |
+
|
| 74 |
+
return redirect(url_for('stego.ui'))
|
| 75 |
+
|
| 76 |
+
@stego_bp.route('/steganography/reveal', methods=['POST'])
|
| 77 |
+
@login_required
|
| 78 |
+
def reveal():
|
| 79 |
+
if 'stego_image' not in request.files:
|
| 80 |
+
flash('Missing image.', 'warning')
|
| 81 |
+
return redirect(url_for('stego.ui'))
|
| 82 |
+
|
| 83 |
+
file = request.files['stego_image']
|
| 84 |
+
password = request.form.get('stego_password', '')
|
| 85 |
+
|
| 86 |
+
if file.filename == '' or not allowed_file(file.filename):
|
| 87 |
+
flash('Invalid file.', 'warning')
|
| 88 |
+
return redirect(url_for('stego.ui'))
|
| 89 |
+
|
| 90 |
+
if not password:
|
| 91 |
+
flash('Password is required.', 'warning')
|
| 92 |
+
return redirect(url_for('stego.ui'))
|
| 93 |
+
|
| 94 |
+
try:
|
| 95 |
+
result = decode_data(file, password)
|
| 96 |
+
|
| 97 |
+
if result['type'] == 'file':
|
| 98 |
+
# It's a file! Send it as a download
|
| 99 |
+
return send_file(
|
| 100 |
+
result['file_bytes'],
|
| 101 |
+
as_attachment=True,
|
| 102 |
+
download_name=result['filename']
|
| 103 |
+
)
|
| 104 |
+
else:
|
| 105 |
+
# It's text! Show it on screen
|
| 106 |
+
flash(f'Successfully Decoded Text', 'success')
|
| 107 |
+
return render_template('stego.html', revealed_secret=result['content'])
|
| 108 |
+
|
| 109 |
+
except ValueError as e:
|
| 110 |
+
flash(str(e), 'danger')
|
| 111 |
+
return redirect(url_for('stego.ui'))
|
| 112 |
+
except Exception as e:
|
| 113 |
+
flash(f'System Error: {str(e)}', 'danger')
|
| 114 |
+
return redirect(url_for('stego.ui'))
|
webpass/routes/tools.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
import hashlib
|
| 3 |
+
import requests
|
| 4 |
+
from PIL import Image, ExifTags
|
| 5 |
+
from flask import Blueprint, render_template, request, jsonify, send_file, flash, redirect, url_for
|
| 6 |
+
from flask_login import login_required
|
| 7 |
+
from webpass.routes._decorators import biometric_required, otp_required
|
| 8 |
+
|
| 9 |
+
tools_bp = Blueprint('tools', __name__)
|
| 10 |
+
|
| 11 |
+
# --- HELPER: ADVANCED GPS DECODER ---
|
| 12 |
+
def get_decimal_from_dms(dms, ref):
|
| 13 |
+
"""
|
| 14 |
+
Converts GPS (Degrees, Minutes, Seconds) to Decimal format.
|
| 15 |
+
"""
|
| 16 |
+
degrees = dms[0]
|
| 17 |
+
minutes = dms[1]
|
| 18 |
+
seconds = dms[2]
|
| 19 |
+
|
| 20 |
+
decimal = degrees + (minutes / 60.0) + (seconds / 3600.0)
|
| 21 |
+
|
| 22 |
+
if ref in ['S', 'W']:
|
| 23 |
+
decimal = -decimal
|
| 24 |
+
return decimal
|
| 25 |
+
|
| 26 |
+
def get_geotagging(exif):
|
| 27 |
+
if not exif:
|
| 28 |
+
return None
|
| 29 |
+
|
| 30 |
+
geotagging = {}
|
| 31 |
+
gps_info = exif.get(34853) # 34853 is the GPS Info Tag ID
|
| 32 |
+
|
| 33 |
+
if not gps_info:
|
| 34 |
+
return None
|
| 35 |
+
|
| 36 |
+
# Decode GPS Tags using PIL.ExifTags.GPSTAGS
|
| 37 |
+
for key in gps_info.keys():
|
| 38 |
+
decode = ExifTags.GPSTAGS.get(key, key)
|
| 39 |
+
geotagging[decode] = gps_info[key]
|
| 40 |
+
|
| 41 |
+
# Calculate actual coordinates
|
| 42 |
+
if 'GPSLatitude' in geotagging and 'GPSLatitudeRef' in geotagging and \
|
| 43 |
+
'GPSLongitude' in geotagging and 'GPSLongitudeRef' in geotagging:
|
| 44 |
+
|
| 45 |
+
lat = get_decimal_from_dms(geotagging['GPSLatitude'], geotagging['GPSLatitudeRef'])
|
| 46 |
+
lon = get_decimal_from_dms(geotagging['GPSLongitude'], geotagging['GPSLongitudeRef'])
|
| 47 |
+
|
| 48 |
+
return {
|
| 49 |
+
"latitude": lat,
|
| 50 |
+
"longitude": lon,
|
| 51 |
+
"map_url": f"https://www.google.com/maps?q={lat},{lon}",
|
| 52 |
+
"raw": geotagging
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
return None
|
| 56 |
+
|
| 57 |
+
# --- 1. GHOST METADATA WIPER ---
|
| 58 |
+
|
| 59 |
+
@tools_bp.route('/tools/metadata', methods=['GET'])
|
| 60 |
+
@login_required
|
| 61 |
+
@biometric_required
|
| 62 |
+
@otp_required
|
| 63 |
+
def metadata_ui():
|
| 64 |
+
return render_template('metadata.html')
|
| 65 |
+
|
| 66 |
+
@tools_bp.route('/tools/metadata/scan', methods=['POST'])
|
| 67 |
+
@login_required
|
| 68 |
+
def metadata_scan():
|
| 69 |
+
if 'image' not in request.files:
|
| 70 |
+
return jsonify({'error': 'No image uploaded'}), 400
|
| 71 |
+
|
| 72 |
+
file = request.files['image']
|
| 73 |
+
try:
|
| 74 |
+
img = Image.open(file)
|
| 75 |
+
raw_exif = img._getexif()
|
| 76 |
+
|
| 77 |
+
privacy_report = {
|
| 78 |
+
"critical": {}, # GPS, Serial Numbers, Dates
|
| 79 |
+
"technical": {}, # Lens, ISO, Shutter
|
| 80 |
+
"status": "clean"
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
if raw_exif:
|
| 84 |
+
# 1. EXTRACT GPS (The "Scary" Part)
|
| 85 |
+
gps_data = get_geotagging(raw_exif)
|
| 86 |
+
if gps_data:
|
| 87 |
+
privacy_report['critical']['GPS Location'] = f"{gps_data['latitude']:.5f}, {gps_data['longitude']:.5f}"
|
| 88 |
+
privacy_report['critical']['Map Link'] = gps_data['map_url']
|
| 89 |
+
privacy_report['status'] = "danger"
|
| 90 |
+
|
| 91 |
+
# 2. EXTRACT STANDARD TAGS
|
| 92 |
+
for tag, value in raw_exif.items():
|
| 93 |
+
tag_name = ExifTags.TAGS.get(tag, tag)
|
| 94 |
+
|
| 95 |
+
# Skip binary/unreadable data
|
| 96 |
+
if isinstance(value, (bytes, bytearray)):
|
| 97 |
+
continue
|
| 98 |
+
|
| 99 |
+
str_val = str(value)
|
| 100 |
+
|
| 101 |
+
# categorize interesting tags
|
| 102 |
+
if tag_name in ['Make', 'Model', 'Software', 'BodySerialNumber', 'LensModel', 'LensSerialNumber', 'Artist', 'HostComputer']:
|
| 103 |
+
privacy_report['critical'][tag_name] = str_val
|
| 104 |
+
privacy_report['status'] = "danger" if privacy_report['status'] == "clean" else "danger"
|
| 105 |
+
|
| 106 |
+
elif tag_name in ['DateTime', 'DateTimeOriginal', 'DateTimeDigitized']:
|
| 107 |
+
privacy_report['critical'][tag_name] = str_val
|
| 108 |
+
|
| 109 |
+
elif tag_name in ['ExposureTime', 'FNumber', 'ISOSpeedRatings', 'FocalLength', 'Flash', 'WhiteBalance']:
|
| 110 |
+
privacy_report['technical'][tag_name] = str_val
|
| 111 |
+
|
| 112 |
+
# If we found nothing
|
| 113 |
+
if not privacy_report['critical'] and not privacy_report['technical']:
|
| 114 |
+
return jsonify({'status': 'clean', 'message': 'No hidden metadata found.'})
|
| 115 |
+
|
| 116 |
+
return jsonify({'status': 'infected', 'data': privacy_report})
|
| 117 |
+
|
| 118 |
+
except Exception as e:
|
| 119 |
+
return jsonify({'error': str(e)}), 500
|
| 120 |
+
|
| 121 |
+
@tools_bp.route('/tools/metadata/wipe', methods=['POST'])
|
| 122 |
+
@login_required
|
| 123 |
+
def metadata_wipe():
|
| 124 |
+
if 'image' not in request.files:
|
| 125 |
+
return redirect(url_for('tools.metadata_ui'))
|
| 126 |
+
|
| 127 |
+
file = request.files['image']
|
| 128 |
+
try:
|
| 129 |
+
img = Image.open(file)
|
| 130 |
+
|
| 131 |
+
# Strip metadata by creating a fresh image copy
|
| 132 |
+
data = list(img.getdata())
|
| 133 |
+
clean_img = Image.new(img.mode, img.size)
|
| 134 |
+
clean_img.putdata(data)
|
| 135 |
+
|
| 136 |
+
output = io.BytesIO()
|
| 137 |
+
# Clean save (format handling)
|
| 138 |
+
fmt = img.format or "JPEG"
|
| 139 |
+
clean_img.save(output, format=fmt)
|
| 140 |
+
output.seek(0)
|
| 141 |
+
|
| 142 |
+
return send_file(
|
| 143 |
+
output,
|
| 144 |
+
mimetype=file.content_type,
|
| 145 |
+
as_attachment=True,
|
| 146 |
+
download_name=f"ghost_clean_{file.filename}"
|
| 147 |
+
)
|
| 148 |
+
except Exception as e:
|
| 149 |
+
flash(f"Error processing image: {str(e)}", "danger")
|
| 150 |
+
return redirect(url_for('tools.metadata_ui'))
|
| 151 |
+
|
| 152 |
+
# --- 2. BREACH CHECKER (Keep existing code) ---
|
| 153 |
+
|
| 154 |
+
@tools_bp.route('/tools/breach', methods=['GET'])
|
| 155 |
+
@login_required
|
| 156 |
+
@biometric_required
|
| 157 |
+
@otp_required
|
| 158 |
+
def breach_ui():
|
| 159 |
+
return render_template('breach.html')
|
| 160 |
+
|
| 161 |
+
@tools_bp.route('/tools/breach/check', methods=['POST'])
|
| 162 |
+
@login_required
|
| 163 |
+
def breach_check():
|
| 164 |
+
password = request.json.get('password', '')
|
| 165 |
+
if not password: return jsonify({'error': 'Password required'}), 400
|
| 166 |
+
|
| 167 |
+
sha1 = hashlib.sha1(password.encode('utf-8')).hexdigest().upper()
|
| 168 |
+
prefix, suffix = sha1[:5], sha1[5:]
|
| 169 |
+
|
| 170 |
+
try:
|
| 171 |
+
res = requests.get(f"https://api.pwnedpasswords.com/range/{prefix}")
|
| 172 |
+
if res.status_code != 200: return jsonify({'error': 'API Error'}), 500
|
| 173 |
+
|
| 174 |
+
hashes = (line.split(':') for line in res.text.splitlines())
|
| 175 |
+
count = 0
|
| 176 |
+
for h, c in hashes:
|
| 177 |
+
if h == suffix:
|
| 178 |
+
count = int(c)
|
| 179 |
+
break
|
| 180 |
+
|
| 181 |
+
return jsonify({'leaked': count > 0, 'count': count})
|
| 182 |
+
|
| 183 |
+
except Exception as e:
|
| 184 |
+
return jsonify({'error': str(e)}), 500
|
webpass/security_utils.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# webpass/security_utils.py
|
| 2 |
+
import os
|
| 3 |
+
from urllib.parse import urlparse, urljoin
|
| 4 |
+
from flask import request
|
| 5 |
+
from cryptography.hazmat.primitives import padding
|
| 6 |
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
| 7 |
+
|
| 8 |
+
def is_safe_url(target):
|
| 9 |
+
"""
|
| 10 |
+
Ensures the redirect target is safe and belongs to OUR website.
|
| 11 |
+
Prevents Open Redirect Vulnerabilities (OWASP Top 10).
|
| 12 |
+
"""
|
| 13 |
+
ref_url = urlparse(request.host_url)
|
| 14 |
+
test_url = urlparse(urljoin(request.host_url, target))
|
| 15 |
+
return test_url.scheme in ('http', 'https') and \
|
| 16 |
+
ref_url.netloc == test_url.netloc
|
| 17 |
+
|
| 18 |
+
def encrypt_stream(input_stream, output_stream, key, iv):
|
| 19 |
+
"""
|
| 20 |
+
Encrypts data in small chunks (64KB) so we never load the whole file into RAM.
|
| 21 |
+
Fixes Denial of Service (DoS) vulnerability via memory exhaustion.
|
| 22 |
+
"""
|
| 23 |
+
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
|
| 24 |
+
encryptor = cipher.encryptor()
|
| 25 |
+
padder = padding.PKCS7(128).padder()
|
| 26 |
+
chunk_size = 64 * 1024 # 64KB chunks
|
| 27 |
+
|
| 28 |
+
while True:
|
| 29 |
+
chunk = input_stream.read(chunk_size)
|
| 30 |
+
if not chunk:
|
| 31 |
+
break
|
| 32 |
+
# Update padder with new chunk, it yields full blocks if available
|
| 33 |
+
padded_data = padder.update(chunk)
|
| 34 |
+
if padded_data:
|
| 35 |
+
output_stream.write(encryptor.update(padded_data))
|
| 36 |
+
|
| 37 |
+
# Finalize padding and encryption
|
| 38 |
+
output_stream.write(encryptor.update(padder.finalize()))
|
| 39 |
+
output_stream.write(encryptor.finalize())
|
| 40 |
+
|
| 41 |
+
def decrypt_stream(input_stream, output_stream, key, iv):
|
| 42 |
+
"""
|
| 43 |
+
Decrypts data in small chunks (64KB).
|
| 44 |
+
"""
|
| 45 |
+
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
|
| 46 |
+
decryptor = cipher.decryptor()
|
| 47 |
+
unpadder = padding.PKCS7(128).unpadder()
|
| 48 |
+
chunk_size = 64 * 1024
|
| 49 |
+
|
| 50 |
+
while True:
|
| 51 |
+
chunk = input_stream.read(chunk_size)
|
| 52 |
+
if not chunk:
|
| 53 |
+
break
|
| 54 |
+
|
| 55 |
+
decrypted_chunk = decryptor.update(chunk)
|
| 56 |
+
if decrypted_chunk:
|
| 57 |
+
output_stream.write(unpadder.update(decrypted_chunk))
|
| 58 |
+
|
| 59 |
+
# Finalize decryption and unpadding
|
| 60 |
+
output_stream.write(unpadder.update(decryptor.finalize()))
|
| 61 |
+
output_stream.write(unpadder.finalize())
|
webpass/static/css/login.css
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import url("https://fonts.googleapis.com/css?family=Raleway:400,700");
|
| 2 |
+
*, *:before, *:after {
|
| 3 |
+
box-sizing: border-box;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
body {
|
| 7 |
+
min-height: 100vh;
|
| 8 |
+
font-family: "Raleway", sans-serif;
|
| 9 |
+
margin: 0;
|
| 10 |
+
padding: 0;
|
| 11 |
+
overflow: hidden;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
.container {
|
| 15 |
+
position: absolute;
|
| 16 |
+
width: 100vw;
|
| 17 |
+
height: 100vh;
|
| 18 |
+
top: 0;
|
| 19 |
+
left: 0;
|
| 20 |
+
overflow: hidden;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.top:before, .top:after, .bottom:before, .bottom:after {
|
| 24 |
+
content: "";
|
| 25 |
+
display: block;
|
| 26 |
+
position: absolute;
|
| 27 |
+
width: 200vmax;
|
| 28 |
+
height: 200vmax;
|
| 29 |
+
top: 50%;
|
| 30 |
+
left: 50%;
|
| 31 |
+
margin-top: -100vmax;
|
| 32 |
+
transform-origin: 0 50%;
|
| 33 |
+
transition: all 0.5s cubic-bezier(0.445, 0.05, 0, 1);
|
| 34 |
+
z-index: 20;
|
| 35 |
+
opacity: 0.65;
|
| 36 |
+
transition-delay: 0.2s;
|
| 37 |
+
}
|
| 38 |
+
.top:before { transform: rotate(45deg); background: #e46569; }
|
| 39 |
+
.top:after { transform: rotate(135deg); background: #ecaf81; }
|
| 40 |
+
.bottom:before { transform: rotate(-45deg); background: #60b8d4; }
|
| 41 |
+
.bottom:after { transform: rotate(-135deg); background: #3745b5; }
|
| 42 |
+
|
| 43 |
+
.container:hover .top:before,
|
| 44 |
+
.container:hover .top:after,
|
| 45 |
+
.container:hover .bottom:before,
|
| 46 |
+
.container:hover .bottom:after,
|
| 47 |
+
.container:active .top:before,
|
| 48 |
+
.container:active .top:after,
|
| 49 |
+
.container:active .bottom:before,
|
| 50 |
+
.container:active .bottom:after {
|
| 51 |
+
margin-left: 200px;
|
| 52 |
+
transform-origin: -200px 50%;
|
| 53 |
+
transition-delay: 0s;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* Only show the content on hover, with no box, border, or background */
|
| 57 |
+
.center {
|
| 58 |
+
position: absolute;
|
| 59 |
+
top: 50%;
|
| 60 |
+
left: 50%;
|
| 61 |
+
transform: translate(-50%, -50%);
|
| 62 |
+
display: flex;
|
| 63 |
+
flex-direction: column;
|
| 64 |
+
justify-content: center;
|
| 65 |
+
align-items: center;
|
| 66 |
+
padding: 0; /* No padding for box effect */
|
| 67 |
+
opacity: 0;
|
| 68 |
+
pointer-events: none;
|
| 69 |
+
background: none;
|
| 70 |
+
box-shadow: none;
|
| 71 |
+
border: none;
|
| 72 |
+
outline: none;
|
| 73 |
+
z-index: 10;
|
| 74 |
+
transition: opacity 0.5s cubic-bezier(0.445, 0.05, 0, 1);
|
| 75 |
+
transition-delay: 0s;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.container:hover .center,
|
| 79 |
+
.container:active .center {
|
| 80 |
+
opacity: 1;
|
| 81 |
+
pointer-events: auto;
|
| 82 |
+
background: none;
|
| 83 |
+
box-shadow: none;
|
| 84 |
+
border: none;
|
| 85 |
+
outline: none;
|
| 86 |
+
transition-delay: 0.2s;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.center h2 {
|
| 90 |
+
margin-bottom: 20px;
|
| 91 |
+
font-weight: 700;
|
| 92 |
+
color: #333;
|
| 93 |
+
background: none;
|
| 94 |
+
border: none;
|
| 95 |
+
outline: none;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
/* Google button styling */
|
| 99 |
+
.google-btn {
|
| 100 |
+
display: inline-block;
|
| 101 |
+
padding: 10px 20px;
|
| 102 |
+
font-size: 16px;
|
| 103 |
+
font-weight: 600;
|
| 104 |
+
color: #fff;
|
| 105 |
+
background: #4285F4;
|
| 106 |
+
border: none;
|
| 107 |
+
border-radius: 2px;
|
| 108 |
+
text-decoration: none;
|
| 109 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
| 110 |
+
transition: background 0.2s;
|
| 111 |
+
margin-top: 10px;
|
| 112 |
+
}
|
| 113 |
+
.google-btn:hover {
|
| 114 |
+
background: #3367D6;
|
| 115 |
+
}
|
webpass/static/css/modern.css
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--main-bg: #0f172a; /* Deep Navy */
|
| 3 |
+
--second-bg: #1e293b; /* Lighter Navy */
|
| 4 |
+
--sidebar-bg: #0b1120; /* Darkest Navy */
|
| 5 |
+
--text-color: #cbd5e1; /* Soft White */
|
| 6 |
+
--accent-color: #0ea5e9; /* Cyber Blue */
|
| 7 |
+
--accent-glow: rgba(14, 165, 233, 0.5);
|
| 8 |
+
--glass-bg: rgba(30, 41, 59, 0.7);
|
| 9 |
+
--glass-border: 1px solid rgba(255, 255, 255, 0.1);
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
body {
|
| 13 |
+
background-color: var(--main-bg);
|
| 14 |
+
color: var(--text-color);
|
| 15 |
+
font-family: 'Inter', sans-serif;
|
| 16 |
+
overflow-x: hidden;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
h1, h2, h3, h4, h5, h6, .font-monospace {
|
| 20 |
+
font-family: 'JetBrains Mono', monospace;
|
| 21 |
+
color: #fff;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/* Sidebar Styling */
|
| 25 |
+
#wrapper { overflow-x: hidden; }
|
| 26 |
+
.cyber-sidebar {
|
| 27 |
+
min-height: 100vh;
|
| 28 |
+
margin-left: -15rem;
|
| 29 |
+
transition: margin 0.25s ease-out;
|
| 30 |
+
background-color: var(--sidebar-bg);
|
| 31 |
+
border-right: 1px solid rgba(255,255,255,0.05);
|
| 32 |
+
}
|
| 33 |
+
.cyber-sidebar .list-group { width: 15rem; }
|
| 34 |
+
#wrapper.toggled .cyber-sidebar { margin-left: 0; }
|
| 35 |
+
#page-content-wrapper { width: 100%; transition: margin 0.25s ease-out; }
|
| 36 |
+
|
| 37 |
+
@media (min-width: 768px) {
|
| 38 |
+
.cyber-sidebar { margin-left: 0; }
|
| 39 |
+
#wrapper.toggled .cyber-sidebar { margin-left: -15rem; }
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.sidebar-heading { color: var(--accent-color); letter-spacing: 2px; }
|
| 43 |
+
.list-group-item {
|
| 44 |
+
border: none; padding: 15px 30px;
|
| 45 |
+
color: var(--text-color);
|
| 46 |
+
transition: all 0.3s;
|
| 47 |
+
border-left: 3px solid transparent;
|
| 48 |
+
}
|
| 49 |
+
.list-group-item:hover {
|
| 50 |
+
color: var(--accent-color);
|
| 51 |
+
background: rgba(255,255,255,0.03) !important;
|
| 52 |
+
padding-left: 35px;
|
| 53 |
+
}
|
| 54 |
+
.active-link {
|
| 55 |
+
color: var(--accent-color) !important;
|
| 56 |
+
background: linear-gradient(90deg, rgba(14,165,233,0.1), transparent) !important;
|
| 57 |
+
border-left: 3px solid var(--accent-color);
|
| 58 |
+
}
|
| 59 |
+
.locked-link { opacity: 0.5; cursor: not-allowed; }
|
| 60 |
+
|
| 61 |
+
/* Cyber Glass Cards */
|
| 62 |
+
.cyber-card {
|
| 63 |
+
background: var(--glass-bg);
|
| 64 |
+
backdrop-filter: blur(12px);
|
| 65 |
+
-webkit-backdrop-filter: blur(12px);
|
| 66 |
+
border: var(--glass-border);
|
| 67 |
+
border-radius: 16px;
|
| 68 |
+
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3);
|
| 69 |
+
transition: transform 0.3s ease, box-shadow 0.3s ease, border-color 0.3s;
|
| 70 |
+
}
|
| 71 |
+
.cyber-card:hover {
|
| 72 |
+
transform: translateY(-5px);
|
| 73 |
+
box-shadow: 0 10px 40px rgba(14, 165, 233, 0.15);
|
| 74 |
+
border-color: rgba(14, 165, 233, 0.3);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/* Neon Buttons */
|
| 78 |
+
.btn-cyber {
|
| 79 |
+
background: linear-gradient(135deg, var(--accent-color), #3b82f6);
|
| 80 |
+
border: none;
|
| 81 |
+
color: white;
|
| 82 |
+
font-weight: 600;
|
| 83 |
+
padding: 12px 24px;
|
| 84 |
+
border-radius: 8px;
|
| 85 |
+
transition: all 0.3s;
|
| 86 |
+
box-shadow: 0 4px 15px rgba(14, 165, 233, 0.3);
|
| 87 |
+
}
|
| 88 |
+
.btn-cyber:hover {
|
| 89 |
+
box-shadow: 0 0 25px var(--accent-color);
|
| 90 |
+
transform: scale(1.02);
|
| 91 |
+
color: white;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/* Form Inputs */
|
| 95 |
+
.form-control {
|
| 96 |
+
background: rgba(0,0,0,0.3);
|
| 97 |
+
border: 1px solid rgba(255,255,255,0.1);
|
| 98 |
+
color: white;
|
| 99 |
+
padding: 12px;
|
| 100 |
+
}
|
| 101 |
+
.form-control:focus {
|
| 102 |
+
background: rgba(0,0,0,0.5);
|
| 103 |
+
border-color: var(--accent-color);
|
| 104 |
+
box-shadow: 0 0 0 2px rgba(14, 165, 233, 0.25);
|
| 105 |
+
color: white;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
/* Animations */
|
| 109 |
+
.fade-in-up { animation: fadeInUp 0.6s ease forwards; opacity: 0; transform: translateY(20px); }
|
| 110 |
+
@keyframes fadeInUp { to { opacity: 1; transform: translateY(0); } }
|
| 111 |
+
|
| 112 |
+
.cyber-alert {
|
| 113 |
+
background: rgba(14, 165, 233, 0.1);
|
| 114 |
+
border: 1px solid var(--accent-color);
|
| 115 |
+
color: white;
|
| 116 |
+
backdrop-filter: blur(5px);
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
/* Watchtower Badges */
|
| 120 |
+
.watchtower-badge {
|
| 121 |
+
margin-top: 10px;
|
| 122 |
+
padding: 8px 12px;
|
| 123 |
+
border-radius: 6px;
|
| 124 |
+
font-size: 0.85rem;
|
| 125 |
+
display: flex; align-items: center; gap: 8px;
|
| 126 |
+
animation: slideDown 0.3s ease-out;
|
| 127 |
+
}
|
| 128 |
+
.watchtower-badge.safe { background: rgba(16, 185, 129, 0.15); border: 1px solid #10b981; color: #10b981; }
|
| 129 |
+
.watchtower-badge.danger { background: rgba(239, 68, 68, 0.15); border: 1px solid #ef4444; color: #ef4444; }
|
| 130 |
+
@keyframes slideDown { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } }
|
webpass/static/js/dashboard.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener("DOMContentLoaded", () => {
|
| 2 |
+
const show = id => document.getElementById(id)?.classList.remove("d-none");
|
| 3 |
+
const hide = id => document.getElementById(id)?.classList.add("d-none");
|
| 4 |
+
const setErr = (id, msg) => {
|
| 5 |
+
const el = document.getElementById(id);
|
| 6 |
+
if (!el) return;
|
| 7 |
+
el.textContent = msg;
|
| 8 |
+
show(id);
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
// View credential on modal show
|
| 12 |
+
document.getElementById("viewModal")
|
| 13 |
+
.addEventListener("show.bs.modal", () => {
|
| 14 |
+
const details = document.getElementById("credentialDetails");
|
| 15 |
+
details.textContent = "Loading…";
|
| 16 |
+
fetch("/api/credentials")
|
| 17 |
+
.then(r => r.json())
|
| 18 |
+
.then(data => {
|
| 19 |
+
if (data.status === "ok" && data.credential) {
|
| 20 |
+
const c = data.credential;
|
| 21 |
+
details.textContent =
|
| 22 |
+
`Account: ${c.account}\nUsername: ${c.username}`;
|
| 23 |
+
} else {
|
| 24 |
+
details.textContent = "No credentials found.";
|
| 25 |
+
}
|
| 26 |
+
})
|
| 27 |
+
.catch(() => {
|
| 28 |
+
details.textContent = "Error loading credentials.";
|
| 29 |
+
});
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
// Toggle links/buttons based on existing credentials
|
| 33 |
+
(async function checkCredential() {
|
| 34 |
+
try {
|
| 35 |
+
const res = await fetch("/api/credentials");
|
| 36 |
+
const data = await res.json();
|
| 37 |
+
if (data.status === "ok" && data.credential) {
|
| 38 |
+
hide("storeLink");
|
| 39 |
+
show("changeLink");
|
| 40 |
+
show("viewLink");
|
| 41 |
+
show("encryptLink");
|
| 42 |
+
show("decryptLink");
|
| 43 |
+
document.getElementById("account_change").value = data.credential.account;
|
| 44 |
+
document.getElementById("username_change").value = data.credential.username;
|
| 45 |
+
} else {
|
| 46 |
+
show("storeLink");
|
| 47 |
+
hide("changeLink");
|
| 48 |
+
hide("viewLink");
|
| 49 |
+
hide("encryptLink");
|
| 50 |
+
hide("decryptLink");
|
| 51 |
+
}
|
| 52 |
+
} catch {
|
| 53 |
+
show("storeLink");
|
| 54 |
+
hide("changeLink");
|
| 55 |
+
hide("viewLink");
|
| 56 |
+
hide("encryptLink");
|
| 57 |
+
hide("decryptLink");
|
| 58 |
+
}
|
| 59 |
+
})();
|
| 60 |
+
|
| 61 |
+
// Create / Change credential
|
| 62 |
+
document.getElementById("changeForm")
|
| 63 |
+
.addEventListener("submit", async e => {
|
| 64 |
+
e.preventDefault();
|
| 65 |
+
hide("changeError");
|
| 66 |
+
|
| 67 |
+
const form = e.target;
|
| 68 |
+
const payload = {
|
| 69 |
+
account: form.account?.value.trim(),
|
| 70 |
+
username: form.username?.value.trim(),
|
| 71 |
+
password: form.password?.value,
|
| 72 |
+
old_master_password: form.old_master_password?.value,
|
| 73 |
+
new_master_password: form.new_master_password?.value,
|
| 74 |
+
confirm_new_master: form.confirm_new_master?.value,
|
| 75 |
+
master_password: form.master_password?.value
|
| 76 |
+
};
|
| 77 |
+
|
| 78 |
+
// Client-side validation
|
| 79 |
+
if (payload.account && !/^\d+$/.test(payload.account)) {
|
| 80 |
+
return setErr("changeError", "Account must be numeric");
|
| 81 |
+
}
|
| 82 |
+
if (payload.new_master_password &&
|
| 83 |
+
payload.new_master_password !== payload.confirm_new_master) {
|
| 84 |
+
return setErr("changeError", "New passwords do not match");
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
const res = await fetch("/api/credential", {
|
| 88 |
+
method: "POST",
|
| 89 |
+
headers: { "Content-Type": "application/json" },
|
| 90 |
+
body: JSON.stringify(payload)
|
| 91 |
+
});
|
| 92 |
+
const result = await res.json();
|
| 93 |
+
|
| 94 |
+
if (!res.ok) {
|
| 95 |
+
return setErr("changeError", result.error || "Failed to save");
|
| 96 |
+
}
|
| 97 |
+
// success: reload dashboard to reflect changes
|
| 98 |
+
window.location.reload();
|
| 99 |
+
});
|
| 100 |
+
|
| 101 |
+
// Encrypt File
|
| 102 |
+
document.getElementById("encryptForm")
|
| 103 |
+
.addEventListener("submit", async e => {
|
| 104 |
+
e.preventDefault();
|
| 105 |
+
hide("encryptError");
|
| 106 |
+
|
| 107 |
+
const res = await fetch("/api/encrypt-file", {
|
| 108 |
+
method: "POST",
|
| 109 |
+
body: new FormData(e.target)
|
| 110 |
+
});
|
| 111 |
+
|
| 112 |
+
if (!res.ok) {
|
| 113 |
+
const err = await res.json();
|
| 114 |
+
document.getElementById("encryptError").textContent =
|
| 115 |
+
err.error || "Encryption failed";
|
| 116 |
+
show("encryptError");
|
| 117 |
+
return;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
const blob = await res.blob();
|
| 121 |
+
const url = URL.createObjectURL(blob);
|
| 122 |
+
const a = document.createElement("a");
|
| 123 |
+
a.href = url;
|
| 124 |
+
a.download = res.headers.get("Content-Disposition")
|
| 125 |
+
?.split("filename=")[1] || "encrypted_file.zip";
|
| 126 |
+
document.body.append(a);
|
| 127 |
+
a.click();
|
| 128 |
+
a.remove();
|
| 129 |
+
URL.revokeObjectURL(url);
|
| 130 |
+
bootstrap.Modal.getInstance(
|
| 131 |
+
document.getElementById("encryptModal")
|
| 132 |
+
).hide();
|
| 133 |
+
});
|
| 134 |
+
|
| 135 |
+
// Decrypt File
|
| 136 |
+
document.getElementById("decryptForm")
|
| 137 |
+
.addEventListener("submit", async e => {
|
| 138 |
+
e.preventDefault();
|
| 139 |
+
hide("decryptError");
|
| 140 |
+
|
| 141 |
+
const res = await fetch("/api/decrypt-file", {
|
| 142 |
+
method: "POST",
|
| 143 |
+
body: new FormData(e.target)
|
| 144 |
+
});
|
| 145 |
+
|
| 146 |
+
if (!res.ok) {
|
| 147 |
+
const err = await res.json();
|
| 148 |
+
document.getElementById("decryptError").textContent =
|
| 149 |
+
err.error || "Decryption failed";
|
| 150 |
+
show("decryptError");
|
| 151 |
+
return;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
const blob = await res.blob();
|
| 155 |
+
const url = URL.createObjectURL(blob);
|
| 156 |
+
const a = document.createElement("a");
|
| 157 |
+
a.href = url;
|
| 158 |
+
a.download = res.headers.get("Content-Disposition")
|
| 159 |
+
?.split("filename=")[1] || "decrypted_file.zip";
|
| 160 |
+
document.body.append(a);
|
| 161 |
+
a.click();
|
| 162 |
+
a.remove();
|
| 163 |
+
URL.revokeObjectURL(url);
|
| 164 |
+
bootstrap.Modal.getInstance(
|
| 165 |
+
document.getElementById("decryptModal")
|
| 166 |
+
).hide();
|
| 167 |
+
});
|
| 168 |
+
|
| 169 |
+
// Sidebar toggle
|
| 170 |
+
document.getElementById('menu-toggle')
|
| 171 |
+
?.addEventListener('click', e => {
|
| 172 |
+
e.preventDefault();
|
| 173 |
+
document.getElementById('wrapper')?.classList.toggle('toggled');
|
| 174 |
+
});
|
| 175 |
+
|
| 176 |
+
});
|
webpass/static/js/file_tools.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener("DOMContentLoaded", () => {
|
| 2 |
+
// ENCRYPT
|
| 3 |
+
const encryptForm = document.getElementById("encryptForm");
|
| 4 |
+
const encryptError = document.getElementById("encryptError");
|
| 5 |
+
|
| 6 |
+
encryptForm.addEventListener("submit", async e => {
|
| 7 |
+
e.preventDefault();
|
| 8 |
+
encryptError.classList.add("d-none");
|
| 9 |
+
const form = new FormData(encryptForm);
|
| 10 |
+
|
| 11 |
+
try {
|
| 12 |
+
const res = await fetch("/api/encrypt-file", {
|
| 13 |
+
method: "POST",
|
| 14 |
+
body: form,
|
| 15 |
+
credentials: "same-origin"
|
| 16 |
+
});
|
| 17 |
+
if (!res.ok) {
|
| 18 |
+
const err = await res.json();
|
| 19 |
+
throw new Error(err.error || res.statusText);
|
| 20 |
+
}
|
| 21 |
+
const blob = await res.blob();
|
| 22 |
+
const cd = res.headers.get("Content-Disposition");
|
| 23 |
+
const name = cd?.split("filename=")[1] || "encrypted.zip";
|
| 24 |
+
const url = URL.createObjectURL(blob);
|
| 25 |
+
const a = document.createElement("a");
|
| 26 |
+
a.href = url;
|
| 27 |
+
a.download = name;
|
| 28 |
+
document.body.appendChild(a);
|
| 29 |
+
a.click();
|
| 30 |
+
a.remove();
|
| 31 |
+
URL.revokeObjectURL(url);
|
| 32 |
+
bootstrap.Modal.getInstance(
|
| 33 |
+
encryptForm.closest(".modal")
|
| 34 |
+
).hide();
|
| 35 |
+
}
|
| 36 |
+
catch (err) {
|
| 37 |
+
encryptError.textContent = err.message;
|
| 38 |
+
encryptError.classList.remove("d-none");
|
| 39 |
+
}
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
// DECRYPT
|
| 43 |
+
const decryptForm = document.getElementById("decryptForm");
|
| 44 |
+
const decryptError = document.getElementById("decryptError");
|
| 45 |
+
|
| 46 |
+
decryptForm.addEventListener("submit", async e => {
|
| 47 |
+
e.preventDefault();
|
| 48 |
+
decryptError.classList.add("d-none");
|
| 49 |
+
const form = new FormData(decryptForm);
|
| 50 |
+
|
| 51 |
+
try {
|
| 52 |
+
const res = await fetch("/api/decrypt-file", {
|
| 53 |
+
method: "POST",
|
| 54 |
+
body: form,
|
| 55 |
+
credentials: "same-origin"
|
| 56 |
+
});
|
| 57 |
+
if (!res.ok) {
|
| 58 |
+
const err = await res.json();
|
| 59 |
+
throw new Error(err.error || res.statusText);
|
| 60 |
+
}
|
| 61 |
+
const blob = await res.blob();
|
| 62 |
+
const cd = res.headers.get("Content-Disposition");
|
| 63 |
+
const name = cd?.split("filename=")[1] || "decrypted.bin";
|
| 64 |
+
const url = URL.createObjectURL(blob);
|
| 65 |
+
const a = document.createElement("a");
|
| 66 |
+
a.href = url;
|
| 67 |
+
a.download = name;
|
| 68 |
+
document.body.appendChild(a);
|
| 69 |
+
a.click();
|
| 70 |
+
a.remove();
|
| 71 |
+
URL.revokeObjectURL(url);
|
| 72 |
+
bootstrap.Modal.getInstance(
|
| 73 |
+
decryptForm.closest(".modal")
|
| 74 |
+
).hide();
|
| 75 |
+
}
|
| 76 |
+
catch (err) {
|
| 77 |
+
decryptError.textContent = err.message;
|
| 78 |
+
decryptError.classList.remove("d-none");
|
| 79 |
+
}
|
| 80 |
+
});
|
| 81 |
+
});
|
webpass/static/js/flash_modal.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener("DOMContentLoaded", () => {
|
| 2 |
+
let messages = [];
|
| 3 |
+
const el = document.getElementById("flash-data");
|
| 4 |
+
if (el) {
|
| 5 |
+
try {
|
| 6 |
+
messages = JSON.parse(el.textContent) || [];
|
| 7 |
+
} catch (e) {
|
| 8 |
+
console.warn("Invalid flash-data JSON", e);
|
| 9 |
+
}
|
| 10 |
+
}
|
| 11 |
+
if (!messages.length) return;
|
| 12 |
+
|
| 13 |
+
// Show first flash only
|
| 14 |
+
const [ category, message ] = messages[0];
|
| 15 |
+
const body = document.getElementById("flashModalBody");
|
| 16 |
+
body.className = "alert alert-" + category;
|
| 17 |
+
body.textContent = message;
|
| 18 |
+
|
| 19 |
+
const modalEl = document.getElementById("flashModal");
|
| 20 |
+
const modal = new bootstrap.Modal(modalEl);
|
| 21 |
+
modal.show();
|
| 22 |
+
|
| 23 |
+
setTimeout(() => {
|
| 24 |
+
modalEl.classList.add("hide");
|
| 25 |
+
setTimeout(() => modal.hide(), 500);
|
| 26 |
+
}, 5000);
|
| 27 |
+
});
|
webpass/static/js/network.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener("DOMContentLoaded", () => {
|
| 2 |
+
let table = null;
|
| 3 |
+
const allPackets = [];
|
| 4 |
+
const protoCounts = { TCP: 0, UDP: 0, DNS: 0, Other: 0 };
|
| 5 |
+
const srcCounts = {};
|
| 6 |
+
let protocolChart = null;
|
| 7 |
+
|
| 8 |
+
// --- New variables for smoother, dynamic updates ---
|
| 9 |
+
let uiUpdateScheduled = false;
|
| 10 |
+
const UI_UPDATE_INTERVAL = 1000; // Update heavy visuals once per second
|
| 11 |
+
|
| 12 |
+
function initChart() {
|
| 13 |
+
const ctx = document.getElementById('protocolChart')?.getContext('2d');
|
| 14 |
+
if (!ctx) return;
|
| 15 |
+
protocolChart = new Chart(ctx, {
|
| 16 |
+
type: 'doughnut',
|
| 17 |
+
data: {
|
| 18 |
+
labels: ['TCP', 'UDP', 'DNS', 'Other'],
|
| 19 |
+
datasets: [{ data: [0, 0, 0, 0], backgroundColor: ['#0d6efd', '#ffc107', '#dc3545', '#6f42c1'] }]
|
| 20 |
+
},
|
| 21 |
+
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' } } }
|
| 22 |
+
});
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
function initDataTable() {
|
| 26 |
+
const cols = ["Time", "Source", "Destination", "Protocol", "Length"];
|
| 27 |
+
let headerRow = '<tr>';
|
| 28 |
+
cols.forEach(txt => headerRow += `<th>${txt}</th>`);
|
| 29 |
+
headerRow += '</tr>';
|
| 30 |
+
$("#table-header").html(headerRow);
|
| 31 |
+
|
| 32 |
+
table = $('#packet-table').DataTable({
|
| 33 |
+
deferRender: true,
|
| 34 |
+
scroller: true,
|
| 35 |
+
scrollY: "60vh",
|
| 36 |
+
scrollCollapse: true,
|
| 37 |
+
paging: true,
|
| 38 |
+
lengthChange: false,
|
| 39 |
+
info: true,
|
| 40 |
+
order: [[0, 'desc']],
|
| 41 |
+
language: { search: "", searchPlaceholder: "Search..." },
|
| 42 |
+
createdRow: function(row, data, dataIndex) {
|
| 43 |
+
if (allPackets[dataIndex]) {
|
| 44 |
+
$(row).data('packet', allPackets[dataIndex]);
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
});
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
function getProtocol(pkt) {
|
| 51 |
+
if (pkt.proto === "DNS") return "DNS";
|
| 52 |
+
if (pkt.proto === 6) return "TCP";
|
| 53 |
+
if (pkt.proto === 17) return "UDP";
|
| 54 |
+
return "Other";
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// --- Main Update Logic ---
|
| 58 |
+
// This function updates all the heavy visual elements
|
| 59 |
+
function updateHeavyVisuals() {
|
| 60 |
+
// Update KPI cards (except total, which is updated instantly)
|
| 61 |
+
$('#count-TCP').text(protoCounts.TCP);
|
| 62 |
+
$('#count-UDP').text(protoCounts.UDP);
|
| 63 |
+
$('#count-DNS').text(protoCounts.DNS);
|
| 64 |
+
|
| 65 |
+
if (protocolChart) {
|
| 66 |
+
protocolChart.data.datasets[0].data = [protoCounts.TCP, protoCounts.UDP, protoCounts.DNS, protoCounts.Other];
|
| 67 |
+
protocolChart.update('none'); // 'none' for smooth animation
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
const topSources = Object.entries(srcCounts).sort(([, a], [, b]) => b - a).slice(0, 5);
|
| 71 |
+
const ul = $("#top-sources").empty();
|
| 72 |
+
topSources.forEach(([ip, count]) => {
|
| 73 |
+
$("<li>").addClass("list-group-item d-flex justify-content-between align-items-center").html(`${ip} <span class="badge bg-primary rounded-pill">${count}</span>`).appendTo(ul);
|
| 74 |
+
});
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// This function processes all historical data once on load
|
| 78 |
+
function processHistoricalData(historicalPackets) {
|
| 79 |
+
historicalPackets.forEach(pkt => {
|
| 80 |
+
allPackets.push(pkt);
|
| 81 |
+
const proto = getProtocol(pkt);
|
| 82 |
+
if (protoCounts.hasOwnProperty(proto)) protoCounts[proto]++;
|
| 83 |
+
srcCounts[pkt.src] = (srcCounts[pkt.src] || 0) + 1;
|
| 84 |
+
});
|
| 85 |
+
|
| 86 |
+
const rowsToAdd = allPackets.map(p => {
|
| 87 |
+
const time = new Date(p.timestamp * 1000).toLocaleTimeString();
|
| 88 |
+
return [time, p.src, p.dst, getProtocol(p), p.length || '-'];
|
| 89 |
+
});
|
| 90 |
+
|
| 91 |
+
table.rows.add(rowsToAdd).draw();
|
| 92 |
+
$('#count-All').text(allPackets.length);
|
| 93 |
+
updateHeavyVisuals(); // Update heavy visuals once after history is loaded
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
// --- INITIALIZATION AND DATA LOADING ---
|
| 97 |
+
initChart();
|
| 98 |
+
initDataTable();
|
| 99 |
+
|
| 100 |
+
fetch("/api/packets")
|
| 101 |
+
.then(r => r.ok ? r.json() : [])
|
| 102 |
+
.then(processHistoricalData)
|
| 103 |
+
.catch(err => console.error("Error loading history:", err));
|
| 104 |
+
|
| 105 |
+
// --- LIVE SOCKET.IO UPDATES ---
|
| 106 |
+
const socket = io();
|
| 107 |
+
socket.on('new_packet', pkt => {
|
| 108 |
+
// 1. Instantly update the in-memory data
|
| 109 |
+
allPackets.push(pkt);
|
| 110 |
+
const proto = getProtocol(pkt);
|
| 111 |
+
if (protoCounts.hasOwnProperty(proto)) protoCounts[proto]++;
|
| 112 |
+
srcCounts[pkt.src] = (srcCounts[pkt.src] || 0) + 1;
|
| 113 |
+
|
| 114 |
+
// 2. Instantly update the total count and add the row to the table
|
| 115 |
+
$('#count-All').text(allPackets.length);
|
| 116 |
+
const time = new Date(pkt.timestamp * 1000).toLocaleTimeString();
|
| 117 |
+
|
| 118 |
+
// ** THIS IS THE BUG FIX **
|
| 119 |
+
// It was `p.length` before, which is undefined. It is now `pkt.length`.
|
| 120 |
+
const rowData = [time, pkt.src, pkt.dst, getProtocol(pkt), pkt.length || '-'];
|
| 121 |
+
table.row.add(rowData).draw(false);
|
| 122 |
+
|
| 123 |
+
// 3. Schedule a throttled update for the heavy visuals
|
| 124 |
+
if (!uiUpdateScheduled) {
|
| 125 |
+
uiUpdateScheduled = true;
|
| 126 |
+
setTimeout(() => {
|
| 127 |
+
updateHeavyVisuals();
|
| 128 |
+
uiUpdateScheduled = false;
|
| 129 |
+
}, UI_UPDATE_INTERVAL);
|
| 130 |
+
}
|
| 131 |
+
});
|
| 132 |
+
|
| 133 |
+
// --- EVENT HANDLERS ---
|
| 134 |
+
$("#packet-table tbody").on("click", "tr", function(){
|
| 135 |
+
const pkt = $(this).data('packet');
|
| 136 |
+
if (!pkt) return;
|
| 137 |
+
let html = '<ul class="list-group">';
|
| 138 |
+
Object.entries(pkt).forEach(([k,v])=> {
|
| 139 |
+
html += `<li class="list-group-item"><strong>${k}:</strong> ${v}</li>`;
|
| 140 |
+
});
|
| 141 |
+
html += '</ul>';
|
| 142 |
+
$("#packet-detail-body").html(html);
|
| 143 |
+
new bootstrap.Modal($("#packetDetailModal")).show();
|
| 144 |
+
});
|
| 145 |
+
|
| 146 |
+
$("#download-btn").on("click", () => {
|
| 147 |
+
if (!allPackets.length) {
|
| 148 |
+
return alert("No packet data to download.");
|
| 149 |
+
}
|
| 150 |
+
const ws = XLSX.utils.json_to_sheet(allPackets);
|
| 151 |
+
const wb = XLSX.utils.book_new();
|
| 152 |
+
XLSX.utils.book_append_sheet(wb, ws, "NetworkData");
|
| 153 |
+
const ts = new Date().toISOString().replace(/[:.]/g,"-");
|
| 154 |
+
const name = `network_data_${ts}.xlsx`;
|
| 155 |
+
XLSX.writeFile(wb, name);
|
| 156 |
+
});
|
| 157 |
+
});
|
webpass/static/js/watchtower.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// static/js/watchtower.js
|
| 2 |
+
|
| 3 |
+
const Watchtower = {
|
| 4 |
+
// 1. SHA-1 Hash (Required for HIBP API)
|
| 5 |
+
sha1: async (message) => {
|
| 6 |
+
const msgBuffer = new TextEncoder().encode(message);
|
| 7 |
+
const hashBuffer = await crypto.subtle.digest('SHA-1', msgBuffer);
|
| 8 |
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
| 9 |
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('').toUpperCase();
|
| 10 |
+
},
|
| 11 |
+
|
| 12 |
+
// 2. The Check Function
|
| 13 |
+
checkPassword: async (password) => {
|
| 14 |
+
if (!password) return 0;
|
| 15 |
+
|
| 16 |
+
// Hash it
|
| 17 |
+
const hash = await Watchtower.sha1(password);
|
| 18 |
+
const prefix = hash.substring(0, 5);
|
| 19 |
+
const suffix = hash.substring(5);
|
| 20 |
+
|
| 21 |
+
try {
|
| 22 |
+
// Ask our backend proxy
|
| 23 |
+
const res = await fetch(`/api/watchtower/pwned-check/${prefix}`);
|
| 24 |
+
const text = await res.text();
|
| 25 |
+
|
| 26 |
+
// Parse response (Suffix:Count)
|
| 27 |
+
// Example line: 0018A45C4D985303FC1:1
|
| 28 |
+
const lines = text.split('\n');
|
| 29 |
+
for (const line of lines) {
|
| 30 |
+
const [serverSuffix, count] = line.split(':');
|
| 31 |
+
if (serverSuffix.trim() === suffix) {
|
| 32 |
+
return parseInt(count); // Found a match!
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
return 0; // No match found (Safe)
|
| 36 |
+
} catch (err) {
|
| 37 |
+
console.error("Watchtower Check Failed:", err);
|
| 38 |
+
return -1; // Error state
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
};
|
| 42 |
+
|
| 43 |
+
// Auto-hook into password fields
|
| 44 |
+
document.addEventListener("DOMContentLoaded", () => {
|
| 45 |
+
const passInputs = document.querySelectorAll('input[type="password"].watchtower-monitor');
|
| 46 |
+
|
| 47 |
+
passInputs.forEach(input => {
|
| 48 |
+
// Create the warning badge
|
| 49 |
+
const badge = document.createElement("div");
|
| 50 |
+
badge.className = "mt-1 small fw-bold d-none";
|
| 51 |
+
input.parentNode.appendChild(badge);
|
| 52 |
+
|
| 53 |
+
let timeout = null;
|
| 54 |
+
input.addEventListener("input", () => {
|
| 55 |
+
clearTimeout(timeout);
|
| 56 |
+
badge.classList.add("d-none");
|
| 57 |
+
|
| 58 |
+
// Debounce to avoid spamming API while typing
|
| 59 |
+
timeout = setTimeout(async () => {
|
| 60 |
+
const val = input.value;
|
| 61 |
+
if (val.length < 4) return;
|
| 62 |
+
|
| 63 |
+
const count = await Watchtower.checkPassword(val);
|
| 64 |
+
|
| 65 |
+
if (count > 0) {
|
| 66 |
+
badge.innerHTML = `<i class="bi bi-exclamation-triangle-fill text-danger"></i>
|
| 67 |
+
<span class="text-danger">This password appears in ${count.toLocaleString()} known data breaches!</span>`;
|
| 68 |
+
badge.classList.remove("d-none");
|
| 69 |
+
} else if (count === 0) {
|
| 70 |
+
badge.innerHTML = `<i class="bi bi-shield-check text-success"></i>
|
| 71 |
+
<span class="text-success">No breaches found.</span>`;
|
| 72 |
+
badge.classList.remove("d-none");
|
| 73 |
+
}
|
| 74 |
+
}, 500);
|
| 75 |
+
});
|
| 76 |
+
});
|
| 77 |
+
});
|
webpass/static/js/zk_crypto.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// static/js/zk_crypto.js
|
| 2 |
+
|
| 3 |
+
const ZKCrypto = {
|
| 4 |
+
// 1. Generate a random salt/IV
|
| 5 |
+
randomBytes: (len) => window.crypto.getRandomValues(new Uint8Array(len)),
|
| 6 |
+
|
| 7 |
+
// 2. Derive a robust AES-GCM Key from the Master Password
|
| 8 |
+
deriveKey: async (password, salt) => {
|
| 9 |
+
const enc = new TextEncoder();
|
| 10 |
+
const keyMaterial = await window.crypto.subtle.importKey(
|
| 11 |
+
"raw", enc.encode(password), { name: "PBKDF2" }, false, ["deriveKey"]
|
| 12 |
+
);
|
| 13 |
+
return window.crypto.subtle.deriveKey(
|
| 14 |
+
{
|
| 15 |
+
name: "PBKDF2",
|
| 16 |
+
salt: salt,
|
| 17 |
+
iterations: 100000,
|
| 18 |
+
hash: "SHA-256"
|
| 19 |
+
},
|
| 20 |
+
keyMaterial,
|
| 21 |
+
{ name: "AES-GCM", length: 256 },
|
| 22 |
+
false,
|
| 23 |
+
["encrypt", "decrypt"]
|
| 24 |
+
);
|
| 25 |
+
},
|
| 26 |
+
|
| 27 |
+
// 3. Encrypt File (Zero Knowledge - Outputs raw bytes for .enc)
|
| 28 |
+
encryptFile: async (file, password) => {
|
| 29 |
+
const salt = ZKCrypto.randomBytes(16);
|
| 30 |
+
const iv = ZKCrypto.randomBytes(12);
|
| 31 |
+
const key = await ZKCrypto.deriveKey(password, salt);
|
| 32 |
+
|
| 33 |
+
// Read the file as an ArrayBuffer
|
| 34 |
+
const fileBuffer = await file.arrayBuffer();
|
| 35 |
+
|
| 36 |
+
// Encrypt the file
|
| 37 |
+
const encrypted = await window.crypto.subtle.encrypt(
|
| 38 |
+
{ name: "AES-GCM", iv: iv },
|
| 39 |
+
key,
|
| 40 |
+
fileBuffer
|
| 41 |
+
);
|
| 42 |
+
|
| 43 |
+
// Pack the payload: [16 bytes Salt] + [12 bytes IV] + [Ciphertext]
|
| 44 |
+
const encryptedArray = new Uint8Array(encrypted);
|
| 45 |
+
const payload = new Uint8Array(16 + 12 + encryptedArray.byteLength);
|
| 46 |
+
|
| 47 |
+
payload.set(salt, 0);
|
| 48 |
+
payload.set(iv, 16);
|
| 49 |
+
payload.set(encryptedArray, 28);
|
| 50 |
+
|
| 51 |
+
return payload; // vault.html wraps this in a Blob and downloads as .enc
|
| 52 |
+
},
|
| 53 |
+
|
| 54 |
+
// 4. Decrypt File (Reads .enc format)
|
| 55 |
+
decryptFile: async (encFile, password) => {
|
| 56 |
+
const payload = new Uint8Array(await encFile.arrayBuffer());
|
| 57 |
+
|
| 58 |
+
// A valid .enc file must be at least 28 bytes (Salt + IV)
|
| 59 |
+
if (payload.byteLength < 28) {
|
| 60 |
+
throw new Error("Invalid or corrupted file format.");
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// Extract the Salt, IV, and Ciphertext
|
| 64 |
+
const salt = payload.slice(0, 16);
|
| 65 |
+
const iv = payload.slice(16, 28);
|
| 66 |
+
const ciphertext = payload.slice(28);
|
| 67 |
+
|
| 68 |
+
const key = await ZKCrypto.deriveKey(password, salt);
|
| 69 |
+
|
| 70 |
+
// Decrypt
|
| 71 |
+
const decryptedBuffer = await window.crypto.subtle.decrypt(
|
| 72 |
+
{ name: "AES-GCM", iv: iv },
|
| 73 |
+
key,
|
| 74 |
+
ciphertext
|
| 75 |
+
);
|
| 76 |
+
|
| 77 |
+
// Strip the ".enc" extension to restore the original file name
|
| 78 |
+
let originalName = encFile.name;
|
| 79 |
+
if (originalName.endsWith(".enc")) {
|
| 80 |
+
originalName = originalName.slice(0, -4);
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
return {
|
| 84 |
+
data: decryptedBuffer,
|
| 85 |
+
name: originalName
|
| 86 |
+
};
|
| 87 |
+
}
|
| 88 |
+
};
|
| 89 |
+
|
| 90 |
+
// Make it globally available for your HTML files
|
| 91 |
+
window.ZKCrypto = ZKCrypto;
|
webpass/stego_utils.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from PIL import Image
|
| 2 |
+
import io
|
| 3 |
+
import json
|
| 4 |
+
import base64
|
| 5 |
+
from webpass.crypto_utils import generate_key, encrypt_password, decrypt_password
|
| 6 |
+
|
| 7 |
+
# --- HELPER: BINARY CONVERSION ---
|
| 8 |
+
def str_to_bin(message):
|
| 9 |
+
return ''.join(format(ord(c), '08b') for c in message)
|
| 10 |
+
|
| 11 |
+
def bin_to_str(binary):
|
| 12 |
+
chars = [binary[i:i+8] for i in range(0, len(binary), 8)]
|
| 13 |
+
return ''.join(chr(int(c, 2)) for c in chars)
|
| 14 |
+
|
| 15 |
+
# --- CORE LOGIC ---
|
| 16 |
+
|
| 17 |
+
def encode_data(cover_image, data, password, filename=None):
|
| 18 |
+
"""
|
| 19 |
+
Universal Encoder: Handles both TEXT and FILES.
|
| 20 |
+
1. Wraps data in a JSON payload.
|
| 21 |
+
2. Encrypts the JSON string.
|
| 22 |
+
3. Embeds into pixels.
|
| 23 |
+
"""
|
| 24 |
+
# 1. Prepare Payload (JSON)
|
| 25 |
+
if filename:
|
| 26 |
+
# It's a file: Base64 encode the bytes first
|
| 27 |
+
b64_data = base64.b64encode(data.read()).decode('utf-8')
|
| 28 |
+
payload_dict = {
|
| 29 |
+
"type": "file",
|
| 30 |
+
"filename": filename,
|
| 31 |
+
"content": b64_data
|
| 32 |
+
}
|
| 33 |
+
else:
|
| 34 |
+
# It's text
|
| 35 |
+
payload_dict = {
|
| 36 |
+
"type": "text",
|
| 37 |
+
"content": data
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
# Convert to string
|
| 41 |
+
raw_payload = json.dumps(payload_dict)
|
| 42 |
+
|
| 43 |
+
# 2. Encrypt (AES-256)
|
| 44 |
+
static_salt = b'WebPass_Stego_Salt'
|
| 45 |
+
key = generate_key(password, static_salt)
|
| 46 |
+
encrypted_payload = encrypt_password(raw_payload, key)
|
| 47 |
+
|
| 48 |
+
# 3. Add Delimiter
|
| 49 |
+
final_message = encrypted_secret = encrypted_payload + "#####END#####"
|
| 50 |
+
binary_message = str_to_bin(final_message)
|
| 51 |
+
data_len = len(binary_message)
|
| 52 |
+
|
| 53 |
+
# 4. Image Processing
|
| 54 |
+
img = Image.open(cover_image)
|
| 55 |
+
img = img.convert("RGB")
|
| 56 |
+
pixels = list(img.getdata())
|
| 57 |
+
|
| 58 |
+
max_capacity = len(pixels) * 3
|
| 59 |
+
if data_len > max_capacity:
|
| 60 |
+
raise ValueError(f"File too large! Need {data_len} bits, image has {max_capacity}.")
|
| 61 |
+
|
| 62 |
+
new_pixels = []
|
| 63 |
+
idx = 0
|
| 64 |
+
|
| 65 |
+
for pixel in pixels:
|
| 66 |
+
if idx < data_len:
|
| 67 |
+
r, g, b = pixel
|
| 68 |
+
if idx < data_len:
|
| 69 |
+
r = int(format(r, '08b')[:-1] + binary_message[idx], 2)
|
| 70 |
+
idx += 1
|
| 71 |
+
if idx < data_len:
|
| 72 |
+
g = int(format(g, '08b')[:-1] + binary_message[idx], 2)
|
| 73 |
+
idx += 1
|
| 74 |
+
if idx < data_len:
|
| 75 |
+
b = int(format(b, '08b')[:-1] + binary_message[idx], 2)
|
| 76 |
+
idx += 1
|
| 77 |
+
new_pixels.append((r, g, b))
|
| 78 |
+
else:
|
| 79 |
+
new_pixels.append(pixel)
|
| 80 |
+
|
| 81 |
+
img.putdata(new_pixels)
|
| 82 |
+
|
| 83 |
+
output = io.BytesIO()
|
| 84 |
+
img.save(output, format="PNG")
|
| 85 |
+
output.seek(0)
|
| 86 |
+
return output
|
| 87 |
+
|
| 88 |
+
def decode_data(stego_image, password):
|
| 89 |
+
"""
|
| 90 |
+
Universal Decoder.
|
| 91 |
+
Returns a dictionary: {"type": "text"|"file", "content": ..., "filename": ...}
|
| 92 |
+
"""
|
| 93 |
+
img = Image.open(stego_image)
|
| 94 |
+
img = img.convert("RGB")
|
| 95 |
+
pixels = list(img.getdata())
|
| 96 |
+
|
| 97 |
+
binary_data = ""
|
| 98 |
+
encrypted_payload = ""
|
| 99 |
+
|
| 100 |
+
# 1. Extract Bits
|
| 101 |
+
# Optimization: We check for delimiter every 1000 pixels to avoid decoding massive images unnecessarily
|
| 102 |
+
count = 0
|
| 103 |
+
for pixel in pixels:
|
| 104 |
+
r, g, b = pixel
|
| 105 |
+
binary_data += format(r, '08b')[-1]
|
| 106 |
+
binary_data += format(g, '08b')[-1]
|
| 107 |
+
binary_data += format(b, '08b')[-1]
|
| 108 |
+
|
| 109 |
+
count += 1
|
| 110 |
+
# Check every ~100 bytes (800 bits)
|
| 111 |
+
if count % 270 == 0:
|
| 112 |
+
# Quick check on the tail
|
| 113 |
+
current_tail = binary_data[-800:] # rough check
|
| 114 |
+
# Full convert is expensive, so we do it partially or wait until we have enough
|
| 115 |
+
# For robustness, we check properly:
|
| 116 |
+
pass # (Skipping optimization for code clarity/stability)
|
| 117 |
+
|
| 118 |
+
# Full extraction (Stable approach)
|
| 119 |
+
# Note: In production, you'd stop reading once delimiter is found.
|
| 120 |
+
# For now, let's look for the delimiter in the binary stream logic.
|
| 121 |
+
# To save memory, we'll convert chunks.
|
| 122 |
+
|
| 123 |
+
# RE-IMPLEMENTATION FOR SPEED:
|
| 124 |
+
# We will build the string character by character
|
| 125 |
+
chars = []
|
| 126 |
+
for i in range(0, len(binary_data), 8):
|
| 127 |
+
byte = binary_data[i:i+8]
|
| 128 |
+
if len(byte) < 8: break
|
| 129 |
+
char = chr(int(byte, 2))
|
| 130 |
+
chars.append(char)
|
| 131 |
+
if len(chars) > 13 and "".join(chars[-13:]) == "#####END#####":
|
| 132 |
+
encrypted_payload = "".join(chars[:-13])
|
| 133 |
+
break
|
| 134 |
+
|
| 135 |
+
if not encrypted_payload:
|
| 136 |
+
raise ValueError("No hidden payload found.")
|
| 137 |
+
|
| 138 |
+
# 2. Decrypt
|
| 139 |
+
try:
|
| 140 |
+
static_salt = b'WebPass_Stego_Salt'
|
| 141 |
+
key = generate_key(password, static_salt)
|
| 142 |
+
json_payload = decrypt_password(encrypted_payload, key)
|
| 143 |
+
except:
|
| 144 |
+
raise ValueError("Incorrect password.")
|
| 145 |
+
|
| 146 |
+
# 3. Parse JSON
|
| 147 |
+
try:
|
| 148 |
+
data = json.loads(json_payload)
|
| 149 |
+
|
| 150 |
+
if data['type'] == 'file':
|
| 151 |
+
# Decode the file bytes
|
| 152 |
+
file_bytes = base64.b64decode(data['content'])
|
| 153 |
+
return {
|
| 154 |
+
"type": "file",
|
| 155 |
+
"filename": data['filename'],
|
| 156 |
+
"file_bytes": io.BytesIO(file_bytes)
|
| 157 |
+
}
|
| 158 |
+
else:
|
| 159 |
+
return {
|
| 160 |
+
"type": "text",
|
| 161 |
+
"content": data['content']
|
| 162 |
+
}
|
| 163 |
+
except json.JSONDecodeError:
|
| 164 |
+
# Fallback for old legacy text-only stego images
|
| 165 |
+
return {"type": "text", "content": json_payload}
|
webpass/templates/base.html
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" data-bs-theme="dark">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>{% block title %}WebPass{% endblock %}</title>
|
| 7 |
+
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
|
| 9 |
+
|
| 10 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 11 |
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
| 12 |
+
|
| 13 |
+
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
| 14 |
+
|
| 15 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/modern.css') }}">
|
| 16 |
+
|
| 17 |
+
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
|
| 18 |
+
<script src="https://cdn.socket.io/4.6.0/socket.io.min.js"></script>
|
| 19 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
| 20 |
+
</head>
|
| 21 |
+
<body>
|
| 22 |
+
|
| 23 |
+
<div class="d-flex" id="wrapper">
|
| 24 |
+
<div class="cyber-sidebar border-end" id="sidebar-wrapper">
|
| 25 |
+
<div class="sidebar-heading text-center py-4 primary-text fs-4 fw-bold text-uppercase border-bottom">
|
| 26 |
+
<i class="bi bi-shield-lock-fill me-2"></i>WebPass
|
| 27 |
+
</div>
|
| 28 |
+
<div class="list-group list-group-flush my-3">
|
| 29 |
+
<a href="{{ url_for('dashboard.dashboard') }}" class="list-group-item list-group-item-action bg-transparent second-text">
|
| 30 |
+
<i class="bi bi-grid-1x2-fill me-2"></i>Dashboard
|
| 31 |
+
</a>
|
| 32 |
+
|
| 33 |
+
{% if has_credentials %}
|
| 34 |
+
<a href="{{ url_for('dashboard.secure_tools') }}" class="list-group-item list-group-item-action bg-transparent second-text">
|
| 35 |
+
<i class="bi bi-safe2-fill me-2"></i>Zero-Know Vault
|
| 36 |
+
</a>
|
| 37 |
+
{% else %}
|
| 38 |
+
<a href="{{ url_for('dashboard.profile') }}" class="list-group-item list-group-item-action bg-transparent text-muted locked-link">
|
| 39 |
+
<i class="bi bi-lock-fill me-2"></i>Zero-Know Vault
|
| 40 |
+
</a>
|
| 41 |
+
{% endif %}
|
| 42 |
+
|
| 43 |
+
{% if has_credentials %}
|
| 44 |
+
<a href="{{ url_for('share.share_ui') }}" class="list-group-item list-group-item-action bg-transparent second-text">
|
| 45 |
+
<i class="bi bi-send-x-fill me-2"></i>Dead Drop
|
| 46 |
+
</a>
|
| 47 |
+
{% else %}
|
| 48 |
+
<a href="{{ url_for('dashboard.profile') }}" class="list-group-item list-group-item-action bg-transparent text-muted locked-link">
|
| 49 |
+
<i class="bi bi-lock-fill me-2"></i>Dead Drop
|
| 50 |
+
</a>
|
| 51 |
+
{% endif %}
|
| 52 |
+
|
| 53 |
+
{% if has_credentials %}
|
| 54 |
+
<a href="{{ url_for('stego.ui') }}" class="list-group-item list-group-item-action bg-transparent second-text">
|
| 55 |
+
<i class="bi bi-eye-slash-fill me-2"></i>Steganography
|
| 56 |
+
</a>
|
| 57 |
+
{% else %}
|
| 58 |
+
<a href="{{ url_for('dashboard.profile') }}" class="list-group-item list-group-item-action bg-transparent text-muted locked-link">
|
| 59 |
+
<i class="bi bi-eye-slash-fill me-2"></i>Steganography
|
| 60 |
+
</a>
|
| 61 |
+
{% endif %}
|
| 62 |
+
|
| 63 |
+
{% if has_credentials %}
|
| 64 |
+
<a href="{{ url_for('dashboard.network_monitor') }}" class="list-group-item list-group-item-action bg-transparent second-text">
|
| 65 |
+
<i class="bi bi-activity me-2"></i>Net Monitor
|
| 66 |
+
</a>
|
| 67 |
+
{% else %}
|
| 68 |
+
<a href="{{ url_for('dashboard.profile') }}" class="list-group-item list-group-item-action bg-transparent text-muted locked-link">
|
| 69 |
+
<i class="bi bi-lock-fill me-2"></i>Net Monitor
|
| 70 |
+
</a>
|
| 71 |
+
{% endif %}
|
| 72 |
+
|
| 73 |
+
<a href="{{ url_for('dashboard.profile') }}" class="list-group-item list-group-item-action bg-transparent second-text">
|
| 74 |
+
<i class="bi bi-person-circle me-2"></i>Profile
|
| 75 |
+
</a>
|
| 76 |
+
|
| 77 |
+
<a href="{{ url_for('auth.logout') }}" class="list-group-item list-group-item-action bg-transparent text-danger fw-bold mt-5 logout-btn">
|
| 78 |
+
<i class="bi bi-box-arrow-left me-2"></i>Logout
|
| 79 |
+
</a>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
|
| 83 |
+
<div id="page-content-wrapper">
|
| 84 |
+
<nav class="navbar navbar-expand-lg navbar-dark bg-transparent py-4 px-4">
|
| 85 |
+
<div class="d-flex align-items-center">
|
| 86 |
+
<i class="bi bi-list fs-3 me-3 text-white" id="menu-toggle" style="cursor: pointer;"></i>
|
| 87 |
+
<h2 class="fs-2 m-0 text-white fw-bold">{% block page_title %}{% endblock %}</h2>
|
| 88 |
+
</div>
|
| 89 |
+
</nav>
|
| 90 |
+
|
| 91 |
+
<div class="container-fluid px-4 pb-5">
|
| 92 |
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
| 93 |
+
{% if messages %}
|
| 94 |
+
{% for category, message in messages %}
|
| 95 |
+
<div class="alert alert-{{ category }} alert-dismissible fade show cyber-alert" role="alert">
|
| 96 |
+
<i class="bi bi-info-circle-fill me-2"></i> {{ message }}
|
| 97 |
+
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
| 98 |
+
</div>
|
| 99 |
+
{% endfor %}
|
| 100 |
+
{% endif %}
|
| 101 |
+
{% endwith %}
|
| 102 |
+
|
| 103 |
+
{% block content %}{% endblock %}
|
| 104 |
+
</div>
|
| 105 |
+
</div>
|
| 106 |
+
</div>
|
| 107 |
+
|
| 108 |
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
| 109 |
+
|
| 110 |
+
<script>
|
| 111 |
+
// Sidebar Toggle
|
| 112 |
+
var el = document.getElementById("wrapper");
|
| 113 |
+
var toggleButton = document.getElementById("menu-toggle");
|
| 114 |
+
toggleButton.onclick = function () {
|
| 115 |
+
el.classList.toggle("toggled");
|
| 116 |
+
};
|
| 117 |
+
|
| 118 |
+
// Active Link Highlighter
|
| 119 |
+
const currentPath = window.location.pathname;
|
| 120 |
+
document.querySelectorAll('.list-group-item').forEach(link => {
|
| 121 |
+
if(link.getAttribute('href') === currentPath) {
|
| 122 |
+
link.classList.add('active-link');
|
| 123 |
+
}
|
| 124 |
+
});
|
| 125 |
+
</script>
|
| 126 |
+
{% block scripts %}{% endblock %}
|
| 127 |
+
</body>
|
| 128 |
+
</html>
|
webpass/templates/bio_lock.html
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>WebPass Locked</title>
|
| 7 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 8 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
| 9 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
|
| 10 |
+
|
| 11 |
+
<style>
|
| 12 |
+
body { background-color: #121212; color: #ffffff; height: 100vh; display: flex; align-items: center; justify-content: center; font-family: 'Segoe UI', sans-serif; }
|
| 13 |
+
.lock-container { background: #1e1e1e; padding: 40px; border-radius: 20px; box-shadow: 0 0 30px rgba(0, 255, 136, 0.1); text-align: center; max-width: 400px; width: 100%; border: 1px solid #333; }
|
| 14 |
+
.qr-box { background: white; padding: 15px; border-radius: 10px; margin: 20px auto; width: fit-content; }
|
| 15 |
+
.status-text { color: #00ff88; margin-top: 15px; font-size: 0.9rem; min-height: 20px; }
|
| 16 |
+
</style>
|
| 17 |
+
</head>
|
| 18 |
+
<body>
|
| 19 |
+
|
| 20 |
+
<div class="lock-container">
|
| 21 |
+
<h2 class="mb-3">Biometric Lock</h2>
|
| 22 |
+
<p class="text-muted">Scan to Unlock</p>
|
| 23 |
+
<div class="qr-box"><div id="qrcode"></div></div>
|
| 24 |
+
<div class="status-text" id="status">Waiting for device...</div>
|
| 25 |
+
</div>
|
| 26 |
+
|
| 27 |
+
<script>
|
| 28 |
+
var socket = io();
|
| 29 |
+
var channel = "{{ channel_id }}";
|
| 30 |
+
|
| 31 |
+
socket.on('connect', function() {
|
| 32 |
+
socket.emit('join_channel', { channel: channel });
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
// --- THE FIX: DESKTOP CLAIMS SESSION ---
|
| 36 |
+
socket.on('unlock_command', function(data) {
|
| 37 |
+
document.getElementById('status').innerText = "Identity Verified! Logging in...";
|
| 38 |
+
|
| 39 |
+
// Call the server to update THIS browser's session
|
| 40 |
+
fetch('/api/bio/finalize-login/' + channel)
|
| 41 |
+
.then(response => response.json())
|
| 42 |
+
.then(data => {
|
| 43 |
+
if (data.status === 'success') {
|
| 44 |
+
window.location.href = data.redirect;
|
| 45 |
+
} else {
|
| 46 |
+
console.error("Session update failed");
|
| 47 |
+
}
|
| 48 |
+
})
|
| 49 |
+
.catch(err => console.error(err));
|
| 50 |
+
});
|
| 51 |
+
|
| 52 |
+
new QRCode(document.getElementById("qrcode"), {
|
| 53 |
+
text: "{{ mobile_url }}",
|
| 54 |
+
width: 180,
|
| 55 |
+
height: 180
|
| 56 |
+
});
|
| 57 |
+
</script>
|
| 58 |
+
</body>
|
| 59 |
+
</html>
|
webpass/templates/bio_mobile.html
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
| 6 |
+
<title>Secure Login</title>
|
| 7 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 8 |
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
| 9 |
+
|
| 10 |
+
<style>
|
| 11 |
+
body {
|
| 12 |
+
background-color: #0d0d0d;
|
| 13 |
+
color: #e0e0e0;
|
| 14 |
+
height: 100vh;
|
| 15 |
+
display: flex;
|
| 16 |
+
align-items: center;
|
| 17 |
+
justify-content: center;
|
| 18 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
| 19 |
+
}
|
| 20 |
+
.auth-card {
|
| 21 |
+
background: #1a1a1a;
|
| 22 |
+
border: 1px solid #333;
|
| 23 |
+
border-radius: 24px;
|
| 24 |
+
padding: 2rem;
|
| 25 |
+
width: 90%;
|
| 26 |
+
max-width: 400px;
|
| 27 |
+
text-align: center;
|
| 28 |
+
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
| 29 |
+
transition: transform 0.3s ease;
|
| 30 |
+
}
|
| 31 |
+
.btn-action {
|
| 32 |
+
width: 100%;
|
| 33 |
+
padding: 16px;
|
| 34 |
+
font-size: 1.1rem;
|
| 35 |
+
border-radius: 16px;
|
| 36 |
+
margin-top: 1.5rem;
|
| 37 |
+
font-weight: 600;
|
| 38 |
+
transition: all 0.2s;
|
| 39 |
+
}
|
| 40 |
+
.btn-auth {
|
| 41 |
+
background: linear-gradient(135deg, #00b09b, #96c93d);
|
| 42 |
+
border: none;
|
| 43 |
+
color: white;
|
| 44 |
+
box-shadow: 0 4px 15px rgba(0, 255, 136, 0.3);
|
| 45 |
+
}
|
| 46 |
+
.btn-auth:active { transform: scale(0.98); }
|
| 47 |
+
|
| 48 |
+
.btn-reg {
|
| 49 |
+
background: linear-gradient(135deg, #4facfe, #00f2fe);
|
| 50 |
+
border: none;
|
| 51 |
+
color: white;
|
| 52 |
+
box-shadow: 0 4px 15px rgba(0, 168, 255, 0.3);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
.user-badge {
|
| 56 |
+
background: #2a2a2a;
|
| 57 |
+
padding: 8px 16px;
|
| 58 |
+
border-radius: 50px;
|
| 59 |
+
font-size: 0.9rem;
|
| 60 |
+
color: #aaa;
|
| 61 |
+
display: inline-block;
|
| 62 |
+
margin-bottom: 20px;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.icon-box {
|
| 66 |
+
font-size: 3rem;
|
| 67 |
+
margin-bottom: 1rem;
|
| 68 |
+
color: #fff;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.status-msg {
|
| 72 |
+
margin-top: 15px;
|
| 73 |
+
font-size: 0.9rem;
|
| 74 |
+
height: 24px;
|
| 75 |
+
}
|
| 76 |
+
.text-success-custom { color: #00ff88; }
|
| 77 |
+
.text-error-custom { color: #ff5555; }
|
| 78 |
+
|
| 79 |
+
/* Utility to hide elements */
|
| 80 |
+
.d-none { display: none !important; }
|
| 81 |
+
</style>
|
| 82 |
+
</head>
|
| 83 |
+
<body>
|
| 84 |
+
|
| 85 |
+
<div class="auth-card">
|
| 86 |
+
<div class="icon-box">
|
| 87 |
+
<i class="bi bi-shield-lock-fill"></i>
|
| 88 |
+
</div>
|
| 89 |
+
|
| 90 |
+
<h3 class="mb-2">WebPass Secure</h3>
|
| 91 |
+
<div class="user-badge"><i class="bi bi-person-fill"></i> {{ user_email }}</div>
|
| 92 |
+
|
| 93 |
+
<div id="section-auth" class="{% if not has_registered %}d-none{% endif %}">
|
| 94 |
+
<p class="text-muted small">Verify your identity to unlock your desktop.</p>
|
| 95 |
+
<button onclick="authenticate()" class="btn btn-action btn-auth">
|
| 96 |
+
<i class="bi bi-fingerprint me-2"></i> Authenticate
|
| 97 |
+
</button>
|
| 98 |
+
</div>
|
| 99 |
+
|
| 100 |
+
<div id="section-reg" class="{% if has_registered %}d-none{% endif %}">
|
| 101 |
+
<p class="text-muted small">Setup this device for secure login.</p>
|
| 102 |
+
<button onclick="register()" class="btn btn-action btn-reg">
|
| 103 |
+
<i class="bi bi-plus-circle-fill me-2"></i> Register Device
|
| 104 |
+
</button>
|
| 105 |
+
</div>
|
| 106 |
+
|
| 107 |
+
<div id="section-success" class="d-none">
|
| 108 |
+
<div class="text-success-custom" style="font-size: 4rem;">
|
| 109 |
+
<i class="bi bi-check-circle-fill"></i>
|
| 110 |
+
</div>
|
| 111 |
+
<h4 class="mt-3">Verified!</h4>
|
| 112 |
+
<p class="text-muted">Check your desktop screen.</p>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
<div id="status" class="status-msg text-muted"></div>
|
| 116 |
+
</div>
|
| 117 |
+
|
| 118 |
+
<script src="https://unpkg.com/@simplewebauthn/browser/dist/bundle/index.umd.min.js"></script>
|
| 119 |
+
<script>
|
| 120 |
+
const { startRegistration, startAuthentication } = SimpleWebAuthnBrowser;
|
| 121 |
+
|
| 122 |
+
// Extract Channel from URL path
|
| 123 |
+
const pathParts = window.location.pathname.split('/').filter(p => p.length > 0);
|
| 124 |
+
const CHANNEL_ID = pathParts[pathParts.length - 1];
|
| 125 |
+
|
| 126 |
+
function log(msg, type='normal') {
|
| 127 |
+
const el = document.getElementById('status');
|
| 128 |
+
el.innerText = msg;
|
| 129 |
+
if(type === 'error') el.className = 'status-msg text-error-custom';
|
| 130 |
+
else if(type === 'success') el.className = 'status-msg text-success-custom';
|
| 131 |
+
else el.className = 'status-msg text-muted';
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
async function register() {
|
| 135 |
+
log("Requesting setup...");
|
| 136 |
+
try {
|
| 137 |
+
const resp = await fetch('/api/bio/register/begin', {
|
| 138 |
+
method: 'POST',
|
| 139 |
+
headers: { 'Content-Type': 'application/json' },
|
| 140 |
+
body: JSON.stringify({ channel: CHANNEL_ID })
|
| 141 |
+
});
|
| 142 |
+
|
| 143 |
+
if (!resp.ok) throw new Error("Server rejected setup request");
|
| 144 |
+
|
| 145 |
+
const options = await resp.json();
|
| 146 |
+
const attResp = await startRegistration(options);
|
| 147 |
+
|
| 148 |
+
const verifyResp = await fetch('/api/bio/register/complete', {
|
| 149 |
+
method: 'POST',
|
| 150 |
+
headers: { 'Content-Type': 'application/json' },
|
| 151 |
+
body: JSON.stringify({ response: attResp, channel: CHANNEL_ID })
|
| 152 |
+
});
|
| 153 |
+
|
| 154 |
+
const verifyJson = await verifyResp.json();
|
| 155 |
+
if (verifyJson.status === 'ok') {
|
| 156 |
+
log("Setup Complete!", "success");
|
| 157 |
+
|
| 158 |
+
// UX: Switch to Auth Mode immediately
|
| 159 |
+
document.getElementById('section-reg').classList.add('d-none');
|
| 160 |
+
document.getElementById('section-auth').classList.remove('d-none');
|
| 161 |
+
|
| 162 |
+
// Optional: Auto-trigger auth? Or let user click.
|
| 163 |
+
// Let's let user click to be clear what happened.
|
| 164 |
+
alert("Registration Successful! Now click Authenticate to unlock.");
|
| 165 |
+
} else {
|
| 166 |
+
throw new Error("Verification failed");
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
} catch (error) {
|
| 170 |
+
log("Error: " + error.message, "error");
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
async function authenticate() {
|
| 175 |
+
log("Requesting verification...");
|
| 176 |
+
try {
|
| 177 |
+
const resp = await fetch('/api/bio/auth/begin', {
|
| 178 |
+
method: 'POST',
|
| 179 |
+
headers: { 'Content-Type': 'application/json' },
|
| 180 |
+
body: JSON.stringify({ channel: CHANNEL_ID })
|
| 181 |
+
});
|
| 182 |
+
|
| 183 |
+
if (!resp.ok) throw new Error("Server rejected auth request");
|
| 184 |
+
|
| 185 |
+
const options = await resp.json();
|
| 186 |
+
const asseResp = await startAuthentication(options);
|
| 187 |
+
|
| 188 |
+
const verifyResp = await fetch('/api/bio/auth/complete', {
|
| 189 |
+
method: 'POST',
|
| 190 |
+
headers: { 'Content-Type': 'application/json' },
|
| 191 |
+
body: JSON.stringify({ credentialId: asseResp.id, response: asseResp, channel: CHANNEL_ID })
|
| 192 |
+
});
|
| 193 |
+
|
| 194 |
+
const verifyJson = await verifyResp.json();
|
| 195 |
+
if (verifyJson.status === 'ok') {
|
| 196 |
+
// UX: Show Success Screen
|
| 197 |
+
document.getElementById('section-auth').classList.add('d-none');
|
| 198 |
+
document.getElementById('section-reg').classList.add('d-none');
|
| 199 |
+
document.getElementById('section-success').classList.remove('d-none');
|
| 200 |
+
log(""); // Clear status text
|
| 201 |
+
} else {
|
| 202 |
+
log("Authentication Failed", "error");
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
} catch (error) {
|
| 206 |
+
log("Error: " + error.message, "error");
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
</script>
|
| 210 |
+
</body>
|
| 211 |
+
</html>
|
webpass/templates/breach.html
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Breach Checker{% endblock %}
|
| 3 |
+
{% block page_title %}OSINT Data Watch{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="row justify-content-center mt-5">
|
| 7 |
+
<div class="col-lg-6 fade-in-up">
|
| 8 |
+
<div class="cyber-card p-5">
|
| 9 |
+
<h3 class="text-danger mb-4 text-center"><i class="bi bi-shield-lock-fill me-2"></i>Have You Been Pwned?</h3>
|
| 10 |
+
<p class="text-muted text-center mb-4">
|
| 11 |
+
We search 11 Billion leaked records. Your password is <strong>never sent</strong> to us or the database (we use K-Anonymity Hashing).
|
| 12 |
+
</p>
|
| 13 |
+
|
| 14 |
+
<div class="mb-3">
|
| 15 |
+
<label class="form-label text-accent">Test Password</label>
|
| 16 |
+
<div class="input-group">
|
| 17 |
+
<input type="password" id="pwInput" class="form-control" placeholder="Enter a password...">
|
| 18 |
+
<button class="btn btn-outline-secondary" type="button" onclick="togglePw()">
|
| 19 |
+
<i class="bi bi-eye"></i>
|
| 20 |
+
</button>
|
| 21 |
+
</div>
|
| 22 |
+
</div>
|
| 23 |
+
|
| 24 |
+
<button class="btn btn-outline-danger w-100 py-3 mb-4" onclick="checkBreach()">
|
| 25 |
+
<i class="bi bi-search me-2"></i>Run OSINT Check
|
| 26 |
+
</button>
|
| 27 |
+
|
| 28 |
+
<div id="result-safe" class="d-none alert alert-success text-center fade-in-up">
|
| 29 |
+
<i class="bi bi-check-circle-fill fs-1"></i><br>
|
| 30 |
+
<strong>Safe!</strong><br>
|
| 31 |
+
This password was not found in any known data breaches.
|
| 32 |
+
</div>
|
| 33 |
+
|
| 34 |
+
<div id="result-leak" class="d-none alert alert-danger text-center fade-in-up">
|
| 35 |
+
<i class="bi bi-exclamation-octagon-fill fs-1"></i><br>
|
| 36 |
+
<strong>COMPROMISED!</strong><br>
|
| 37 |
+
This password appears in <span id="leak-count" class="fw-bold fs-4">0</span> known data breaches.<br>
|
| 38 |
+
Change it immediately.
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
{% endblock %}
|
| 44 |
+
|
| 45 |
+
{% block scripts %}
|
| 46 |
+
<script>
|
| 47 |
+
function togglePw() {
|
| 48 |
+
const x = document.getElementById("pwInput");
|
| 49 |
+
x.type = x.type === "password" ? "text" : "password";
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
async function checkBreach() {
|
| 53 |
+
const pw = document.getElementById("pwInput").value;
|
| 54 |
+
if (!pw) return;
|
| 55 |
+
|
| 56 |
+
const btn = document.querySelector('button[onclick="checkBreach()"]');
|
| 57 |
+
const originalText = btn.innerHTML;
|
| 58 |
+
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Scanning Dark Web...';
|
| 59 |
+
btn.disabled = true;
|
| 60 |
+
|
| 61 |
+
try {
|
| 62 |
+
const res = await fetch("{{ url_for('tools.breach_check') }}", {
|
| 63 |
+
method: "POST",
|
| 64 |
+
headers: {
|
| 65 |
+
"Content-Type": "application/json",
|
| 66 |
+
"X-CSRFToken": "{{ csrf_token() }}"
|
| 67 |
+
},
|
| 68 |
+
body: JSON.stringify({ password: pw })
|
| 69 |
+
});
|
| 70 |
+
|
| 71 |
+
const data = await res.json();
|
| 72 |
+
|
| 73 |
+
document.getElementById("result-safe").classList.add("d-none");
|
| 74 |
+
document.getElementById("result-leak").classList.add("d-none");
|
| 75 |
+
|
| 76 |
+
if (data.leaked) {
|
| 77 |
+
document.getElementById("leak-count").textContent = data.count.toLocaleString();
|
| 78 |
+
document.getElementById("result-leak").classList.remove("d-none");
|
| 79 |
+
} else {
|
| 80 |
+
document.getElementById("result-safe").classList.remove("d-none");
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
} catch (e) {
|
| 84 |
+
alert("System Error: " + e);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
btn.innerHTML = originalText;
|
| 88 |
+
btn.disabled = false;
|
| 89 |
+
}
|
| 90 |
+
</script>
|
| 91 |
+
{% endblock %}
|
webpass/templates/dashboard.html
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Command Center{% endblock %}
|
| 3 |
+
{% block page_title %}Dashboard{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="row g-4 mb-4">
|
| 7 |
+
<div class="col-md-3 fade-in-up" style="animation-delay: 0.1s;">
|
| 8 |
+
<div class="cyber-card p-3 d-flex justify-content-between align-items-center">
|
| 9 |
+
<div>
|
| 10 |
+
<h6 class="text-muted text-uppercase small">Identity</h6>
|
| 11 |
+
<h4 class="mb-0">{{ credential.username if credential else 'Guest' }}</h4>
|
| 12 |
+
</div>
|
| 13 |
+
<div class="bg-primary bg-opacity-10 p-3 rounded-circle">
|
| 14 |
+
<i class="bi bi-person-bounding-box fs-4 text-primary"></i>
|
| 15 |
+
</div>
|
| 16 |
+
</div>
|
| 17 |
+
</div>
|
| 18 |
+
|
| 19 |
+
<div class="col-md-3 fade-in-up" style="animation-delay: 0.2s;">
|
| 20 |
+
<div class="cyber-card p-3 d-flex justify-content-between align-items-center">
|
| 21 |
+
<div>
|
| 22 |
+
<h6 class="text-muted text-uppercase small">System Status</h6>
|
| 23 |
+
<h4 class="mb-0 text-success">Online</h4>
|
| 24 |
+
</div>
|
| 25 |
+
<div class="bg-success bg-opacity-10 p-3 rounded-circle">
|
| 26 |
+
<i class="bi bi-cpu fs-4 text-success"></i>
|
| 27 |
+
</div>
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
|
| 31 |
+
<div class="col-md-3 fade-in-up" style="animation-delay: 0.3s;">
|
| 32 |
+
<div class="cyber-card p-3 d-flex justify-content-between align-items-center">
|
| 33 |
+
<div>
|
| 34 |
+
<h6 class="text-muted text-uppercase small">Encryption</h6>
|
| 35 |
+
<h4 class="mb-0">AES-GCM</h4>
|
| 36 |
+
</div>
|
| 37 |
+
<div class="bg-warning bg-opacity-10 p-3 rounded-circle">
|
| 38 |
+
<i class="bi bi-shield-check fs-4 text-warning"></i>
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
</div>
|
| 42 |
+
|
| 43 |
+
<div class="col-md-3 fade-in-up" style="animation-delay: 0.4s;">
|
| 44 |
+
<div class="cyber-card p-3 d-flex justify-content-between align-items-center">
|
| 45 |
+
<div>
|
| 46 |
+
<h6 class="text-muted text-uppercase small">Connection</h6>
|
| 47 |
+
<h4 class="mb-0">Secure</h4>
|
| 48 |
+
</div>
|
| 49 |
+
<div class="bg-danger bg-opacity-10 p-3 rounded-circle">
|
| 50 |
+
<i class="bi bi-wifi fs-4 text-danger"></i>
|
| 51 |
+
</div>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
|
| 56 |
+
<div class="row g-4">
|
| 57 |
+
<div class="col-lg-8 fade-in-up" style="animation-delay: 0.5s;">
|
| 58 |
+
<div class="cyber-card h-100 p-4 border-start border-4 border-info">
|
| 59 |
+
<h4 class="mb-4 text-white"><i class="bi bi-grid-fill me-2 text-info"></i>Operations Grid</h4>
|
| 60 |
+
|
| 61 |
+
<div class="row g-3">
|
| 62 |
+
<div class="col-md-4">
|
| 63 |
+
{% if has_credentials %}
|
| 64 |
+
<a href="{{ url_for('dashboard.secure_tools') }}" class="btn btn-outline-info w-100 py-4 d-flex flex-column align-items-center justify-content-center gap-2 h-100 border-2">
|
| 65 |
+
<i class="bi bi-file-earmark-lock fs-1"></i>
|
| 66 |
+
<span class="fw-bold">File Vault</span>
|
| 67 |
+
</a>
|
| 68 |
+
{% else %}
|
| 69 |
+
<a href="{{ url_for('dashboard.profile') }}" class="btn btn-dark w-100 py-4 d-flex flex-column align-items-center justify-content-center gap-2 h-100 border-secondary text-muted">
|
| 70 |
+
<i class="bi bi-lock-fill fs-1"></i>
|
| 71 |
+
<span>Setup Vault</span>
|
| 72 |
+
</a>
|
| 73 |
+
{% endif %}
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
<div class="col-md-4">
|
| 77 |
+
{% if has_credentials %}
|
| 78 |
+
<a href="{{ url_for('share.share_ui') }}" class="btn btn-outline-warning w-100 py-4 d-flex flex-column align-items-center justify-content-center gap-2 h-100 border-2">
|
| 79 |
+
<i class="bi bi-send-exclamation fs-1"></i>
|
| 80 |
+
<span class="fw-bold">Dead Drop</span>
|
| 81 |
+
</a>
|
| 82 |
+
{% else %}
|
| 83 |
+
<button class="btn btn-dark w-100 py-4 d-flex flex-column align-items-center justify-content-center gap-2 h-100 border-secondary text-muted" disabled>
|
| 84 |
+
<i class="bi bi-lock-fill fs-1"></i>
|
| 85 |
+
</button>
|
| 86 |
+
{% endif %}
|
| 87 |
+
</div>
|
| 88 |
+
|
| 89 |
+
<div class="col-md-4">
|
| 90 |
+
{% if has_credentials %}
|
| 91 |
+
<a href="{{ url_for('stego.ui') }}" class="btn btn-outline-success w-100 py-4 d-flex flex-column align-items-center justify-content-center gap-2 h-100 border-2">
|
| 92 |
+
<i class="bi bi-eye-slash-fill fs-1"></i>
|
| 93 |
+
<span class="fw-bold">Steganography</span>
|
| 94 |
+
</a>
|
| 95 |
+
{% else %}
|
| 96 |
+
<button class="btn btn-dark w-100 py-4 d-flex flex-column align-items-center justify-content-center gap-2 h-100 border-secondary text-muted" disabled>
|
| 97 |
+
<i class="bi bi-lock-fill fs-1"></i>
|
| 98 |
+
</button>
|
| 99 |
+
{% endif %}
|
| 100 |
+
</div>
|
| 101 |
+
|
| 102 |
+
<div class="col-md-4 mt-3">
|
| 103 |
+
{% if has_credentials %}
|
| 104 |
+
<a href="{{ url_for('tools.metadata_ui') }}" class="btn btn-outline-light w-100 py-4 d-flex flex-column align-items-center justify-content-center gap-2 h-100 border-2">
|
| 105 |
+
<i class="bi bi-eraser-fill fs-1"></i>
|
| 106 |
+
<span class="fw-bold">Ghost Wiper</span>
|
| 107 |
+
</a>
|
| 108 |
+
{% else %}
|
| 109 |
+
<button class="btn btn-dark w-100 py-4 d-flex flex-column align-items-center justify-content-center gap-2 h-100 border-secondary text-muted" disabled>
|
| 110 |
+
<i class="bi bi-lock-fill fs-1"></i>
|
| 111 |
+
</button>
|
| 112 |
+
{% endif %}
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
<div class="col-md-4 mt-3">
|
| 116 |
+
{% if has_credentials %}
|
| 117 |
+
<a href="{{ url_for('tools.breach_ui') }}" class="btn btn-outline-danger w-100 py-4 d-flex flex-column align-items-center justify-content-center gap-2 h-100 border-2">
|
| 118 |
+
<i class="bi bi-shield-lock-fill fs-1"></i>
|
| 119 |
+
<span class="fw-bold">Breach Check</span>
|
| 120 |
+
</a>
|
| 121 |
+
{% else %}
|
| 122 |
+
<button class="btn btn-dark w-100 py-4 d-flex flex-column align-items-center justify-content-center gap-2 h-100 border-secondary text-muted" disabled>
|
| 123 |
+
<i class="bi bi-lock-fill fs-1"></i>
|
| 124 |
+
</button>
|
| 125 |
+
{% endif %}
|
| 126 |
+
</div>
|
| 127 |
+
|
| 128 |
+
<div class="col-md-4 mt-3">
|
| 129 |
+
{% if has_credentials %}
|
| 130 |
+
<a href="{{ url_for('dashboard.network_monitor') }}" class="btn btn-outline-primary w-100 py-4 d-flex flex-column align-items-center justify-content-center gap-2 h-100 border-2">
|
| 131 |
+
<i class="bi bi-activity fs-1"></i>
|
| 132 |
+
<span class="fw-bold">Net Monitor</span>
|
| 133 |
+
</a>
|
| 134 |
+
{% else %}
|
| 135 |
+
<button class="btn btn-dark w-100 py-4 d-flex flex-column align-items-center justify-content-center gap-2 h-100 border-secondary text-muted" disabled>
|
| 136 |
+
<i class="bi bi-lock-fill fs-1"></i>
|
| 137 |
+
</button>
|
| 138 |
+
{% endif %}
|
| 139 |
+
</div>
|
| 140 |
+
|
| 141 |
+
<div class="col-12 mt-4">
|
| 142 |
+
<label class="text-muted small mb-2 text-uppercase fw-bold">Quick Watchtower Check</label>
|
| 143 |
+
<div class="input-group">
|
| 144 |
+
<span class="input-group-text bg-dark border-secondary text-danger"><i class="bi bi-tower"></i></span>
|
| 145 |
+
<input type="password" id="watchtower-input" class="form-control watchtower-monitor" placeholder="Test a password for leaks (k-Anonymity)...">
|
| 146 |
+
</div>
|
| 147 |
+
</div>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
|
| 152 |
+
<div class="col-lg-4 fade-in-up" style="animation-delay: 0.6s;">
|
| 153 |
+
<div class="cyber-card h-100 p-4">
|
| 154 |
+
<h4 class="mb-3 text-white">System Log</h4>
|
| 155 |
+
<div class="border-start border-secondary ps-3 ms-2">
|
| 156 |
+
<div class="mb-3">
|
| 157 |
+
<small class="text-muted d-block">10:42:01 AM</small>
|
| 158 |
+
<span class="text-success"><i class="bi bi-check-circle me-1"></i> Zero-Knowledge Engine Ready</span>
|
| 159 |
+
</div>
|
| 160 |
+
<div class="mb-3">
|
| 161 |
+
<small class="text-muted d-block">10:42:05 AM</small>
|
| 162 |
+
<span class="text-info"><i class="bi bi-database me-1"></i> Database Connected</span>
|
| 163 |
+
</div>
|
| 164 |
+
<div class="mb-3">
|
| 165 |
+
<small class="text-muted d-block">10:42:06 AM</small>
|
| 166 |
+
<span class="text-warning"><i class="bi bi-shield-exclamation me-1"></i> HIBP API Latency: 45ms</span>
|
| 167 |
+
</div>
|
| 168 |
+
<div class="mb-3">
|
| 169 |
+
<small class="text-muted d-block">10:42:08 AM</small>
|
| 170 |
+
<span class="text-light"><i class="bi bi-eye-slash me-1"></i> Steganography Module Loaded</span>
|
| 171 |
+
</div>
|
| 172 |
+
</div>
|
| 173 |
+
|
| 174 |
+
<div class="mt-4 pt-4 border-top border-secondary">
|
| 175 |
+
<small class="text-muted text-uppercase">Current Session</small>
|
| 176 |
+
<div class="d-flex align-items-center mt-2">
|
| 177 |
+
<img src="{{ current_user.profile_image }}" class="rounded-circle me-3 border border-secondary" width="40" height="40">
|
| 178 |
+
<div>
|
| 179 |
+
<div class="text-white small fw-bold">{{ current_user.email }}</div>
|
| 180 |
+
<div class="text-success x-small" style="font-size: 0.75rem;">● Active</div>
|
| 181 |
+
</div>
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
+
{% endblock %}
|
| 188 |
+
|
| 189 |
+
{% block scripts %}
|
| 190 |
+
<script src="{{ url_for('static', filename='js/watchtower.js') }}"></script>
|
| 191 |
+
{% endblock %}
|
webpass/templates/index.html
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<!-- Critical meta tag for mobile responsiveness -->
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 7 |
+
<title>Home – Password Vault</title>
|
| 8 |
+
<!-- Bootstrap CSS -->
|
| 9 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 10 |
+
</head>
|
| 11 |
+
<body>
|
| 12 |
+
<!-- Navigation Bar -->
|
| 13 |
+
<nav class="navbar navbar-light bg-light">
|
| 14 |
+
<div class="container-fluid">
|
| 15 |
+
<span class="navbar-brand">Password Vault</span>
|
| 16 |
+
<a class="nav-link" href="{{ url_for('logout') }}">Logout</a>
|
| 17 |
+
</div>
|
| 18 |
+
</nav>
|
| 19 |
+
|
| 20 |
+
<div class="container mt-4">
|
| 21 |
+
<h1>Welcome, {{ current_user.email }}!</h1>
|
| 22 |
+
<!-- Your home page content goes here -->
|
| 23 |
+
</div>
|
| 24 |
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js"></script>
|
| 25 |
+
</body>
|
| 26 |
+
</html>
|
webpass/templates/login.html
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<title>Sign In – Password Vault</title>
|
| 7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/login.css') }}">
|
| 8 |
+
<script src="https://kit.fontawesome.com/your-kit-id.js" crossorigin="anonymous"></script>
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<div class="container">
|
| 12 |
+
<div class="top"></div>
|
| 13 |
+
<div class="bottom"></div>
|
| 14 |
+
<div class="center">
|
| 15 |
+
<h2>Please Sign In</h2>
|
| 16 |
+
|
| 17 |
+
<!-- Only change: point at google.login so Flask-Dance redirects -->
|
| 18 |
+
<a href="{{ url_for('google.login') }}" class="google-btn">
|
| 19 |
+
<i class="fab fa-google me-2"></i>Sign in with Google
|
| 20 |
+
</a>
|
| 21 |
+
</div>
|
| 22 |
+
</div>
|
| 23 |
+
</body>
|
| 24 |
+
</html>
|
webpass/templates/metadata.html
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Ghost Metadata Wiper{% endblock %}
|
| 3 |
+
{% block page_title %}OpSec Imaging Station{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="row justify-content-center mt-4">
|
| 7 |
+
<div class="col-lg-8 fade-in-up">
|
| 8 |
+
<div class="cyber-card p-5 text-center">
|
| 9 |
+
<h3 class="text-info mb-4"><i class="bi bi-eraser-fill me-2"></i>Ghost Metadata Wiper</h3>
|
| 10 |
+
<p class="text-muted mb-4">
|
| 11 |
+
Advanced Forensic Scan. Detects hidden <strong>GPS Coordinates, Serial Numbers, and Timestamp</strong> data embedded by cameras and phones.
|
| 12 |
+
</p>
|
| 13 |
+
|
| 14 |
+
<div class="mb-4">
|
| 15 |
+
<input type="file" id="imageInput" class="form-control" accept="image/*">
|
| 16 |
+
<button class="btn btn-cyber mt-3 w-100 py-3" onclick="scanImage()">
|
| 17 |
+
<i class="bi bi-upc-scan me-2"></i>Run Forensic Analysis
|
| 18 |
+
</button>
|
| 19 |
+
</div>
|
| 20 |
+
|
| 21 |
+
<div id="scan-animation" class="d-none my-4">
|
| 22 |
+
<div class="progress" style="height: 5px;">
|
| 23 |
+
<div class="progress-bar progress-bar-striped progress-bar-animated bg-info" style="width: 100%"></div>
|
| 24 |
+
</div>
|
| 25 |
+
<small class="text-info blink-text">DECODING EXIF LAYERS...</small>
|
| 26 |
+
</div>
|
| 27 |
+
|
| 28 |
+
<div id="result-area" class="d-none text-start bg-dark border border-secondary p-3 rounded">
|
| 29 |
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
| 30 |
+
<h5 class="text-white mb-0">Scan Report</h5>
|
| 31 |
+
<span id="threat-level" class="badge bg-secondary">Unknown</span>
|
| 32 |
+
</div>
|
| 33 |
+
|
| 34 |
+
<h6 class="text-danger text-uppercase small fw-bold mb-2"><i class="bi bi-shield-exclamation me-1"></i>Privacy Risks (Critical)</h6>
|
| 35 |
+
<ul id="critical-list" class="list-group list-group-flush mb-4 text-monospace small">
|
| 36 |
+
</ul>
|
| 37 |
+
|
| 38 |
+
<h6 class="text-muted text-uppercase small fw-bold mb-2"><i class="bi bi-camera me-1"></i>Device Fingerprint (Technical)</h6>
|
| 39 |
+
<ul id="technical-list" class="list-group list-group-flush mb-3 text-monospace small text-muted">
|
| 40 |
+
</ul>
|
| 41 |
+
|
| 42 |
+
<div id="clean-action" class="d-none text-center border-top border-secondary pt-3">
|
| 43 |
+
<div class="alert alert-danger mb-3 border-danger bg-danger bg-opacity-10">
|
| 44 |
+
<i class="bi bi-geo-alt-fill me-2"></i><strong>Location/Identity Data Found!</strong><br>
|
| 45 |
+
This image reveals where and when it was taken.
|
| 46 |
+
</div>
|
| 47 |
+
<form action="{{ url_for('tools.metadata_wipe') }}" method="POST" enctype="multipart/form-data">
|
| 48 |
+
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
| 49 |
+
<p class="text-muted small">Upload again to confirm wipe & download safe copy.</p>
|
| 50 |
+
<input type="file" name="image" class="form-control mb-2" required>
|
| 51 |
+
<button type="submit" class="btn btn-success w-100 fw-bold">
|
| 52 |
+
<i class="bi bi-shield-check me-2"></i>WIPE METADATA & DOWNLOAD
|
| 53 |
+
</button>
|
| 54 |
+
</form>
|
| 55 |
+
</div>
|
| 56 |
+
|
| 57 |
+
<div id="clean-msg" class="d-none text-center mt-3">
|
| 58 |
+
<div class="alert alert-success border-success bg-success bg-opacity-10">
|
| 59 |
+
<i class="bi bi-check-circle-fill me-2"></i><strong>Clean.</strong><br> No hidden metadata found.
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
</div>
|
| 63 |
+
</div>
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
{% endblock %}
|
| 67 |
+
|
| 68 |
+
{% block scripts %}
|
| 69 |
+
<script>
|
| 70 |
+
async function scanImage() {
|
| 71 |
+
const input = document.getElementById('imageInput');
|
| 72 |
+
if (input.files.length === 0) { alert("Select an image first."); return; }
|
| 73 |
+
|
| 74 |
+
document.getElementById('scan-animation').classList.remove('d-none');
|
| 75 |
+
document.getElementById('result-area').classList.add('d-none');
|
| 76 |
+
|
| 77 |
+
const formData = new FormData();
|
| 78 |
+
formData.append('image', input.files[0]);
|
| 79 |
+
|
| 80 |
+
try {
|
| 81 |
+
const res = await fetch("{{ url_for('tools.metadata_scan') }}", {
|
| 82 |
+
method: "POST",
|
| 83 |
+
body: formData,
|
| 84 |
+
headers: { "X-CSRFToken": "{{ csrf_token() }}" }
|
| 85 |
+
});
|
| 86 |
+
|
| 87 |
+
const data = await res.json();
|
| 88 |
+
|
| 89 |
+
document.getElementById('scan-animation').classList.add('d-none');
|
| 90 |
+
document.getElementById('result-area').classList.remove('d-none');
|
| 91 |
+
|
| 92 |
+
const critList = document.getElementById('critical-list');
|
| 93 |
+
const techList = document.getElementById('technical-list');
|
| 94 |
+
critList.innerHTML = "";
|
| 95 |
+
techList.innerHTML = "";
|
| 96 |
+
|
| 97 |
+
if (data.status === 'clean') {
|
| 98 |
+
document.getElementById('clean-msg').classList.remove('d-none');
|
| 99 |
+
document.getElementById('clean-action').classList.add('d-none');
|
| 100 |
+
document.getElementById('threat-level').className = "badge bg-success";
|
| 101 |
+
document.getElementById('threat-level').textContent = "SAFE";
|
| 102 |
+
critList.innerHTML = "<li class='list-group-item bg-transparent text-muted'>No critical data found.</li>";
|
| 103 |
+
} else {
|
| 104 |
+
// Found Data!
|
| 105 |
+
document.getElementById('clean-msg').classList.add('d-none');
|
| 106 |
+
document.getElementById('clean-action').classList.remove('d-none');
|
| 107 |
+
document.getElementById('threat-level').className = "badge bg-danger blink-text";
|
| 108 |
+
document.getElementById('threat-level').textContent = "COMPROMISED";
|
| 109 |
+
|
| 110 |
+
// Fill Critical Data
|
| 111 |
+
if (Object.keys(data.data.critical).length > 0) {
|
| 112 |
+
for (const [key, val] of Object.entries(data.data.critical)) {
|
| 113 |
+
let displayVal = val;
|
| 114 |
+
// If it's a map link, make it clickable
|
| 115 |
+
if (key === 'Map Link') {
|
| 116 |
+
displayVal = `<a href="${val}" target="_blank" class="text-info text-decoration-none"><i class="bi bi-box-arrow-up-right me-1"></i>View on Google Maps</a>`;
|
| 117 |
+
}
|
| 118 |
+
const li = document.createElement('li');
|
| 119 |
+
li.className = "list-group-item bg-transparent text-light border-secondary d-flex justify-content-between";
|
| 120 |
+
li.innerHTML = `<span class="text-danger fw-bold">${key}</span> <span>${displayVal}</span>`;
|
| 121 |
+
critList.appendChild(li);
|
| 122 |
+
}
|
| 123 |
+
} else {
|
| 124 |
+
critList.innerHTML = "<li class='list-group-item bg-transparent text-muted'>None detected.</li>";
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
// Fill Technical Data
|
| 128 |
+
if (Object.keys(data.data.technical).length > 0) {
|
| 129 |
+
for (const [key, val] of Object.entries(data.data.technical)) {
|
| 130 |
+
const li = document.createElement('li');
|
| 131 |
+
li.className = "list-group-item bg-transparent text-muted border-secondary d-flex justify-content-between";
|
| 132 |
+
li.innerHTML = `<span>${key}</span> <span class="text-light">${val}</span>`;
|
| 133 |
+
techList.appendChild(li);
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
} catch (e) {
|
| 138 |
+
alert("Scan Error: " + e);
|
| 139 |
+
document.getElementById('scan-animation').classList.add('d-none');
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
</script>
|
| 143 |
+
{% endblock %}
|
webpass/templates/network.html
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Network Monitor{% endblock %}
|
| 3 |
+
{% block page_title %}Network Traffic Analysis{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="row g-4 mt-2">
|
| 7 |
+
<div class="col-md-3 fade-in-up">
|
| 8 |
+
<div class="cyber-card p-3 text-center border-primary">
|
| 9 |
+
<h6 class="text-muted text-uppercase">Total Packets</h6>
|
| 10 |
+
<h2 class="text-white display-6 fw-bold" id="count-All">0</h2>
|
| 11 |
+
</div>
|
| 12 |
+
</div>
|
| 13 |
+
<div class="col-md-3 fade-in-up" style="animation-delay: 0.1s;">
|
| 14 |
+
<div class="cyber-card p-3 text-center border-info">
|
| 15 |
+
<h6 class="text-muted text-uppercase">TCP Traffic</h6>
|
| 16 |
+
<h2 class="text-info display-6 fw-bold" id="count-TCP">0</h2>
|
| 17 |
+
</div>
|
| 18 |
+
</div>
|
| 19 |
+
<div class="col-md-3 fade-in-up" style="animation-delay: 0.2s;">
|
| 20 |
+
<div class="cyber-card p-3 text-center border-warning">
|
| 21 |
+
<h6 class="text-muted text-uppercase">UDP Traffic</h6>
|
| 22 |
+
<h2 class="text-warning display-6 fw-bold" id="count-UDP">0</h2>
|
| 23 |
+
</div>
|
| 24 |
+
</div>
|
| 25 |
+
<div class="col-md-3 fade-in-up" style="animation-delay: 0.3s;">
|
| 26 |
+
<div class="cyber-card p-3 text-center border-danger">
|
| 27 |
+
<h6 class="text-muted text-uppercase">DNS Queries</h6>
|
| 28 |
+
<h2 class="text-danger display-6 fw-bold" id="count-DNS">0</h2>
|
| 29 |
+
</div>
|
| 30 |
+
</div>
|
| 31 |
+
</div>
|
| 32 |
+
|
| 33 |
+
<div class="row g-4 mt-3">
|
| 34 |
+
<div class="col-lg-4 fade-in-up" style="animation-delay: 0.4s;">
|
| 35 |
+
<div class="cyber-card p-4 h-100">
|
| 36 |
+
<h5 class="mb-4 text-accent">Protocol Distribution</h5>
|
| 37 |
+
<div style="height: 300px;">
|
| 38 |
+
<canvas id="protocolChart"></canvas>
|
| 39 |
+
</div>
|
| 40 |
+
<div class="mt-4">
|
| 41 |
+
<h6 class="text-muted mb-3">Top Talkers (Source IP)</h6>
|
| 42 |
+
<ul class="list-group list-group-flush" id="top-sources">
|
| 43 |
+
<li class="list-group-item bg-transparent text-muted">Listening...</li>
|
| 44 |
+
</ul>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
|
| 49 |
+
<div class="col-lg-8 fade-in-up" style="animation-delay: 0.5s;">
|
| 50 |
+
<div class="cyber-card p-4 h-100">
|
| 51 |
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
| 52 |
+
<h5 class="text-white"><i class="bi bi-list-columns-reverse me-2"></i>Live Packet Feed</h5>
|
| 53 |
+
<button class="btn btn-sm btn-outline-success" id="download-btn">
|
| 54 |
+
<i class="bi bi-download me-1"></i> Export Log
|
| 55 |
+
</button>
|
| 56 |
+
</div>
|
| 57 |
+
<div class="table-responsive">
|
| 58 |
+
<table id="packet-table" class="table table-dark table-hover table-sm w-100" style="font-size: 0.9rem;">
|
| 59 |
+
<thead>
|
| 60 |
+
<tr class="text-accent">
|
| 61 |
+
<th>Time</th>
|
| 62 |
+
<th>Source</th>
|
| 63 |
+
<th>Destination</th>
|
| 64 |
+
<th>Protocol</th>
|
| 65 |
+
<th>Length</th>
|
| 66 |
+
</tr>
|
| 67 |
+
</thead>
|
| 68 |
+
<tbody class="font-monospace">
|
| 69 |
+
</tbody>
|
| 70 |
+
</table>
|
| 71 |
+
</div>
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
<div class="modal fade" id="packetDetailModal" tabindex="-1">
|
| 77 |
+
<div class="modal-dialog modal-dialog-centered">
|
| 78 |
+
<div class="cyber-card modal-content bg-dark text-white">
|
| 79 |
+
<div class="modal-header border-bottom border-secondary">
|
| 80 |
+
<h5 class="modal-title">Packet Inspection</h5>
|
| 81 |
+
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
| 82 |
+
</div>
|
| 83 |
+
<div class="modal-body" id="packet-detail-body">
|
| 84 |
+
</div>
|
| 85 |
+
</div>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
{% endblock %}
|
| 89 |
+
|
| 90 |
+
{% block scripts %}
|
| 91 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 92 |
+
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
| 93 |
+
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
| 94 |
+
<script src="https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js"></script>
|
| 95 |
+
<script src="{{ url_for('static', filename='js/network.js') }}"></script>
|
| 96 |
+
{% endblock %}
|
webpass/templates/network_demo.html
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Network Monitor{% endblock %}
|
| 3 |
+
{% block page_title %}Local Reconnaissance{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="row justify-content-center mt-5">
|
| 7 |
+
<div class="col-lg-8 fade-in-up">
|
| 8 |
+
<div class="cyber-card p-5 text-center border-danger">
|
| 9 |
+
<i class="bi bi-shield-lock-fill text-danger mb-3" style="font-size: 5rem;"></i>
|
| 10 |
+
<h2 class="text-white">Feature Locked in Cloud Environment</h2>
|
| 11 |
+
<p class="text-muted mt-3 fs-5">
|
| 12 |
+
The <strong>Hardware Network Monitor</strong> requires direct `root` access to the host machine's Network Interface Card (NIC) to sniff raw packets via Nmap and SocketIO.
|
| 13 |
+
</p>
|
| 14 |
+
<p class="text-muted">
|
| 15 |
+
Because this instance is deployed in a sandboxed Cloud Container (Hugging Face), hardware-level packet sniffing is disabled to comply with cloud security policies.
|
| 16 |
+
</p>
|
| 17 |
+
|
| 18 |
+
<div class="alert alert-info mt-4 bg-dark border-info text-start d-inline-block">
|
| 19 |
+
<i class="bi bi-github me-2"></i><strong>Want to see it in action?</strong><br>
|
| 20 |
+
Clone the repository and run it locally with Administrator privileges:
|
| 21 |
+
<code class="d-block mt-2 text-success">git clone https://github.com/yourusername/webpass.git</code>
|
| 22 |
+
<code class="d-block text-success">python wsgi.py</code>
|
| 23 |
+
</div>
|
| 24 |
+
</div>
|
| 25 |
+
</div>
|
| 26 |
+
</div>
|
| 27 |
+
{% endblock %}
|
webpass/templates/profile.html
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Profile{% endblock %}
|
| 3 |
+
{% block page_title %}Identity Management{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="row justify-content-center mt-4">
|
| 7 |
+
<div class="col-md-6 fade-in-up">
|
| 8 |
+
<div class="cyber-card p-5 text-center h-100 border-info">
|
| 9 |
+
<div class="position-relative d-inline-block mb-4">
|
| 10 |
+
<img src="{{ current_user.profile_image }}"
|
| 11 |
+
class="rounded-circle border border-2 border-info shadow-lg"
|
| 12 |
+
style="width: 150px; height: 150px; object-fit: cover;">
|
| 13 |
+
<span class="position-absolute bottom-0 end-0 bg-success border border-dark rounded-circle p-2" title="Active"></span>
|
| 14 |
+
</div>
|
| 15 |
+
|
| 16 |
+
<h3 class="text-white">{{ current_user.email }}</h3>
|
| 17 |
+
<p class="text-muted text-uppercase small letter-spacing-2">Authorized Operator</p>
|
| 18 |
+
|
| 19 |
+
<hr class="border-secondary my-4">
|
| 20 |
+
|
| 21 |
+
<form action="{{ url_for('dashboard.profile') }}" method="post" enctype="multipart/form-data">
|
| 22 |
+
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
| 23 |
+
|
| 24 |
+
<label class="form-label text-accent small fw-bold">UPDATE HOLOGRAM (AVATAR)</label>
|
| 25 |
+
<div class="input-group mb-3">
|
| 26 |
+
<input type="file" name="avatar" class="form-control bg-dark border-secondary text-light" accept="image/*" required>
|
| 27 |
+
<button class="btn btn-outline-info" type="submit"><i class="bi bi-upload"></i></button>
|
| 28 |
+
</div>
|
| 29 |
+
</form>
|
| 30 |
+
|
| 31 |
+
{% if current_user.orig_profile_image and current_user.profile_image != current_user.orig_profile_image %}
|
| 32 |
+
<form action="{{ url_for('dashboard.remove_avatar') }}" method="post" class="mt-2">
|
| 33 |
+
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
| 34 |
+
<button class="btn btn-sm btn-link text-danger text-decoration-none opacity-75 hover-opacity-100">
|
| 35 |
+
<i class="bi bi-x-circle me-1"></i>Reset to Default
|
| 36 |
+
</button>
|
| 37 |
+
</form>
|
| 38 |
+
{% endif %}
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
|
| 42 |
+
<div class="col-md-6 fade-in-up" style="animation-delay: 0.1s;">
|
| 43 |
+
<div class="cyber-card p-5 h-100 border-success">
|
| 44 |
+
<h4 class="text-success mb-4"><i class="bi bi-shield-check me-2"></i>Security Clearance</h4>
|
| 45 |
+
|
| 46 |
+
<div class="d-flex align-items-center mb-4">
|
| 47 |
+
<div class="bg-success bg-opacity-10 p-3 rounded me-3">
|
| 48 |
+
<i class="bi bi-fingerprint fs-2 text-success"></i>
|
| 49 |
+
</div>
|
| 50 |
+
<div>
|
| 51 |
+
<h5 class="mb-0 text-white">Biometric Lock</h5>
|
| 52 |
+
<small class="text-muted">FIDO2 / WebAuthn Active</small>
|
| 53 |
+
</div>
|
| 54 |
+
<div class="ms-auto">
|
| 55 |
+
<span class="badge bg-success">ENABLED</span>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
|
| 59 |
+
<div class="alert alert-dark border border-secondary d-flex align-items-start">
|
| 60 |
+
<i class="bi bi-info-circle text-info me-3 mt-1"></i>
|
| 61 |
+
<div class="small text-muted">
|
| 62 |
+
Your account is secured by <strong>Device Biometrics</strong>.
|
| 63 |
+
Passwords are no longer stored on this server. Access is granted solely via physical token verification (Phone/Key).
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
{% endblock %}
|
webpass/templates/share.html
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Dead Drop{% endblock %}
|
| 3 |
+
{% block page_title %}Secure Dead Drop{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="row justify-content-center mt-4">
|
| 7 |
+
<div class="col-lg-8 fade-in-up">
|
| 8 |
+
<div class="cyber-card p-5">
|
| 9 |
+
<div class="text-center mb-4">
|
| 10 |
+
<i class="bi bi-incognito fs-1 text-danger"></i>
|
| 11 |
+
<h3 class="mt-2">Self-Destructing Messages</h3>
|
| 12 |
+
<p class="text-muted">
|
| 13 |
+
Create a one-time link. The message is encrypted in your browser, stored encrypted, and deleted immediately after viewing.
|
| 14 |
+
</p>
|
| 15 |
+
</div>
|
| 16 |
+
|
| 17 |
+
<form id="create-share-form">
|
| 18 |
+
<div class="mb-3">
|
| 19 |
+
<label class="form-label text-accent">Secret Message</label>
|
| 20 |
+
<textarea id="secret-text" class="form-control" rows="5" placeholder="Paste your password or secret key here..." required></textarea>
|
| 21 |
+
</div>
|
| 22 |
+
|
| 23 |
+
<div class="mb-4">
|
| 24 |
+
<label class="form-label text-accent">Self-Destruct View Time (Seconds)</label>
|
| 25 |
+
<div class="input-group">
|
| 26 |
+
<span class="input-group-text bg-dark border-secondary text-warning"><i class="bi bi-stopwatch"></i></span>
|
| 27 |
+
<input type="number" id="view-time" class="form-control bg-dark text-white border-secondary" placeholder="e.g. 15" min="1" max="3600" required>
|
| 28 |
+
</div>
|
| 29 |
+
<small class="text-muted mt-1 d-block">Number of seconds the receiver has to read it before it wipes from their screen.</small>
|
| 30 |
+
</div>
|
| 31 |
+
|
| 32 |
+
<button type="submit" class="btn btn-cyber w-100 py-3">
|
| 33 |
+
<i class="bi bi-link-45deg me-2"></i>Generate Secure Link
|
| 34 |
+
</button>
|
| 35 |
+
</form>
|
| 36 |
+
|
| 37 |
+
<div id="result-area" class="d-none mt-4 p-3 border border-success rounded bg-dark">
|
| 38 |
+
<label class="fw-bold text-success mb-2">Your One-Time Link:</label>
|
| 39 |
+
<div class="input-group">
|
| 40 |
+
<input type="text" id="share-link" class="form-control text-success font-monospace" readonly>
|
| 41 |
+
<button class="btn btn-outline-success" onclick="copyLink()">Copy</button>
|
| 42 |
+
</div>
|
| 43 |
+
<div class="mt-2 text-danger small">
|
| 44 |
+
<i class="bi bi-exclamation-triangle-fill"></i> Warning: If you close this tab, the link is lost forever.
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
{% endblock %}
|
| 51 |
+
|
| 52 |
+
{% block scripts %}
|
| 53 |
+
<script src="{{ url_for('static', filename='js/zk_crypto.js') }}"></script>
|
| 54 |
+
<script>
|
| 55 |
+
document.getElementById("create-share-form").addEventListener("submit", async (e) => {
|
| 56 |
+
e.preventDefault();
|
| 57 |
+
|
| 58 |
+
const password = Math.random().toString(36).slice(-10) + Math.random().toString(36).slice(-10);
|
| 59 |
+
const text = document.getElementById("secret-text").value;
|
| 60 |
+
const viewTime = document.getElementById("view-time").value;
|
| 61 |
+
|
| 62 |
+
const enc = new TextEncoder();
|
| 63 |
+
const salt = ZKCrypto.randomBytes(16);
|
| 64 |
+
const iv = ZKCrypto.randomBytes(12);
|
| 65 |
+
const key = await ZKCrypto.deriveKey(password, salt);
|
| 66 |
+
|
| 67 |
+
const encryptedContent = await window.crypto.subtle.encrypt(
|
| 68 |
+
{ name: "AES-GCM", iv: iv },
|
| 69 |
+
key,
|
| 70 |
+
enc.encode(text)
|
| 71 |
+
);
|
| 72 |
+
|
| 73 |
+
const b64Data = btoa(String.fromCharCode(...new Uint8Array(encryptedContent)));
|
| 74 |
+
const b64Salt = btoa(String.fromCharCode(...salt));
|
| 75 |
+
const b64Iv = btoa(String.fromCharCode(...iv));
|
| 76 |
+
|
| 77 |
+
const res = await fetch("/share/create", {
|
| 78 |
+
method: "POST",
|
| 79 |
+
headers: { "Content-Type": "application/json" },
|
| 80 |
+
body: JSON.stringify({
|
| 81 |
+
ciphertext: b64Data,
|
| 82 |
+
iv: b64Iv,
|
| 83 |
+
salt: b64Salt,
|
| 84 |
+
ttl: 60, // Link valid for 1 hour by default before it expires
|
| 85 |
+
view_time: viewTime
|
| 86 |
+
})
|
| 87 |
+
});
|
| 88 |
+
|
| 89 |
+
const data = await res.json();
|
| 90 |
+
|
| 91 |
+
let finalUrl = "";
|
| 92 |
+
if (data.link) {
|
| 93 |
+
finalUrl = `${data.link}#${password}`;
|
| 94 |
+
} else {
|
| 95 |
+
finalUrl = `${window.location.origin}/share/v/${data.id}#${password}`;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
document.getElementById("share-link").value = finalUrl;
|
| 99 |
+
document.getElementById("result-area").classList.remove("d-none");
|
| 100 |
+
});
|
| 101 |
+
|
| 102 |
+
function copyLink() {
|
| 103 |
+
const copyText = document.getElementById("share-link");
|
| 104 |
+
copyText.select();
|
| 105 |
+
document.execCommand("copy");
|
| 106 |
+
alert("Link copied!");
|
| 107 |
+
}
|
| 108 |
+
</script>
|
| 109 |
+
{% endblock %}
|
webpass/templates/share_error.html
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Link Expired{% endblock %}
|
| 3 |
+
{% block page_title %}Dead Drop Protocol{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="row justify-content-center mt-5">
|
| 7 |
+
<div class="col-md-6 fade-in-up">
|
| 8 |
+
<div class="cyber-card p-5 text-center border-danger">
|
| 9 |
+
<div class="mb-4">
|
| 10 |
+
<i class="bi bi-x-octagon-fill text-danger" style="font-size: 4rem;"></i>
|
| 11 |
+
</div>
|
| 12 |
+
|
| 13 |
+
<h3 class="text-white mb-3">Link Terminated</h3>
|
| 14 |
+
<p class="text-muted fs-5 mb-4">
|
| 15 |
+
{{ message }}
|
| 16 |
+
</p>
|
| 17 |
+
|
| 18 |
+
<div class="alert alert-danger bg-opacity-10 border border-danger text-danger mb-4">
|
| 19 |
+
<i class="bi bi-shield-x me-2"></i>
|
| 20 |
+
This data has been permanently incinerated from the server.
|
| 21 |
+
</div>
|
| 22 |
+
|
| 23 |
+
<div class="d-grid gap-3">
|
| 24 |
+
<a href="{{ url_for('share.share_ui') }}" class="btn btn-cyber">
|
| 25 |
+
<i class="bi bi-plus-circle me-2"></i>Create New Drop
|
| 26 |
+
</a>
|
| 27 |
+
<a href="{{ url_for('dashboard.dashboard') }}" class="btn btn-outline-secondary">
|
| 28 |
+
Back to Dashboard
|
| 29 |
+
</a>
|
| 30 |
+
</div>
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
</div>
|
| 34 |
+
{% endblock %}
|
webpass/templates/share_view.html
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Secure Message{% endblock %}
|
| 3 |
+
{% block page_title %}Dead Drop Protocol{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="row justify-content-center mt-5">
|
| 7 |
+
<div class="col-lg-6 fade-in-up">
|
| 8 |
+
<div class="cyber-card p-5 text-center">
|
| 9 |
+
|
| 10 |
+
<div id="locked-state">
|
| 11 |
+
<div class="mb-4 position-relative d-inline-block">
|
| 12 |
+
<i class="bi bi-file-earmark-lock2-fill text-warning" style="font-size: 4rem;"></i>
|
| 13 |
+
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
|
| 14 |
+
1 View Left
|
| 15 |
+
</span>
|
| 16 |
+
</div>
|
| 17 |
+
|
| 18 |
+
<h3 class="text-white mb-3">Encrypted Transmission</h3>
|
| 19 |
+
<p class="text-muted mb-4">
|
| 20 |
+
This message is stored in a Zero-Knowledge Vault.
|
| 21 |
+
Once you reveal it, it will be <strong>permanently deleted</strong> from the server and a self-destruct timer will begin.
|
| 22 |
+
</p>
|
| 23 |
+
|
| 24 |
+
<button id="reveal-btn" class="btn btn-warning w-100 py-3 fw-bold">
|
| 25 |
+
<i class="bi bi-eye-fill me-2"></i>Reveal Secret Message
|
| 26 |
+
</button>
|
| 27 |
+
</div>
|
| 28 |
+
|
| 29 |
+
<div id="unlocked-state" class="d-none text-start">
|
| 30 |
+
<div class="d-flex align-items-center mb-3">
|
| 31 |
+
<i class="bi bi-unlock-fill text-success fs-2 me-3"></i>
|
| 32 |
+
<h4 class="m-0 text-success">Decryption Successful</h4>
|
| 33 |
+
</div>
|
| 34 |
+
|
| 35 |
+
<div class="alert alert-warning border-warning bg-warning bg-opacity-10 d-flex justify-content-between align-items-center mb-4">
|
| 36 |
+
<div class="text-warning">
|
| 37 |
+
<i class="bi bi-stopwatch fs-4 me-2"></i> <strong>Self-Destructing in:</strong>
|
| 38 |
+
</div>
|
| 39 |
+
<div id="countdown-timer" class="fs-1 fw-bold text-danger font-monospace">--</div>
|
| 40 |
+
</div>
|
| 41 |
+
|
| 42 |
+
<label class="small text-muted text-uppercase fw-bold">Message Contents:</label>
|
| 43 |
+
<div class="p-4 bg-dark border border-success rounded mt-2 position-relative">
|
| 44 |
+
<code id="secret-content" class="text-white fs-5" style="word-break: break-all;"></code>
|
| 45 |
+
<button class="btn btn-sm btn-outline-success position-absolute top-0 end-0 m-2" onclick="copySecret()">
|
| 46 |
+
Copy
|
| 47 |
+
</button>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
|
| 51 |
+
<div id="error-state" class="d-none">
|
| 52 |
+
<i class="bi bi-x-octagon-fill text-danger mb-3" style="font-size: 4rem;"></i>
|
| 53 |
+
<h3 class="text-danger">Access Denied</h3>
|
| 54 |
+
<p class="text-muted" id="error-msg"></p>
|
| 55 |
+
<a href="{{ url_for('share.share_ui') }}" class="btn btn-outline-light mt-3">Create New Drop</a>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
|
| 61 |
+
<script src="{{ url_for('static', filename='js/zk_crypto.js') }}"></script>
|
| 62 |
+
<script>
|
| 63 |
+
function copySecret() {
|
| 64 |
+
const text = document.getElementById("secret-content").innerText;
|
| 65 |
+
navigator.clipboard.writeText(text);
|
| 66 |
+
alert("Copied to clipboard!");
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
document.getElementById("reveal-btn").addEventListener("click", async () => {
|
| 70 |
+
const btn = document.getElementById("reveal-btn");
|
| 71 |
+
btn.disabled = true;
|
| 72 |
+
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Decrypting...';
|
| 73 |
+
|
| 74 |
+
try {
|
| 75 |
+
const keyFromHash = window.location.hash.substring(1);
|
| 76 |
+
if (!keyFromHash) throw new Error("Decryption key missing. The link is incomplete.");
|
| 77 |
+
|
| 78 |
+
const dropId = "{{ drop_id }}";
|
| 79 |
+
const res = await fetch(`/api/share/${dropId}`, { method: "POST" });
|
| 80 |
+
|
| 81 |
+
if (res.status === 410 || res.status === 404) {
|
| 82 |
+
throw new Error("This message has expired or has already been viewed.");
|
| 83 |
+
}
|
| 84 |
+
if (!res.ok) throw new Error("Server communication error.");
|
| 85 |
+
|
| 86 |
+
const data = await res.json();
|
| 87 |
+
|
| 88 |
+
const key = await ZKCrypto.deriveKey(keyFromHash, Uint8Array.from(atob(data.salt), c => c.charCodeAt(0)));
|
| 89 |
+
const iv = Uint8Array.from(atob(data.iv), c => c.charCodeAt(0));
|
| 90 |
+
const encrypted = Uint8Array.from(atob(data.ciphertext), c => c.charCodeAt(0));
|
| 91 |
+
|
| 92 |
+
const decryptedBuf = await window.crypto.subtle.decrypt(
|
| 93 |
+
{ name: "AES-GCM", iv: iv },
|
| 94 |
+
key,
|
| 95 |
+
encrypted
|
| 96 |
+
);
|
| 97 |
+
|
| 98 |
+
const dec = new TextDecoder();
|
| 99 |
+
const plainText = dec.decode(decryptedBuf);
|
| 100 |
+
|
| 101 |
+
document.getElementById("locked-state").classList.add("d-none");
|
| 102 |
+
document.getElementById("secret-content").textContent = plainText;
|
| 103 |
+
document.getElementById("unlocked-state").classList.remove("d-none");
|
| 104 |
+
|
| 105 |
+
// --- START COUNTDOWN TIMER ---
|
| 106 |
+
let timeLeft = data.view_time || 30; // Default fallback
|
| 107 |
+
const timerDisplay = document.getElementById("countdown-timer");
|
| 108 |
+
timerDisplay.textContent = timeLeft + "s";
|
| 109 |
+
|
| 110 |
+
const timerInterval = setInterval(() => {
|
| 111 |
+
timeLeft--;
|
| 112 |
+
timerDisplay.textContent = timeLeft + "s";
|
| 113 |
+
|
| 114 |
+
if (timeLeft <= 0) {
|
| 115 |
+
clearInterval(timerInterval);
|
| 116 |
+
// Timer hit 0. Force page reload.
|
| 117 |
+
// Because data is burned on server, reload will show 'Link Terminated'.
|
| 118 |
+
window.location.reload(true);
|
| 119 |
+
}
|
| 120 |
+
}, 1000);
|
| 121 |
+
|
| 122 |
+
} catch (err) {
|
| 123 |
+
document.getElementById("locked-state").classList.add("d-none");
|
| 124 |
+
document.getElementById("error-msg").textContent = err.message;
|
| 125 |
+
document.getElementById("error-state").classList.remove("d-none");
|
| 126 |
+
}
|
| 127 |
+
});
|
| 128 |
+
</script>
|
| 129 |
+
{% endblock %}
|
webpass/templates/stego.html
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Universal Steganography{% endblock %}
|
| 3 |
+
{% block page_title %}Covert Ops Station{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="row g-4 mt-2">
|
| 7 |
+
<div class="col-md-6 fade-in-up">
|
| 8 |
+
<div class="cyber-card p-4 h-100 border-info">
|
| 9 |
+
<h3 class="text-info mb-3"><i class="bi bi-eye-slash-fill me-2"></i>Hide Data</h3>
|
| 10 |
+
<p class="text-muted small">
|
| 11 |
+
Embed secure data into any Image file.
|
| 12 |
+
</p>
|
| 13 |
+
|
| 14 |
+
<ul class="nav nav-tabs border-secondary mb-3" id="hideTabs" role="tablist">
|
| 15 |
+
<li class="nav-item" role="presentation">
|
| 16 |
+
<button class="nav-link active text-info bg-dark border-secondary" id="text-tab" data-bs-toggle="tab" data-bs-target="#text-pane" type="button" onclick="setMode('text')">Text</button>
|
| 17 |
+
</li>
|
| 18 |
+
<li class="nav-item" role="presentation">
|
| 19 |
+
<button class="nav-link text-info bg-dark border-secondary" id="file-tab" data-bs-toggle="tab" data-bs-target="#file-pane" type="button" onclick="setMode('file')">File</button>
|
| 20 |
+
</li>
|
| 21 |
+
</ul>
|
| 22 |
+
|
| 23 |
+
<form action="{{ url_for('stego.hide') }}" method="POST" enctype="multipart/form-data">
|
| 24 |
+
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
| 25 |
+
<input type="hidden" name="mode" id="stego-mode" value="text">
|
| 26 |
+
|
| 27 |
+
<div class="mb-3">
|
| 28 |
+
<label class="form-label text-accent">1. Cover Image (Container)</label>
|
| 29 |
+
<input type="file" name="cover_image" class="form-control" accept=".png, .jpg, .jpeg" required>
|
| 30 |
+
</div>
|
| 31 |
+
|
| 32 |
+
<div class="tab-content mb-3" id="hideTabsContent">
|
| 33 |
+
<div class="tab-pane fade show active" id="text-pane">
|
| 34 |
+
<label class="form-label text-accent">2. Secret Text</label>
|
| 35 |
+
<textarea name="secret_text" class="form-control" rows="3" placeholder="Enter sensitive data..."></textarea>
|
| 36 |
+
</div>
|
| 37 |
+
<div class="tab-pane fade" id="file-pane">
|
| 38 |
+
<label class="form-label text-accent">2. Secret File</label>
|
| 39 |
+
<input type="file" name="secret_file" class="form-control">
|
| 40 |
+
<div class="form-text text-warning"><i class="bi bi-exclamation-triangle"></i> Large files require very large cover images!</div>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
<div class="mb-3">
|
| 45 |
+
<label class="form-label text-accent">3. Password</label>
|
| 46 |
+
<input type="password" name="stego_password" class="form-control watchtower-monitor" placeholder="Required for encryption" required>
|
| 47 |
+
</div>
|
| 48 |
+
|
| 49 |
+
<button type="submit" class="btn btn-cyber w-100">
|
| 50 |
+
<i class="bi bi-magic me-2"></i>Encrypt & Embed
|
| 51 |
+
</button>
|
| 52 |
+
</form>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
|
| 56 |
+
<div class="col-md-6 fade-in-up" style="animation-delay: 0.2s;">
|
| 57 |
+
<div class="cyber-card p-4 h-100 border-warning">
|
| 58 |
+
<h3 class="text-warning mb-3"><i class="bi bi-search me-2"></i>Reveal Data</h3>
|
| 59 |
+
<p class="text-muted small">
|
| 60 |
+
Extracts hidden text OR files automatically.
|
| 61 |
+
</p>
|
| 62 |
+
|
| 63 |
+
<form action="{{ url_for('stego.reveal') }}" method="POST" enctype="multipart/form-data">
|
| 64 |
+
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
| 65 |
+
|
| 66 |
+
<div class="mb-3">
|
| 67 |
+
<label class="form-label text-warning">Stego Image (.png)</label>
|
| 68 |
+
<input type="file" name="stego_image" class="form-control" accept=".png" required>
|
| 69 |
+
</div>
|
| 70 |
+
|
| 71 |
+
<div class="mb-3">
|
| 72 |
+
<label class="form-label text-warning">Password</label>
|
| 73 |
+
<input type="password" name="stego_password" class="form-control" placeholder="Enter original password" required>
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
<button type="submit" class="btn btn-outline-warning w-100">
|
| 77 |
+
<i class="bi bi-eye me-2"></i>Scan & Decrypt
|
| 78 |
+
</button>
|
| 79 |
+
</form>
|
| 80 |
+
|
| 81 |
+
{% if revealed_secret %}
|
| 82 |
+
<div class="mt-4 p-3 bg-dark border border-success rounded position-relative fade-in-up">
|
| 83 |
+
<label class="small text-muted text-uppercase mb-1">Decrypted Payload:</label>
|
| 84 |
+
<code class="text-success d-block fs-5" style="word-break: break-all;">{{ revealed_secret }}</code>
|
| 85 |
+
</div>
|
| 86 |
+
{% endif %}
|
| 87 |
+
</div>
|
| 88 |
+
</div>
|
| 89 |
+
</div>
|
| 90 |
+
{% endblock %}
|
| 91 |
+
|
| 92 |
+
{% block scripts %}
|
| 93 |
+
<script>
|
| 94 |
+
function setMode(mode) {
|
| 95 |
+
document.getElementById('stego-mode').value = mode;
|
| 96 |
+
}
|
| 97 |
+
</script>
|
| 98 |
+
<script src="{{ url_for('static', filename='js/watchtower.js') }}"></script>
|
| 99 |
+
{% endblock %}
|
webpass/templates/vault.html
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Vault{% endblock %}
|
| 3 |
+
{% block page_title %}Zero-Knowledge Vault{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="row g-4 mt-2">
|
| 7 |
+
<div class="col-md-6 fade-in-up">
|
| 8 |
+
<div class="cyber-card p-4 h-100">
|
| 9 |
+
<h3 class="text-info"><i class="bi bi-lock-fill"></i> Encrypt</h3>
|
| 10 |
+
<p class="text-muted">Encrypts files locally using AES-GCM-256. The server never sees the key.</p>
|
| 11 |
+
<form id="zk-encrypt-form" class="mt-4">
|
| 12 |
+
<input type="file" id="enc-file" class="form-control mb-3" required>
|
| 13 |
+
<input type="password" id="enc-pass" class="form-control mb-3" placeholder="Set a unique password" required>
|
| 14 |
+
<button type="submit" class="btn btn-cyber w-100" id="btn-encrypt">Encrypt & Download</button>
|
| 15 |
+
</form>
|
| 16 |
+
<div id="enc-status" class="mt-3"></div>
|
| 17 |
+
</div>
|
| 18 |
+
</div>
|
| 19 |
+
|
| 20 |
+
<div class="col-md-6 fade-in-up" style="animation-delay: 0.2s;">
|
| 21 |
+
<div class="cyber-card p-4 h-100" style="border-color: rgba(255, 193, 7, 0.3);">
|
| 22 |
+
<h3 class="text-warning"><i class="bi bi-unlock-fill"></i> Decrypt</h3>
|
| 23 |
+
<p class="text-muted">Decrypts .enc files created by this vault.</p>
|
| 24 |
+
<form id="zk-decrypt-form" class="mt-4">
|
| 25 |
+
<input type="file" id="dec-file" class="form-control mb-3" accept=".enc" required>
|
| 26 |
+
<input type="password" id="dec-pass" class="form-control mb-3" placeholder="Enter the password" required>
|
| 27 |
+
<button type="submit" class="btn btn-outline-warning w-100" id="btn-decrypt">Decrypt & Restore</button>
|
| 28 |
+
</form>
|
| 29 |
+
<div id="dec-status" class="mt-3"></div>
|
| 30 |
+
</div>
|
| 31 |
+
</div>
|
| 32 |
+
{% endblock %}
|
| 33 |
+
|
| 34 |
+
{% block scripts %}
|
| 35 |
+
<script src="{{ url_for('static', filename='js/zk_crypto.js') }}"></script>
|
| 36 |
+
<script>
|
| 37 |
+
document.getElementById("zk-encrypt-form").addEventListener("submit", async (e) => {
|
| 38 |
+
e.preventDefault();
|
| 39 |
+
const btn = document.getElementById("btn-encrypt");
|
| 40 |
+
const status = document.getElementById("enc-status");
|
| 41 |
+
|
| 42 |
+
try {
|
| 43 |
+
btn.disabled = true;
|
| 44 |
+
btn.textContent = "Processing...";
|
| 45 |
+
status.innerHTML = '<span class="text-info"><i class="spinner-border spinner-border-sm"></i> Encrypting...</span>';
|
| 46 |
+
|
| 47 |
+
const file = document.getElementById("enc-file").files[0];
|
| 48 |
+
const pass = document.getElementById("enc-pass").value;
|
| 49 |
+
|
| 50 |
+
// 1. Get raw encrypted data
|
| 51 |
+
const rawEncryptedData = await ZKCrypto.encryptFile(file, pass, "000");
|
| 52 |
+
|
| 53 |
+
// 2. THE FIX: Wrap it in a Blob
|
| 54 |
+
const blob = new Blob([rawEncryptedData], { type: "application/octet-stream" });
|
| 55 |
+
|
| 56 |
+
// 3. Create download link safely
|
| 57 |
+
const url = URL.createObjectURL(blob);
|
| 58 |
+
const a = document.createElement("a");
|
| 59 |
+
a.href = url;
|
| 60 |
+
a.download = file.name + ".enc"; // Changed to .enc to differentiate from standard zip
|
| 61 |
+
document.body.appendChild(a);
|
| 62 |
+
a.click();
|
| 63 |
+
document.body.removeChild(a);
|
| 64 |
+
URL.revokeObjectURL(url);
|
| 65 |
+
|
| 66 |
+
status.innerHTML = '<span class="text-success fw-bold"><i class="bi bi-check-circle"></i> Success! Download started.</span>';
|
| 67 |
+
} catch (err) {
|
| 68 |
+
status.innerHTML = `<span class="text-danger"><i class="bi bi-exclamation-triangle"></i> Error: ${err.message}</span>`;
|
| 69 |
+
} finally {
|
| 70 |
+
btn.disabled = false;
|
| 71 |
+
btn.textContent = "Encrypt & Download";
|
| 72 |
+
}
|
| 73 |
+
});
|
| 74 |
+
|
| 75 |
+
document.getElementById("zk-decrypt-form").addEventListener("submit", async (e) => {
|
| 76 |
+
e.preventDefault();
|
| 77 |
+
const btn = document.getElementById("btn-decrypt");
|
| 78 |
+
const status = document.getElementById("dec-status");
|
| 79 |
+
|
| 80 |
+
try {
|
| 81 |
+
btn.disabled = true;
|
| 82 |
+
btn.textContent = "Processing...";
|
| 83 |
+
status.innerHTML = '<span class="text-warning"><i class="spinner-border spinner-border-sm"></i> Decrypting...</span>';
|
| 84 |
+
|
| 85 |
+
const file = document.getElementById("dec-file").files[0];
|
| 86 |
+
const pass = document.getElementById("dec-pass").value;
|
| 87 |
+
|
| 88 |
+
// 1. Decrypt to get the raw data
|
| 89 |
+
const result = await ZKCrypto.decryptFile(file, pass);
|
| 90 |
+
|
| 91 |
+
// 2. THE FIX: Wrap decrypted data in a Blob
|
| 92 |
+
const blob = new Blob([result.data], { type: "application/octet-stream" });
|
| 93 |
+
|
| 94 |
+
// 3. Create download link safely
|
| 95 |
+
const url = URL.createObjectURL(blob);
|
| 96 |
+
const a = document.createElement("a");
|
| 97 |
+
a.href = url;
|
| 98 |
+
a.download = result.name || "decrypted_file";
|
| 99 |
+
document.body.appendChild(a);
|
| 100 |
+
a.click();
|
| 101 |
+
document.body.removeChild(a);
|
| 102 |
+
URL.revokeObjectURL(url);
|
| 103 |
+
|
| 104 |
+
status.innerHTML = '<span class="text-success fw-bold"><i class="bi bi-check-circle"></i> Success! File restored.</span>';
|
| 105 |
+
} catch (err) {
|
| 106 |
+
console.error(err);
|
| 107 |
+
status.innerHTML = '<span class="text-danger"><i class="bi bi-exclamation-triangle"></i> Decryption Failed. Wrong password or file.</span>';
|
| 108 |
+
} finally {
|
| 109 |
+
btn.disabled = false;
|
| 110 |
+
btn.textContent = "Decrypt & Restore";
|
| 111 |
+
}
|
| 112 |
+
});
|
| 113 |
+
</script>
|
| 114 |
+
{% endblock %}
|
webpass/templates/verify_otp.html
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Verify Identity{% endblock %}
|
| 3 |
+
{% block page_title %}Security Checkpoint{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="row justify-content-center mt-5">
|
| 7 |
+
<div class="col-md-5 fade-in-up">
|
| 8 |
+
<div class="cyber-card p-5 border-primary">
|
| 9 |
+
<div class="text-center mb-4">
|
| 10 |
+
<i class="bi bi-shield-lock-fill text-primary" style="font-size: 3rem;"></i>
|
| 11 |
+
<h3 class="mt-3">Two-Factor Auth</h3>
|
| 12 |
+
<p class="text-muted">
|
| 13 |
+
An OTP has been sent to your email for <span class="text-white">{{ feature }}</span>.
|
| 14 |
+
</p>
|
| 15 |
+
</div>
|
| 16 |
+
|
| 17 |
+
<form method="POST">
|
| 18 |
+
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
| 19 |
+
<div class="mb-4">
|
| 20 |
+
<label class="form-label text-accent small text-uppercase fw-bold">Enter One-Time Code</label>
|
| 21 |
+
<input type="text"
|
| 22 |
+
name="otp"
|
| 23 |
+
class="form-control form-control-lg text-center font-monospace fs-2 border-primary"
|
| 24 |
+
placeholder="000000"
|
| 25 |
+
maxlength="6"
|
| 26 |
+
pattern="\d{6}"
|
| 27 |
+
autofocus
|
| 28 |
+
required>
|
| 29 |
+
</div>
|
| 30 |
+
|
| 31 |
+
<div class="d-grid gap-2">
|
| 32 |
+
<button type="submit" class="btn btn-cyber py-3">
|
| 33 |
+
<i class="bi bi-fingerprint me-2"></i>Verify Identity
|
| 34 |
+
</button>
|
| 35 |
+
<a href="{{ url_for('dashboard.dashboard') }}" class="btn btn-outline-secondary btn-sm mt-2">
|
| 36 |
+
Cancel Action
|
| 37 |
+
</a>
|
| 38 |
+
</div>
|
| 39 |
+
</form>
|
| 40 |
+
</div>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
{% endblock %}
|
wsgi.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
from collections import deque
|
| 4 |
+
# REMOVED: ProxyFix (We don't need it for Localhost login)
|
| 5 |
+
|
| 6 |
+
# Allow OAuth over HTTP
|
| 7 |
+
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
|
| 8 |
+
|
| 9 |
+
from webpass import create_app, socketio
|
| 10 |
+
from webpass.network_monitor import start_packet_capture
|
| 11 |
+
from scapy.all import conf
|
| 12 |
+
|
| 13 |
+
app = create_app()
|
| 14 |
+
|
| 15 |
+
# REMOVED: app.wsgi_app = ProxyFix(...)
|
| 16 |
+
# We are back to standard local running.
|
| 17 |
+
|
| 18 |
+
app.captured_packets = deque(maxlen=1000)
|
| 19 |
+
|
| 20 |
+
@socketio.on('connect')
|
| 21 |
+
def handle_connect():
|
| 22 |
+
print(" [+] Client connected to Live Feed")
|
| 23 |
+
|
| 24 |
+
def get_best_interface():
|
| 25 |
+
try:
|
| 26 |
+
iface = conf.iface
|
| 27 |
+
print(f" [*] Auto-detected best interface: {iface}")
|
| 28 |
+
return iface
|
| 29 |
+
except Exception as e:
|
| 30 |
+
print(f" [!] Error detecting interface: {e}")
|
| 31 |
+
return None
|
| 32 |
+
|
| 33 |
+
if __name__ == '__main__':
|
| 34 |
+
print("--- WEBPASS SECURITY SERVER STARTING ---")
|
| 35 |
+
target_interface = get_best_interface()
|
| 36 |
+
|
| 37 |
+
if target_interface:
|
| 38 |
+
print(f" [+] Launching Packet Sniffer on: {target_interface}")
|
| 39 |
+
start_packet_capture(app, socketio, interface=target_interface)
|
| 40 |
+
|
| 41 |
+
print(" [+] Server running on http://127.0.0.1:5000")
|
| 42 |
+
print("--------------------------------------------")
|
| 43 |
+
|
| 44 |
+
socketio.run(app, host='127.0.0.1', port=5000, debug=True, use_reloader=False)
|