diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..027fd11 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,298 @@ +# PacCrypt Security Features 🔒 + +This document outlines the security enhancements added to PacCrypt, including setup instructions and configuration options. + +## 🚀 New Security Features + +### 1. Rate Limiting +- **API Endpoints**: Prevents abuse with configurable rate limits +- **Default Limits**: + - `/api/algorithms`: 100 requests/minute + - `/api/encrypt`, `/api/decrypt`: 30 requests/minute + - `/api/generate-keypair`: 10 requests/minute + - `/api/pacshare`: 10 requests/minute + - Global default: 1000 requests/hour + +### 2. Session Timeout +- **Admin Sessions**: Automatic timeout after configurable period (default: 30 minutes) +- **Security**: Sessions are cleared and require re-authentication +- **Logging**: Session timeouts are logged for audit purposes + +### 3. File Virus Scanning +- **Integration**: ClamAV antivirus scanning before encryption +- **Automatic**: All uploaded files are scanned +- **Logging**: Scan results and virus detections are logged +- **Graceful Degradation**: If ClamAV is unavailable, scanning is skipped with warning + +### 4. IP Whitelisting +- **Admin Access**: Restrict admin panel access to specific IP addresses +- **CIDR Support**: Supports both single IPs and CIDR notation (e.g., `192.168.1.0/24`) +- **Flexible**: Empty whitelist allows all IPs (default behavior) +- **Logging**: Unauthorized access attempts are logged + +### 5. Enhanced Audit Logging +- **Encrypted Logs**: All admin actions are encrypted and logged +- **Comprehensive**: Login attempts, file operations, security events +- **IP Tracking**: Source IP addresses are logged for security monitoring + +## 🛠️ Installation & Setup + +### Prerequisites +```bash +# Update package lists +sudo apt update + +# Install Python dependencies +pip install -r application_data/requirements.txt +``` + +### ClamAV Setup (Required for Virus Scanning) + +#### Ubuntu/Debian: +```bash +# Install ClamAV +sudo apt install clamav clamav-daemon + +# Update virus definitions +sudo freshclam + +# Start ClamAV daemon +sudo systemctl start clamav-daemon +sudo systemctl enable clamav-daemon + +# Verify installation +sudo systemctl status clamav-daemon +``` + +#### CentOS/RHEL: +```bash +# Install EPEL repository +sudo yum install epel-release + +# Install ClamAV +sudo yum install clamav clamav-server clamav-update + +# Update virus definitions +sudo freshclam + +# Start services +sudo systemctl start clamd@scan +sudo systemctl enable clamd@scan +``` + +#### Manual Configuration: +If ClamAV fails to start, you may need to configure it manually: + +```bash +# Edit configuration +sudo nano /etc/clamav/clamd.conf + +# Remove or comment out the "Example" line +# Example + +# Set socket permissions +sudo chown clamav:clamav /var/run/clamav/clamd.ctl +sudo chmod 666 /var/run/clamav/clamd.ctl + +# Restart daemon +sudo systemctl restart clamav-daemon +``` + +### Testing ClamAV Integration +```bash +# Test if ClamAV is working +clamscan --version + +# Test daemon connection +clamdscan --version + +# Test with EICAR test file (harmless test virus) +echo 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > /tmp/eicar.txt +clamscan /tmp/eicar.txt +``` + +## ⚙️ Configuration + +### Admin Settings Panel +Access the admin settings at `/admin-settings` to configure: + +1. **Session Timeout**: Set admin session timeout (minutes) +2. **Virus Scanning**: Enable/disable ClamAV scanning +3. **IP Whitelist**: Configure allowed admin IP addresses +4. **File Limits**: Upload size and retention settings + +### Manual Configuration +Edit `application_data/settings.json`: + +```json +{ + "upload_folder": "pacshare", + "max_file_age_days": 14, + "max_file_size_bytes": 26843545600, + "admin_ip_whitelist": [ + "192.168.1.100", + "10.0.0.0/8", + "127.0.0.1" + ], + "virus_scanning_enabled": true, + "session_timeout_minutes": 30, + "rate_limit_per_minute": 60, + "rate_limit_per_hour": 1000 +} +``` + +### IP Whitelist Examples +```json +"admin_ip_whitelist": [ + "127.0.0.1", // Local access only + "192.168.1.100", // Specific IP + "192.168.1.0/24", // Local network + "10.0.0.0/8", // Private network range + "203.0.113.0/24" // Public IP range +] +``` + +## 🔍 Security Monitoring + +### Log Files +- **Admin Logs**: `application_data/admin_logs.enc` (encrypted) +- **Application Logs**: Check console output for security events + +### Key Events Logged +- Admin login/logout attempts +- Session timeouts +- IP whitelist violations +- Virus scan results +- File upload/download activities +- Rate limit violations + +### Viewing Admin Logs +Access encrypted logs via the admin panel at `/admin-logs` or programmatically: + +```python +# Example: View recent security events +key = load_admin_key() +cipher = Fernet(key) +with open('application_data/admin_logs.enc', 'rb') as f: + for line in f: + if line.strip(): + decrypted = cipher.decrypt(line.strip()) + print(decrypted.decode()) +``` + +## 🚨 Security Best Practices + +### 1. Regular Updates +```bash +# Update virus definitions +sudo freshclam + +# Update Python dependencies +pip install --upgrade -r application_data/requirements.txt +``` + +### 2. Firewall Configuration +```bash +# UFW example - restrict admin access +sudo ufw allow from 192.168.1.0/24 to any port 5000 +sudo ufw deny 5000 +``` + +### 3. HTTPS Configuration +Always use HTTPS in production. Example nginx config: + +```nginx +server { + listen 443 ssl http2; + server_name your-domain.com; + + # Rate limiting + limit_req_zone $binary_remote_addr zone=api:10m rate=10r/m; + + location /api/ { + limit_req zone=api burst=5 nodelay; + proxy_pass http://127.0.0.1:5000; + } + + location /admin { + # Additional admin restrictions + allow 192.168.1.0/24; + deny all; + proxy_pass http://127.0.0.1:5000; + } +} +``` + +### 4. Regular Security Audits +- Review admin logs regularly +- Monitor rate limit violations +- Check for unauthorized access attempts +- Verify virus scan effectiveness + +## 🐛 Troubleshooting + +### ClamAV Issues +```bash +# Check ClamAV status +sudo systemctl status clamav-daemon + +# View ClamAV logs +sudo journalctl -u clamav-daemon + +# Test socket connection +sudo -u clamav clamdscan --ping + +# Manual socket creation +sudo mkdir -p /var/run/clamav +sudo chown clamav:clamav /var/run/clamav +``` + +### Rate Limiting Issues +- Check if requests are being properly limited +- Verify Flask-Limiter configuration +- Monitor application logs for rate limit errors + +### Session Timeout Issues +- Verify session configuration in settings +- Check if `session.permanent = True` is set +- Ensure proper timezone handling + +### IP Whitelist Issues +- Verify IP address format (CIDR notation) +- Check if client IP is correctly detected +- Consider proxy/load balancer IP forwarding + +## 📋 Security Checklist + +- [ ] ClamAV installed and running +- [ ] Virus definitions up to date +- [ ] Admin IP whitelist configured +- [ ] Session timeout configured +- [ ] Rate limiting tested +- [ ] HTTPS enabled in production +- [ ] Firewall rules configured +- [ ] Regular log monitoring set up +- [ ] Backup procedures for encrypted logs +- [ ] Security update schedule established + +## 🔗 Related Documentation + +- [Main README](README.md) - General installation and usage +- [API Documentation](API.md) - API endpoint details +- [Roadmap](ROADMAP.md) - Future security enhancements + +--- + +**⚠️ Important Security Notes:** + +1. **Default Configuration**: By default, IP whitelisting is disabled (empty list). Configure it for production use. + +2. **ClamAV Dependency**: Virus scanning requires ClamAV. If not installed, scanning is skipped with warnings. + +3. **Rate Limiting**: Default limits are conservative. Adjust based on your usage patterns. + +4. **Log Encryption**: Admin logs are encrypted with the same key as admin credentials. Backup this key securely. + +5. **Session Security**: Sessions use Flask's built-in session management. Consider Redis for distributed deployments. + +For security questions or issues, please refer to the GitHub Issues page. \ No newline at end of file diff --git a/app.py b/app.py index a68e3b6..4ebe2e8 100644 --- a/app.py +++ b/app.py @@ -13,6 +13,12 @@ import sys import psutil from flask_cors import CORS from io import BytesIO +import ipaddress +from functools import wraps +import time +from collections import defaultdict +from datetime import datetime, timedelta +import clamd # ===== Third-Party Imports ===== from flask import ( @@ -27,6 +33,8 @@ from cryptography.fernet import Fernet import pyotp import qrcode from io import BytesIO +from flask_limiter import Limiter +from flask_limiter.util import get_remote_address # ===== PacCrypt Algorithm Imports ===== from paccrypt_algos import aes_cbc, aes_gcm, xchacha, rsa_hybrid @@ -37,6 +45,16 @@ app = Flask(__name__) app.secret_key = os.getenv("FLASK_SECRET", os.urandom(24)) CORS(app, origins=["https://pdf.unnaturalll.dev"]) +# Initialize rate limiter +limiter = Limiter( + key_func=get_remote_address, + default_limits=["1000 per hour"] +) +limiter.init_app(app) + +# Session timeout configuration +app.permanent_session_lifetime = timedelta(minutes=30) # 30 minute timeout + # ===== Constants ===== ADMIN_CRED_FILE = 'application_data/admin_creds.json' ADMIN_KEY_FILE = 'application_data/admin_key.key' @@ -46,7 +64,12 @@ SETTINGS_FILE = 'application_data/settings.json' DEFAULT_SETTINGS = { "upload_folder": "pacshare", "max_file_age_days": 14, - "max_file_size_bytes": 25 * 1024 * 1024 * 1024 # 25GB + "max_file_size_bytes": 25 * 1024 * 1024 * 1024, # 25GB + "admin_ip_whitelist": [], # Empty list means all IPs allowed + "virus_scanning_enabled": True, + "session_timeout_minutes": 30, + "rate_limit_per_minute": 60, + "rate_limit_per_hour": 1000 } # ===== Available Encryption Algorithms ===== @@ -228,6 +251,89 @@ def log_admin_event(message: str): except Exception as e: print("[ERROR] Failed to write admin log:", e) +# ===== Security Functions ===== +def check_ip_whitelist(ip_address): + """Check if IP address is in admin whitelist.""" + whitelist = settings.get("admin_ip_whitelist", []) + if not whitelist: # Empty list means all IPs allowed + return True + + try: + client_ip = ipaddress.ip_address(ip_address) + for allowed_ip in whitelist: + if '/' in allowed_ip: # CIDR notation + if client_ip in ipaddress.ip_network(allowed_ip, strict=False): + return True + else: # Single IP + if client_ip == ipaddress.ip_address(allowed_ip): + return True + return False + except ValueError: + return False + +def admin_required(f): + """Decorator to require admin authentication and IP whitelist check.""" + @wraps(f) + def decorated_function(*args, **kwargs): + # Check session timeout + if 'admin_logged_in' not in session: + return redirect(url_for('admin_login')) + + # Check if session has expired + if 'login_time' in session: + login_time = datetime.fromisoformat(session['login_time']) + if datetime.now() - login_time > timedelta(minutes=settings.get("session_timeout_minutes", 30)): + session.clear() + log_admin_event(f"Session expired for IP {request.remote_addr}") + return redirect(url_for('admin_login')) + + # Check IP whitelist + if not check_ip_whitelist(request.remote_addr): + log_admin_event(f"Unauthorized IP access attempt: {request.remote_addr}") + return jsonify({"error": "Access denied: IP not whitelisted"}), 403 + + return f(*args, **kwargs) + return decorated_function + +def scan_file_for_viruses(file_path): + """Scan file for viruses using ClamAV.""" + if not settings.get("virus_scanning_enabled", True): + return True, "Virus scanning disabled" + + try: + cd = clamd.ClamdUnixSocket() + # Test connection + cd.ping() + + # Scan file + result = cd.scan(file_path) + if result is None: + return True, "File is clean" + + # If infected + for file, status in result.items(): + if status[0] == 'FOUND': + return False, f"Virus detected: {status[1]}" + + return True, "File is clean" + + except clamd.ConnectionError: + # ClamAV daemon not running + log_admin_event("ClamAV daemon not available - virus scanning skipped") + return True, "ClamAV not available - scan skipped" + except Exception as e: + log_admin_event(f"Virus scan error: {str(e)}") + return True, f"Scan error: {str(e)}" + +def check_session_timeout(): + """Check if admin session has timed out.""" + if 'admin_logged_in' in session and 'login_time' in session: + login_time = datetime.fromisoformat(session['login_time']) + if datetime.now() - login_time > timedelta(minutes=settings.get("session_timeout_minutes", 30)): + session.clear() + return True + return False + # ===== File Management ===== def cleanup_expired_files(): """Remove files older than MAX_FILE_AGE_DAYS.""" @@ -284,6 +390,15 @@ def handle_file_upload(request): temp_path = os.path.join(UPLOAD_FOLDER, filename) file.save(temp_path) + # Virus scan the uploaded file + is_clean, scan_message = scan_file_for_viruses(temp_path) + if not is_clean: + os.remove(temp_path) # Remove infected file + log_admin_event(f"Virus detected in upload: {filename} - {scan_message}") + return jsonify({"error": f"File rejected: {scan_message}"}), 400 + + log_admin_event(f"File uploaded and scanned: {filename} - {scan_message}") + try: # Use the selected algorithm for encryption module = algo_config["module"] @@ -477,6 +592,7 @@ def handle_file_pickup(request, meta_path, enc_path, file_id): # ===== 2FA QR Code Routes ===== @app.route("/admin-qr") +@admin_required def admin_qr_code(): """Generate QR code for admin 2FA setup.""" if not session.get("admin_logged_in"): @@ -554,6 +670,7 @@ def generate_qr_code(file_id): # ===== Admin Routes ===== @app.route("/admin-logs") +@admin_required def admin_logs(): """View admin activity logs.""" if not session.get("admin_logged_in"): @@ -579,11 +696,9 @@ def admin_logs(): return jsonify(logs=logs) @app.route("/admin-settings", methods=["GET", "POST"]) +@admin_required 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': @@ -597,10 +712,23 @@ def handle_settings_update(request, current_settings): 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) + # Security settings + session_timeout_minutes = int(request.form.get('session_timeout_minutes', current_settings.get('session_timeout_minutes', 30))) + virus_scanning_enabled = request.form.get('virus_scanning_enabled') == 'on' + + # IP whitelist (one per line) + ip_whitelist_text = request.form.get('admin_ip_whitelist', '') + admin_ip_whitelist = [ip.strip() for ip in ip_whitelist_text.split('\n') if ip.strip()] + updated_settings = { "upload_folder": upload_folder, "max_file_age_days": max_file_age_days, - "max_file_size_bytes": max_file_size_bytes + "max_file_size_bytes": max_file_size_bytes, + "admin_ip_whitelist": admin_ip_whitelist, + "virus_scanning_enabled": virus_scanning_enabled, + "session_timeout_minutes": session_timeout_minutes, + "rate_limit_per_minute": current_settings.get("rate_limit_per_minute", 60), + "rate_limit_per_hour": current_settings.get("rate_limit_per_hour", 1000) } with open(SETTINGS_FILE, 'w') as f: @@ -645,9 +773,17 @@ def admin_login(): p = request.form.get("password") totp_code = request.form.get("totp_code") + # Check IP whitelist first + if not check_ip_whitelist(request.remote_addr): + log_admin_event(f"Login attempt from unauthorized IP: {request.remote_addr}") + flash("Access denied: IP not authorized") + return render_template("admin_login.html", requires_2fa=get_admin_2fa_status()) + if check_creds(u, p, totp_code): session["admin_logged_in"] = True - log_admin_event("Admin login successful.") + session["login_time"] = datetime.now().isoformat() + session.permanent = True # Enable session timeout + log_admin_event(f"Admin login successful from IP {request.remote_addr}") return redirect(url_for("admin_page")) else: log_admin_event("Admin login failed.") @@ -664,13 +800,9 @@ def admin_logout(): return redirect(url_for("index")) @app.route("/adminpage") +@admin_required 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'] @@ -748,6 +880,7 @@ exec "$2" "$3" return jsonify({"error": f"Failed to restart server: {str(e)}"}), 500 @app.route("/admin-reset", methods=["POST"]) +@admin_required def admin_reset(): """Reset admin credentials.""" if not session.get("admin_logged_in"): @@ -765,6 +898,7 @@ def admin_reset(): return redirect(url_for("admin_setup")) @app.route("/admin-change-password", methods=["POST"]) +@admin_required def admin_change_password(): """Change admin password.""" if not session.get("admin_logged_in"): @@ -800,6 +934,7 @@ def admin_change_password(): return redirect(url_for("admin_page")) @app.route("/admin-enable-2fa", methods=["POST"]) +@admin_required def admin_enable_2fa(): """Enable 2FA for admin account.""" if not session.get("admin_logged_in"): @@ -831,6 +966,7 @@ def admin_enable_2fa(): return redirect(url_for("admin_page")) @app.route("/admin-disable-2fa", methods=["POST"]) +@admin_required def admin_disable_2fa(): """Disable 2FA for admin account.""" if not session.get("admin_logged_in"): @@ -874,6 +1010,7 @@ def admin_disable_2fa(): return redirect(url_for("admin_page")) @app.route("/admin-clear-uploads", methods=["POST"]) +@admin_required def admin_clear_uploads(): """Clear all uploaded files.""" if not session.get("admin_logged_in"): @@ -892,6 +1029,7 @@ def admin_clear_uploads(): return redirect(url_for("admin_page")) @app.route("/admin-update-server", methods=["POST"]) +@admin_required def admin_update_server(): """Update server from GitHub repository.""" if not session.get("admin_logged_in"): @@ -974,6 +1112,7 @@ def admin_update_server(): return jsonify({"error": error_msg}), 500 @app.route("/admin-switch-dev-mode", methods=["POST"]) +@admin_required def admin_switch_dev_mode(): """Switch server to development mode.""" if not session.get("admin_logged_in"): @@ -995,6 +1134,7 @@ def admin_switch_dev_mode(): return jsonify({"error": error_msg}), 500 @app.route("/admin-switch-prod-mode", methods=["POST"]) +@admin_required def admin_switch_prod_mode(): """Switch server to production mode.""" if not session.get("admin_logged_in"): @@ -1048,6 +1188,7 @@ def robots_txt(): # ===== API Endpoints ===== @app.route("/api/algorithms", methods=["GET"]) +@limiter.limit("100 per minute") def api_algorithms(): """Get list of available encryption algorithms.""" algorithms = {} @@ -1062,6 +1203,7 @@ def api_algorithms(): return jsonify(algorithms=algorithms) @app.route("/api/generate-keypair", methods=["POST"]) +@limiter.limit("10 per minute") def api_generate_keypair(): """Generate RSA key pair for hybrid algorithms.""" try: @@ -1085,6 +1227,7 @@ def api_generate_keypair(): return jsonify({"error": str(e)}), 500 @app.route("/api/encrypt", methods=["POST"]) +@limiter.limit("30 per minute") def api_encrypt(): try: # Text encryption @@ -1164,6 +1307,7 @@ def api_encrypt(): return jsonify({"error": str(e)}), 500 @app.route("/api/decrypt", methods=["POST"]) +@limiter.limit("30 per minute") def api_decrypt(): try: # Text decryption @@ -1260,6 +1404,7 @@ def api_decrypt(): return jsonify({"error": str(e)}), 500 @app.route("/api/pacshare", methods=["POST"]) +@limiter.limit("10 per minute") def api_pacshare(): try: enc_password = request.form.get("enc_password") diff --git a/application_data/requirements.txt b/application_data/requirements.txt index f3daa0c..af86a7a 100644 --- a/application_data/requirements.txt +++ b/application_data/requirements.txt @@ -14,4 +14,13 @@ pqcrypto # Utility psutil +# Security and rate limiting +flask-limiter +clamd +ipaddress + +# TOTP for 2FA +pyotp +qrcode + # Run pip install -r application_data/requirements.txt diff --git a/static/css/styles.css b/static/css/styles.css index 42ed193..126cb54 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -5,6 +5,733 @@ padding: 0; } +/* ===== Bulk Operations Styles ===== */ +.drop-zone { + border: 2px dashed #00ff99; + border-radius: 8px; + padding: 40px 20px; + text-align: center; + background-color: #001100; + transition: all 0.3s ease; + cursor: pointer; +} + +.drop-zone:hover, +.drop-zone.drag-over { + background-color: #002200; + border-color: #00ff44; +} + +.drop-zone-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +.drop-zone-icon { + font-size: 2em; + margin-bottom: 10px; +} + +.file-list { + max-height: 300px; + overflow-y: auto; + border: 1px solid #333; + border-radius: 5px; + background-color: #111; +} + +.file-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 15px; + border-bottom: 1px solid #333; + background-color: #1a1a1a; +} + +.file-item:last-child { + border-bottom: none; +} + +.file-info { + display: flex; + flex-direction: column; + flex: 1; + gap: 5px; +} + +.file-name { + font-weight: bold; + color: #00ff99; +} + +.file-size { + font-size: 0.8em; + color: #888; +} + +.file-actions { + display: flex; + gap: 10px; +} + +.progress-container { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 10px; +} + +.progress-bar { + flex: 1; + height: 20px; + background-color: #333; + border-radius: 10px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background-color: #00ff99; + border-radius: 10px; + transition: width 0.3s ease; + width: 0%; +} + +.file-progress-list { + max-height: 200px; + overflow-y: auto; + border: 1px solid #333; + border-radius: 5px; + background-color: #111; +} + +.file-progress-item { + padding: 10px 15px; + border-bottom: 1px solid #333; + display: flex; + align-items: center; + justify-content: space-between; +} + +.file-progress-item:last-child { + border-bottom: none; +} + +.file-progress-name { + flex: 1; + font-size: 0.9em; + color: #ccc; +} + +.file-progress-status { + font-size: 0.8em; + padding: 3px 8px; + border-radius: 3px; +} + +.status-processing { + background-color: #ffaa00; + color: #000; +} + +.status-completed { + background-color: #00ff99; + color: #000; +} + +.status-error { + background-color: #ff4444; + color: #fff; +} + +.results-list { + max-height: 400px; + overflow-y: auto; + border: 1px solid #333; + border-radius: 5px; + background-color: #111; +} + +.result-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 15px; + border-bottom: 1px solid #333; +} + +.result-item:last-child { + border-bottom: none; +} + +.result-info { + flex: 1; +} + +.result-name { + font-weight: bold; + color: #00ff99; + margin-bottom: 5px; +} + +.result-details { + font-size: 0.8em; + color: #888; +} + +.result-actions { + display: flex; + gap: 10px; +} + +/* File Preview Styles */ +.file-preview-container { + max-height: 200px; + overflow-y: auto; + border: 1px solid #333; + border-radius: 5px; + background-color: #111; + margin-top: 10px; +} + +.file-preview-content { + padding: 15px; + font-family: monospace; + white-space: pre-wrap; + font-size: 0.8em; + color: #ccc; +} + +.image-preview { + max-width: 100%; + max-height: 150px; + border-radius: 5px; +} + +.file-preview-header { + padding: 10px 15px; + border-bottom: 1px solid #333; + background-color: #1a1a1a; + font-weight: bold; + color: #00ff99; +} + +/* ===== Password Settings Modal ===== */ +.settings-button { + background: none; + border: 2px solid #00ff99; + color: #00ff99; + border-radius: 50%; + width: 40px; + height: 40px; + font-size: 1.2em; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.settings-button:hover { + background-color: #00ff99; + color: #000; + transform: rotate(90deg); +} + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + backdrop-filter: blur(5px); +} + +.modal-content { + background-color: #1a1a1a; + border: 2px solid #00ff99; + border-radius: 10px; + width: 90%; + max-width: 600px; + max-height: 80vh; + overflow-y: auto; + box-shadow: 0 10px 30px rgba(0, 255, 153, 0.3); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px; + border-bottom: 1px solid #333; + background-color: #222; +} + +.modal-header h3 { + margin: 0; + color: #00ff99; + font-size: 1.1em; +} + +.close-button { + background: none; + border: none; + color: #ff6b6b; + font-size: 1.5em; + cursor: pointer; + padding: 0; + width: 30px; + height: 30px; + border-radius: 50%; + transition: all 0.3s ease; +} + +.close-button:hover { + background-color: #ff6b6b; + color: #fff; + transform: rotate(90deg); +} + +.modal-body { + padding: 20px; +} + +.modal-footer { + display: flex; + justify-content: space-between; + padding: 20px; + border-top: 1px solid #333; + background-color: #222; +} + +.setting-group { + margin-bottom: 25px; +} + +.setting-group h4 { + color: #00ff99; + margin-bottom: 15px; + font-size: 1em; + border-bottom: 1px solid #333; + padding-bottom: 5px; +} + +.length-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +.length-input-container { + display: flex; + align-items: center; + gap: 8px; +} + +.length-number-input { + width: 70px; + padding: 6px 10px; + background-color: #333; + border: 2px solid #666; + border-radius: 5px; + color: #00ff99; + font-weight: bold; + text-align: center; + font-size: 1em; + transition: all 0.3s ease; +} + +.length-number-input:focus { + outline: none; + border-color: #00ff99; + box-shadow: 0 0 10px rgba(0, 255, 153, 0.3); + background-color: #222; +} + +.length-number-input::-webkit-outer-spin-button, +.length-number-input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.length-number-input[type=number] { + -moz-appearance: textfield; +} + +.length-unit { + font-size: 0.9em; + color: #888; + font-weight: normal; +} + +.length-slider { + width: 100%; + height: 8px; + border-radius: 5px; + background: #333; + outline: none; + margin: 10px 0; + -webkit-appearance: none; +} + +.length-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 20px; + height: 20px; + border-radius: 50%; + background: #00ff99; + cursor: pointer; + box-shadow: 0 0 10px rgba(0, 255, 153, 0.5); + transition: all 0.3s ease; +} + +.length-slider::-webkit-slider-thumb:hover { + transform: scale(1.1); + box-shadow: 0 0 15px rgba(0, 255, 153, 0.7); +} + +.length-slider::-moz-range-thumb { + width: 20px; + height: 20px; + border-radius: 50%; + background: #00ff99; + cursor: pointer; + border: none; + box-shadow: 0 0 10px rgba(0, 255, 153, 0.5); + transition: all 0.3s ease; +} + +.length-slider::-moz-range-thumb:hover { + transform: scale(1.1); + box-shadow: 0 0 15px rgba(0, 255, 153, 0.7); +} + +.length-labels { + display: flex; + justify-content: space-between; + font-size: 0.8em; + color: #888; + margin-top: 5px; +} + +.checkbox-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; +} + +.checkbox-item { + display: flex; + align-items: center; + cursor: pointer; + padding: 10px; + border-radius: 5px; + transition: background-color 0.3s ease; + position: relative; +} + +.checkbox-item:hover { + background-color: #333; +} + +.checkbox-item input[type="checkbox"] { + display: none; +} + +.checkmark { + width: 20px; + height: 20px; + border: 2px solid #666; + border-radius: 3px; + margin-right: 10px; + position: relative; + transition: all 0.3s ease; +} + +.checkbox-item input[type="checkbox"]:checked + .checkmark { + background-color: #00ff99; + border-color: #00ff99; +} + +.checkbox-item input[type="checkbox"]:checked + .checkmark::after { + content: "✓"; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: #000; + font-weight: bold; + font-size: 0.9em; +} + +.custom-input { + width: 100%; + padding: 10px; + background-color: #333; + border: 1px solid #666; + border-radius: 5px; + color: #fff; + font-family: monospace; + margin-top: 5px; +} + +.custom-input:focus { + outline: none; + border-color: #00ff99; + box-shadow: 0 0 5px rgba(0, 255, 153, 0.3); +} + +.setting-hint { + font-size: 0.8em; + color: #888; + margin-top: 5px; +} + +.charset-preview { + background-color: #333; + border: 1px solid #666; + border-radius: 5px; + padding: 15px; + font-family: monospace; + font-size: 0.9em; + color: #ccc; + max-height: 100px; + overflow-y: auto; + word-break: break-all; + line-height: 1.4; +} + +.primary-button { + background-color: #00ff99; + color: #000; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + font-weight: bold; + transition: all 0.3s ease; +} + +.primary-button:hover { + background-color: #00cc77; + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(0, 255, 153, 0.3); +} + +.secondary-button { + background: none; + color: #ccc; + border: 1px solid #666; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + transition: all 0.3s ease; +} + +.secondary-button:hover { + background-color: #333; + border-color: #999; + color: #fff; +} + +/* Radio Button Styles */ +.mode-selection { + display: flex; + flex-direction: column; + gap: 15px; +} + +.radio-item { + display: flex; + align-items: center; + cursor: pointer; + padding: 15px; + border: 2px solid #333; + border-radius: 8px; + transition: all 0.3s ease; + position: relative; +} + +.radio-item:hover { + border-color: #666; + background-color: #333; +} + +.radio-item input[type="radio"] { + display: none; +} + +.radiomark { + width: 20px; + height: 20px; + border: 2px solid #666; + border-radius: 50%; + margin-right: 15px; + position: relative; + transition: all 0.3s ease; + flex-shrink: 0; +} + +.radio-item input[type="radio"]:checked + .radiomark { + border-color: #00ff99; + background-color: #00ff99; +} + +.radio-item input[type="radio"]:checked + .radiomark::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #000; +} + +.radio-item input[type="radio"]:checked { + border-color: #00ff99; + background-color: #001100; +} + +.radio-content { + flex: 1; +} + +.radio-title { + font-weight: bold; + color: #00ff99; + margin-bottom: 5px; +} + +.radio-description { + font-size: 0.8em; + color: #888; + line-height: 1.3; +} + +/* Size Input Styles */ +.size-input-container { + display: flex; + align-items: center; + gap: 8px; + margin-top: 5px; +} + +.size-number-input { + width: 80px; + padding: 6px 10px; + background-color: #333; + border: 2px solid #666; + border-radius: 5px; + color: #00ff99; + font-weight: bold; + text-align: center; + font-size: 1em; + transition: all 0.3s ease; +} + +.size-number-input:focus { + outline: none; + border-color: #00ff99; + box-shadow: 0 0 5px rgba(0, 255, 153, 0.3); + background-color: #222; +} + +.size-number-input::-webkit-outer-spin-button, +.size-number-input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.size-number-input[type=number] { + -moz-appearance: textfield; +} + +.size-unit { + font-size: 0.9em; + color: #888; + font-weight: normal; +} + +.setting-item { + margin-bottom: 10px; +} + +/* Enhanced Checkbox Styles for Descriptions */ +.checkbox-content { + flex: 1; +} + +.checkbox-title { + font-weight: bold; + color: #00ff99; + margin-bottom: 3px; + font-size: 0.95em; +} + +.checkbox-description { + font-size: 0.8em; + color: #888; + line-height: 1.3; +} + +/* Responsive adjustments */ +@media (max-width: 600px) { + .drop-zone { + padding: 30px 15px; + } + + .file-item, + .result-item { + flex-direction: column; + align-items: flex-start; + gap: 10px; + } + + .file-actions, + .result-actions { + width: 100%; + justify-content: flex-start; + } + + .progress-container { + flex-direction: column; + align-items: stretch; + gap: 10px; + } + + .modal-content { + width: 95%; + max-height: 90vh; + } + + .checkbox-grid { + grid-template-columns: 1fr; + } + + .modal-footer { + flex-direction: column; + gap: 10px; + } + + .settings-button { + width: 35px; + height: 35px; + font-size: 1em; + } +} + /* ===== Body ===== */ body { font-family: 'Press Start 2P', monospace; diff --git a/static/js/bulk-operations.js b/static/js/bulk-operations.js new file mode 100644 index 0000000..b8af62e --- /dev/null +++ b/static/js/bulk-operations.js @@ -0,0 +1,508 @@ +/** + * Bulk Operations Module + * Handles bulk file encryption/decryption, drag & drop, and file preview + */ + +class BulkOperations { + constructor() { + this.files = []; + this.results = []; + this.isProcessing = false; + this.setupEventListeners(); + this.populateAlgorithmDropdown(); + } + + setupEventListeners() { + // Drag & Drop Zone + const dropZone = document.getElementById('bulk-drop-zone'); + const fileInput = document.getElementById('bulk-file-input'); + const fileSelect = document.getElementById('bulk-file-select'); + + console.log('Bulk setup - dropZone:', dropZone, 'fileInput:', fileInput, 'fileSelect:', fileSelect); + + if (dropZone && fileInput) { + console.log('Setting up bulk drag & drop events'); + // Drag & Drop Events + dropZone.addEventListener('dragover', this.handleDragOver.bind(this)); + dropZone.addEventListener('dragleave', this.handleDragLeave.bind(this)); + dropZone.addEventListener('drop', this.handleDrop.bind(this)); + dropZone.addEventListener('click', () => { + console.log('Bulk drop zone clicked, opening file input'); + fileInput.click(); + }); + + // File Input Events + fileInput.addEventListener('change', this.handleFileSelect.bind(this)); + } else { + console.error('Bulk elements not found - dropZone:', dropZone, 'fileInput:', fileInput); + } + + if (fileSelect) { + console.log('Setting up bulk file select button'); + fileSelect.addEventListener('click', (e) => { + console.log('Bulk file select button clicked'); + e.stopPropagation(); + fileInput.click(); + }); + } else { + console.error('Bulk file select button not found'); + } + + // Control Buttons + const processBtn = document.getElementById('bulk-process-btn'); + const clearBtn = document.getElementById('bulk-clear-btn'); + const downloadAllBtn = document.getElementById('bulk-download-all'); + const resetBtn = document.getElementById('bulk-reset'); + + if (processBtn) processBtn.addEventListener('click', this.processFiles.bind(this)); + if (clearBtn) clearBtn.addEventListener('click', this.clearFiles.bind(this)); + if (downloadAllBtn) downloadAllBtn.addEventListener('click', this.downloadAllResults.bind(this)); + if (resetBtn) resetBtn.addEventListener('click', this.reset.bind(this)); + } + + handleDragOver(e) { + e.preventDefault(); + e.stopPropagation(); + document.getElementById('bulk-drop-zone').classList.add('drag-over'); + } + + handleDragLeave(e) { + e.preventDefault(); + e.stopPropagation(); + document.getElementById('bulk-drop-zone').classList.remove('drag-over'); + } + + handleDrop(e) { + e.preventDefault(); + e.stopPropagation(); + document.getElementById('bulk-drop-zone').classList.remove('drag-over'); + + const files = Array.from(e.dataTransfer.files); + this.addFiles(files); + } + + handleFileSelect(e) { + const files = Array.from(e.target.files); + this.addFiles(files); + } + + addFiles(newFiles) { + // Filter out duplicates + newFiles = newFiles.filter(newFile => + !this.files.some(existingFile => + existingFile.name === newFile.name && existingFile.size === newFile.size + ) + ); + + this.files.push(...newFiles); + this.updateFileList(); + this.showFilePreview(); + } + + updateFileList() { + const fileList = document.getElementById('bulk-file-list'); + if (!fileList) return; + + fileList.innerHTML = ''; + + this.files.forEach((file, index) => { + const fileItem = document.createElement('div'); + fileItem.className = 'file-item'; + fileItem.innerHTML = ` +
+
${file.name}
+
${this.formatFileSize(file.size)}
+
+
+ + +
+ `; + fileList.appendChild(fileItem); + }); + } + + showFilePreview() { + const previewSection = document.getElementById('bulk-file-preview'); + if (previewSection) { + previewSection.style.display = this.files.length > 0 ? 'block' : 'none'; + } + } + + async previewFile(index) { + const file = this.files[index]; + if (!file) return; + + const previewContainer = document.createElement('div'); + previewContainer.className = 'file-preview-container'; + + const header = document.createElement('div'); + header.className = 'file-preview-header'; + header.textContent = `Preview: ${file.name}`; + + const content = document.createElement('div'); + content.className = 'file-preview-content'; + + // Handle different file types + if (file.type.startsWith('text/') || this.isTextFile(file.name)) { + try { + const text = await this.readFileAsText(file); + content.textContent = text.length > 2000 ? text.substring(0, 2000) + '...' : text; + } catch (error) { + content.textContent = 'Error reading file: ' + error.message; + } + } else if (file.type.startsWith('image/')) { + const img = document.createElement('img'); + img.className = 'image-preview'; + img.src = URL.createObjectURL(file); + img.onload = () => URL.revokeObjectURL(img.src); + content.appendChild(img); + } else { + content.innerHTML = ` +
+ File Type: ${file.type || 'Unknown'}
+ Size: ${this.formatFileSize(file.size)}
+ Preview not available for this file type. +
+ `; + } + + previewContainer.appendChild(header); + previewContainer.appendChild(content); + + // Remove existing preview + const existingPreview = document.querySelector('.file-preview-container'); + if (existingPreview) { + existingPreview.remove(); + } + + // Add new preview after the file list + const fileList = document.getElementById('bulk-file-list'); + if (fileList) { + fileList.parentNode.insertBefore(previewContainer, fileList.nextSibling); + } + } + + isTextFile(filename) { + const textExtensions = ['.txt', '.md', '.js', '.html', '.css', '.json', '.xml', '.csv', '.log', '.py', '.java', '.c', '.cpp', '.h']; + return textExtensions.some(ext => filename.toLowerCase().endsWith(ext)); + } + + readFileAsText(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = e => resolve(e.target.result); + reader.onerror = e => reject(new Error('Failed to read file')); + reader.readAsText(file); + }); + } + + removeFile(index) { + this.files.splice(index, 1); + this.updateFileList(); + this.showFilePreview(); + + // Remove preview if it exists + const existingPreview = document.querySelector('.file-preview-container'); + if (existingPreview) { + existingPreview.remove(); + } + } + + clearFiles() { + this.files = []; + this.updateFileList(); + this.showFilePreview(); + + // Remove preview if it exists + const existingPreview = document.querySelector('.file-preview-container'); + if (existingPreview) { + existingPreview.remove(); + } + + // Clear file input + const fileInput = document.getElementById('bulk-file-input'); + if (fileInput) fileInput.value = ''; + } + + async processFiles() { + if (this.files.length === 0) { + alert('Please select files to process'); + return; + } + + const password = document.getElementById('bulk-password')?.value; + if (!password) { + alert('Please enter a password'); + return; + } + + const algorithm = document.getElementById('bulk-algorithm')?.value; + if (!algorithm) { + alert('Please select an algorithm'); + return; + } + + const isDecrypt = document.getElementById('bulk-operation-toggle')?.checked; + + this.isProcessing = true; + this.results = []; + + // Show progress section + const progressSection = document.getElementById('bulk-progress-section'); + if (progressSection) progressSection.style.display = 'block'; + + // Initialize progress + this.updateOverallProgress(0, this.files.length); + this.initializeFileProgress(); + + // Process files sequentially to avoid overwhelming the server + for (let i = 0; i < this.files.length; i++) { + const file = this.files[i]; + this.updateFileProgress(i, 'processing'); + + try { + const result = await this.processFile(file, password, algorithm, isDecrypt); + this.results.push({ file, result, success: true }); + this.updateFileProgress(i, 'completed'); + } catch (error) { + this.results.push({ file, error: error.message, success: false }); + this.updateFileProgress(i, 'error'); + } + + this.updateOverallProgress(i + 1, this.files.length); + } + + this.isProcessing = false; + this.showResults(); + } + + async processFile(file, password, algorithm, isDecrypt) { + const formData = new FormData(); + formData.append('file', file); + formData.append('enc_password', password); + formData.append('algorithm', algorithm); + + const endpoint = isDecrypt ? '/api/decrypt' : '/api/encrypt'; + + const response = await fetch(endpoint, { + method: 'POST', + body: formData + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Processing failed'); + } + + // Return the blob for download + return await response.blob(); + } + + updateOverallProgress(completed, total) { + const progressBar = document.getElementById('bulk-overall-bar'); + const progressText = document.getElementById('bulk-overall-text'); + + if (progressBar) { + const percentage = total > 0 ? (completed / total) * 100 : 0; + progressBar.style.width = `${percentage}%`; + } + + if (progressText) { + progressText.textContent = `${completed} / ${total} files processed`; + } + } + + initializeFileProgress() { + const progressList = document.getElementById('bulk-file-progress-list'); + if (!progressList) return; + + progressList.innerHTML = ''; + + this.files.forEach((file, index) => { + const progressItem = document.createElement('div'); + progressItem.className = 'file-progress-item'; + progressItem.innerHTML = ` +
${file.name}
+
Waiting
+ `; + progressList.appendChild(progressItem); + }); + } + + updateFileProgress(index, status) { + const statusElement = document.getElementById(`progress-status-${index}`); + if (!statusElement) return; + + statusElement.className = `file-progress-status status-${status}`; + + switch (status) { + case 'processing': + statusElement.textContent = 'Processing...'; + break; + case 'completed': + statusElement.textContent = 'Completed'; + break; + case 'error': + statusElement.textContent = 'Error'; + break; + default: + statusElement.textContent = 'Waiting'; + } + } + + showResults() { + const resultsSection = document.getElementById('bulk-results-section'); + if (!resultsSection) return; + + resultsSection.style.display = 'block'; + + const resultsList = document.getElementById('bulk-results-list'); + if (!resultsList) return; + + resultsList.innerHTML = ''; + + this.results.forEach((result, index) => { + const resultItem = document.createElement('div'); + resultItem.className = 'result-item'; + + const successCount = this.results.filter(r => r.success).length; + const totalCount = this.results.length; + + if (result.success) { + resultItem.innerHTML = ` +
+
✅ ${result.file.name}
+
Successfully processed
+
+
+ +
+ `; + } else { + resultItem.innerHTML = ` +
+
❌ ${result.file.name}
+
${result.error}
+
+ `; + } + + resultsList.appendChild(resultItem); + }); + + // Add summary + const summary = document.createElement('div'); + summary.style.cssText = 'padding: 15px; border-bottom: 1px solid #333; background-color: #1a1a1a; font-weight: bold;'; + summary.innerHTML = ` +
Processing Complete
+
+ ${successCount} successful, ${totalCount - successCount} failed out of ${totalCount} files +
+ `; + resultsList.insertBefore(summary, resultsList.firstChild); + } + + downloadResult(index) { + const result = this.results[index]; + if (!result.success) return; + + const isDecrypt = document.getElementById('bulk-operation-toggle')?.checked; + const algorithm = document.getElementById('bulk-algorithm')?.value; + + let filename; + if (isDecrypt) { + // For decryption, try to restore original filename + filename = result.file.name.replace(/\.(aes_cbc|aes_gcm|xchacha|rsa_hybrid)\.encrypted$/, ''); + } else { + // For encryption, add algorithm extension + filename = `${result.file.name}.${algorithm}.encrypted`; + } + + this.downloadBlob(result.result, filename); + } + + downloadAllResults() { + const successfulResults = this.results.filter(r => r.success); + + if (successfulResults.length === 0) { + alert('No successful results to download'); + return; + } + + successfulResults.forEach((result, index) => { + setTimeout(() => { + this.downloadResult(this.results.indexOf(result)); + }, index * 500); // Stagger downloads + }); + } + + downloadBlob(blob, filename) { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + + reset() { + this.clearFiles(); + this.results = []; + this.isProcessing = false; + + // Hide sections + const sections = ['bulk-progress-section', 'bulk-results-section']; + sections.forEach(id => { + const section = document.getElementById(id); + if (section) section.style.display = 'none'; + }); + + // Clear password + const passwordField = document.getElementById('bulk-password'); + if (passwordField) passwordField.value = ''; + } + + formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + } + + async populateAlgorithmDropdown() { + try { + const response = await fetch('/api/algorithms'); + const data = await response.json(); + + if (response.ok && data.algorithms) { + const dropdown = document.getElementById('bulk-algorithm'); + if (dropdown) { + dropdown.innerHTML = ''; + + for (const [key, algo] of Object.entries(data.algorithms)) { + if (algo.supports_file) { + const option = document.createElement('option'); + option.value = key; + option.textContent = algo.name; + dropdown.appendChild(option); + } + } + } + } + } catch (error) { + console.error('Failed to load algorithms for bulk operations:', error); + } + } +} + +// Initialize bulk operations when DOM is loaded +let bulkOps; +document.addEventListener('DOMContentLoaded', () => { + bulkOps = new BulkOperations(); + // Make bulkOps available globally for onclick handlers + window.bulkOps = bulkOps; +}); \ No newline at end of file diff --git a/static/js/crypto-settings.js b/static/js/crypto-settings.js new file mode 100644 index 0000000..5ce32b7 --- /dev/null +++ b/static/js/crypto-settings.js @@ -0,0 +1,333 @@ +/** + * Crypto Settings Module + * Handles the encryption settings modal and mode switching + */ + +class CryptoSettings { + constructor() { + this.currentMode = 'single'; + this.settings = { + processingMode: 'single', + enableFilePreview: true, + autoDownloadResults: true, + sequentialProcessing: true, + showDetailedProgress: true, + stopOnError: false, + maxFileSizeMB: 100 + }; + this.setupEventListeners(); + this.loadSettings(); + } + + setupEventListeners() { + // Modal controls + const settingsBtn = document.getElementById("crypto-settings-btn"); + const modal = document.getElementById("crypto-settings-modal"); + const closeBtn = document.getElementById("close-crypto-settings"); + const applyBtn = document.getElementById("apply-crypto-settings"); + const resetBtn = document.getElementById("reset-crypto-settings"); + + if (settingsBtn && modal) { + settingsBtn.addEventListener("click", () => { + modal.style.display = "flex"; + this.updateModalFromSettings(); + }); + } + + if (closeBtn && modal) { + closeBtn.addEventListener("click", () => { + modal.style.display = "none"; + }); + } + + // Close modal when clicking outside + if (modal) { + modal.addEventListener("click", (e) => { + if (e.target === modal) { + modal.style.display = "none"; + } + }); + } + + if (applyBtn && modal) { + applyBtn.addEventListener("click", () => { + this.applySettings(); + modal.style.display = "none"; + }); + } + + if (resetBtn) { + resetBtn.addEventListener("click", () => { + this.resetToDefaults(); + }); + } + + // Processing mode radio buttons + const singleModeRadio = document.getElementById("single-file-mode-radio"); + const bulkModeRadio = document.getElementById("bulk-file-mode-radio"); + + if (singleModeRadio) { + singleModeRadio.addEventListener("change", () => { + if (singleModeRadio.checked) { + this.toggleBulkOptions(false); + } + }); + } + + if (bulkModeRadio) { + bulkModeRadio.addEventListener("change", () => { + if (bulkModeRadio.checked) { + this.toggleBulkOptions(true); + } + }); + } + + // File size input validation + const fileSizeInput = document.getElementById("max-file-size-input"); + if (fileSizeInput) { + fileSizeInput.addEventListener("input", () => { + let value = parseInt(fileSizeInput.value); + if (value < 1) { + fileSizeInput.value = 1; + } else if (value > 1000) { + fileSizeInput.value = 1000; + } + }); + } + } + + toggleBulkOptions(show) { + const bulkOptions = document.getElementById("bulk-options"); + if (bulkOptions) { + bulkOptions.style.display = show ? "block" : "none"; + } + } + + updateModalFromSettings() { + // Set radio buttons + const singleModeRadio = document.getElementById("single-file-mode-radio"); + const bulkModeRadio = document.getElementById("bulk-file-mode-radio"); + + if (this.settings.processingMode === 'single') { + if (singleModeRadio) singleModeRadio.checked = true; + this.toggleBulkOptions(false); + } else { + if (bulkModeRadio) bulkModeRadio.checked = true; + this.toggleBulkOptions(true); + } + + // Set checkboxes + const checkboxes = [ + { id: "enable-file-preview", setting: "enableFilePreview" }, + { id: "auto-download-results", setting: "autoDownloadResults" }, + { id: "sequential-processing", setting: "sequentialProcessing" }, + { id: "show-detailed-progress", setting: "showDetailedProgress" }, + { id: "stop-on-error", setting: "stopOnError" } + ]; + + checkboxes.forEach(checkbox => { + const element = document.getElementById(checkbox.id); + if (element) { + element.checked = this.settings[checkbox.setting]; + } + }); + + // Set file size + const fileSizeInput = document.getElementById("max-file-size-input"); + if (fileSizeInput) { + fileSizeInput.value = this.settings.maxFileSizeMB; + } + } + + applySettings() { + // Get processing mode + const singleModeRadio = document.getElementById("single-file-mode-radio"); + const bulkModeRadio = document.getElementById("bulk-file-mode-radio"); + + if (singleModeRadio && singleModeRadio.checked) { + this.settings.processingMode = 'single'; + } else if (bulkModeRadio && bulkModeRadio.checked) { + this.settings.processingMode = 'bulk'; + } + + // Get checkbox values + const checkboxes = [ + { id: "enable-file-preview", setting: "enableFilePreview" }, + { id: "auto-download-results", setting: "autoDownloadResults" }, + { id: "sequential-processing", setting: "sequentialProcessing" }, + { id: "show-detailed-progress", setting: "showDetailedProgress" }, + { id: "stop-on-error", setting: "stopOnError" } + ]; + + checkboxes.forEach(checkbox => { + const element = document.getElementById(checkbox.id); + if (element) { + this.settings[checkbox.setting] = element.checked; + } + }); + + // Get file size + const fileSizeInput = document.getElementById("max-file-size-input"); + if (fileSizeInput) { + this.settings.maxFileSizeMB = parseInt(fileSizeInput.value) || 100; + } + + // Apply the mode change + this.switchMode(this.settings.processingMode); + + // Save settings + this.saveSettings(); + + // Show feedback + this.showFeedback("Settings applied successfully!"); + } + + switchMode(mode) { + this.currentMode = mode; + + const singleFileMode = document.getElementById("single-file-mode"); + const bulkFileMode = document.getElementById("bulk-file-mode"); + + if (mode === 'single') { + if (singleFileMode) singleFileMode.style.display = "block"; + if (bulkFileMode) bulkFileMode.style.display = "none"; + } else { + if (singleFileMode) singleFileMode.style.display = "none"; + if (bulkFileMode) bulkFileMode.style.display = "block"; + } + + // Update the form submit handler + this.updateFormHandler(); + } + + updateFormHandler() { + const cryptoForm = document.getElementById("crypto-form"); + if (!cryptoForm) return; + + // Remove existing event listeners by cloning the form + const newForm = cryptoForm.cloneNode(true); + cryptoForm.parentNode.replaceChild(newForm, cryptoForm); + + // Add the appropriate event listener + if (this.currentMode === 'single') { + newForm.addEventListener("submit", this.handleSingleFileSubmit.bind(this)); + } else { + newForm.addEventListener("submit", this.handleBulkFileSubmit.bind(this)); + } + } + + async handleSingleFileSubmit(event) { + event.preventDefault(); + + const algorithm = document.getElementById("algorithm")?.value; + const password = document.getElementById("password")?.value; + const fileInput = document.getElementById("file-input"); + const isDecrypt = document.getElementById("operation-toggle").checked; + + if (!algorithm || !fileInput) return; + + // Use existing single file handling logic + if (window.handleSubmit) { + window.handleSubmit(event); + } + } + + async handleBulkFileSubmit(event) { + event.preventDefault(); + + // Use bulk operations functionality + if (window.bulkOps && window.bulkOps.processFiles) { + await window.bulkOps.processFiles(); + } + } + + resetToDefaults() { + this.settings = { + processingMode: 'single', + enableFilePreview: true, + autoDownloadResults: true, + sequentialProcessing: true, + showDetailedProgress: true, + stopOnError: false, + maxFileSizeMB: 100 + }; + + this.updateModalFromSettings(); + this.showFeedback("Settings reset to defaults!"); + } + + loadSettings() { + try { + const saved = localStorage.getItem('paccrypt-crypto-settings'); + if (saved) { + this.settings = { ...this.settings, ...JSON.parse(saved) }; + } + } catch (error) { + console.warn('Failed to load crypto settings:', error); + } + + // Apply the loaded settings + this.switchMode(this.settings.processingMode); + } + + saveSettings() { + try { + localStorage.setItem('paccrypt-crypto-settings', JSON.stringify(this.settings)); + } catch (error) { + console.warn('Failed to save crypto settings:', error); + } + } + + showFeedback(message) { + // Use the existing feedback system or create a temporary one + const feedbackDiv = document.createElement('div'); + feedbackDiv.className = 'copy-feedback'; + feedbackDiv.textContent = message; + feedbackDiv.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background-color: #00ff99; + color: #000; + padding: 10px 20px; + border-radius: 5px; + font-weight: bold; + z-index: 10000; + display: block; + `; + + document.body.appendChild(feedbackDiv); + + setTimeout(() => { + feedbackDiv.style.opacity = '0'; + feedbackDiv.style.transition = 'opacity 0.3s ease'; + setTimeout(() => { + if (feedbackDiv.parentNode) { + feedbackDiv.parentNode.removeChild(feedbackDiv); + } + }, 300); + }, 2000); + } + + // Public methods for external access + getCurrentMode() { + return this.currentMode; + } + + getSettings() { + return { ...this.settings }; + } + + isBulkMode() { + return this.currentMode === 'bulk'; + } +} + +// Initialize crypto settings when DOM is loaded +let cryptoSettings; +document.addEventListener('DOMContentLoaded', () => { + cryptoSettings = new CryptoSettings(); +}); + +// Make cryptoSettings available globally +window.cryptoSettings = cryptoSettings; \ No newline at end of file diff --git a/static/js/pacshare-enhanced.js b/static/js/pacshare-enhanced.js new file mode 100644 index 0000000..f750d06 --- /dev/null +++ b/static/js/pacshare-enhanced.js @@ -0,0 +1,796 @@ +/** + * Enhanced PacShare Module + * Handles bulk uploads and single file uploads seamlessly + */ + +class PacShareEnhanced { + constructor() { + this.selectedFiles = []; + this.uploadResults = []; + this.settings = { + enable2FA: false, + autoClearPasswords: true, + autoCopyLinks: true, + showUploadProgress: true, + scrollToResults: true, + maxUploadSizeMB: 25, + validateFileTypes: false, + concurrentUploads: 1, + enableFilePreview: true, + rememberAlgorithm: true + }; + this.setupEventListeners(); + this.loadSettings(); + } + + setupEventListeners() { + // Drag & Drop Zone + const dropZone = document.getElementById('pacshare-drop-zone'); + const fileInput = document.getElementById('upload-file'); + const fileSelect = document.getElementById('pacshare-file-select'); + + console.log('PacShare setup - dropZone:', dropZone, 'fileInput:', fileInput, 'fileSelect:', fileSelect); + + if (dropZone && fileInput) { + console.log('Setting up PacShare drag & drop events'); + // Drag & Drop Events + dropZone.addEventListener('dragover', this.handleDragOver.bind(this)); + dropZone.addEventListener('dragleave', this.handleDragLeave.bind(this)); + dropZone.addEventListener('drop', this.handleDrop.bind(this)); + dropZone.addEventListener('click', () => { + console.log('PacShare drop zone clicked, opening file input'); + fileInput.click(); + }); + + // File Input Events + fileInput.addEventListener('change', this.handleFileSelect.bind(this)); + } else { + console.error('PacShare elements not found - dropZone:', dropZone, 'fileInput:', fileInput); + } + + if (fileSelect) { + console.log('Setting up PacShare file select button'); + fileSelect.addEventListener('click', (e) => { + console.log('PacShare file select button clicked'); + e.stopPropagation(); + fileInput.click(); + }); + } else { + console.error('PacShare file select button not found'); + } + + // Clear files button + const clearBtn = document.getElementById('pacshare-clear-files'); + if (clearBtn) { + clearBtn.addEventListener('click', this.clearFiles.bind(this)); + } + + // Enhanced form submission + const uploadForm = document.getElementById('upload-form'); + if (uploadForm) { + // Remove existing event listener first + uploadForm.replaceWith(uploadForm.cloneNode(true)); + const newForm = document.getElementById('upload-form'); + newForm.addEventListener('submit', this.handleEnhancedSubmit.bind(this)); + } + + // Settings modal controls + this.setupSettingsModal(); + } + + handleDragOver(e) { + e.preventDefault(); + e.stopPropagation(); + document.getElementById('pacshare-drop-zone').classList.add('drag-over'); + } + + handleDragLeave(e) { + e.preventDefault(); + e.stopPropagation(); + document.getElementById('pacshare-drop-zone').classList.remove('drag-over'); + } + + handleDrop(e) { + e.preventDefault(); + e.stopPropagation(); + document.getElementById('pacshare-drop-zone').classList.remove('drag-over'); + + const files = Array.from(e.dataTransfer.files); + this.addFiles(files); + } + + handleFileSelect(e) { + const files = Array.from(e.target.files); + this.addFiles(files); + } + + addFiles(newFiles) { + // Filter out duplicates + newFiles = newFiles.filter(newFile => + !this.selectedFiles.some(existingFile => + existingFile.name === newFile.name && existingFile.size === newFile.size + ) + ); + + this.selectedFiles.push(...newFiles); + this.updateFileDisplay(); + this.updateUI(); + } + + updateFileDisplay() { + const fileListContainer = document.getElementById('pacshare-file-list'); + const filesContainer = document.getElementById('pacshare-files-container'); + const uploadBtn = document.getElementById('pacshare-upload-btn'); + + if (!filesContainer || !fileListContainer) return; + + if (this.selectedFiles.length === 0) { + fileListContainer.style.display = 'none'; + if (uploadBtn) uploadBtn.textContent = 'Upload and Generate Link'; + return; + } + + fileListContainer.style.display = 'block'; + filesContainer.innerHTML = ''; + + // Update button text based on file count + if (uploadBtn) { + uploadBtn.textContent = this.selectedFiles.length === 1 + ? 'Upload and Generate Link' + : `Upload ${this.selectedFiles.length} Files and Generate Links`; + } + + this.selectedFiles.forEach((file, index) => { + const fileItem = document.createElement('div'); + fileItem.className = 'file-item'; + fileItem.innerHTML = ` +
+
${file.name}
+
${this.formatFileSize(file.size)}
+
+
+ + +
+ `; + filesContainer.appendChild(fileItem); + }); + } + + async previewFile(index) { + const file = this.selectedFiles[index]; + if (!file) return; + + const previewContainer = document.createElement('div'); + previewContainer.className = 'file-preview-container'; + + const header = document.createElement('div'); + header.className = 'file-preview-header'; + header.textContent = `Preview: ${file.name}`; + + const content = document.createElement('div'); + content.className = 'file-preview-content'; + + // Handle different file types + if (file.type.startsWith('text/') || this.isTextFile(file.name)) { + try { + const text = await this.readFileAsText(file); + content.textContent = text.length > 2000 ? text.substring(0, 2000) + '...' : text; + } catch (error) { + content.textContent = 'Error reading file: ' + error.message; + } + } else if (file.type.startsWith('image/')) { + const img = document.createElement('img'); + img.className = 'image-preview'; + img.src = URL.createObjectURL(file); + img.onload = () => URL.revokeObjectURL(img.src); + content.appendChild(img); + } else { + content.innerHTML = ` +
+ File Type: ${file.type || 'Unknown'}
+ Size: ${this.formatFileSize(file.size)}
+ Preview not available for this file type. +
+ `; + } + + previewContainer.appendChild(header); + previewContainer.appendChild(content); + + // Remove existing preview + const existingPreview = document.querySelector('.file-preview-container'); + if (existingPreview) { + existingPreview.remove(); + } + + // Add new preview after the file list + const fileList = document.getElementById('pacshare-files-container'); + if (fileList) { + fileList.parentNode.insertBefore(previewContainer, fileList.nextSibling); + } + } + + removeFile(index) { + this.selectedFiles.splice(index, 1); + this.updateFileDisplay(); + + // Remove preview if it exists + const existingPreview = document.querySelector('.file-preview-container'); + if (existingPreview) { + existingPreview.remove(); + } + } + + clearFiles() { + this.selectedFiles = []; + this.updateFileDisplay(); + + // Clear file input + const fileInput = document.getElementById('upload-file'); + if (fileInput) fileInput.value = ''; + + // Remove preview if it exists + const existingPreview = document.querySelector('.file-preview-container'); + if (existingPreview) { + existingPreview.remove(); + } + + this.hideResults(); + } + + async handleEnhancedSubmit(e) { + e.preventDefault(); + + if (this.selectedFiles.length === 0) { + alert('Please select at least one file to upload.'); + return; + } + + const algorithm = document.getElementById('share-algorithm')?.value; + const encPassword = document.querySelector('input[name="enc_password"]')?.value; + const pickupPassword = document.querySelector('input[name="pickup_password"]')?.value; + const enable2FA = this.settings.enable2FA; + + if (!algorithm || !encPassword || !pickupPassword) { + alert('Please fill in all required fields.'); + return; + } + + if (this.selectedFiles.length === 1) { + // Single file - use existing logic + await this.uploadSingleFile(this.selectedFiles[0], algorithm, encPassword, pickupPassword, enable2FA); + } else { + // Multiple files - use bulk upload + await this.uploadMultipleFiles(algorithm, encPassword, pickupPassword, enable2FA); + } + } + + async uploadSingleFile(file, algorithm, encPassword, pickupPassword, enable2FA) { + const formData = new FormData(); + formData.append('file', file); + formData.append('algorithm', algorithm); + formData.append('enc_password', encPassword); + formData.append('pickup_password', pickupPassword); + if (enable2FA) formData.append('enable_2fa', 'on'); + + try { + const response = await fetch('/', { + method: 'POST', + body: formData + }); + + const data = await response.json(); + + if (data.error) { + alert(data.error); + return; + } + + if (data.success && data.pickup_url) { + this.showSingleResult(data); + } + + } catch (error) { + alert('Error uploading file: ' + error.message); + } + } + + async uploadMultipleFiles(algorithm, encPassword, pickupPassword, enable2FA) { + this.uploadResults = []; + this.showProgress(); + + // Upload files sequentially to avoid overwhelming the server + for (let i = 0; i < this.selectedFiles.length; i++) { + const file = this.selectedFiles[i]; + this.updateFileProgress(i, 'uploading'); + + try { + const formData = new FormData(); + formData.append('file', file); + formData.append('algorithm', algorithm); + formData.append('enc_password', encPassword); + formData.append('pickup_password', pickupPassword); + if (enable2FA) formData.append('enable_2fa', 'on'); + + const response = await fetch('/', { + method: 'POST', + body: formData + }); + + const data = await response.json(); + + if (data.error) { + this.uploadResults.push({ file, error: data.error, success: false }); + this.updateFileProgress(i, 'error'); + } else if (data.success && data.pickup_url) { + this.uploadResults.push({ file, data, success: true }); + this.updateFileProgress(i, 'completed'); + } + + } catch (error) { + this.uploadResults.push({ file, error: error.message, success: false }); + this.updateFileProgress(i, 'error'); + } + + this.updateOverallProgress(i + 1, this.selectedFiles.length); + } + + this.showResults(); + } + + showProgress() { + const progressSection = document.getElementById('pacshare-progress'); + if (progressSection) { + progressSection.style.display = 'block'; + } + + this.updateOverallProgress(0, this.selectedFiles.length); + this.initializeFileProgress(); + } + + initializeFileProgress() { + const progressContainer = document.getElementById('pacshare-file-progress'); + if (!progressContainer) return; + + progressContainer.innerHTML = ''; + + this.selectedFiles.forEach((file, index) => { + const progressItem = document.createElement('div'); + progressItem.className = 'file-progress-item'; + progressItem.innerHTML = ` +
${file.name}
+
Waiting
+ `; + progressContainer.appendChild(progressItem); + }); + } + + updateFileProgress(index, status) { + const statusElement = document.getElementById(`pacshare-progress-${index}`); + if (!statusElement) return; + + statusElement.className = `file-progress-status status-${status}`; + + switch (status) { + case 'uploading': + statusElement.textContent = 'Uploading...'; + break; + case 'completed': + statusElement.textContent = 'Completed'; + break; + case 'error': + statusElement.textContent = 'Error'; + break; + default: + statusElement.textContent = 'Waiting'; + } + } + + updateOverallProgress(completed, total) { + const progressBar = document.getElementById('pacshare-overall-bar'); + const progressText = document.getElementById('pacshare-overall-text'); + + if (progressBar) { + const percentage = total > 0 ? (completed / total) * 100 : 0; + progressBar.style.width = `${percentage}%`; + } + + if (progressText) { + progressText.textContent = `${completed} / ${total} files uploaded`; + } + } + + showSingleResult(data) { + // Use existing single result display logic + const shareLink = document.getElementById('share-link'); + const shareLinkContainer = document.getElementById('share-link-container'); + + if (shareLink && shareLinkContainer) { + shareLink.href = data.pickup_url; + shareLink.textContent = data.pickup_url; + shareLinkContainer.style.display = 'flex'; + + // Handle 2FA if enabled + if (data.qr_code_url) { + this.showTwoFactorSetup(data.qr_code_url, data.service_name, data.totp_secret); + } + + // Clear form + this.clearForm(); + + // Scroll to results + shareLinkContainer.scrollIntoView({ behavior: 'smooth' }); + } + } + + showResults() { + const resultsSection = document.getElementById('pacshare-results'); + const resultsList = document.getElementById('pacshare-results-list'); + + if (!resultsSection || !resultsList) return; + + resultsSection.style.display = 'block'; + resultsList.innerHTML = ''; + + const successCount = this.uploadResults.filter(r => r.success).length; + const totalCount = this.uploadResults.length; + + // Add summary + const summary = document.createElement('div'); + summary.style.cssText = 'padding: 15px; border-bottom: 1px solid #333; background-color: #1a1a1a; font-weight: bold;'; + summary.innerHTML = ` +
Upload Complete
+
+ ${successCount} successful, ${totalCount - successCount} failed out of ${totalCount} files +
+ `; + resultsList.appendChild(summary); + + // Add individual results + this.uploadResults.forEach((result, index) => { + const resultItem = document.createElement('div'); + resultItem.className = 'result-item'; + + if (result.success) { + resultItem.innerHTML = ` +
+
✅ ${result.file.name}
+
+ ${result.data.pickup_url} +
+
+
+ +
+ `; + } else { + resultItem.innerHTML = ` +
+
❌ ${result.file.name}
+
${result.error}
+
+ `; + } + + resultsList.appendChild(resultItem); + }); + + // Clear form and scroll to results + this.clearForm(); + resultsSection.scrollIntoView({ behavior: 'smooth' }); + } + + copyLink(url) { + navigator.clipboard.writeText(url).then(() => { + this.showToast('Link copied to clipboard!'); + }).catch(() => { + // Fallback + const textArea = document.createElement('textarea'); + textArea.value = url; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + this.showToast('Link copied to clipboard!'); + }); + } + + showToast(message) { + const toast = document.createElement('div'); + toast.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background-color: #00ff99; + color: #000; + padding: 10px 20px; + border-radius: 5px; + font-weight: bold; + z-index: 10000; + opacity: 1; + transition: opacity 0.3s ease; + `; + toast.textContent = message; + + document.body.appendChild(toast); + + setTimeout(() => { + toast.style.opacity = '0'; + setTimeout(() => { + if (toast.parentNode) { + toast.parentNode.removeChild(toast); + } + }, 300); + }, 2000); + } + + clearForm() { + // Clear passwords but keep algorithm + const encPassword = document.querySelector('input[name="enc_password"]'); + const pickupPassword = document.querySelector('input[name="pickup_password"]'); + const enable2FA = document.getElementById('enable-2fa'); + + if (encPassword) encPassword.value = ''; + if (pickupPassword) pickupPassword.value = ''; + if (enable2FA) enable2FA.checked = false; + + // Clear selected files + this.clearFiles(); + } + + hideResults() { + const sections = ['pacshare-results', 'pacshare-progress', 'share-link-container']; + sections.forEach(id => { + const section = document.getElementById(id); + if (section) section.style.display = 'none'; + }); + } + + updateUI() { + // Hide results when new files are selected + this.hideResults(); + } + + // Utility methods + formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + } + + isTextFile(filename) { + const textExtensions = ['.txt', '.md', '.js', '.html', '.css', '.json', '.xml', '.csv', '.log', '.py', '.java', '.c', '.cpp', '.h']; + return textExtensions.some(ext => filename.toLowerCase().endsWith(ext)); + } + + readFileAsText(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = e => resolve(e.target.result); + reader.onerror = e => reject(new Error('Failed to read file')); + reader.readAsText(file); + }); + } + + showTwoFactorSetup(qrCodeUrl, serviceName, totpSecret) { + const container = document.getElementById('tfa-setup-container'); + const qrImage = document.getElementById('tfa-qr-image'); + const tfaString = document.getElementById('tfa-string'); + + if (container && qrImage && tfaString) { + qrImage.src = qrCodeUrl; + tfaString.value = totpSecret; + container.style.display = 'block'; + container.scrollIntoView({ behavior: 'smooth' }); + } + } + + // Settings Modal Methods + setupSettingsModal() { + const settingsBtn = document.getElementById("pacshare-settings-btn"); + const modal = document.getElementById("pacshare-settings-modal"); + const closeBtn = document.getElementById("close-pacshare-settings"); + const applyBtn = document.getElementById("apply-pacshare-settings"); + const resetBtn = document.getElementById("reset-pacshare-settings"); + + if (settingsBtn && modal) { + settingsBtn.addEventListener("click", () => { + modal.style.display = "flex"; + this.updateSettingsModal(); + }); + } + + if (closeBtn && modal) { + closeBtn.addEventListener("click", () => { + modal.style.display = "none"; + }); + } + + // Close modal when clicking outside + if (modal) { + modal.addEventListener("click", (e) => { + if (e.target === modal) { + modal.style.display = "none"; + } + }); + } + + if (applyBtn && modal) { + applyBtn.addEventListener("click", () => { + this.applySettings(); + modal.style.display = "none"; + }); + } + + if (resetBtn) { + resetBtn.addEventListener("click", () => { + this.resetSettings(); + }); + } + + // Input validation + const uploadSizeInput = document.getElementById("max-upload-size-input"); + const concurrentInput = document.getElementById("concurrent-uploads-input"); + + if (uploadSizeInput) { + uploadSizeInput.addEventListener("input", () => { + let value = parseInt(uploadSizeInput.value); + if (value < 1) uploadSizeInput.value = 1; + else if (value > 1000) uploadSizeInput.value = 1000; + this.updateSettingsSummary(); + }); + } + + if (concurrentInput) { + concurrentInput.addEventListener("input", () => { + let value = parseInt(concurrentInput.value); + if (value < 1) concurrentInput.value = 1; + else if (value > 10) concurrentInput.value = 10; + this.updateSettingsSummary(); + }); + } + + // Update summary when checkboxes change + const checkboxIds = [ + "enable-2fa-setting", "auto-clear-passwords", "auto-copy-links", + "show-upload-progress", "scroll-to-results", "validate-file-types", + "enable-file-preview", "remember-algorithm" + ]; + + checkboxIds.forEach(id => { + const checkbox = document.getElementById(id); + if (checkbox) { + checkbox.addEventListener("change", () => { + this.updateSettingsSummary(); + }); + } + }); + } + + updateSettingsModal() { + // Set checkbox values + const checkboxMap = { + "enable-2fa-setting": "enable2FA", + "auto-clear-passwords": "autoClearPasswords", + "auto-copy-links": "autoCopyLinks", + "show-upload-progress": "showUploadProgress", + "scroll-to-results": "scrollToResults", + "validate-file-types": "validateFileTypes", + "enable-file-preview": "enableFilePreview", + "remember-algorithm": "rememberAlgorithm" + }; + + Object.entries(checkboxMap).forEach(([id, setting]) => { + const checkbox = document.getElementById(id); + if (checkbox) { + checkbox.checked = this.settings[setting]; + } + }); + + // Set number inputs + const uploadSizeInput = document.getElementById("max-upload-size-input"); + const concurrentInput = document.getElementById("concurrent-uploads-input"); + + if (uploadSizeInput) uploadSizeInput.value = this.settings.maxUploadSizeMB; + if (concurrentInput) concurrentInput.value = this.settings.concurrentUploads; + + this.updateSettingsSummary(); + } + + updateSettingsSummary() { + const summary = document.getElementById("pacshare-settings-summary"); + if (!summary) return; + + const enable2FA = document.getElementById("enable-2fa-setting")?.checked || this.settings.enable2FA; + const autoClearPasswords = document.getElementById("auto-clear-passwords")?.checked || this.settings.autoClearPasswords; + const maxSize = document.getElementById("max-upload-size-input")?.value || this.settings.maxUploadSizeMB; + const concurrent = document.getElementById("concurrent-uploads-input")?.value || this.settings.concurrentUploads; + + summary.innerHTML = ` + • 2FA: ${enable2FA ? 'Enabled' : 'Disabled'}
+ • Auto-clear passwords: ${autoClearPasswords ? 'Yes' : 'No'}
+ • Max file size: ${maxSize} MB
+ • Upload mode: ${concurrent == 1 ? 'Sequential' : `${concurrent} concurrent`}
+ • File preview: ${this.settings.enableFilePreview ? 'Enabled' : 'Disabled'} + `; + } + + applySettings() { + // Get checkbox values + const checkboxMap = { + "enable-2fa-setting": "enable2FA", + "auto-clear-passwords": "autoClearPasswords", + "auto-copy-links": "autoCopyLinks", + "show-upload-progress": "showUploadProgress", + "scroll-to-results": "scrollToResults", + "validate-file-types": "validateFileTypes", + "enable-file-preview": "enableFilePreview", + "remember-algorithm": "rememberAlgorithm" + }; + + Object.entries(checkboxMap).forEach(([id, setting]) => { + const checkbox = document.getElementById(id); + if (checkbox) { + this.settings[setting] = checkbox.checked; + } + }); + + // Get number inputs + const uploadSizeInput = document.getElementById("max-upload-size-input"); + const concurrentInput = document.getElementById("concurrent-uploads-input"); + + if (uploadSizeInput) this.settings.maxUploadSizeMB = parseInt(uploadSizeInput.value) || 25; + if (concurrentInput) this.settings.concurrentUploads = parseInt(concurrentInput.value) || 1; + + this.saveSettings(); + this.showToast("PacShare settings applied successfully!"); + } + + resetSettings() { + this.settings = { + enable2FA: false, + autoClearPasswords: true, + autoCopyLinks: true, + showUploadProgress: true, + scrollToResults: true, + maxUploadSizeMB: 25, + validateFileTypes: false, + concurrentUploads: 1, + enableFilePreview: true, + rememberAlgorithm: true + }; + + this.updateSettingsModal(); + this.showToast("Settings reset to defaults!"); + } + + loadSettings() { + try { + const saved = localStorage.getItem('paccrypt-pacshare-settings'); + if (saved) { + this.settings = { ...this.settings, ...JSON.parse(saved) }; + } + } catch (error) { + console.warn('Failed to load PacShare settings:', error); + } + } + + saveSettings() { + try { + localStorage.setItem('paccrypt-pacshare-settings', JSON.stringify(this.settings)); + } catch (error) { + console.warn('Failed to save PacShare settings:', error); + } + } +} + +// Initialize enhanced PacShare when DOM is loaded +let pacShareEnhanced; +document.addEventListener('DOMContentLoaded', () => { + pacShareEnhanced = new PacShareEnhanced(); + // Make available globally for onclick handlers + window.pacShareEnhanced = pacShareEnhanced; +}); \ No newline at end of file diff --git a/static/js/ui.js b/static/js/ui.js index e408dd6..1927f68 100644 --- a/static/js/ui.js +++ b/static/js/ui.js @@ -61,6 +61,9 @@ function setupElementListeners(elements) { console.log("Mode:", elements.toggleSwitch.checked ? "Decrypt" : "Encrypt"); }); + // Password generator controls + setupPasswordGeneratorListeners(); + // Key pair management listeners elements.generateKeypairBtn?.addEventListener("click", generateAndDownloadKeyPair); elements.loadPublicKeyBtn?.addEventListener("click", () => elements.publicKeyFile?.click()); @@ -218,19 +221,379 @@ function removeFile() { toggleInputMode(); } +// ===== Advanced Password Generator ===== function generateRandomPassword() { - const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+[]{}|;:,.<>?/~"; - const length = 30; - const password = Array.from({ length }, () => - charset.charAt(Math.floor(Math.random() * charset.length)) - ).join(""); + const settings = getPasswordSettings(); + + if (!settings.charset || settings.charset.length === 0) { + alert("Please select at least one character type for password generation!"); + return; + } + + const password = generatePassword(settings.length, settings.charset); const passwordField = document.getElementById("generated-password"); + if (passwordField) { passwordField.value = password; + updatePasswordStrength(password); checkForPacman(); } } +function getPasswordSettings() { + const length = parseInt(document.getElementById("password-length-input")?.value || 16); + const includeUppercase = document.getElementById("include-uppercase")?.checked; + const includeLowercase = document.getElementById("include-lowercase")?.checked; + const includeNumbers = document.getElementById("include-numbers")?.checked; + const includeSpecial = document.getElementById("include-special")?.checked; + const excludeAmbiguous = document.getElementById("exclude-ambiguous")?.checked; + const customCharacters = document.getElementById("custom-characters")?.value || ""; + + let charset = ""; + + // Character sets + const sets = { + uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + lowercase: "abcdefghijklmnopqrstuvwxyz", + numbers: "0123456789", + special: "!@#$%^&*()_+-=[]{}|;:,.<>?/~" + }; + + // Ambiguous characters to exclude + const ambiguous = "0O1lI"; + + if (includeUppercase) charset += sets.uppercase; + if (includeLowercase) charset += sets.lowercase; + if (includeNumbers) charset += sets.numbers; + if (includeSpecial) charset += sets.special; + + // Add custom characters + if (customCharacters) { + charset += customCharacters; + } + + // Remove ambiguous characters if requested + if (excludeAmbiguous) { + charset = charset.split('').filter(char => !ambiguous.includes(char)).join(''); + } + + // Remove duplicates + charset = [...new Set(charset)].join(''); + + return { length, charset, settings: { includeUppercase, includeLowercase, includeNumbers, includeSpecial } }; +} + +function generatePassword(length, charset) { + // Use crypto.getRandomValues for cryptographically secure random generation + const array = new Uint32Array(length); + crypto.getRandomValues(array); + + return Array.from(array, (x) => charset[x % charset.length]).join(''); +} + +function updatePasswordStrength(password) { + const score = calculatePasswordStrength(password); + const strengthText = document.getElementById("password-strength-text"); + const strengthFill = document.getElementById("password-strength-fill"); + const strengthScore = document.getElementById("strength-score"); + const strengthFeedback = document.getElementById("strength-feedback"); + + if (!strengthText || !strengthFill || !strengthScore || !strengthFeedback) return; + + strengthScore.textContent = `Score: ${score.score}/100`; + strengthFeedback.textContent = score.feedback; + + // Update strength level and colors + let level, color, width; + if (score.score < 30) { + level = "Very Weak"; + color = "#ff4444"; + width = "20%"; + } else if (score.score < 50) { + level = "Weak"; + color = "#ff8800"; + width = "40%"; + } else if (score.score < 70) { + level = "Fair"; + color = "#ffaa00"; + width = "60%"; + } else if (score.score < 85) { + level = "Strong"; + color = "#88ff00"; + width = "80%"; + } else { + level = "Very Strong"; + color = "#00ff44"; + width = "100%"; + } + + strengthText.textContent = level; + strengthText.style.color = color; + strengthFill.style.backgroundColor = color; + strengthFill.style.width = width; +} + +function calculatePasswordStrength(password) { + if (!password) return { score: 0, feedback: "Enter a password to see strength analysis" }; + + let score = 0; + const feedback = []; + + // Length scoring + if (password.length >= 8) score += 10; + if (password.length >= 12) score += 10; + if (password.length >= 16) score += 10; + if (password.length >= 20) score += 5; + + // Character variety scoring + const hasLower = /[a-z]/.test(password); + const hasUpper = /[A-Z]/.test(password); + const hasNumber = /[0-9]/.test(password); + const hasSpecial = /[^a-zA-Z0-9]/.test(password); + + let varieties = 0; + if (hasLower) { score += 5; varieties++; } + if (hasUpper) { score += 5; varieties++; } + if (hasNumber) { score += 5; varieties++; } + if (hasSpecial) { score += 10; varieties++; } + + // Bonus for character variety + if (varieties >= 3) score += 10; + if (varieties === 4) score += 5; + + // Pattern penalties + if (/(.)\1{2,}/.test(password)) { + score -= 10; + feedback.push("Avoid repeating characters"); + } + + if (/123|abc|qwe|password|admin|test/i.test(password)) { + score -= 15; + feedback.push("Avoid common patterns or words"); + } + + // Entropy calculation + const uniqueChars = new Set(password).size; + const entropy = password.length * Math.log2(uniqueChars); + if (entropy > 60) score += 15; + else if (entropy > 40) score += 10; + else if (entropy > 30) score += 5; + + // Generate specific feedback + if (password.length < 8) feedback.push("Use at least 8 characters"); + if (password.length < 12) feedback.push("12+ characters recommended"); + if (!hasLower) feedback.push("Add lowercase letters"); + if (!hasUpper) feedback.push("Add uppercase letters"); + if (!hasNumber) feedback.push("Add numbers"); + if (!hasSpecial) feedback.push("Add special characters"); + + if (feedback.length === 0) { + feedback.push("Excellent password strength!"); + } + + return { + score: Math.min(100, Math.max(0, score)), + feedback: feedback.join(", ") + }; +} + +function setupPasswordGeneratorListeners() { + // Modal controls + const settingsBtn = document.getElementById("password-settings-btn"); + const modal = document.getElementById("password-settings-modal"); + const closeBtn = document.getElementById("close-password-settings"); + const applyBtn = document.getElementById("apply-password-settings"); + const resetBtn = document.getElementById("reset-password-settings"); + + if (settingsBtn && modal) { + settingsBtn.addEventListener("click", () => { + modal.style.display = "flex"; + updateCharsetPreview(); + }); + } + + if (closeBtn && modal) { + closeBtn.addEventListener("click", () => { + modal.style.display = "none"; + }); + } + + // Close modal when clicking outside + if (modal) { + modal.addEventListener("click", (e) => { + if (e.target === modal) { + modal.style.display = "none"; + } + }); + } + + if (applyBtn && modal) { + applyBtn.addEventListener("click", () => { + generateRandomPassword(); + modal.style.display = "none"; + showPasswordFeedback("Settings applied and password regenerated!"); + }); + } + + if (resetBtn) { + resetBtn.addEventListener("click", () => { + resetPasswordSettings(); + updateCharsetPreview(); + showPasswordFeedback("Settings reset to defaults!"); + }); + } + + // Length controls (slider and number input) + const lengthSlider = document.getElementById("password-length"); + const lengthInput = document.getElementById("password-length-input"); + + if (lengthSlider && lengthInput) { + // Sync slider to number input + lengthSlider.addEventListener("input", () => { + lengthInput.value = lengthSlider.value; + updateCharsetPreview(); + }); + + // Sync number input to slider + lengthInput.addEventListener("input", () => { + let value = parseInt(lengthInput.value); + + // Validate bounds + if (value < 8) { + value = 8; + lengthInput.value = 8; + } else if (value > 128) { + value = 128; + lengthInput.value = 128; + } + + lengthSlider.value = value; + updateCharsetPreview(); + }); + + // Handle edge cases for number input + lengthInput.addEventListener("blur", () => { + if (!lengthInput.value || lengthInput.value < 8) { + lengthInput.value = 8; + lengthSlider.value = 8; + updateCharsetPreview(); + } + }); + + // Allow Enter key to apply changes + lengthInput.addEventListener("keypress", (e) => { + if (e.key === "Enter") { + lengthInput.blur(); + } + }); + } + + // Character set checkboxes + const checkboxes = [ + "include-uppercase", + "include-lowercase", + "include-numbers", + "include-special", + "exclude-ambiguous" + ]; + + checkboxes.forEach(id => { + const checkbox = document.getElementById(id); + if (checkbox) { + checkbox.addEventListener("change", () => { + updateCharsetPreview(); + }); + } + }); + + // Custom characters input + const customCharsInput = document.getElementById("custom-characters"); + if (customCharsInput) { + customCharsInput.addEventListener("input", () => { + updateCharsetPreview(); + }); + } + + // Password visibility toggle + const toggleVisibilityBtn = document.getElementById("toggle-password-visibility"); + const passwordField = document.getElementById("generated-password"); + + if (toggleVisibilityBtn && passwordField) { + toggleVisibilityBtn.addEventListener("click", () => { + if (passwordField.type === "password") { + passwordField.type = "text"; + toggleVisibilityBtn.textContent = "🙈"; + } else { + passwordField.type = "password"; + toggleVisibilityBtn.textContent = "👁️"; + } + }); + } + + // Use password in form button + const usePasswordBtn = document.getElementById("use-password-btn"); + if (usePasswordBtn) { + usePasswordBtn.addEventListener("click", () => { + const generatedPassword = document.getElementById("generated-password")?.value; + const passwordInput = document.getElementById("password"); + + if (generatedPassword && passwordInput) { + passwordInput.value = generatedPassword; + showPasswordFeedback("Password applied to form!"); + } + }); + } + + // Monitor password field for manual changes to update strength + if (passwordField) { + passwordField.addEventListener("input", () => { + updatePasswordStrength(passwordField.value); + }); + } + + // Generate initial password + generateRandomPassword(); +} + +function updateCharsetPreview() { + const settings = getPasswordSettings(); + const preview = document.getElementById("charset-preview"); + + if (preview) { + if (settings.charset && settings.charset.length > 0) { + preview.textContent = `Characters (${settings.charset.length}): ${settings.charset}`; + } else { + preview.textContent = "⚠️ No character types selected! Please select at least one character type."; + preview.style.color = "#ff6b6b"; + } + } +} + +function resetPasswordSettings() { + // Reset to default values + document.getElementById("password-length").value = 16; + document.getElementById("password-length-input").value = 16; + document.getElementById("include-uppercase").checked = true; + document.getElementById("include-lowercase").checked = true; + document.getElementById("include-numbers").checked = true; + document.getElementById("include-special").checked = true; + document.getElementById("exclude-ambiguous").checked = false; + document.getElementById("custom-characters").value = ""; +} + +function showPasswordFeedback(message) { + const feedback = document.getElementById("password-copy-feedback"); + if (feedback) { + const originalText = feedback.textContent; + feedback.textContent = message; + showFeedback(feedback); + // Reset feedback text after showing + setTimeout(() => { + feedback.textContent = originalText; + }, 3000); + } +} + function copyToClipboard(elementId, feedbackId) { const el = document.getElementById(elementId); const feedback = document.getElementById(feedbackId); diff --git a/templates/index.html b/templates/index.html index 3a59107..458bd60 100644 --- a/templates/index.html +++ b/templates/index.html @@ -15,6 +15,9 @@ + + + @@ -32,17 +35,343 @@
-

Password Generator

+
+

Password Generator

+ +
+ +
- -
- + +
+ + +
+ + +
+
+ + No Password +
+
+
+
+
+
Score: 0/100
+
+
+
+ +
+ +
Password copied to clipboard!
+ + + + + + + + +

Key Management

@@ -106,7 +435,12 @@
-

Encrypt & Decrypt

+
+

Encrypt & Decrypt

+ +
@@ -159,8 +493,55 @@
- - + +
+ + +
+ + +
@@ -180,8 +561,15 @@
-

PacShare

-

Securely share encrypted files.

+
+
+

PacShare

+

Securely share encrypted files.

+
+ +
{% with messages = get_flashed_messages() %} @@ -206,6 +594,63 @@ + + +
+ + +
+ + +
+ +
+
+
📤
+

Drag & drop files here or

+

Single file or multiple files supported

+
+ +
+ + + +
+ + + + +
+ +
+ + + + + + + + - +

⚠️ SAVE THIS QR CODE OR STRING NOW! It will not be shown again for security reasons.

Recommended apps: Google Authenticator, Authy, Microsoft Authenticator

- +
-
- -
- - -
- - - - - -
- -
- -
- -
-

BOTH PASSWORDS ARE REQUIRED FOR PICKUP

+