ag235772 commited on
Commit
136c0f7
·
0 Parent(s):

Initial Release: WebPass V2 with Steganography, Crypto Vault, and Cloud Toggle

Browse files
Files changed (47) hide show
  1. .gitignore +7 -0
  2. Dockerfile +20 -0
  3. requirements.txt +0 -0
  4. test.py +25 -0
  5. webpass/__init__.py +125 -0
  6. webpass/config.py +40 -0
  7. webpass/crypto_utils.py +209 -0
  8. webpass/models.py +38 -0
  9. webpass/network_monitor.py +57 -0
  10. webpass/routes/__init__.py +0 -0
  11. webpass/routes/_decorators.py +34 -0
  12. webpass/routes/api.py +56 -0
  13. webpass/routes/auth.py +67 -0
  14. webpass/routes/bio_auth.py +166 -0
  15. webpass/routes/dashboard.py +269 -0
  16. webpass/routes/otp.py +94 -0
  17. webpass/routes/share.py +101 -0
  18. webpass/routes/stego.py +114 -0
  19. webpass/routes/tools.py +184 -0
  20. webpass/security_utils.py +61 -0
  21. webpass/static/css/login.css +115 -0
  22. webpass/static/css/modern.css +130 -0
  23. webpass/static/js/dashboard.js +176 -0
  24. webpass/static/js/file_tools.js +81 -0
  25. webpass/static/js/flash_modal.js +27 -0
  26. webpass/static/js/network.js +157 -0
  27. webpass/static/js/watchtower.js +77 -0
  28. webpass/static/js/zk_crypto.js +91 -0
  29. webpass/stego_utils.py +165 -0
  30. webpass/templates/base.html +128 -0
  31. webpass/templates/bio_lock.html +59 -0
  32. webpass/templates/bio_mobile.html +211 -0
  33. webpass/templates/breach.html +91 -0
  34. webpass/templates/dashboard.html +191 -0
  35. webpass/templates/index.html +26 -0
  36. webpass/templates/login.html +24 -0
  37. webpass/templates/metadata.html +143 -0
  38. webpass/templates/network.html +96 -0
  39. webpass/templates/network_demo.html +27 -0
  40. webpass/templates/profile.html +69 -0
  41. webpass/templates/share.html +109 -0
  42. webpass/templates/share_error.html +34 -0
  43. webpass/templates/share_view.html +129 -0
  44. webpass/templates/stego.html +99 -0
  45. webpass/templates/vault.html +114 -0
  46. webpass/templates/verify_otp.html +43 -0
  47. 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)