diff --git a/README.md b/README.md index 4a98056..71eda50 100644 --- a/README.md +++ b/README.md @@ -1,177 +1,177 @@ -# PacCrypt WebApp - -**PacCrypt** is a secure, feature-rich web app for encrypting and decrypting text and files โ€” built with Flask, JavaScript, and AES-GCM encryption. -Now with an admin control panel, GitHub updater, and a built-in Pac-Man easter egg! ๐Ÿ•น๏ธ - -Live demo: [paccrypt.unnaturalll.dev](http://paccrypt.unnaturalll.dev) - ---- - -## โœจ Features - -- ๐Ÿ”’ Basic and Advanced Encryption for Text & Files -- ๐Ÿ“ Secure File Uploads with Pickup Passwords -- ๐Ÿ”‘ Random Password Generator -- ๐ŸŽฎ Hidden Pac-Man Game โ€” type `pacman` to play -- ๐Ÿง  Smart UI: Auto-switches input sections, toggles encryption labels -- ๐Ÿ“‹ Clipboard Copy Feedback with styled status boxes -- ๐Ÿงพ Admin Panel: - - Site map with live route list - - Server restart & GitHub update button - - Secure admin credential management - - Server logs & upload cleanup -- ๐Ÿงฉ System Settings Page for upload config -- ๐Ÿ“œ Custom 403, 404, and 500 Error Pages -- ๐Ÿค– robots.txt and /sitemap for crawlers -- ๐Ÿ“ฑ Mobile-Responsive UI - ---- - -## ๐Ÿ‘จโ€๐Ÿ’ป Installation - -### ๐Ÿ“‹ Prerequisites - -- Python 3.7+ -- Flask 3+ -- Cryptography 42+ -- Waitress 2.1+ -- Git (for update feature) -- Nginx (recommended) - ---- - -### โšก Quick Setup - -```bash -git clone https://github.com/TySP-Dev/PacCrypt.git -cd paccrypt-webapp-final -python -m venv venv -source venv/bin/activate # or venv\Scripts\activate on Windows -pip install -r requirements.txt -``` - -Then run: - -- Development Mode: - ```bash - ./start_dev.sh # or start_dev.bat - ``` - -- Production Mode: - ```bash - ./start_prod.sh # or start_prod.bat - ``` - -Visit [http://127.0.0.1:5000](http://127.0.0.1:5000) - ---- - -## ๐Ÿงญ Navigation & Usage - -### ๐Ÿ” Encrypt & Decrypt - -- Choose between Basic Cipher or Advanced AES -- Type your message or upload a file -- Enter password (if AES) -- Select mode using toggle (Encrypt/Decrypt) -- Hit Execute - -### ๐Ÿ“ค Share Files - -- Upload a file with two passwords: - - Encryption password - - Pickup password -- Get a shareable URL and click ๐Ÿ“‹ Copy Link - -### ๐Ÿ”‘ Generate Passwords - -- Click Generate -- Then hit ๐Ÿ“‹ Copy - -### ๐ŸŽฎ Pac-Man Game - -- Type `pacman` in the input box -- Game appears with Restart/Exit controls -- Classic arrow key controls ๐Ÿ•น๏ธ - ---- - -## ๐Ÿ› ๏ธ Admin Panel - -Visit `/adminpage` after setting up credentials at `/admin-setup`. - -Features: -- ๐Ÿ”„ Restart server -- ๐Ÿ”ƒ Update from GitHub (git pull) -- ๐Ÿงฝ Clear uploads -- ๐Ÿ” Change admin password -- ๐Ÿ“ View logs -- โš™๏ธ Adjust upload settings - ---- - -## ๐Ÿ›ก๏ธ Deployment Tips - -Minimal Nginx config: - -```nginx -server { - listen 80; - server_name yourdomain.com; - - location / { - proxy_pass http://127.0.0.1:5000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } -} -``` - -Use Let's Encrypt to add SSL/TLS support. - ---- - -## ๐Ÿ—‚๏ธ Project Structure - -``` -paccrypt-webapp-final/ -โ”œโ”€โ”€ app.py -โ”œโ”€โ”€ requirements.txt -โ”œโ”€โ”€ README.md -โ”œโ”€โ”€ templates/ -โ”‚ โ”œโ”€โ”€ index.html -โ”‚ โ”œโ”€โ”€ 404.html -โ”‚ โ””โ”€โ”€ 403.html -โ”‚ โ””โ”€โ”€ 500.html -โ”‚ โ””โ”€โ”€ admin.html -โ”‚ โ””โ”€โ”€ admin_login.html -โ”‚ โ””โ”€โ”€ admin_settings.html -โ”‚ โ””โ”€โ”€ admin_setup.html -โ”‚ โ””โ”€โ”€ pickup.html -โ”œโ”€โ”€ static/ -โ”‚ โ”œโ”€โ”€ css/ -โ”‚ โ”‚ โ””โ”€โ”€ styles.css -โ”‚ โ”œโ”€โ”€ js/ -โ”‚ โ”‚ โ””โ”€โ”€ ui.js -โ”‚ โ”‚ โ””โ”€โ”€ pacman.js -โ”‚ โ”‚ โ””โ”€โ”€ main.js -โ”‚ โ”‚ โ””โ”€โ”€ fileops.js -โ”‚ โ”‚ โ””โ”€โ”€ encryption.js -โ”‚ โ”œโ”€โ”€ img/ -โ”‚ โ”‚ โ””โ”€โ”€ PacCrypt.png -โ”‚ โ”‚ โ””โ”€โ”€ Github_logo.png -โ”‚ โ”‚ โ””โ”€โ”€ sitemap.png -โ”‚ โ””โ”€โ”€ audio/ -โ”‚ โ””โ”€โ”€ chomp.mp3 -โ”œโ”€โ”€ start_dev.bat -โ”œโ”€โ”€ start_prod.bat -โ”œโ”€โ”€ start_dev.sh -โ”œโ”€โ”€ start_prod.sh -``` - ---- - -## ๐Ÿ“„ License - -MIT ยฉ [TySP-Dev](https://github.com/TySP-Dev) +# PacCrypt WebApp + +**PacCrypt** is a secure, feature-rich web app for encrypting and decrypting text and files โ€” built with Flask, JavaScript, and AES-GCM encryption. +Now with an admin control panel, GitHub updater, and a built-in Pac-Man easter egg! ๐Ÿ•น๏ธ + +Live demo: [paccrypt.unnaturalll.dev](http://paccrypt.unnaturalll.dev) + +--- + +## โœจ Features + +- ๐Ÿ”’ Basic and Advanced Encryption for Text & Files +- ๐Ÿ“ Secure File Uploads with Pickup Passwords +- ๐Ÿ”‘ Random Password Generator +- ๐ŸŽฎ Hidden Pac-Man Game โ€” type `pacman` to play +- ๐Ÿง  Smart UI: Auto-switches input sections, toggles encryption labels +- ๐Ÿ“‹ Clipboard Copy Feedback with styled status boxes +- ๐Ÿงพ Admin Panel: + - Site map with live route list + - Server restart & GitHub update button + - Secure admin credential management + - Server logs & upload cleanup +- ๐Ÿงฉ System Settings Page for upload config +- ๐Ÿ“œ Custom 403, 404, and 500 Error Pages +- ๐Ÿค– robots.txt and /sitemap for crawlers +- ๐Ÿ“ฑ Mobile-Responsive UI + +--- + +## ๐Ÿ‘จโ€๐Ÿ’ป Installation + +### ๐Ÿ“‹ Prerequisites + +- Python 3.7+ +- Flask 3+ +- Cryptography 42+ +- Waitress 2.1+ +- Git (for update feature) +- Nginx (recommended) + +--- + +### โšก Quick Setup + +```bash +git clone https://github.com/TySP-Dev/PacCrypt.git +cd paccrypt-webapp-final +python -m venv venv +source venv/bin/activate # or venv\Scripts\activate on Windows +pip install -r requirements.txt +``` + +Then run: + +- Development Mode: + ```bash + ./start_dev.sh # or start_dev.bat + ``` + +- Production Mode: + ```bash + ./start_prod.sh # or start_prod.bat + ``` + +Visit [http://127.0.0.1:5000](http://127.0.0.1:5000) + +--- + +## ๐Ÿงญ Navigation & Usage + +### ๐Ÿ” Encrypt & Decrypt + +- Choose between Basic Cipher or Advanced AES +- Type your message or upload a file +- Enter password (if AES) +- Select mode using toggle (Encrypt/Decrypt) +- Hit Execute + +### ๐Ÿ“ค Share Files + +- Upload a file with two passwords: + - Encryption password + - Pickup password +- Get a shareable URL and click ๐Ÿ“‹ Copy Link + +### ๐Ÿ”‘ Generate Passwords + +- Click Generate +- Then hit ๐Ÿ“‹ Copy + +### ๐ŸŽฎ Pac-Man Game + +- Type `pacman` in the input box +- Game appears with Restart/Exit controls +- Classic arrow key controls ๐Ÿ•น๏ธ + +--- + +## ๐Ÿ› ๏ธ Admin Panel + +Visit `/adminpage` after setting up credentials at `/admin-setup`. + +Features: +- ๐Ÿ”„ Restart server +- ๐Ÿ”ƒ Update from GitHub (git pull) +- ๐Ÿงฝ Clear uploads +- ๐Ÿ” Change admin password +- ๐Ÿ“ View logs +- โš™๏ธ Adjust upload settings + +--- + +## ๐Ÿ›ก๏ธ Deployment Tips + +Minimal Nginx config: + +```nginx +server { + listen 80; + server_name yourdomain.com; + + location / { + proxy_pass http://127.0.0.1:5000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} +``` + +Use Let's Encrypt to add SSL/TLS support. + +--- + +## ๐Ÿ—‚๏ธ Project Structure + +``` +paccrypt-webapp-final/ +โ”œโ”€โ”€ app.py +โ”œโ”€โ”€ requirements.txt +โ”œโ”€โ”€ README.md +โ”œโ”€โ”€ templates/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ 404.html +โ”‚ โ””โ”€โ”€ 403.html +โ”‚ โ””โ”€โ”€ 500.html +โ”‚ โ””โ”€โ”€ admin.html +โ”‚ โ””โ”€โ”€ admin_login.html +โ”‚ โ””โ”€โ”€ admin_settings.html +โ”‚ โ””โ”€โ”€ admin_setup.html +โ”‚ โ””โ”€โ”€ pickup.html +โ”œโ”€โ”€ static/ +โ”‚ โ”œโ”€โ”€ css/ +โ”‚ โ”‚ โ””โ”€โ”€ styles.css +โ”‚ โ”œโ”€โ”€ js/ +โ”‚ โ”‚ โ””โ”€โ”€ ui.js +โ”‚ โ”‚ โ””โ”€โ”€ pacman.js +โ”‚ โ”‚ โ””โ”€โ”€ main.js +โ”‚ โ”‚ โ””โ”€โ”€ fileops.js +โ”‚ โ”‚ โ””โ”€โ”€ encryption.js +โ”‚ โ”œโ”€โ”€ img/ +โ”‚ โ”‚ โ””โ”€โ”€ PacCrypt.png +โ”‚ โ”‚ โ””โ”€โ”€ Github_logo.png +โ”‚ โ”‚ โ””โ”€โ”€ sitemap.png +โ”‚ โ””โ”€โ”€ audio/ +โ”‚ โ””โ”€โ”€ chomp.mp3 +โ”œโ”€โ”€ start_dev.bat +โ”œโ”€โ”€ start_prod.bat +โ”œโ”€โ”€ start_dev.sh +โ”œโ”€โ”€ start_prod.sh +``` + +--- + +## ๐Ÿ“„ License + +MIT ยฉ [TySP-Dev](https://github.com/TySP-Dev) diff --git a/app.py b/app.py index 9fe0c30..ed9cf27 100644 --- a/app.py +++ b/app.py @@ -1,715 +1,720 @@ -# ===== Standard Library Imports ===== -import os -import io -import json -import html -import base64 -import hashlib -import secrets -import datetime -import subprocess -import platform -from datetime import UTC -import sys - -# ===== Third-Party Imports ===== -from flask import ( - Flask, render_template, request, jsonify, session, - redirect, url_for, flash, send_file -) -from werkzeug.utils import secure_filename -from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC -from cryptography.hazmat.primitives.hashes import SHA256 -from cryptography.hazmat.primitives.ciphers.aead import AESGCM -from cryptography.fernet import Fernet - -# ===== Application Configuration ===== -app = Flask(__name__) -app.secret_key = os.getenv("FLASK_SECRET", os.urandom(24)) - -# ===== Constants ===== -ADMIN_CRED_FILE = 'admin_creds.json' -ADMIN_KEY_FILE = 'admin_key.key' -ADMIN_LOG_FILE = 'admin_logs.enc' -SETTINGS_FILE = 'settings.json' -ALPHABET = list('abcdefghijklmnopqrstuvwxyz') - -DEFAULT_SETTINGS = { - "upload_folder": "uploads", - "max_file_age_days": 14, - "max_file_size_bytes": 25 * 1024 * 1024 * 1024 # 25GB -} - -# ===== Settings Management ===== -def load_settings(): - """Load application settings from file or create with defaults.""" - if not os.path.exists(SETTINGS_FILE): - with open(SETTINGS_FILE, 'w') as f: - json.dump(DEFAULT_SETTINGS, f) - with open(SETTINGS_FILE, 'r') as f: - return json.load(f) - -settings = load_settings() -UPLOAD_FOLDER = settings["upload_folder"] -MAX_FILE_AGE_DAYS = settings["max_file_age_days"] -MAX_FILE_SIZE_BYTES = settings["max_file_size_bytes"] - -# Ensure upload folder exists and has proper permissions -if not os.path.exists(UPLOAD_FOLDER): - try: - os.makedirs(UPLOAD_FOLDER, exist_ok=True) - # Set permissions to 755 (rwxr-xr-x) - os.chmod(UPLOAD_FOLDER, 0o755) - print(f"[INFO] Created upload directory: {UPLOAD_FOLDER}") - except Exception as e: - print(f"[ERROR] Failed to create upload directory: {str(e)}") - raise - -# ===== Cryptographic Functions ===== -def derive_key(password: str, salt: bytes) -> bytes: - """Derive a cryptographic key from password using PBKDF2.""" - return PBKDF2HMAC(algorithm=SHA256(), length=32, salt=salt, iterations=200_000).derive(password.encode()) - -def hash_password(password: str, salt: bytes) -> str: - """Hash a password with salt for secure storage.""" - return base64.urlsafe_b64encode(derive_key(password, salt)).decode() - -def simple_encode(text: str) -> str: - """Basic Caesar cipher encryption.""" - return ''.join(ALPHABET[(ALPHABET.index(c) + 3) % 26] if c in ALPHABET else c for c in text.lower()) - -def simple_decode(text: str) -> str: - """Basic Caesar cipher decryption.""" - return ''.join(ALPHABET[(ALPHABET.index(c) - 3) % 26] if c in ALPHABET else c for c in text.lower()) - -def advanced_encrypt(plaintext: str, password: str) -> str: - """Encrypt text using AES-GCM with password-derived key.""" - salt = os.urandom(16) - key = derive_key(password, salt) - nonce = os.urandom(12) - ct = AESGCM(key).encrypt(nonce, plaintext.encode(), None) - return base64.urlsafe_b64encode(salt + nonce + ct).decode() - -def advanced_decrypt(token_b64: str, password: str) -> str: - """Decrypt text using AES-GCM with password-derived key.""" - try: - data = base64.urlsafe_b64decode(token_b64.encode()) - salt, nonce, ct = data[:16], data[16:28], data[28:] - key = derive_key(password, salt) - return AESGCM(key).decrypt(nonce, ct, None).decode() - except Exception: - return "[Error] Invalid password or corrupted data!" - -# ===== Admin Authentication ===== -def load_admin_key(): - """Load or generate admin encryption key.""" - if not os.path.exists(ADMIN_KEY_FILE): - with open(ADMIN_KEY_FILE, 'wb') as f: - f.write(Fernet.generate_key()) - with open(ADMIN_KEY_FILE, 'rb') as f: - return f.read() - -def encrypt_creds(username, password): - """Encrypt and store admin credentials.""" - key = load_admin_key() - cipher = Fernet(key) - salt = os.urandom(16) - hashed_pw = hash_password(password, salt) - data = json.dumps({"u": username, "p": hashed_pw, "s": base64.b64encode(salt).decode()}).encode() - with open(ADMIN_CRED_FILE, 'wb') as f: - f.write(cipher.encrypt(data)) - -def check_creds(username, password): - """Verify admin credentials.""" - try: - key = load_admin_key() - cipher = Fernet(key) - with open(ADMIN_CRED_FILE, 'rb') as f: - decrypted = cipher.decrypt(f.read()) - creds = json.loads(decrypted) - salt = base64.b64decode(creds["s"]) - return creds["u"] == username and creds["p"] == hash_password(password, salt) - except Exception as e: - print("[ERROR] check_creds failed:", e) - return False - -def log_admin_event(message: str): - """Log admin actions securely.""" - try: - key = load_admin_key() - cipher = Fernet(key) - timestamp = datetime.datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S") - encrypted = cipher.encrypt(f"[{timestamp}] {message}".encode()) - with open(ADMIN_LOG_FILE, 'ab') as f: - f.write(encrypted + b"\n") - except Exception as e: - print("[ERROR] Failed to write admin log:", e) - -# ===== File Management ===== -def cleanup_expired_files(): - """Remove files older than MAX_FILE_AGE_DAYS.""" - try: - now = datetime.datetime.now(UTC) - for fname in os.listdir(UPLOAD_FOLDER): - if fname.endswith(".enc") or fname.endswith(".json"): - path = os.path.join(UPLOAD_FOLDER, fname) - try: - file_time = datetime.datetime.fromtimestamp(os.path.getmtime(path), UTC) - age = (now - file_time).days - if age > MAX_FILE_AGE_DAYS: - os.remove(path) - print(f"[INFO] Deleted expired file: {fname}") - except Exception as e: - print(f"[ERROR] Could not check/delete file {fname}: {e}") - except Exception as e: - print(f"[ERROR] Failed to cleanup expired files: {str(e)}") - -# ===== Route Handlers ===== -@app.route("/", methods=["GET", "POST"]) -def index(): - """Main application route handling encryption/decryption and file uploads.""" - if request.method == 'POST': - if 'file' in request.files: - return handle_file_upload(request) - else: - return handle_text_operation(request) - return render_template("index.html", result="", password="", encryption_type="advanced", settings=settings) - -def handle_file_upload(request): - """Process file upload and encryption.""" - file = request.files['file'] - enc_password = request.form.get('enc_password') - pickup_password = request.form.get('pickup_password') - - if not file or not enc_password or not pickup_password: - return jsonify({"error": "Missing fields"}), 400 - - if file.content_length and file.content_length > MAX_FILE_SIZE_BYTES: - return jsonify({"error": f"File too large! Limit: {MAX_FILE_SIZE_BYTES / (1024**3):.2f} GB"}), 400 - - filename = secure_filename(file.filename) - temp_path = os.path.join(UPLOAD_FOLDER, filename) - file.save(temp_path) - - with open(temp_path, 'rb') as f: - data = f.read() - - salt = os.urandom(16) - key = derive_key(enc_password, salt) - nonce = os.urandom(12) - ct = AESGCM(key).encrypt(nonce, data, None) - - random_id = secrets.token_urlsafe(24) - - with open(os.path.join(UPLOAD_FOLDER, f"{random_id}.enc"), 'wb') as f: - f.write(salt + nonce + ct) - os.remove(temp_path) - - meta = { - 'pickup_password': base64.urlsafe_b64encode(hashlib.sha256(pickup_password.encode()).digest()).decode(), - 'original_name': filename, - 'timestamp': datetime.datetime.now(UTC).isoformat() - } - with open(os.path.join(UPLOAD_FOLDER, f"{random_id}.json"), 'w') as f: - json.dump(meta, f) - - pickup_url = request.host_url.rstrip('/') + url_for('pickup_file', file_id=random_id) - return jsonify({"success": True, "pickup_url": pickup_url}) - -def handle_text_operation(request): - """Process text encryption/decryption operations.""" - data = request.get_json() - encryption_type = data.get("encryption-type", "basic") - operation = data.get("operation", "") - message = data.get("message", "") - password = data.get("password", "") - - if encryption_type == "basic": - result = simple_encode(message) if operation == "encrypt" else simple_decode(message) - else: - result = advanced_encrypt(message, password) if operation == "encrypt" else advanced_decrypt(message, password) - - return jsonify(result=html.escape(result)) - -# ===== File Pickup Route ===== -@app.route("/pickup/", methods=["GET", "POST"]) -def pickup_file(file_id): - """Handle file pickup and decryption.""" - meta_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.json") - enc_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.enc") - - if not os.path.exists(meta_path) or not os.path.exists(enc_path): - flash("File not found or expired") - return redirect(url_for('index')) - - if request.method == 'POST': - return handle_file_pickup(request, meta_path, enc_path, file_id) - return render_template("pickup.html", file_id=file_id) - -def handle_file_pickup(request, meta_path, enc_path, file_id): - """Process file pickup and decryption.""" - pickup_password = request.form.get('pickup_password') - enc_password = request.form.get('enc_password') - - if not pickup_password or not enc_password: - flash("Missing fields") - return redirect(request.url) - - with open(meta_path, 'r') as f: - meta = json.load(f) - - expected_hash = base64.urlsafe_b64encode(hashlib.sha256(pickup_password.encode()).digest()).decode() - if expected_hash != meta['pickup_password']: - flash("Incorrect pickup password") - return redirect(request.url) - - with open(enc_path, 'rb') as f: - enc_data = f.read() - salt, nonce, ct = enc_data[:16], enc_data[16:28], enc_data[28:] - key = derive_key(enc_password, salt) - - try: - decrypted = AESGCM(key).decrypt(nonce, ct, None) - except Exception: - flash("Decryption failed") - return redirect(request.url) - - os.remove(meta_path) - os.remove(enc_path) - log_admin_event(f"File {file_id} downloaded and deleted.") - - response = send_file( - io.BytesIO(decrypted), - as_attachment=True, - download_name=meta['original_name'], - mimetype='application/octet-stream' - ) - - # Add headers for better mobile compatibility - response.headers['Content-Disposition'] = f'attachment; filename="{meta["original_name"]}"' - response.headers['Content-Type'] = 'application/octet-stream' - response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' - response.headers['Pragma'] = 'no-cache' - response.headers['Expires'] = '0' - - return response - -# ===== Admin Routes ===== -@app.route("/admin-logs") -def admin_logs(): - """View admin activity logs.""" - if not session.get("admin_logged_in"): - return redirect(url_for("admin_login")) - - logs = [] - try: - key = load_admin_key() - cipher = Fernet(key) - if os.path.exists(ADMIN_LOG_FILE): - with open(ADMIN_LOG_FILE, 'rb') as f: - lines = f.readlines() - for line in lines[-100:]: - if line.strip(): - try: - decrypted = cipher.decrypt(line.strip()) - logs.append(decrypted.decode()) - except Exception: - logs.append("[Error] Corrupted log entry.") - except Exception as e: - logs.append(f"[Error loading logs] {str(e)}") - - return jsonify(logs=logs) - -@app.route("/admin-settings", methods=["GET", "POST"]) -def admin_settings(): - """Manage application settings.""" - if not session.get("admin_logged_in"): - return redirect(url_for("admin_login")) - - current_settings = load_settings() - - if request.method == 'POST': - return handle_settings_update(request, current_settings) - return render_template("admin_settings.html", settings=current_settings) - -def handle_settings_update(request, current_settings): - """Process settings update request.""" - upload_folder = request.form.get('upload_folder', current_settings.get('upload_folder', 'uploads')) - max_file_age_days = int(request.form.get('max_file_age_days', current_settings.get('max_file_age_days', 14))) - max_file_size_gb = float(request.form.get('max_file_size_gb', current_settings.get('max_file_size_bytes', 25 * 1024 * 1024 * 1024) / (1024 * 1024 * 1024))) - max_file_size_bytes = int(max_file_size_gb * 1024 * 1024 * 1024) - - updated_settings = { - "upload_folder": upload_folder, - "max_file_age_days": max_file_age_days, - "max_file_size_bytes": max_file_size_bytes - } - - with open(SETTINGS_FILE, 'w') as f: - json.dump(updated_settings, f) - - flash("Settings updated successfully!") - - global settings, UPLOAD_FOLDER, MAX_FILE_AGE_DAYS, MAX_FILE_SIZE_BYTES - settings = load_settings() - UPLOAD_FOLDER = settings.get('upload_folder', 'uploads') - MAX_FILE_AGE_DAYS = settings.get('max_file_age_days', 14) - MAX_FILE_SIZE_BYTES = settings.get('max_file_size_bytes', 25 * 1024 * 1024 * 1024) - - if not os.path.exists(UPLOAD_FOLDER): - os.makedirs(UPLOAD_FOLDER) - - return redirect(url_for("admin_settings")) - -@app.route("/admin-setup", methods=["GET", "POST"]) -def admin_setup(): - """Initial admin account setup.""" - if os.path.exists(ADMIN_CRED_FILE): - return redirect(url_for("admin_login")) - if request.method == "POST": - u = request.form.get("username") - p = request.form.get("password") - if u and p: - encrypt_creds(u, p) - session["admin_logged_in"] = True - return redirect(url_for("admin_page")) - flash("Both fields required") - return render_template("admin_setup.html") - -@app.route("/admin-login", methods=["GET", "POST"]) -def admin_login(): - """Admin login handler.""" - if request.method == "POST": - u = request.form.get("username") - p = request.form.get("password") - if check_creds(u, p): - session["admin_logged_in"] = True - log_admin_event("Admin login successful.") - return redirect(url_for("admin_page")) - else: - log_admin_event("Admin login failed.") - flash("Incorrect credentials") - return render_template("admin_login.html") - -@app.route("/admin-logout") -def admin_logout(): - """Admin logout handler.""" - session.pop("admin_logged_in", None) - return redirect(url_for("index")) - -@app.route("/adminpage") -def admin_page(): - """Admin dashboard.""" - if not session.get("admin_logged_in"): - if not os.path.exists(ADMIN_CRED_FILE): - return redirect(url_for("admin_setup")) - return redirect(url_for("admin_login")) - - cleanup_expired_files() - routes = [rule.rule for rule in app.url_map.iter_rules() if rule.endpoint != 'static'] - - # Get uptime based on OS - if platform.system() == "Windows": - try: - # Windows uptime using PowerShell - ps_command = "(Get-CimInstance -ClassName Win32_OperatingSystem).LastBootUpTime" - uptime_output = subprocess.check_output(["powershell", "-Command", ps_command], shell=True).decode() - # Convert the PowerShell DateTime to Python datetime - boot_time = datetime.datetime.strptime(uptime_output.strip(), "%A, %B %d, %Y %I:%M:%S %p") - # Make boot_time timezone-aware (assuming local time) - boot_time = boot_time.replace(tzinfo=datetime.timezone.utc) - current_time = datetime.datetime.now(UTC) - uptime = current_time - boot_time - uptime_str = f"{uptime.days} days, {uptime.seconds // 3600} hours, {(uptime.seconds % 3600) // 60} minutes" - except Exception as e: - print(f"[ERROR] Failed to get Windows uptime: {str(e)}") - uptime_str = "Unavailable" - else: - try: - # Try reading from /proc/uptime first - with open('/proc/uptime', 'r') as f: - uptime_seconds = float(f.readline().split()[0]) - days = int(uptime_seconds // 86400) - hours = int((uptime_seconds % 86400) // 3600) - minutes = int((uptime_seconds % 3600) // 60) - uptime_str = f"{days} days, {hours} hours, {minutes} minutes" - except Exception: - try: - # Fallback to uptime command if /proc/uptime fails - uptime_str = subprocess.check_output("uptime -p", shell=True).decode().strip() - except Exception as e: - print(f"[ERROR] Failed to get Linux uptime: {str(e)}") - uptime_str = "Unavailable" - - server_info = { - "uptime": uptime_str, - "time": datetime.datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S"), - "python": platform.python_version(), - "debug": app.debug - } - - return render_template("admin.html", routes=routes, server_info=server_info) - -@app.route("/restart-server", methods=["POST"]) -def restart_server(): - """Restart the server.""" - if not session.get("admin_logged_in"): - return jsonify({"error": "Unauthorized"}), 401 - - try: - if platform.system() == "Windows": - # Get the current process ID - current_pid = os.getpid() - # Create a batch file to restart the server - restart_script = f""" - @echo off - timeout /t 2 /nobreak - start "" "python" "app.py" - """ - with open("restart.bat", "w") as f: - f.write(restart_script) - - # Start the restart script and exit - subprocess.Popen(["restart.bat"], shell=True) - return jsonify({"message": "Server restart initiated"}), 200 - else: - # For Linux/Unix systems, use a Python-based restart - # Get the current Python interpreter and script path - python_path = sys.executable - script_path = os.path.abspath(__file__) - - # Create a shell script to restart the server - restart_script = f"""#!/bin/bash - sleep 2 - {python_path} {script_path} - """ - - # Write and make the script executable - with open("restart.sh", "w") as f: - f.write(restart_script) - os.chmod("restart.sh", 0o755) - - # Start the restart script and exit - subprocess.Popen(["./restart.sh"], shell=True) - return jsonify({"message": "Server restart initiated"}), 200 - except Exception as e: - print(f"[ERROR] Failed to restart server: {str(e)}") - return jsonify({"error": f"Failed to restart server: {str(e)}"}), 500 - -@app.route("/admin-reset", methods=["POST"]) -def admin_reset(): - """Reset admin credentials.""" - if not session.get("admin_logged_in"): - return redirect(url_for("admin_login")) - try: - if os.path.exists(ADMIN_CRED_FILE): - os.remove(ADMIN_CRED_FILE) - if os.path.exists(ADMIN_KEY_FILE): - os.remove(ADMIN_KEY_FILE) - session.pop("admin_logged_in", None) - flash("Admin credentials reset. Please create new credentials.") - except Exception as e: - flash("Failed to reset admin credentials.") - print("[ERROR] admin_reset failed:", e) - return redirect(url_for("admin_setup")) - -@app.route("/admin-change-password", methods=["POST"]) -def admin_change_password(): - """Change admin password.""" - if not session.get("admin_logged_in"): - return redirect(url_for("admin_login")) - - current = request.form.get("current_password") - new = request.form.get("new_password") - - try: - key = load_admin_key() - cipher = Fernet(key) - with open(ADMIN_CRED_FILE, 'rb') as file: - decrypted = cipher.decrypt(file.read()) - creds = json.loads(decrypted) - - salt = base64.b64decode(creds["s"]) - if hash_password(current, salt) != creds["p"]: - flash("Current password is incorrect") - return redirect(url_for("admin_page")) - - creds["p"] = hash_password(new, salt) - encrypted = cipher.encrypt(json.dumps(creds).encode()) - with open(ADMIN_CRED_FILE, 'wb') as file: - file.write(encrypted) - - log_admin_event("Admin password changed.") - flash("Password updated successfully", "password-feedback") - return redirect(url_for("admin_page")) - - except Exception as e: - flash("Failed to update password") - print("[ERROR] Password change failed:", e) - return redirect(url_for("admin_page")) - -@app.route("/admin-clear-uploads", methods=["POST"]) -def admin_clear_uploads(): - """Clear all uploaded files.""" - if not session.get("admin_logged_in"): - return redirect(url_for("admin_login")) - - deleted = 0 - for filename in os.listdir(UPLOAD_FOLDER): - if filename.endswith(".enc") or filename.endswith(".json"): - try: - os.remove(os.path.join(UPLOAD_FOLDER, filename)) - deleted += 1 - except Exception as e: - print("[ERROR] Failed to delete:", filename, e) - - flash(f"Cleared {deleted} uploaded file(s).", "clear-feedback") - return redirect(url_for("admin_page")) - -@app.route("/admin-update-server", methods=["POST"]) -def admin_update_server(): - """Update server from GitHub repository.""" - if not session.get("admin_logged_in"): - return jsonify({"error": "Unauthorized"}), 401 - - try: - # Get the absolute path of the current directory - current_dir = os.path.abspath(os.path.dirname(__file__)) - - # Try to find git executable - git_paths = [ - "/usr/bin/git", # Standard Debian path - "/usr/local/bin/git", - "/bin/git", - "git" # Fallback to PATH - ] - - git_cmd = None - for path in git_paths: - if os.path.exists(path) or path == "git": - try: - # Test if git is executable - subprocess.run([path, "--version"], check=True, capture_output=True) - git_cmd = path - break - except Exception: - continue - - if not git_cmd: - return jsonify({"error": "Git executable not found. Please ensure git is installed and accessible."}), 500 - - # Try to find the git repository by checking parent directories - repo_dir = current_dir - max_depth = 5 # Limit how far up we'll look - found_git = False - - for _ in range(max_depth): - git_dir = os.path.join(repo_dir, ".git") - if os.path.exists(git_dir): - found_git = True - break - parent_dir = os.path.dirname(repo_dir) - if parent_dir == repo_dir: # We've reached the root directory - break - repo_dir = parent_dir - - if not found_git: - return jsonify({ - "error": "Git repository not found. Current directory: " + current_dir, - "details": "Please ensure the application is running from within the git repository directory." - }), 400 - - # Execute git commands with proper error handling - try: - # Fetch latest changes - fetch_result = subprocess.run([git_cmd, "fetch"], cwd=repo_dir, check=True, capture_output=True, text=True) - - # Reset to origin/main - reset_result = subprocess.run([git_cmd, "reset", "--hard", "origin/main"], cwd=repo_dir, check=True, capture_output=True, text=True) - - # Pull latest changes - pull_result = subprocess.run([git_cmd, "pull"], cwd=repo_dir, check=True, capture_output=True, text=True) - - return jsonify({ - "message": "Server updated successfully from GitHub!", - "details": { - "fetch": fetch_result.stdout, - "reset": reset_result.stdout, - "pull": pull_result.stdout - } - }), 200 - except subprocess.CalledProcessError as e: - error_msg = f"Git operation failed: {e.stderr if e.stderr else e.stdout}" - print(f"[ERROR] {error_msg}") - return jsonify({"error": error_msg}), 500 - - except Exception as e: - error_msg = f"Update failed: {str(e)}" - print(f"[ERROR] {error_msg}") - return jsonify({"error": error_msg}), 500 - -# ===== Sitemap and Robots ===== -@app.route("/sitemap", methods=["GET"]) -def sitemap(): - """Generate sitemap.xml.""" - sitemap_xml = ''' - - https://paccrypt.unnaturalll.dev/ - https://paccrypt.unnaturalll.dev/pickup - https://paccrypt.unnaturalll.dev/adminpage - https://paccrypt.unnaturalll.dev/sitemap -''' - return sitemap_xml, 200, {'Content-Type': 'application/xml'} - -@app.route("/robots.txt") -def robots_txt(): - """Generate robots.txt.""" - lines = [ - "User-agent: *", - "Disallow: /adminpage", - "Disallow: /admin-login", - "Disallow: /admin-setup", - "Disallow: /admin-reset", - "Disallow: /admin-settings", - "Disallow: /restart-server", - "Disallow: /pickup", - "Disallow: /admin-change-password", - "Allow: /", - f"Sitemap: {url_for('sitemap', _external=True)}" - ] - return "\n".join(lines), 200, {"Content-Type": "text/plain"} - -# ===== Error Handlers ===== -@app.errorhandler(404) -def page_not_found(e): - return render_template('404.html'), 404 - -@app.errorhandler(500) -def server_error(e): - return render_template('500.html'), 500 - -@app.errorhandler(403) -def forbidden(e): - return render_template('403.html'), 403 - -@app.errorhandler(405) -def method_not_allowed(e): - return render_template('403.html'), 403 - -@app.errorhandler(FileNotFoundError) -def handle_file_not_found(e): - if os.getenv("PRODUCTION", "false").lower() == "true": - return render_template('500.html'), 500 - else: - raise e - -# ===== Application Entry Point ===== -if __name__ == "__main__": - PRODUCTION = os.getenv("PRODUCTION", "false").lower() == "true" - if PRODUCTION: - from waitress import serve - print("[INFO] Running in PRODUCTION mode with Waitress.") - serve(app, host="0.0.0.0", port=5000) - else: - print("[INFO] Running in DEVELOPMENT mode with Flask server.") +# ===== Standard Library Imports ===== +import os +import io +import json +import html +import base64 +import hashlib +import secrets +import datetime +import subprocess +import platform +from datetime import UTC +import sys + +# ===== Third-Party Imports ===== +from flask import ( + Flask, render_template, request, jsonify, session, + redirect, url_for, flash, send_file +) +from werkzeug.utils import secure_filename +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.primitives.hashes import SHA256 +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.fernet import Fernet + +# ===== Application Configuration ===== +app = Flask(__name__) +app.secret_key = os.getenv("FLASK_SECRET", os.urandom(24)) + +# ===== Constants ===== +ADMIN_CRED_FILE = 'admin_creds.json' +ADMIN_KEY_FILE = 'admin_key.key' +ADMIN_LOG_FILE = 'admin_logs.enc' +SETTINGS_FILE = 'settings.json' +ALPHABET = list('abcdefghijklmnopqrstuvwxyz') + +DEFAULT_SETTINGS = { + "upload_folder": "uploads", + "max_file_age_days": 14, + "max_file_size_bytes": 25 * 1024 * 1024 * 1024 # 25GB +} + +# ===== Settings Management ===== +def load_settings(): + """Load application settings from file or create with defaults.""" + if not os.path.exists(SETTINGS_FILE): + with open(SETTINGS_FILE, 'w') as f: + json.dump(DEFAULT_SETTINGS, f) + with open(SETTINGS_FILE, 'r') as f: + return json.load(f) + +settings = load_settings() +UPLOAD_FOLDER = settings["upload_folder"] +MAX_FILE_AGE_DAYS = settings["max_file_age_days"] +MAX_FILE_SIZE_BYTES = settings["max_file_size_bytes"] + +# Ensure upload folder exists and has proper permissions +if not os.path.exists(UPLOAD_FOLDER): + try: + os.makedirs(UPLOAD_FOLDER, exist_ok=True) + # Set permissions to 755 (rwxr-xr-x) + os.chmod(UPLOAD_FOLDER, 0o755) + print(f"[INFO] Created upload directory: {UPLOAD_FOLDER}") + except Exception as e: + print(f"[ERROR] Failed to create upload directory: {str(e)}") + raise + +# ===== Cryptographic Functions ===== +def derive_key(password: str, salt: bytes) -> bytes: + """Derive a cryptographic key from password using PBKDF2.""" + return PBKDF2HMAC(algorithm=SHA256(), length=32, salt=salt, iterations=200_000).derive(password.encode()) + +def hash_password(password: str, salt: bytes) -> str: + """Hash a password with salt for secure storage.""" + return base64.urlsafe_b64encode(derive_key(password, salt)).decode() + +def simple_encode(text: str) -> str: + """Basic Caesar cipher encryption.""" + return ''.join(ALPHABET[(ALPHABET.index(c) + 3) % 26] if c in ALPHABET else c for c in text.lower()) + +def simple_decode(text: str) -> str: + """Basic Caesar cipher decryption.""" + return ''.join(ALPHABET[(ALPHABET.index(c) - 3) % 26] if c in ALPHABET else c for c in text.lower()) + +def advanced_encrypt(plaintext: str, password: str) -> str: + """Encrypt text using AES-GCM with password-derived key.""" + salt = os.urandom(16) + key = derive_key(password, salt) + nonce = os.urandom(12) + ct = AESGCM(key).encrypt(nonce, plaintext.encode(), None) + return base64.urlsafe_b64encode(salt + nonce + ct).decode() + +def advanced_decrypt(token_b64: str, password: str) -> str: + """Decrypt text using AES-GCM with password-derived key.""" + try: + data = base64.urlsafe_b64decode(token_b64.encode()) + salt, nonce, ct = data[:16], data[16:28], data[28:] + key = derive_key(password, salt) + return AESGCM(key).decrypt(nonce, ct, None).decode() + except Exception: + return "[Error] Invalid password or corrupted data!" + +# ===== Admin Authentication ===== +def load_admin_key(): + """Load or generate admin encryption key.""" + if not os.path.exists(ADMIN_KEY_FILE): + with open(ADMIN_KEY_FILE, 'wb') as f: + f.write(Fernet.generate_key()) + with open(ADMIN_KEY_FILE, 'rb') as f: + return f.read() + +def encrypt_creds(username, password): + """Encrypt and store admin credentials.""" + key = load_admin_key() + cipher = Fernet(key) + salt = os.urandom(16) + hashed_pw = hash_password(password, salt) + data = json.dumps({"u": username, "p": hashed_pw, "s": base64.b64encode(salt).decode()}).encode() + with open(ADMIN_CRED_FILE, 'wb') as f: + f.write(cipher.encrypt(data)) + +def check_creds(username, password): + """Verify admin credentials.""" + try: + key = load_admin_key() + cipher = Fernet(key) + with open(ADMIN_CRED_FILE, 'rb') as f: + decrypted = cipher.decrypt(f.read()) + creds = json.loads(decrypted) + salt = base64.b64decode(creds["s"]) + return creds["u"] == username and creds["p"] == hash_password(password, salt) + except Exception as e: + print("[ERROR] check_creds failed:", e) + return False + +def log_admin_event(message: str): + """Log admin actions securely.""" + try: + key = load_admin_key() + cipher = Fernet(key) + timestamp = datetime.datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S") + encrypted = cipher.encrypt(f"[{timestamp}] {message}".encode()) + with open(ADMIN_LOG_FILE, 'ab') as f: + f.write(encrypted + b"\n") + except Exception as e: + print("[ERROR] Failed to write admin log:", e) + +# ===== File Management ===== +def cleanup_expired_files(): + """Remove files older than MAX_FILE_AGE_DAYS.""" + try: + now = datetime.datetime.now(UTC) + for fname in os.listdir(UPLOAD_FOLDER): + if fname.endswith(".enc") or fname.endswith(".json"): + path = os.path.join(UPLOAD_FOLDER, fname) + try: + file_time = datetime.datetime.fromtimestamp(os.path.getmtime(path), UTC) + age = (now - file_time).days + if age > MAX_FILE_AGE_DAYS: + os.remove(path) + print(f"[INFO] Deleted expired file: {fname}") + except Exception as e: + print(f"[ERROR] Could not check/delete file {fname}: {e}") + except Exception as e: + print(f"[ERROR] Failed to cleanup expired files: {str(e)}") + +# ===== Route Handlers ===== +@app.route("/", methods=["GET", "POST"]) +def index(): + """Main application route handling encryption/decryption and file uploads.""" + if request.method == 'POST': + if 'file' in request.files: + return handle_file_upload(request) + else: + return handle_text_operation(request) + return render_template("index.html", result="", password="", encryption_type="advanced", settings=settings) + +def handle_file_upload(request): + """Process file upload and encryption.""" + file = request.files['file'] + enc_password = request.form.get('enc_password') + pickup_password = request.form.get('pickup_password') + + if not file or not enc_password or not pickup_password: + return jsonify({"error": "Missing fields"}), 400 + + if file.content_length and file.content_length > MAX_FILE_SIZE_BYTES: + return jsonify({"error": f"File too large! Limit: {MAX_FILE_SIZE_BYTES / (1024**3):.2f} GB"}), 400 + + filename = secure_filename(file.filename) + temp_path = os.path.join(UPLOAD_FOLDER, filename) + file.save(temp_path) + + with open(temp_path, 'rb') as f: + data = f.read() + + salt = os.urandom(16) + key = derive_key(enc_password, salt) + nonce = os.urandom(12) + ct = AESGCM(key).encrypt(nonce, data, None) + + random_id = secrets.token_urlsafe(24) + + with open(os.path.join(UPLOAD_FOLDER, f"{random_id}.enc"), 'wb') as f: + f.write(salt + nonce + ct) + os.remove(temp_path) + + meta = { + 'pickup_password': base64.urlsafe_b64encode(hashlib.sha256(pickup_password.encode()).digest()).decode(), + 'original_name': filename, + 'timestamp': datetime.datetime.now(UTC).isoformat() + } + with open(os.path.join(UPLOAD_FOLDER, f"{random_id}.json"), 'w') as f: + json.dump(meta, f) + + pickup_url = request.host_url.rstrip('/') + url_for('pickup_file', file_id=random_id) + return jsonify({"success": True, "pickup_url": pickup_url}) + +def handle_text_operation(request): + """Process text encryption/decryption operations.""" + data = request.get_json() + encryption_type = data.get("encryption-type", "basic") + operation = data.get("operation", "") + message = data.get("message", "") + password = data.get("password", "") + + if encryption_type == "basic": + result = simple_encode(message) if operation == "encrypt" else simple_decode(message) + else: + result = advanced_encrypt(message, password) if operation == "encrypt" else advanced_decrypt(message, password) + + return jsonify(result=html.escape(result)) + +# ===== File Pickup Route ===== +@app.route("/pickup/", methods=["GET", "POST"]) +def pickup_file(file_id): + """Handle file pickup and decryption.""" + meta_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.json") + enc_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.enc") + + if not os.path.exists(meta_path) or not os.path.exists(enc_path): + flash("File not found or expired") + return redirect(url_for('index')) + + if request.method == 'POST': + return handle_file_pickup(request, meta_path, enc_path, file_id) + return render_template("pickup.html", file_id=file_id) + +def handle_file_pickup(request, meta_path, enc_path, file_id): + """Process file pickup and decryption.""" + pickup_password = request.form.get('pickup_password') + enc_password = request.form.get('enc_password') + + if not pickup_password or not enc_password: + flash("Missing fields") + return redirect(request.url) + + with open(meta_path, 'r') as f: + meta = json.load(f) + + expected_hash = base64.urlsafe_b64encode(hashlib.sha256(pickup_password.encode()).digest()).decode() + if expected_hash != meta['pickup_password']: + flash("Incorrect pickup password") + return redirect(request.url) + + with open(enc_path, 'rb') as f: + enc_data = f.read() + salt, nonce, ct = enc_data[:16], enc_data[16:28], enc_data[28:] + key = derive_key(enc_password, salt) + + try: + decrypted = AESGCM(key).decrypt(nonce, ct, None) + except Exception: + flash("Decryption failed") + return redirect(request.url) + + os.remove(meta_path) + os.remove(enc_path) + log_admin_event(f"File {file_id} downloaded and deleted.") + + response = send_file( + io.BytesIO(decrypted), + as_attachment=True, + download_name=meta['original_name'], + mimetype='application/octet-stream' + ) + + # Add headers for better mobile compatibility + response.headers['Content-Disposition'] = f'attachment; filename="{meta["original_name"]}"' + response.headers['Content-Type'] = 'application/octet-stream' + response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' + response.headers['Pragma'] = 'no-cache' + response.headers['Expires'] = '0' + + return response + +# ===== Admin Routes ===== +@app.route("/admin-logs") +def admin_logs(): + """View admin activity logs.""" + if not session.get("admin_logged_in"): + return redirect(url_for("admin_login")) + + logs = [] + try: + key = load_admin_key() + cipher = Fernet(key) + if os.path.exists(ADMIN_LOG_FILE): + with open(ADMIN_LOG_FILE, 'rb') as f: + lines = f.readlines() + for line in lines[-100:]: + if line.strip(): + try: + decrypted = cipher.decrypt(line.strip()) + logs.append(decrypted.decode()) + except Exception: + logs.append("[Error] Corrupted log entry.") + except Exception as e: + logs.append(f"[Error loading logs] {str(e)}") + + return jsonify(logs=logs) + +@app.route("/admin-settings", methods=["GET", "POST"]) +def admin_settings(): + """Manage application settings.""" + if not session.get("admin_logged_in"): + return redirect(url_for("admin_login")) + + current_settings = load_settings() + + if request.method == 'POST': + return handle_settings_update(request, current_settings) + return render_template("admin_settings.html", settings=current_settings) + +def handle_settings_update(request, current_settings): + """Process settings update request.""" + upload_folder = request.form.get('upload_folder', current_settings.get('upload_folder', 'uploads')) + max_file_age_days = int(request.form.get('max_file_age_days', current_settings.get('max_file_age_days', 14))) + max_file_size_gb = float(request.form.get('max_file_size_gb', current_settings.get('max_file_size_bytes', 25 * 1024 * 1024 * 1024) / (1024 * 1024 * 1024))) + max_file_size_bytes = int(max_file_size_gb * 1024 * 1024 * 1024) + + updated_settings = { + "upload_folder": upload_folder, + "max_file_age_days": max_file_age_days, + "max_file_size_bytes": max_file_size_bytes + } + + with open(SETTINGS_FILE, 'w') as f: + json.dump(updated_settings, f) + + flash("Settings updated successfully!") + + global settings, UPLOAD_FOLDER, MAX_FILE_AGE_DAYS, MAX_FILE_SIZE_BYTES + settings = load_settings() + UPLOAD_FOLDER = settings.get('upload_folder', 'uploads') + MAX_FILE_AGE_DAYS = settings.get('max_file_age_days', 14) + MAX_FILE_SIZE_BYTES = settings.get('max_file_size_bytes', 25 * 1024 * 1024 * 1024) + + if not os.path.exists(UPLOAD_FOLDER): + os.makedirs(UPLOAD_FOLDER) + + return redirect(url_for("admin_settings")) + +@app.route("/admin-setup", methods=["GET", "POST"]) +def admin_setup(): + """Initial admin account setup.""" + if os.path.exists(ADMIN_CRED_FILE): + return redirect(url_for("admin_login")) + if request.method == "POST": + u = request.form.get("username") + p = request.form.get("password") + if u and p: + encrypt_creds(u, p) + session["admin_logged_in"] = True + return redirect(url_for("admin_page")) + flash("Both fields required") + return render_template("admin_setup.html") + +@app.route("/admin-login", methods=["GET", "POST"]) +def admin_login(): + """Admin login handler.""" + if request.method == "POST": + u = request.form.get("username") + p = request.form.get("password") + if check_creds(u, p): + session["admin_logged_in"] = True + log_admin_event("Admin login successful.") + return redirect(url_for("admin_page")) + else: + log_admin_event("Admin login failed.") + flash("Incorrect credentials") + return render_template("admin_login.html") + +@app.route("/admin-logout") +def admin_logout(): + """Admin logout handler.""" + session.pop("admin_logged_in", None) + return redirect(url_for("index")) + +@app.route("/adminpage") +def admin_page(): + """Admin dashboard.""" + if not session.get("admin_logged_in"): + if not os.path.exists(ADMIN_CRED_FILE): + return redirect(url_for("admin_setup")) + return redirect(url_for("admin_login")) + + cleanup_expired_files() + routes = [rule.rule for rule in app.url_map.iter_rules() if rule.endpoint != 'static'] + + # Get uptime based on OS + if platform.system() == "Windows": + try: + # Windows uptime using PowerShell + ps_command = "(Get-CimInstance -ClassName Win32_OperatingSystem).LastBootUpTime" + uptime_output = subprocess.check_output(["powershell", "-Command", ps_command], shell=True).decode() + # Convert the PowerShell DateTime to Python datetime + boot_time = datetime.datetime.strptime(uptime_output.strip(), "%A, %B %d, %Y %I:%M:%S %p") + # Make boot_time timezone-aware (assuming local time) + boot_time = boot_time.replace(tzinfo=datetime.timezone.utc) + current_time = datetime.datetime.now(UTC) + uptime = current_time - boot_time + uptime_str = f"{uptime.days} days, {uptime.seconds // 3600} hours, {(uptime.seconds % 3600) // 60} minutes" + except Exception as e: + print(f"[ERROR] Failed to get Windows uptime: {str(e)}") + uptime_str = "Unavailable" + else: + try: + # Try reading from /proc/uptime first + with open('/proc/uptime', 'r') as f: + uptime_seconds = float(f.readline().split()[0]) + days = int(uptime_seconds // 86400) + hours = int((uptime_seconds % 86400) // 3600) + minutes = int((uptime_seconds % 3600) // 60) + uptime_str = f"{days} days, {hours} hours, {minutes} minutes" + except Exception: + try: + # Fallback to uptime command if /proc/uptime fails + uptime_str = subprocess.check_output("uptime -p", shell=True).decode().strip() + except Exception as e: + print(f"[ERROR] Failed to get Linux uptime: {str(e)}") + uptime_str = "Unavailable" + + server_info = { + "uptime": uptime_str, + "time": datetime.datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S"), + "python": platform.python_version(), + "debug": app.debug + } + + return render_template("admin.html", routes=routes, server_info=server_info) + +@app.route("/restart-server", methods=["POST"]) +def restart_server(): + """Restart the server.""" + if not session.get("admin_logged_in"): + return jsonify({"error": "Unauthorized"}), 401 + + try: + if platform.system() == "Windows": + # Get the current process ID + current_pid = os.getpid() + # Create a batch file to restart the server + restart_script = f""" + @echo off + timeout /t 2 /nobreak + taskkill /F /PID {current_pid} + set PRODUCTION=true + start "" "python" "app.py" + """ + with open("restart.bat", "w") as f: + f.write(restart_script) + + # Start the restart script and exit + subprocess.Popen(["restart.bat"], shell=True) + return jsonify({"message": "Server restart initiated"}), 200 + else: + # For Linux/Unix systems, use a Python-based restart + # Get the current Python interpreter and script path + python_path = sys.executable + script_path = os.path.abspath(__file__) + current_pid = os.getpid() + + # Create a shell script to restart the server + restart_script = f"""#!/bin/bash + sleep 2 + kill -9 {current_pid} + export PRODUCTION=true + {python_path} {script_path} + """ + + # Write and make the script executable + with open("restart.sh", "w") as f: + f.write(restart_script) + os.chmod("restart.sh", 0o755) + + # Start the restart script and exit + subprocess.Popen(["./restart.sh"], shell=True) + return jsonify({"message": "Server restart initiated"}), 200 + except Exception as e: + print(f"[ERROR] Failed to restart server: {str(e)}") + return jsonify({"error": f"Failed to restart server: {str(e)}"}), 500 + +@app.route("/admin-reset", methods=["POST"]) +def admin_reset(): + """Reset admin credentials.""" + if not session.get("admin_logged_in"): + return redirect(url_for("admin_login")) + try: + if os.path.exists(ADMIN_CRED_FILE): + os.remove(ADMIN_CRED_FILE) + if os.path.exists(ADMIN_KEY_FILE): + os.remove(ADMIN_KEY_FILE) + session.pop("admin_logged_in", None) + flash("Admin credentials reset. Please create new credentials.") + except Exception as e: + flash("Failed to reset admin credentials.") + print("[ERROR] admin_reset failed:", e) + return redirect(url_for("admin_setup")) + +@app.route("/admin-change-password", methods=["POST"]) +def admin_change_password(): + """Change admin password.""" + if not session.get("admin_logged_in"): + return redirect(url_for("admin_login")) + + current = request.form.get("current_password") + new = request.form.get("new_password") + + try: + key = load_admin_key() + cipher = Fernet(key) + with open(ADMIN_CRED_FILE, 'rb') as file: + decrypted = cipher.decrypt(file.read()) + creds = json.loads(decrypted) + + salt = base64.b64decode(creds["s"]) + if hash_password(current, salt) != creds["p"]: + flash("Current password is incorrect") + return redirect(url_for("admin_page")) + + creds["p"] = hash_password(new, salt) + encrypted = cipher.encrypt(json.dumps(creds).encode()) + with open(ADMIN_CRED_FILE, 'wb') as file: + file.write(encrypted) + + log_admin_event("Admin password changed.") + flash("Password updated successfully", "password-feedback") + return redirect(url_for("admin_page")) + + except Exception as e: + flash("Failed to update password") + print("[ERROR] Password change failed:", e) + return redirect(url_for("admin_page")) + +@app.route("/admin-clear-uploads", methods=["POST"]) +def admin_clear_uploads(): + """Clear all uploaded files.""" + if not session.get("admin_logged_in"): + return redirect(url_for("admin_login")) + + deleted = 0 + for filename in os.listdir(UPLOAD_FOLDER): + if filename.endswith(".enc") or filename.endswith(".json"): + try: + os.remove(os.path.join(UPLOAD_FOLDER, filename)) + deleted += 1 + except Exception as e: + print("[ERROR] Failed to delete:", filename, e) + + flash(f"Cleared {deleted} uploaded file(s).", "clear-feedback") + return redirect(url_for("admin_page")) + +@app.route("/admin-update-server", methods=["POST"]) +def admin_update_server(): + """Update server from GitHub repository.""" + if not session.get("admin_logged_in"): + return jsonify({"error": "Unauthorized"}), 401 + + try: + # Get the absolute path of the current directory + current_dir = os.path.abspath(os.path.dirname(__file__)) + + # Try to find git executable + git_paths = [ + "/usr/bin/git", # Standard Debian path + "/usr/local/bin/git", + "/bin/git", + "git" # Fallback to PATH + ] + + git_cmd = None + for path in git_paths: + if os.path.exists(path) or path == "git": + try: + # Test if git is executable + subprocess.run([path, "--version"], check=True, capture_output=True) + git_cmd = path + break + except Exception: + continue + + if not git_cmd: + return jsonify({"error": "Git executable not found. Please ensure git is installed and accessible."}), 500 + + # Try to find the git repository by checking parent directories + repo_dir = current_dir + max_depth = 5 # Limit how far up we'll look + found_git = False + + for _ in range(max_depth): + git_dir = os.path.join(repo_dir, ".git") + if os.path.exists(git_dir): + found_git = True + break + parent_dir = os.path.dirname(repo_dir) + if parent_dir == repo_dir: # We've reached the root directory + break + repo_dir = parent_dir + + if not found_git: + return jsonify({ + "error": "Git repository not found. Current directory: " + current_dir, + "details": "Please ensure the application is running from within the git repository directory." + }), 400 + + # Execute git commands with proper error handling + try: + # Fetch latest changes + fetch_result = subprocess.run([git_cmd, "fetch"], cwd=repo_dir, check=True, capture_output=True, text=True) + + # Reset to origin/main + reset_result = subprocess.run([git_cmd, "reset", "--hard", "origin/main"], cwd=repo_dir, check=True, capture_output=True, text=True) + + # Pull latest changes + pull_result = subprocess.run([git_cmd, "pull"], cwd=repo_dir, check=True, capture_output=True, text=True) + + return jsonify({ + "message": "Server updated successfully from GitHub!", + "details": { + "fetch": fetch_result.stdout, + "reset": reset_result.stdout, + "pull": pull_result.stdout + } + }), 200 + except subprocess.CalledProcessError as e: + error_msg = f"Git operation failed: {e.stderr if e.stderr else e.stdout}" + print(f"[ERROR] {error_msg}") + return jsonify({"error": error_msg}), 500 + + except Exception as e: + error_msg = f"Update failed: {str(e)}" + print(f"[ERROR] {error_msg}") + return jsonify({"error": error_msg}), 500 + +# ===== Sitemap and Robots ===== +@app.route("/sitemap", methods=["GET"]) +def sitemap(): + """Generate sitemap.xml.""" + sitemap_xml = ''' + + https://paccrypt.unnaturalll.dev/ + https://paccrypt.unnaturalll.dev/pickup + https://paccrypt.unnaturalll.dev/adminpage + https://paccrypt.unnaturalll.dev/sitemap +''' + return sitemap_xml, 200, {'Content-Type': 'application/xml'} + +@app.route("/robots.txt") +def robots_txt(): + """Generate robots.txt.""" + lines = [ + "User-agent: *", + "Disallow: /adminpage", + "Disallow: /admin-login", + "Disallow: /admin-setup", + "Disallow: /admin-reset", + "Disallow: /admin-settings", + "Disallow: /restart-server", + "Disallow: /pickup", + "Disallow: /admin-change-password", + "Allow: /", + f"Sitemap: {url_for('sitemap', _external=True)}" + ] + return "\n".join(lines), 200, {"Content-Type": "text/plain"} + +# ===== Error Handlers ===== +@app.errorhandler(404) +def page_not_found(e): + return render_template('404.html'), 404 + +@app.errorhandler(500) +def server_error(e): + return render_template('500.html'), 500 + +@app.errorhandler(403) +def forbidden(e): + return render_template('403.html'), 403 + +@app.errorhandler(405) +def method_not_allowed(e): + return render_template('403.html'), 403 + +@app.errorhandler(FileNotFoundError) +def handle_file_not_found(e): + if os.getenv("PRODUCTION", "false").lower() == "true": + return render_template('500.html'), 500 + else: + raise e + +# ===== Application Entry Point ===== +if __name__ == "__main__": + PRODUCTION = os.getenv("PRODUCTION", "false").lower() == "true" + if PRODUCTION: + from waitress import serve + print("[INFO] Running in PRODUCTION mode with Waitress.") + serve(app, host="0.0.0.0", port=5000) + else: + print("[INFO] Running in DEVELOPMENT mode with Flask server.") app.run(debug=True, host="0.0.0.0", port=5000) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 466165a..b74fdf8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ -### **requirements.txt** - -flask==3.0.3 -cryptography==42.0.5 -waitress==2.1.2 -werkzeug==3.0.1 - -# nginx - Only needed for Nginx integration, not installed via pip +### **requirements.txt** + +flask==3.0.3 +cryptography==42.0.5 +waitress==2.1.2 +werkzeug==3.0.1 + +# nginx - Only needed for Nginx integration, not installed via pip # Run pip install -r requirements.txt \ No newline at end of file diff --git a/static/js/encryption.js b/static/js/encryption.js index 1b9835e..82633c9 100644 --- a/static/js/encryption.js +++ b/static/js/encryption.js @@ -1,104 +1,104 @@ -/** - * Encryption module. - * Handles cryptographic operations using Web Crypto API. - * Implements AES-GCM encryption with PBKDF2 key derivation. - */ - -// ===== Constants ===== -const SALT_LENGTH = 16; -const IV_LENGTH = 12; -const PBKDF2_ITERATIONS = 200_000; -const KEY_LENGTH = 256; - -// ===== Key Derivation ===== -/** - * Derives an AES-GCM key from a password using PBKDF2. - * @param {string} password - User-supplied password. - * @param {Uint8Array} salt - Randomly generated salt. - * @returns {Promise} - Derived cryptographic key. - */ -export async function deriveKey(password, salt) { - const encoder = new TextEncoder(); - const keyMaterial = await crypto.subtle.importKey( - 'raw', - encoder.encode(password), - { name: 'PBKDF2' }, - false, - ['deriveKey'] - ); - - return crypto.subtle.deriveKey( - { - name: 'PBKDF2', - salt, - iterations: PBKDF2_ITERATIONS, - hash: 'SHA-256' - }, - keyMaterial, - { name: 'AES-GCM', length: KEY_LENGTH }, - false, - ['encrypt', 'decrypt'] - ); -} - -// ===== Encryption ===== -/** - * Encrypts a message using AES-GCM with a derived key. - * @param {string} message - Plaintext message to encrypt. - * @param {string} password - User password for key derivation. - * @returns {Promise} - Base64-encoded encrypted string. - */ -export async function encryptAdvanced(message, password) { - const encoder = new TextEncoder(); - const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH)); - const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH)); - const key = await deriveKey(password, salt); - const encoded = encoder.encode(message); - - const ciphertext = await crypto.subtle.encrypt( - { name: 'AES-GCM', iv }, - key, - encoded - ); - - const output = new Uint8Array(salt.length + iv.length + ciphertext.byteLength); - output.set(salt); - output.set(iv, salt.length); - output.set(new Uint8Array(ciphertext), salt.length + iv.length); - - return btoa(String.fromCharCode(...output)); -} - -// ===== Decryption ===== -/** - * Decrypts an AES-GCM encrypted string. - * @param {string} encryptedData - Base64-encoded ciphertext. - * @param {string} password - Password used to derive the decryption key. - * @returns {Promise} - Decrypted plaintext. - */ -export async function decryptAdvanced(encryptedData, password) { - const encrypted = new Uint8Array( - atob(encryptedData).split('').map(c => c.charCodeAt(0)) - ); - - const salt = encrypted.slice(0, SALT_LENGTH); - const iv = encrypted.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH); - const ciphertext = encrypted.slice(SALT_LENGTH + IV_LENGTH); - const key = await deriveKey(password, salt); - - const decrypted = await crypto.subtle.decrypt( - { name: 'AES-GCM', iv }, - key, - ciphertext - ); - - return new TextDecoder().decode(decrypted); -} - -// ===== Module Initialization ===== -/** - * Initializes the encryption module and logs its status. - */ -export function setupEncryption() { - console.log('[Encryption] Module loaded'); -} +/** + * Encryption module. + * Handles cryptographic operations using Web Crypto API. + * Implements AES-GCM encryption with PBKDF2 key derivation. + */ + +// ===== Constants ===== +const SALT_LENGTH = 16; +const IV_LENGTH = 12; +const PBKDF2_ITERATIONS = 200_000; +const KEY_LENGTH = 256; + +// ===== Key Derivation ===== +/** + * Derives an AES-GCM key from a password using PBKDF2. + * @param {string} password - User-supplied password. + * @param {Uint8Array} salt - Randomly generated salt. + * @returns {Promise} - Derived cryptographic key. + */ +export async function deriveKey(password, salt) { + const encoder = new TextEncoder(); + const keyMaterial = await crypto.subtle.importKey( + 'raw', + encoder.encode(password), + { name: 'PBKDF2' }, + false, + ['deriveKey'] + ); + + return crypto.subtle.deriveKey( + { + name: 'PBKDF2', + salt, + iterations: PBKDF2_ITERATIONS, + hash: 'SHA-256' + }, + keyMaterial, + { name: 'AES-GCM', length: KEY_LENGTH }, + false, + ['encrypt', 'decrypt'] + ); +} + +// ===== Encryption ===== +/** + * Encrypts a message using AES-GCM with a derived key. + * @param {string} message - Plaintext message to encrypt. + * @param {string} password - User password for key derivation. + * @returns {Promise} - Base64-encoded encrypted string. + */ +export async function encryptAdvanced(message, password) { + const encoder = new TextEncoder(); + const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH)); + const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH)); + const key = await deriveKey(password, salt); + const encoded = encoder.encode(message); + + const ciphertext = await crypto.subtle.encrypt( + { name: 'AES-GCM', iv }, + key, + encoded + ); + + const output = new Uint8Array(salt.length + iv.length + ciphertext.byteLength); + output.set(salt); + output.set(iv, salt.length); + output.set(new Uint8Array(ciphertext), salt.length + iv.length); + + return btoa(String.fromCharCode(...output)); +} + +// ===== Decryption ===== +/** + * Decrypts an AES-GCM encrypted string. + * @param {string} encryptedData - Base64-encoded ciphertext. + * @param {string} password - Password used to derive the decryption key. + * @returns {Promise} - Decrypted plaintext. + */ +export async function decryptAdvanced(encryptedData, password) { + const encrypted = new Uint8Array( + atob(encryptedData).split('').map(c => c.charCodeAt(0)) + ); + + const salt = encrypted.slice(0, SALT_LENGTH); + const iv = encrypted.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH); + const ciphertext = encrypted.slice(SALT_LENGTH + IV_LENGTH); + const key = await deriveKey(password, salt); + + const decrypted = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv }, + key, + ciphertext + ); + + return new TextDecoder().decode(decrypted); +} + +// ===== Module Initialization ===== +/** + * Initializes the encryption module and logs its status. + */ +export function setupEncryption() { + console.log('[Encryption] Module loaded'); +} diff --git a/static/js/fileops.js b/static/js/fileops.js index 4047dd1..3743710 100644 --- a/static/js/fileops.js +++ b/static/js/fileops.js @@ -1,119 +1,119 @@ -/** - * File operations module. - * Handles file encryption and decryption operations. - */ - -// ===== Constants ===== -const CHUNK_SIZE = 1024 * 1024; // 1MB chunks - -// ===== Public Interface ===== -export async function encryptFile(fileInput, password) { - const file = fileInput.files[0]; - if (!file) return; - - try { - const encryptedChunks = await processFile(file, password, true); - downloadEncryptedFile(encryptedChunks, file.name); - } catch (error) { - alert("Error encrypting file: " + error.message); - } -} - -export async function decryptFile(fileInput, password) { - const file = fileInput.files[0]; - if (!file) return; - - try { - const decryptedChunks = await processFile(file, password, false); - downloadDecryptedFile(decryptedChunks, file.name); - } catch (error) { - alert("Error decrypting file: " + error.message); - } -} - -// ===== File Processing ===== -async function processFile(file, password, isEncrypt) { - const chunks = []; - const totalChunks = Math.ceil(file.size / CHUNK_SIZE); - let processedChunks = 0; - - for (let start = 0; start < file.size; start += CHUNK_SIZE) { - const chunk = file.slice(start, start + CHUNK_SIZE); - const arrayBuffer = await chunk.arrayBuffer(); - const uint8Array = new Uint8Array(arrayBuffer); - - const processedChunk = await processChunk(uint8Array, password, isEncrypt); - chunks.push(processedChunk); - - processedChunks++; - updateProgress(processedChunks, totalChunks); - } - - return chunks; -} - -async function processChunk(data, password, isEncrypt) { - const payload = { - "encryption-type": "advanced", - operation: isEncrypt ? "encrypt" : "decrypt", - message: Array.from(data).join(','), - password: password - }; - - const response = await fetch("/", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload) - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const result = await response.json(); - return new Uint8Array(result.result.split(',').map(Number)); -} - -// ===== File Download ===== -function downloadEncryptedFile(chunks, originalName) { - const blob = new Blob(chunks, { type: 'application/octet-stream' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = originalName + '.encrypted'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); -} - -function downloadDecryptedFile(chunks, originalName) { - const blob = new Blob(chunks, { type: 'application/octet-stream' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = originalName.replace('.encrypted', ''); - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); -} - -// ===== Progress Tracking ===== -function updateProgress(processed, total) { - const progressBar = document.getElementById("file-progress"); - const progressText = document.getElementById("file-progress-text"); - - if (progressBar && progressText) { - const percent = Math.round((processed / total) * 100); - progressBar.style.width = percent + "%"; - progressText.textContent = `Processing: ${percent}%`; - - if (processed === total) { - setTimeout(() => { - progressBar.style.width = "0%"; - progressText.textContent = ""; - }, 1000); - } - } -} +/** + * File operations module. + * Handles file encryption and decryption operations. + */ + +// ===== Constants ===== +const CHUNK_SIZE = 1024 * 1024; // 1MB chunks + +// ===== Public Interface ===== +export async function encryptFile(fileInput, password) { + const file = fileInput.files[0]; + if (!file) return; + + try { + const encryptedChunks = await processFile(file, password, true); + downloadEncryptedFile(encryptedChunks, file.name); + } catch (error) { + alert("Error encrypting file: " + error.message); + } +} + +export async function decryptFile(fileInput, password) { + const file = fileInput.files[0]; + if (!file) return; + + try { + const decryptedChunks = await processFile(file, password, false); + downloadDecryptedFile(decryptedChunks, file.name); + } catch (error) { + alert("Error decrypting file: " + error.message); + } +} + +// ===== File Processing ===== +async function processFile(file, password, isEncrypt) { + const chunks = []; + const totalChunks = Math.ceil(file.size / CHUNK_SIZE); + let processedChunks = 0; + + for (let start = 0; start < file.size; start += CHUNK_SIZE) { + const chunk = file.slice(start, start + CHUNK_SIZE); + const arrayBuffer = await chunk.arrayBuffer(); + const uint8Array = new Uint8Array(arrayBuffer); + + const processedChunk = await processChunk(uint8Array, password, isEncrypt); + chunks.push(processedChunk); + + processedChunks++; + updateProgress(processedChunks, totalChunks); + } + + return chunks; +} + +async function processChunk(data, password, isEncrypt) { + const payload = { + "encryption-type": "advanced", + operation: isEncrypt ? "encrypt" : "decrypt", + message: Array.from(data).join(','), + password: password + }; + + const response = await fetch("/", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + return new Uint8Array(result.result.split(',').map(Number)); +} + +// ===== File Download ===== +function downloadEncryptedFile(chunks, originalName) { + const blob = new Blob(chunks, { type: 'application/octet-stream' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = originalName + '.encrypted'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +function downloadDecryptedFile(chunks, originalName) { + const blob = new Blob(chunks, { type: 'application/octet-stream' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = originalName.replace('.encrypted', ''); + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +// ===== Progress Tracking ===== +function updateProgress(processed, total) { + const progressBar = document.getElementById("file-progress"); + const progressText = document.getElementById("file-progress-text"); + + if (progressBar && progressText) { + const percent = Math.round((processed / total) * 100); + progressBar.style.width = percent + "%"; + progressText.textContent = `Processing: ${percent}%`; + + if (processed === total) { + setTimeout(() => { + progressBar.style.width = "0%"; + progressText.textContent = ""; + }, 1000); + } + } +} diff --git a/static/js/pacman.js b/static/js/pacman.js index 9a25542..c24abf5 100644 --- a/static/js/pacman.js +++ b/static/js/pacman.js @@ -21,6 +21,13 @@ export function setupGame() { } export function startPacman() { + // Scroll to the Pacman section + const pacmanSection = document.getElementById("pacman-section"); + if (pacmanSection) { + pacmanSection.scrollIntoView({ behavior: 'smooth' }); + } + + // Initialize game state initializeGame(); setupGameLoop(); } @@ -28,6 +35,11 @@ export function startPacman() { export function stopPacman() { clearInterval(gameInterval); if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Restore scrolling + document.body.style.overflow = ''; + document.removeEventListener('wheel', preventScroll); + document.removeEventListener('touchmove', preventScroll); } export function resetGame() { @@ -68,6 +80,55 @@ function initializeGame() { pacman.dx = pacman.dy = 0; document.addEventListener("keydown", movePacman); + + // Prevent scrolling + document.body.style.overflow = 'hidden'; + document.addEventListener('wheel', preventScroll, { passive: false }); + document.addEventListener('touchmove', preventScroll, { passive: false }); + + // Add touch controls + let touchStartX = 0; + let touchStartY = 0; + + canvas.addEventListener('touchstart', (e) => { + e.preventDefault(); + touchStartX = e.touches[0].clientX; + touchStartY = e.touches[0].clientY; + }, { passive: false }); + + canvas.addEventListener('touchmove', (e) => { + e.preventDefault(); + }, { passive: false }); + + canvas.addEventListener('touchend', (e) => { + e.preventDefault(); + const touchEndX = e.changedTouches[0].clientX; + const touchEndY = e.changedTouches[0].clientY; + + const dx = touchEndX - touchStartX; + const dy = touchEndY - touchStartY; + + // Determine swipe direction based on the larger movement + if (Math.abs(dx) > Math.abs(dy)) { + // Horizontal swipe + if (dx > 0) { + pacman.dx = PACMAN_SPEED; + pacman.dy = 0; + } else { + pacman.dx = -PACMAN_SPEED; + pacman.dy = 0; + } + } else { + // Vertical swipe + if (dy > 0) { + pacman.dx = 0; + pacman.dy = PACMAN_SPEED; + } else { + pacman.dx = 0; + pacman.dy = -PACMAN_SPEED; + } + } + }, { passive: false }); } function setupGameLoop() { @@ -283,6 +344,20 @@ function eatDots() { return true; }); + // Check if all dots are eaten + if (dots.length === 0) { + // Trigger password generator for new random map + const generateBtn = document.getElementById("generate-btn"); + if (generateBtn) { + generateBtn.click(); + } + + // Auto-restart the game after a short delay + setTimeout(() => { + resetGame(); + }, 1000); + } + ctx.fillStyle = "white"; dots.forEach(d => { ctx.beginPath(); @@ -294,3 +369,8 @@ function eatDots() { // ===== Global Functions ===== window.resetGame = resetGame; window.exitGame = exitGame; + +// Add scroll prevention function +function preventScroll(e) { + e.preventDefault(); +} diff --git a/static/js/ui.js b/static/js/ui.js index b39ad6c..7f41a61 100644 --- a/static/js/ui.js +++ b/static/js/ui.js @@ -1,272 +1,301 @@ -๏ปฟ/** - * UI management module. - * Handles user interface interactions and form handling. - */ - -import { encryptFile, decryptFile } from './fileops.js'; - -// ===== UI Initialization ===== -export function setupUI() { - // Set initial state of remove button to hidden - const removeBtn = document.getElementById("remove-file-btn"); - if (removeBtn) { - removeBtn.style.display = "none"; - } - - initializeEventListeners(); -} - -// ===== Event Listeners ===== -function initializeEventListeners() { - const elements = { - encryptionType: document.getElementById("encryption-type"), - inputText: document.getElementById("input-text"), - form: document.getElementById("crypto-form"), - removeFileBtn: document.getElementById("remove-file-btn"), - clearAllBtn: document.getElementById("clear-all-btn"), - generateBtn: document.getElementById("generate-btn"), - copyPasswordBtn: document.getElementById("copy-btn"), - copyOutputBtn: document.getElementById("copy-output-btn"), - toggleSwitch: document.getElementById("operation-toggle"), - copyShareBtn: document.getElementById("copy-share-btn"), - shareLink: document.getElementById("share-link") - }; - - if (validateElements(elements)) { - setupElementListeners(elements); - } -} - -function validateElements(elements) { - return elements.encryptionType && elements.inputText && elements.form && - elements.removeFileBtn && elements.clearAllBtn && elements.generateBtn && - elements.copyPasswordBtn && elements.toggleSwitch; -} - -function setupElementListeners(elements) { - elements.encryptionType.addEventListener("change", toggleEncryptionOptions); - elements.inputText.addEventListener("input", handleInputChange); - elements.form.addEventListener("submit", handleSubmit); - elements.removeFileBtn.addEventListener("click", removeFile); - elements.clearAllBtn.addEventListener("click", clearAll); - elements.generateBtn.addEventListener("click", generateRandomPassword); - elements.copyPasswordBtn.addEventListener("click", () => copyToClipboard("generated-password", "password-copy-feedback")); - elements.copyOutputBtn?.addEventListener("click", () => copyToClipboard("output-text", "output-copy-feedback")); - elements.toggleSwitch.addEventListener("change", updateToggleLabels); - - // Add file input change listener - const fileInput = document.getElementById("file-input"); - if (fileInput) { - fileInput.addEventListener("change", () => { - const removeBtn = document.getElementById("remove-file-btn"); - if (removeBtn) { - removeBtn.style.display = fileInput.files.length > 0 ? "inline-block" : "none"; - } - }); - } - - setupShareLinkListeners(elements); -} - -function setupShareLinkListeners(elements) { - if (elements.copyShareBtn && elements.shareLink) { - elements.copyShareBtn.addEventListener("click", () => { - const linkText = elements.shareLink.textContent.trim(); - navigator.clipboard.writeText(linkText).then(() => { - const feedback = document.getElementById("shared-link-feedback"); - if (feedback) { - feedback.style.display = "block"; - feedback.classList.add("show"); - setTimeout(() => { - feedback.classList.remove("show"); - setTimeout(() => { - feedback.style.display = "none"; - }, 300); - }, 3000); - } - }); - }); - } -} - -// ===== UI State Management ===== -function toggleEncryptionOptions() { - const type = document.getElementById("encryption-type").value.trim().toLowerCase(); - const passwordInputWrapper = document.getElementById("password-input"); - const fileSection = document.querySelector("#encoding-section #file-section"); - const isAdvanced = type.includes("advanced"); - - if (passwordInputWrapper) { - if (isAdvanced) { - passwordInputWrapper.classList.remove("hidden"); - } else { - passwordInputWrapper.classList.add("hidden"); - } - } - - if (fileSection) { - if (isAdvanced) { - fileSection.classList.remove("hidden"); - } else { - fileSection.classList.add("hidden"); - } - } - - updateToggleLabels(); - toggleInputMode(); -} - -function updateToggleLabels() { - const type = document.getElementById("encryption-type")?.value; - const leftLabel = document.getElementById("toggle-left-label"); - const rightLabel = document.getElementById("toggle-right-label"); - - if (!type || !leftLabel || !rightLabel) return; - - const isAdvanced = type.toLowerCase().includes("advanced"); - leftLabel.textContent = isAdvanced ? "Encrypt" : "Encode"; - rightLabel.textContent = isAdvanced ? "Decrypt" : "Decode"; -} - -function toggleInputMode() { - const fileInput = document.getElementById("file-input"); - const textValue = document.getElementById("input-text")?.value.trim(); - const isAdvanced = document.getElementById("encryption-type")?.value === "advanced"; - - const textSection = document.getElementById("text-section"); - const fileSection = document.getElementById("file-section"); - const removeBtn = document.getElementById("remove-file-btn"); - - if (!fileInput || !textSection || !fileSection || !removeBtn) return; - - const fileSelected = fileInput.files.length > 0; - - textSection.style.display = fileSelected ? "none" : "flex"; - fileSection.style.display = (isAdvanced && !textValue) ? "flex" : "none"; - removeBtn.style.display = fileSelected ? "inline-block" : "none"; -} - -// ===== Form Handling ===== -async function handleSubmit(event) { - event.preventDefault(); - - const encryptionType = document.getElementById("encryption-type")?.value; - const password = document.getElementById("password")?.value; - const fileInput = document.getElementById("file-input"); - const isDecrypt = document.getElementById("operation-toggle").checked; - const operation = isDecrypt ? "decrypt" : "encrypt"; - - if (!encryptionType || !fileInput) return; - - if (encryptionType === "advanced" && !password) { - return alert("Password is required for advanced encryption."); - } - - if (fileInput.files.length > 0) { - return (operation === "encrypt") - ? encryptFile(fileInput, password) - : decryptFile(fileInput, password); - } - - await handleTextOperation(encryptionType, operation, password); -} - -async function handleTextOperation(encryptionType, operation, password) { - const payload = { - "encryption-type": encryptionType, - operation: operation, - message: document.getElementById("input-text")?.value, - password: password - }; - - try { - const response = await fetch("/", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload) - }); - const data = await response.json(); - document.getElementById("output-text").value = data.result; - } catch (err) { - alert("Error processing request: " + err.message); - } -} - -// ===== Utility Functions ===== -function removeFile() { - const fileInput = document.getElementById("file-input"); - if (fileInput) fileInput.value = ""; - const removeBtn = document.getElementById("remove-file-btn"); - if (removeBtn) removeBtn.style.display = 'none'; - toggleInputMode(); -} - -function generateRandomPassword() { - const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+[]{}|;:,.<>?/~"; - const length = 30; - const password = Array.from({ length }, () => - charset.charAt(Math.floor(Math.random() * charset.length)) - ).join(""); - const passwordField = document.getElementById("generated-password"); - if (passwordField) { - passwordField.value = password; - // Check if we should start Pacman - checkForPacman(); - } -} - -function copyToClipboard(elementId, feedbackId) { - const el = document.getElementById(elementId); - const feedback = document.getElementById(feedbackId); - - if (!el || !el.value) return; - - navigator.clipboard.writeText(el.value).then(() => { - if (feedback) { - feedback.style.display = "block"; - feedback.classList.add("show"); - setTimeout(() => { - feedback.classList.remove("show"); - setTimeout(() => { - feedback.style.display = "none"; - }, 300); // Wait for fade-out animation to complete - }, 3000); - } - }); -} - -function clearAll() { - const fields = ["input-text", "output-text", "file-input", "password"]; - fields.forEach(id => { - const el = document.getElementById(id); - if (el) el.value = ""; - }); - removeFile(); - toggleInputMode(); - document.getElementById("pacman-section")?.style.setProperty("display", "none"); - document.getElementById("encoding-section")?.style.setProperty("display", "block"); -} - -function handleInputChange() { - toggleInputMode(); - checkForPacman(); -} - -function checkForPacman() { - const val = document.getElementById("input-text").value.trim().toLowerCase(); - const pacSection = document.getElementById("pacman-section"); - const encSection = document.getElementById("encoding-section"); - - if (val.includes("pacman") && pacSection.style.display !== "block") { - pacSection.style.display = "block"; - encSection.style.display = "none"; - window.startPacman(); - } else if (pacSection.style.display === "block" && !val.includes("pacman")) { - window.exitGame(); - } -} - -function startPacman() { } -function exitGame() { } - - +๏ปฟ/** + * UI management module. + * Handles user interface interactions and form handling. + */ + +import { encryptFile, decryptFile } from './fileops.js'; + +// ===== UI Initialization ===== +export function setupUI() { + // Set initial state of remove button to hidden + const removeBtn = document.getElementById("remove-file-btn"); + if (removeBtn) { + removeBtn.style.display = "none"; + } + + initializeEventListeners(); +} + +// ===== Event Listeners ===== +function initializeEventListeners() { + const elements = { + encryptionType: document.getElementById("encryption-type"), + inputText: document.getElementById("input-text"), + form: document.getElementById("crypto-form"), + removeFileBtn: document.getElementById("remove-file-btn"), + clearAllBtn: document.getElementById("clear-all-btn"), + generateBtn: document.getElementById("generate-btn"), + copyPasswordBtn: document.getElementById("copy-btn"), + copyOutputBtn: document.getElementById("copy-output-btn"), + toggleSwitch: document.getElementById("operation-toggle"), + copyShareBtn: document.getElementById("copy-share-btn"), + shareLink: document.getElementById("share-link") + }; + + if (validateElements(elements)) { + setupElementListeners(elements); + } +} + +function validateElements(elements) { + return elements.encryptionType && elements.inputText && elements.form && + elements.removeFileBtn && elements.clearAllBtn && elements.generateBtn && + elements.copyPasswordBtn && elements.toggleSwitch; +} + +function setupElementListeners(elements) { + elements.encryptionType.addEventListener("change", toggleEncryptionOptions); + elements.inputText.addEventListener("input", handleInputChange); + elements.form.addEventListener("submit", handleSubmit); + elements.removeFileBtn.addEventListener("click", removeFile); + elements.clearAllBtn.addEventListener("click", clearAll); + elements.generateBtn.addEventListener("click", generateRandomPassword); + elements.copyPasswordBtn.addEventListener("click", () => copyToClipboard("generated-password", "password-copy-feedback")); + elements.copyOutputBtn?.addEventListener("click", () => copyToClipboard("output-text", "output-copy-feedback")); + elements.toggleSwitch.addEventListener("change", updateToggleLabels); + + // Add file input change listener + const fileInput = document.getElementById("file-input"); + if (fileInput) { + fileInput.addEventListener("change", () => { + const removeBtn = document.getElementById("remove-file-btn"); + if (removeBtn) { + removeBtn.style.display = fileInput.files.length > 0 ? "inline-block" : "none"; + } + }); + } + + setupShareLinkListeners(elements); +} + +function setupShareLinkListeners(elements) { + if (elements.copyShareBtn && elements.shareLink) { + elements.copyShareBtn.addEventListener("click", () => { + const linkText = elements.shareLink.textContent.trim(); + navigator.clipboard.writeText(linkText).then(() => { + const feedback = document.getElementById("shared-link-feedback"); + if (feedback) { + feedback.style.display = "block"; + feedback.classList.add("show"); + setTimeout(() => { + feedback.classList.remove("show"); + setTimeout(() => { + feedback.style.display = "none"; + }, 300); + }, 3000); + } + }); + }); + } +} + +// ===== UI State Management ===== +function toggleEncryptionOptions() { + const type = document.getElementById("encryption-type").value.trim().toLowerCase(); + const passwordInputWrapper = document.getElementById("password-input"); + const fileSection = document.querySelector("#encoding-section #file-section"); + const isAdvanced = type.includes("advanced"); + + if (passwordInputWrapper) { + if (isAdvanced) { + passwordInputWrapper.classList.remove("hidden"); + } else { + passwordInputWrapper.classList.add("hidden"); + } + } + + if (fileSection) { + if (isAdvanced) { + fileSection.classList.remove("hidden"); + } else { + fileSection.classList.add("hidden"); + } + } + + updateToggleLabels(); + toggleInputMode(); +} + +function updateToggleLabels() { + const type = document.getElementById("encryption-type")?.value; + const leftLabel = document.getElementById("toggle-left-label"); + const rightLabel = document.getElementById("toggle-right-label"); + + if (!type || !leftLabel || !rightLabel) return; + + const isAdvanced = type.toLowerCase().includes("advanced"); + leftLabel.textContent = isAdvanced ? "Encrypt" : "Encode"; + rightLabel.textContent = isAdvanced ? "Decrypt" : "Decode"; +} + +function toggleInputMode() { + const fileInput = document.getElementById("file-input"); + const textValue = document.getElementById("input-text")?.value.trim(); + const isAdvanced = document.getElementById("encryption-type")?.value === "advanced"; + + const textSection = document.getElementById("text-section"); + const fileSection = document.getElementById("file-section"); + const removeBtn = document.getElementById("remove-file-btn"); + + if (!fileInput || !textSection || !fileSection || !removeBtn) return; + + const fileSelected = fileInput.files.length > 0; + + textSection.style.display = fileSelected ? "none" : "flex"; + fileSection.style.display = (isAdvanced && !textValue) ? "flex" : "none"; + removeBtn.style.display = fileSelected ? "inline-block" : "none"; +} + +// ===== Form Handling ===== +async function handleSubmit(event) { + event.preventDefault(); + + const encryptionType = document.getElementById("encryption-type")?.value; + const password = document.getElementById("password")?.value; + const fileInput = document.getElementById("file-input"); + const isDecrypt = document.getElementById("operation-toggle").checked; + const operation = isDecrypt ? "decrypt" : "encrypt"; + + if (!encryptionType || !fileInput) return; + + if (encryptionType === "advanced" && !password) { + return alert("Password is required for advanced encryption."); + } + + if (fileInput.files.length > 0) { + return (operation === "encrypt") + ? encryptFile(fileInput, password) + : decryptFile(fileInput, password); + } + + await handleTextOperation(encryptionType, operation, password); +} + +async function handleTextOperation(encryptionType, operation, password) { + const payload = { + "encryption-type": encryptionType, + operation: operation, + message: document.getElementById("input-text")?.value, + password: password + }; + + try { + const response = await fetch("/", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) + }); + const data = await response.json(); + document.getElementById("output-text").value = data.result; + } catch (err) { + alert("Error processing request: " + err.message); + } +} + +// ===== Utility Functions ===== +function removeFile() { + const fileInput = document.getElementById("file-input"); + if (fileInput) fileInput.value = ""; + const removeBtn = document.getElementById("remove-file-btn"); + if (removeBtn) removeBtn.style.display = 'none'; + toggleInputMode(); +} + +function generateRandomPassword() { + const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+[]{}|;:,.<>?/~"; + const length = 30; + const password = Array.from({ length }, () => + charset.charAt(Math.floor(Math.random() * charset.length)) + ).join(""); + const passwordField = document.getElementById("generated-password"); + if (passwordField) { + passwordField.value = password; + // Check if we should start Pacman + checkForPacman(); + } +} + +function copyToClipboard(elementId, feedbackId) { + const el = document.getElementById(elementId); + const feedback = document.getElementById(feedbackId); + + if (!el || !el.value) return; + + // Create a temporary textarea element + const textarea = document.createElement('textarea'); + textarea.value = el.value; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + + // Select and copy the text + textarea.select(); + textarea.setSelectionRange(0, 99999); // For mobile devices + + try { + // Try using the modern clipboard API first + navigator.clipboard.writeText(el.value).then(() => { + showFeedback(feedback); + }).catch(() => { + // Fallback to execCommand for older browsers + document.execCommand('copy'); + showFeedback(feedback); + }); + } catch (err) { + // Final fallback + document.execCommand('copy'); + showFeedback(feedback); + } + + // Clean up + document.body.removeChild(textarea); +} + +function showFeedback(feedback) { + if (feedback) { + feedback.style.display = "block"; + feedback.classList.add("show"); + setTimeout(() => { + feedback.classList.remove("show"); + setTimeout(() => { + feedback.style.display = "none"; + }, 300); + }, 3000); + } +} + +function clearAll() { + const fields = ["input-text", "output-text", "file-input", "password"]; + fields.forEach(id => { + const el = document.getElementById(id); + if (el) el.value = ""; + }); + removeFile(); + toggleInputMode(); + document.getElementById("pacman-section")?.style.setProperty("display", "none"); + document.getElementById("encoding-section")?.style.setProperty("display", "block"); +} + +function handleInputChange() { + toggleInputMode(); + checkForPacman(); +} + +function checkForPacman() { + const val = document.getElementById("input-text").value.trim().toLowerCase(); + const pacSection = document.getElementById("pacman-section"); + const encSection = document.getElementById("encoding-section"); + + if (val.includes("pacman") && pacSection.style.display !== "block") { + pacSection.style.display = "block"; + encSection.style.display = "none"; + window.startPacman(); + } else if (pacSection.style.display === "block" && !val.includes("pacman")) { + window.exitGame(); + } +} + +function startPacman() { } +function exitGame() { } + +