diff --git a/API.md b/API.md new file mode 100644 index 0000000..edf7a82 --- /dev/null +++ b/API.md @@ -0,0 +1,595 @@ +# PacCrypt API Documentation ๐Ÿ” + +> [!IMPORTANT] +> This document is fully AI generated, pending my review. +> It is only here so I can push to alpha. +> Next push will contain human oversite on the documentation. + +This document provides AI slop documentation for the PacCrypt REST API, covering all endpoints for encryption, decryption, file sharing, and administrative operations. + +## ๐ŸŒ Base URLs + +- **Official Instance**: `https://paccrypt.unnaturalll.dev` +- **Self-hosted**: `http://YOUR_SERVER_IP:5000` or `https://your-domain.com` + +## ๐Ÿ“‹ Table of Contents + +- [Authentication](#authentication) +- [Encryption Operations](#encryption-operations) +- [PacShare File Sharing](#pacshare-file-sharing) +- [Administrative Endpoints](#administrative-endpoints) +- [Error Handling](#error-handling) +- [Examples](#examples) + +--- + +## ๐Ÿ”‘ Authentication + +Most API endpoints are **public** and don't require authentication. Admin endpoints require session-based authentication. + +### Admin Authentication Flow + +1. **Setup Admin Account** (first-time only) + ``` + POST /admin-setup + ``` + +2. **Login** + ``` + POST /admin-login + ``` + +3. **Access Admin Endpoints** (with session) + +--- + +## ๐Ÿ” Encryption Operations + +### 1. Get Available Algorithms + +Get list of supported encryption algorithms and their capabilities. + +**Endpoint**: `GET /api/algorithms` + +**Response**: +```json +{ + "algorithms": { + "aes_gcm": { + "name": "AES-GCM", + "description": "AES-256 with GCM mode (authenticated encryption)", + "supports_text": true, + "supports_file": false, + "requires_keypair": false + }, + "aes_cbc": { + "name": "AES-CBC", + "description": "AES-256 with CBC mode and HMAC authentication", + "supports_text": true, + "supports_file": true, + "requires_keypair": false + }, + "xchacha": { + "name": "XChaCha20-Poly1305", + "description": "XChaCha20 stream cipher with Poly1305 authentication", + "supports_text": true, + "supports_file": true, + "requires_keypair": false + }, + "rsa_hybrid": { + "name": "RSA Hybrid", + "description": "RSA-4096 with AES hybrid encryption", + "supports_text": true, + "supports_file": true, + "requires_keypair": true + } + } +} +``` + +### 2. Generate RSA Key Pair + +Generate RSA key pairs for hybrid encryption algorithms. + +**Endpoint**: `POST /api/generate-keypair` + +**Request Body**: +```json +{ + "algorithm": "rsa_hybrid" +} +``` + +**Response**: +```json +{ + "private_key": "-----BEGIN RSA PRIVATE KEY-----\n...", + "public_key": "-----BEGIN PUBLIC KEY-----\n..." +} +``` + +### 3. Text Encryption + +Encrypt text using various algorithms. + +**Endpoint**: `POST /api/encrypt` + +**Content-Type**: `application/json` + +#### Password-based Algorithms (AES-GCM, AES-CBC, XChaCha20) + +**Request Body**: +```json +{ + "message": "Hello, World!", + "password": "your_secure_password", + "algorithm": "aes_gcm" +} +``` + +#### Key-based Algorithms (RSA Hybrid) + +**Request Body**: +```json +{ + "message": "Hello, World!", + "public_key": "-----BEGIN PUBLIC KEY-----\n...", + "algorithm": "rsa_hybrid" +} +``` + +**Response**: +```json +{ + "result": "base64_encrypted_text", + "algorithm": "aes_gcm" +} +``` + +### 4. Text Decryption + +Decrypt text using various algorithms. + +**Endpoint**: `POST /api/decrypt` + +**Content-Type**: `application/json` + +#### Password-based Algorithms + +**Request Body**: +```json +{ + "message": "base64_encrypted_text", + "password": "your_secure_password", + "algorithm": "aes_gcm" +} +``` + +#### Key-based Algorithms + +**Request Body**: +```json +{ + "message": "base64_encrypted_text", + "private_key": "-----BEGIN RSA PRIVATE KEY-----\n...", + "algorithm": "rsa_hybrid" +} +``` + +**Response**: +```json +{ + "result": "Hello, World!" +} +``` + +### 5. File Encryption + +Encrypt files and download the encrypted result. + +**Endpoint**: `POST /api/encrypt` + +**Content-Type**: `multipart/form-data` + +**Form Data**: +- `file`: File to encrypt +- `enc_password`: Encryption password +- `algorithm`: Algorithm to use (`aes_cbc`, `xchacha`, `rsa_hybrid`) + +**Response**: Binary file download with `.{algorithm}.encrypted` extension + +### 6. File Decryption + +Decrypt files and download the decrypted result. + +**Endpoint**: `POST /api/decrypt` + +**Content-Type**: `multipart/form-data` + +**Form Data**: +- `file`: Encrypted file +- `enc_password`: Decryption password +- `algorithm`: Algorithm used (optional, auto-detected from filename) + +**Response**: Binary file download with original filename + +--- + +## ๐Ÿ“ค PacShare File Sharing + +### 1. Upload File for Sharing + +Upload and encrypt a file for secure sharing with pickup URLs. + +**Endpoint**: `POST /api/pacshare` + +**Content-Type**: `multipart/form-data` + +**Form Data**: +- `file`: File to upload and share +- `enc_password`: Password to encrypt the file +- `pickup_password`: Password required to access pickup page +- `algorithm`: Encryption algorithm (`aes_cbc`, `xchacha`, `rsa_hybrid`) +- `enable_2fa`: Set to `"on"` to enable 2FA (optional) + +**Response**: +```json +{ + "pickup_url": "https://paccrypt.unnaturalll.dev/pickup/abc123def456", + "qr_code_url": "https://paccrypt.unnaturalll.dev/qr/abc123def456", + "totp_secret": "BASE32SECRET", + "service_name": "PacCrypt File: document.pdf..." +} +``` + +**Note**: QR code URL and TOTP fields only present if 2FA is enabled. + +### 2. Generate 2FA QR Code + +Get QR code for 2FA setup for a specific file. + +**Endpoint**: `GET /qr/{file_id}` + +**Response**: PNG image (QR code) + +--- + +## ๐Ÿ› ๏ธ Administrative Endpoints + +### 1. Admin Setup + +Create initial admin account (first-time setup only). + +**Endpoint**: `POST /admin-setup` + +**Content-Type**: `application/x-www-form-urlencoded` + +**Form Data**: +- `username`: Admin username +- `password`: Admin password +- `enable_2fa`: Set to `"on"` to enable 2FA (optional) + +### 2. Admin Login + +Authenticate admin user and create session. + +**Endpoint**: `POST /admin-login` + +**Content-Type**: `application/x-www-form-urlencoded` + +**Form Data**: +- `username`: Admin username +- `password`: Admin password +- `totp_code`: 2FA code (if 2FA enabled) + +### 3. Admin Logout + +End admin session. + +**Endpoint**: `GET /admin-logout` + +### 4. View Admin Logs + +Get encrypted admin activity logs. + +**Endpoint**: `GET /admin-logs` + +**Response**: +```json +{ + "logs": [ + "[2025-01-15 10:30:00] Admin login successful.", + "[2025-01-15 10:31:00] File abc123 downloaded and deleted." + ] +} +``` + +### 5. Server Control + +#### Restart Server +**Endpoint**: `POST /restart-server` + +#### Switch to Development Mode +**Endpoint**: `POST /admin-switch-dev-mode` + +#### Switch to Production Mode +**Endpoint**: `POST /admin-switch-prod-mode` + +#### Update from GitHub +**Endpoint**: `POST /admin-update-server` + +### 6. File Management + +#### Clear All Uploads +**Endpoint**: `POST /admin-clear-uploads` + +### 7. Admin Account Management + +#### Change Password +**Endpoint**: `POST /admin-change-password` + +**Form Data**: +- `current_password`: Current admin password +- `new_password`: New admin password + +#### Enable 2FA +**Endpoint**: `POST /admin-enable-2fa` + +#### Disable 2FA +**Endpoint**: `POST /admin-disable-2fa` + +**Form Data**: +- `totp_code`: Current 2FA code + +#### Reset Admin Credentials +**Endpoint**: `POST /admin-reset` + +--- + +## โŒ Error Handling + +All API endpoints return appropriate HTTP status codes and error messages. + +### Common HTTP Status Codes + +- `200 OK`: Successful operation +- `400 Bad Request`: Invalid request parameters +- `401 Unauthorized`: Authentication required +- `404 Not Found`: Resource not found +- `500 Internal Server Error`: Server error + +### Error Response Format + +```json +{ + "error": "Descriptive error message" +} +``` + +### Common Errors + +- `"Missing message"`: Required message field not provided +- `"Invalid algorithm"`: Unsupported encryption algorithm +- `"Algorithm does not support text/file operations"`: Algorithm limitation +- `"Public/Private key required for this algorithm"`: Missing key for RSA +- `"Password required"`: Missing password for symmetric algorithms +- `"Encryption/Decryption failed"`: Cryptographic operation failed + +--- + +## ๐ŸŒŸ Examples + +### Self-Hosted Usage + +Replace `localhost:5000` with your server's IP address or domain. + +#### Text Encryption Example + +```bash +curl -X POST "http://localhost:5000/api/encrypt" \ + -H "Content-Type: application/json" \ + -d '{ + "message": "Confidential information", + "password": "super_secure_password", + "algorithm": "aes_gcm" + }' +``` + +#### File Upload Example + +```bash +curl -X POST "http://localhost:5000/api/pacshare" \ + -F "file=@confidential.pdf" \ + -F "enc_password=file_encryption_key" \ + -F "pickup_password=pickup_key_123" \ + -F "algorithm=aes_cbc" \ + -F "enable_2fa=on" +``` + +#### RSA Key Generation and Usage + +```bash +# 1. Generate key pair +curl -X POST "http://localhost:5000/api/generate-keypair" \ + -H "Content-Type: application/json" \ + -d '{"algorithm": "rsa_hybrid"}' > keys.json + +# 2. Extract public key and encrypt +PUBLIC_KEY=$(cat keys.json | jq -r '.public_key') +curl -X POST "http://localhost:5000/api/encrypt" \ + -H "Content-Type: application/json" \ + -d "{ + \"message\": \"RSA encrypted message\", + \"public_key\": \"$PUBLIC_KEY\", + \"algorithm\": \"rsa_hybrid\" + }" +``` + +### Official Instance Usage + +#### Text Encryption with Official API + +```bash +curl -X POST "https://paccrypt.unnaturalll.dev/api/encrypt" \ + -H "Content-Type: application/json" \ + -d '{ + "message": "Hello from the official API!", + "password": "my_password_123", + "algorithm": "xchacha" + }' +``` + +#### PacShare Upload to Official Instance + +```bash +curl -X POST "https://paccrypt.unnaturalll.dev/api/pacshare" \ + -F "file=@document.docx" \ + -F "enc_password=encrypt_me_please" \ + -F "pickup_password=come_get_it" \ + -F "algorithm=xchacha" +``` + +#### File Encryption with Official API + +```bash +curl -X POST "https://paccrypt.unnaturalll.dev/api/encrypt" \ + -F "file=@sensitive_data.txt" \ + -F "enc_password=strong_password" \ + -F "algorithm=aes_cbc" \ + -o encrypted_file.aes_cbc.encrypted +``` + +### Python Integration Example + +```python +import requests +import json + +# Official API base URL +BASE_URL = "https://paccrypt.unnaturalll.dev" + +# Text encryption +def encrypt_text(message, password, algorithm="aes_gcm"): + response = requests.post(f"{BASE_URL}/api/encrypt", + json={ + "message": message, + "password": password, + "algorithm": algorithm + }) + return response.json() + +# File sharing via PacShare +def share_file(file_path, enc_password, pickup_password, algorithm="aes_cbc"): + with open(file_path, 'rb') as f: + response = requests.post(f"{BASE_URL}/api/pacshare", + files={"file": f}, + data={ + "enc_password": enc_password, + "pickup_password": pickup_password, + "algorithm": algorithm + }) + return response.json() + +# Usage examples +encrypted = encrypt_text("Secret message", "my_password") +print(f"Encrypted: {encrypted['result']}") + +file_share = share_file("document.pdf", "encrypt123", "pickup123") +print(f"Pickup URL: {file_share['pickup_url']}") +``` + +### JavaScript/Browser Example + +```javascript +// Text encryption using fetch API +async function encryptText(message, password, algorithm = 'aes_gcm') { + const response = await fetch('https://paccrypt.unnaturalll.dev/api/encrypt', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + message: message, + password: password, + algorithm: algorithm + }) + }); + + return await response.json(); +} + +// File upload for sharing +async function shareFile(fileInput, encPassword, pickupPassword) { + const formData = new FormData(); + formData.append('file', fileInput.files[0]); + formData.append('enc_password', encPassword); + formData.append('pickup_password', pickupPassword); + formData.append('algorithm', 'aes_cbc'); + + const response = await fetch('https://paccrypt.unnaturalll.dev/api/pacshare', { + method: 'POST', + body: formData + }); + + return await response.json(); +} + +// Usage +encryptText("Hello API!", "password123").then(result => { + console.log("Encrypted:", result.result); +}); +``` + +--- + +## ๐Ÿ”’ Security Best Practices + +### For API Consumers + +1. **Always use HTTPS** in production environments +2. **Use strong passwords** with high entropy +3. **Implement proper error handling** for failed operations +4. **Don't log sensitive data** like passwords or private keys +5. **Validate inputs** before sending to the API +6. **Use 2FA** for sensitive file shares +7. **Implement rate limiting** to prevent abuse + +### For Self-Hosted Instances + +1. **Configure reverse proxy** (nginx/apache) with HTTPS +2. **Set up firewall rules** to restrict access +3. **Regular security updates** for all dependencies +4. **Monitor admin logs** for suspicious activity +5. **Backup encrypted data** regularly +6. **Use strong admin passwords** with 2FA enabled +7. **Configure CORS** appropriately for your use case + +--- + +## ๐Ÿš€ Advanced Usage + +### Batch Operations + +You can process multiple files or messages by making multiple API calls. Consider implementing: + +- **Concurrent requests** for better performance +- **Progress tracking** for large batches +- **Error retry logic** for failed operations + +### Integration Patterns + +- **CLI Tools**: Build command-line interfaces using the API +- **Web Applications**: Integrate encryption into existing apps +- **Mobile Apps**: Use the API for mobile encryption needs +- **Automation Scripts**: Automate encryption workflows + +### Performance Considerations + +- **File Size Limits**: Check instance-specific upload limits +- **Rate Limiting**: Respect API rate limits if implemented +- **Caching**: Cache algorithm information to reduce API calls +- **Connection Pooling**: Reuse HTTP connections for multiple requests + +--- + +*For more information, see the main [README.md](README.md) or visit the [official instance](https://paccrypt.unnaturalll.dev).* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a9983be --- /dev/null +++ b/README.md @@ -0,0 +1,408 @@ +# PacCrypt ๐Ÿ” + +**PacCrypt** is a modern, secure web application for encrypting and decrypting text and files using multiple encryption algorithms. Built with Flask and featuring a comprehensive REST API, modular encryption engines, and advanced security features including 2FA support. + +> [!IMPORTANT] +> This document contains AI generated pieces that have not been reviewed yet. +> Next push will contain human oversite on the documentation. + +**๐ŸŒ Official Instance**: [paccrypt.unnaturalll.dev](https://paccrypt.unnaturalll.dev) + +--- + +## โœจ Features + +### ๐Ÿ”’ **Multi-Algorithm Encryption** +- **AES-GCM**: Text encryption with authenticated encryption +- **AES-CBC**: Text and file encryption with HMAC authentication +- **XChaCha20-Poly1305**: Modern stream cipher for text and files +- **RSA Hybrid**: RSA-4096 with AES hybrid encryption for text and files + +### ๐ŸŒ **Comprehensive API** +- RESTful API endpoints for all encryption operations +- Text and file encryption/decryption +- Key pair generation for RSA hybrid +- PacShare file sharing with secure pickup URLs +- Full API documentation (see [API.md](API.md)) + +### ๐Ÿ“ **PacShare - Secure File Sharing** +- End-to-end encrypted file uploads +- Dual-password system (pickup + encryption) +- Optional 2FA with TOTP codes +- QR code generation for 2FA setup +- Automatic file expiration +- Secure pickup URLs with one-time download + +### ๐Ÿ›ก๏ธ **Advanced Security** +- Admin panel with 2FA support +- Encrypted admin credentials and logs +- Secure session management +- PBKDF2 key derivation with 200,000 iterations +- Cryptographically secure random ID generation + +### ๐ŸŽฎ **Built-in Entertainment** +- Hidden Pac-Man game (type `pacman` to play) +- Arrow key and swipe controls +- Retro gaming experience with authentic sounds + +### ๐Ÿงพ **Admin Control Panel** +- Real-time server monitoring and statistics +- GitHub auto-update functionality +- Upload management and cleanup +- Server restart capabilities +- Development/Production mode switching +- Comprehensive audit logging + +### ๐Ÿ“ฑ **Modern UI/UX** +- Fully responsive mobile design +- Smart UI state management +- Clipboard integration +- Visual feedback for all operations +- Custom error pages (403, 404, 500) +- SEO-optimized with sitemap and robots.txt + +--- + +## ๐Ÿš€ Quick Start + +### Prerequisites + +- **Python 3.8+** (3.10+ recommended) +- **Git** (for updates and installation) +- **pip** package manager + +### Installation + +```bash +# Clone the repository +git clone -b "dev-only_DO-NOT-USE" https://github.com/TySP-Dev/PacCrypt-Webapp.git +cd PacCrypt-Webapp + +# Create virtual environment +python -m venv venv + +# Activate virtual environment +# On Linux/macOS: +source venv/bin/activate +# On Windows: +venv\Scripts\activate + +# Install dependencies +pip install -r application_data/requirements.txt +``` + +### Running the Application + +#### Development Mode +```bash +# Linux/macOS +python application_data/control_scripts/start_dev.py + +# Windows +python application_data\control_scripts\start_dev.py +``` + +#### Production Mode +```bash +# Linux/macOS +python application_data/control_scripts/start_prod.py + +# Windows +python application_data\control_scripts\start_prod.py +``` + +### Access the Application + +- **Local access**: http://127.0.0.1:5000 +- **Network access**: http://YOUR_IP_ADDRESS:5000 +- **Admin setup**: http://127.0.0.1:5000/admin-setup (first-time only) + +--- + +## ๐Ÿ“– Usage Guide + +### ๐Ÿ” Text Encryption/Decryption + +1. **Select Algorithm**: Choose from AES-GCM, AES-CBC, XChaCha20, or RSA Hybrid +2. **Enter Text**: Type or paste your message +3. **Set Password**: Enter a strong encryption password +4. **For RSA**: Generate key pair first if using RSA Hybrid +5. **Execute**: Click Encrypt/Decrypt +6. **Copy Result**: Use the copy button for easy sharing + +### ๐Ÿ“ File Operations + +1. **Upload File**: Select file using the file picker +2. **Choose Algorithm**: Pick AES-CBC, XChaCha20, or RSA Hybrid (AES-GCM not supported for files) +3. **Set Password**: Enter encryption password +4. **Process**: File will be encrypted/decrypted and downloaded automatically + +### ๐Ÿ“ค PacShare - Secure File Sharing + +1. **Upload File**: Select file to share +2. **Set Passwords**: + - **Encryption Password**: Encrypts the file content + - **Pickup Password**: Required to access the download page +3. **Optional 2FA**: Enable for additional security +4. **Share URL**: Copy the generated pickup URL +5. **Recipient Access**: They need both passwords (and 2FA code if enabled) + +### ๐ŸŽฎ Hidden Pac-Man Game + +- Type `pacman` in any text input +- Use arrow keys or swipe gestures to play +- Authentic retro gaming experience with sound effects + +--- + +## ๐Ÿ› ๏ธ Admin Panel + +Access the admin panel at `/adminpage` after initial setup at `/admin-setup`. + +### ๐Ÿ”‘ Setup Process +1. Visit `/admin-setup` on first run +2. Create admin username and password +3. Optionally enable 2FA for enhanced security +4. Login at `/admin-login` + +### ๐ŸŽ›๏ธ Admin Features +- **๐Ÿ“Š Server Monitoring**: Real-time statistics and uptime +- **๐Ÿ”„ Server Control**: Restart, switch dev/prod modes +- **๐Ÿ“‹ Route Management**: View all available endpoints +- **๐Ÿ”ƒ GitHub Integration**: Auto-update from repository +- **๐Ÿงน File Management**: Clear uploads and expired files +- **๐Ÿ” Security**: Change password, manage 2FA +- **๐Ÿ“ Audit Logs**: View encrypted activity logs +- **โš™๏ธ Settings**: Configure upload limits and file retention + +### ๐Ÿ”’ Security Features +- Encrypted credential storage +- TOTP-based 2FA support +- QR code generation for authenticator apps +- Secure session management +- Encrypted audit logging + +--- + +## ๐Ÿ›ก๏ธ Deployment Tips +##### I recommend using Linux as the host server, the follow confs are Linux focused +The official PacCrypt host is **Arch** minimal install. + +**HTTP** Nginx config (Not recommended): + +```nginx +server { + listen 80; + server_name yourdomain.com; #<-- Your URL here + + # Basic Privacy-Respecting Logging + access_log off; #<-- set to syslog:server=unix:/dev/log; for logging + error_log syslog:server=unix:/dev/log crit; #<-- Currently set for only critical logs, remove crit for all logs + + # Hardened Proxy Settings + location / { + proxy_pass http://127.0.0.1:5000; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_http_version 1.1; + proxy_set_header Connection ""; + + # Timeouts + proxy_connect_timeout 5s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + } + + # Basic Hardening Headers + add_header X-Frame-Options "DENY" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "no-referrer" always; + add_header Permissions-Policy "geolocation=(), microphone=()" always; + + # Prevent Abuse + client_max_body_size 10M; + keepalive_timeout 10; + server_tokens off; +} +``` + +**HTTPS** Nginx config (Recommended): + +```nginx +# Redirect HTTP to HTTPS +server { + listen 80; + server_name yourdomain.com; #<-- Your URL here + + # Basic Privacy-Respecting Logging + access_log off; #<-- set to syslog:server=unix:/dev/log; for logging + error_log syslog:server=unix:/dev/log crit; #<-- Currently set for only critical logs, remove crit for all logs + + location / { + return 301 https://$host$request_uri; + } +} + +# HTTPS Server Block +server { + listen 443 ssl http2; + server_name yourdomain.com; + + ssl_certificate path/to/yourdomain.com.cert; #<-- Could also be .cert.pem + ssl_certificate_key path/to/yourdomain.com.key; #<-- Could also be .key.pem + + # SSL Hardening + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384'; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # Strong security headers (adjust as needed) + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header X-Content-Type-Options nosniff always; + add_header X-Frame-Options DENY always; + add_header Referrer-Policy "no-referrer" always; + add_header Permissions-Policy "geolocation=(), camera=()" always; + add_header X-XSS-Protection "1; mode=block" always; + + # Basic Privacy-Respecting Logging + access_log off; #<-- set to syslog:server=unix:/dev/log; for logging + error_log syslog:server=unix:/dev/log crit; #<-- Currently set for only critical logs, remove crit for all logs + + client_max_body_size xG; #<-- Change to what the max upload for PacCrypt Share + + # Reverse proxy to Flask + location / { + proxy_pass http://127.0.0.1:5000; + proxy_set_header Host $host; + + # Comment these out if you want complete anonymity between client and app + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + + # Optional privacy: strip identifying headers + proxy_hide_header X-Powered-By; + } +} +``` +--- + +## ๐Ÿ“‹ API Integration + +PacCrypt provides a comprehensive REST API for programmatic access. See the detailed [API Documentation](API.md) for: + +- **Encryption Operations**: Text and file encryption/decryption +- **Key Management**: RSA key pair generation +- **PacShare Integration**: Programmatic file sharing +- **Algorithm Discovery**: List available encryption methods + +### Quick API Example + +```bash +# Encrypt text using AES-GCM +curl -X POST "https://paccrypt.unnaturalll.dev/api/encrypt" \ + -H "Content-Type: application/json" \ + -d '{"message": "Hello World!", "password": "secret123", "algorithm": "aes_gcm"}' + +# Upload file via PacShare +curl -X POST "https://paccrypt.unnaturalll.dev/api/pacshare" \ + -F "file=@document.pdf" \ + -F "enc_password=encrypt123" \ + -F "pickup_password=pickup123" \ + -F "algorithm=aes_cbc" +``` + +## ๐Ÿ—‚๏ธ Project Structure + +``` +PacCrypt-Webapp/ +โ”œโ”€โ”€ app.py # Main Flask application +โ”œโ”€โ”€ README.md # This file +โ”œโ”€โ”€ ROADMAP.md # Development roadmap +โ”œโ”€โ”€ API.md # API documentation +โ”œโ”€โ”€ application_data/ # Application configuration +โ”‚ โ”œโ”€โ”€ control_scripts/ # Server management scripts +โ”‚ โ”‚ โ”œโ”€โ”€ start_dev.py # Development mode starter +โ”‚ โ”‚ โ”œโ”€โ”€ start_prod.py # Production mode starter +โ”‚ โ”‚ โ”œโ”€โ”€ restart_dev.py # Development restart +โ”‚ โ”‚ โ”œโ”€โ”€ restart_prod.py # Production restart +โ”‚ โ”‚ โ””โ”€โ”€ stop.py # Server stop script +โ”‚ โ”œโ”€โ”€ requirements.txt # Python dependencies +โ”‚ โ”œโ”€โ”€ settings.json # Application settings +โ”‚ โ”œโ”€โ”€ admin_creds.json # Encrypted admin credentials +โ”‚ โ”œโ”€โ”€ admin_key.key # Admin encryption key +โ”‚ โ””โ”€โ”€ admin_logs.enc # Encrypted audit logs +โ”œโ”€โ”€ paccrypt_algos/ # Encryption modules +โ”‚ โ”œโ”€โ”€ __init__.py # Package initialization +โ”‚ โ”œโ”€โ”€ aes_cbc.py # AES-CBC implementation +โ”‚ โ”œโ”€โ”€ aes_gcm.py # AES-GCM implementation +โ”‚ โ”œโ”€โ”€ xchacha.py # XChaCha20-Poly1305 +โ”‚ โ””โ”€โ”€ rsa_hybrid.py # RSA hybrid encryption +โ”œโ”€โ”€ pacshare/ # File upload storage +โ”‚ โ”œโ”€โ”€ *.encrypted # Encrypted uploaded files +โ”‚ โ””โ”€โ”€ *.json # File metadata +โ”œโ”€โ”€ templates/ # HTML templates +โ”‚ โ”œโ”€โ”€ index.html # Main interface +โ”‚ โ”œโ”€โ”€ pickup.html # File pickup page +โ”‚ โ”œโ”€โ”€ admin*.html # Admin panel pages +โ”‚ โ””โ”€โ”€ error pages (403,404,500) +โ””โ”€โ”€ static/ # Static assets + โ”œโ”€โ”€ css/styles.css # Application styling + โ”œโ”€โ”€ js/ # JavaScript modules + โ”œโ”€โ”€ img/ # Images and icons + โ”œโ”€โ”€ fonts/ # Custom fonts + โ””โ”€โ”€ audio/ # Sound effects +``` + +--- + +## ๐Ÿ”’ Security Considerations + +### โš ๏ธ Important Security Notes + +- **Password Strength**: Use strong, unique passwords for all operations +- **2FA Recommended**: Enable 2FA for admin accounts and sensitive file shares +- **HTTPS Required**: Always use HTTPS in production environments +- **Regular Updates**: Keep dependencies updated for security patches +- **Backup Strategy**: Implement regular backups of encrypted data + +### ๐Ÿ›ก๏ธ Encryption Details + +- **AES-256**: Industry standard symmetric encryption +- **RSA-4096**: Strong asymmetric encryption for key exchange +- **PBKDF2**: 200,000 iterations for key derivation +- **Authenticated Encryption**: GCM and Poly1305 modes prevent tampering +- **Secure Random**: Cryptographically secure random number generation + +## ๐Ÿค Contributing + +We welcome contributions! Please see our [ROADMAP.md](ROADMAP.md) for planned features and development priorities. + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests if applicable +5. Submit a pull request + +## ๐Ÿ“ž Support + +- **Documentation**: See [API.md](API.md) for API details +- **Issues**: Report bugs via GitHub Issues +- **Discussions**: Use GitHub Discussions for questions +- **Official Instance**: [paccrypt.unnaturalll.dev](https://paccrypt.unnaturalll.dev) + +--- + +## ๐Ÿ“„ License + +MIT ยฉ [TySP-Dev](https://github.com/TySP-Dev) + +**๐Ÿ” Secure by design. Simple by choice. Powerful by nature.** + + + diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..ff16b33 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,362 @@ +> [!IMPORTANT] +> Fully modular code for encryption libraries, ensure metadata is stored as encrypted hashs for PacShare, Revamp PacShares secure file send and pickup, and create a CLI and local application (Linux and Android). + +--- + +### Phase 0 + +- [x] ~~Remove docker files (Dropping official docker support)~~ + +- [ ] Readd docker support + +- [x] Update README.md to be current. + +- [x] Add roadmap.md to repo + +- [x] Create /application_data/ folder (for server settings, admin login and creds) + +- [x] Create scripts folder in /application_data/ + +- [x] Create /paccrypt_algos/ folder + +- [x] Builder better start, stop and restart scripts both prod and dev (Cross-platform: Windows & Linux) + +- [x] Add a button in the admin panel to switch to and from prod and dev modes - **COMPLETED: `/admin-switch-dev-mode` and `/admin-switch-prod-mode` endpoints implemented** + +### Phase 1: app.py - Modular Python Web App + +##### app.py Responsibilities + +- [x] Flask app + routing + +- [x] Handle: +- [x] /encrypt (via API endpoints) +- [x] /decrypt (via API endpoints) +- [x] /pickup/ + +- [x] Receive: +- [x] File or text +- [x] pickup_password (required) +- [x] encryption_password (required) +- [x] encryption_mode (algorithm selection implemented) + +- [x] Encrypt metadata using pickup password + +- [x] Encrypt file using encryption password + +- [x] Dynamically load correct engine via decrypted metadata + +- [x] Save .encrypted + .json metadata, return pickup link + +- [ ] Update PacMan like mini game logic revamp "(LOW PRIORITY)" + +- [ ] Update PacMan like mini game base revamp "(LOW PRIORITY)" + +--- + +##### /paccrypt_algos/ - Modular Crypto Engines + +- [x] Create folder + interface + +- [x] Remove basic cypher + +Implement engines: + +- [x] aes_gcm.py + +- [x] aes_cbc.py + +- [x] xchacha.py + +- [x] rsa_hybrid.py + +- [x] ~~PQCrypt_hybrid.py (Testing)~~ **REMOVED: Post-quantum crypto removed for simplicity** + +- [x] Each must expose: + +``` +def encrypt_text(text, key): ... +def decrypt_text(ciphertext, key): ... +def encrypt_file(in_path, out_path, key): ... +def decrypt_file(in_path, out_path, key): ... +def generate_key_pair(): ... (for RSA hybrid) +``` +**COMPLETED: All modules implemented with correct API** + +--- + +### Phase 2: PacShare - Reimplementation + +/encrypt Route Flow + +- [x] JS submits (PacShare "Form"): +- [x] File +- [x] pickup_password (for metadata) +- [x] encryption_password (for file) +- [x] encryption_mode +- [x] 2FA TOTP setup (Yubi/Passkey not implemented) + +- [x] Python logic: +- [x] Encrypt file using selected algo + encryption_password +- [x] Generate metadata dict: +- [x] filename, enc_mode, pickup_hash, timestamp, optional 2FA +- [x] Encrypt metadata using AES-GCM derived from pickup_password +- [x] Save .{algorithm}.encrypted and .json files +- [x] Generate random file_id +- [x] Return /pickup/ link + +> [!IMPORTANT] +> Both passwords are required. One reveals the mode + metadata, the other decrypts the file. + +--- + +##### /pickup/ Route Flow + +- [x] Prompt for pickup_password + +- [x] Decrypt .json metadata and validate hash + +- [x] Show original filename, prompt for encryption_password + +- [x] Load correct module, decrypt file + +- [x] Offer file download + +--- + +##### Metadata Structure (Encrypted JSON) + +``` +"filename": "report.pdf", +"algorithm": "aes_cbc", +"pickup_password": "", +"created_at": "2025-08-05T18:00Z", +"require_2fa": true, // optional +"totp_secret": "base32string", // optional +"service_name": "PacCrypt File: report.pdf..." // optional +``` + +> [!NOTE] +> Stored as .json +> Encrypted with AES-GCM using key derived from pickup_password +> **COMPLETED: Metadata encryption implemented** + +--- + +### Phase 3: External API Access (/api/*) + +##### Endpoint Description + +``` +โœ… GET /api/algorithms List available encryption algorithms +โœ… POST /api/generate-keypair Generate RSA key pairs +โœ… POST /api/encrypt File/text encryption (returns encrypted data) +โœ… POST /api/decrypt File/text decryption +โœ… POST /api/pacshare Upload + encrypt + return pickup link (JSON) +โŒ POST /api/ps-pickup Provide pickup ID + passwords, return decrypted file (Use web interface) +โŒ GET /api/version Return current version tag (Not implemented) +``` + +> [!NOTE] +> **COMPLETED: Core API endpoints implemented** +> Pickup is handled via web interface at /pickup/ +> Encryption password is never saved server-side + +--- + +### Phase 4: CLI Tool (Offline and API Hybrid) + +- [ ] Create PacCrypt-CLI repo + +- [ ] paccrypt-cli command + +- [ ] Local encrypt/decrypt support + +##### Support: + +- [ ] --share-api to change api address (in case user is self hosting PacCrypt-Webapp) +- Default api from https://paccrypt.unnaturalll.dev/ + +- [ ] --share to upload via /api/ps-send + +- [ ] --pickup to download + decrypt via /api/ps-pickup + +##### Always require (Send + Pickup) + +- [ ] --method (to define encryption type) + +- [ ] --pickup-password + +- [ ] --encryption-password + +Optional (Send + Pickup) + +- [ ] 2FA Token +- No Yubi or passkey support for API calls + +- [ ] --help (Shows command usage) + +- [ ] CLI PacMan like mini game (LOW PRIORITY) + +--- + +### Phase 5: Local GUI Applications + +##### Linux (First) + +- [ ] PyQt6 or GTK + +- [ ] Same features as the Webapp + +- [ ] Support for PacShare through API calls +- Default https://paccrypt.unnaturalll.dev/ +- User changeable if the webapp is self hosted + +- [ ] Text Encryption / Decryption mode + +- [ ] Text Password + +- [ ] Text input / output + +- [ ] PacShare Mode selector + +- [ ] PacShare File Uploader + +- [ ] PacShare Pickup Password + +- [ ] PacShare Encryption / Decryption password + +- [ ] PacShare 2FA Token support +- No Yubi/Passkey support for API calls + +- [ ] PacShare error message if devices is offline or server can't be reached + +- [ ] KDE Dolphin context integration (right-click โ†’ encrypt | decrypt | share - share opens the paccrypt gui with the file already staged) + +##### Android + +- [ ] Kivy or BeeWare + +- [ ] Same features as the Webapp + +- [ ] Support for PacShare through API calls +- Default https://paccrypt.unnaturalll.dev/ +- User changeable if the webapp is self hosted + +- [ ] Text Encryption / Decryption mode + +- [ ] Text Password + +- [ ] Text input / output + +- [ ] PS Mode selector + +- [ ] PS File Uploader + +- [ ] PS Pickup Password + +- [ ] PS Encryption / Decryption password + +- [ ] PS 2FA Token support +- No Yubi/Passkey support for API calls + +- [ ] PS error message if devices is offline or server can't be reached + +> [!IMPORTANT] +> No Windows support for a application, only webapp, and maybe CLI support. + +`Linux master race` + +--- + +### PacShare File Format โœ… **COMPLETED** + +``` +pacshare/ +โ”œโ”€โ”€ ..encrypted # Encrypted binary file +โ””โ”€โ”€ .json # Encrypted metadata (JSON) +``` + +**Current Implementation:** +- Files are stored as `.{algorithm}.encrypted` (e.g., `.aes_cbc.encrypted`) +- Metadata stored as `.json` files with encrypted content +- Algorithm info embedded in filename for automatic detection + +--- + +### Development Order + +0. - [x] **Phase 0 Tasks** โœ… +1. - [x] **paccrypt_algos/ + aes_gcm.py** โœ… +2. - [x] **app.py routes: /encrypt, /pickup/** โœ… +3. - [x] **Add /decrypt route** โœ… +4. - [x] **Build metadata encryption helpers** โœ… +5. - [x] **Finish other engine modules** โœ… +6. - [x] **Build /api/* equivalents** โœ… +7. - [x] **Update README.md with all changes to the webapp** โœ… +8. - [x] **Create a new installation guide** โœ… (Included in README.md) +9. - [ ] Build CLI โณ *Next Priority* +10. - [ ] Test CLI with --pickup + --share +12. - [ ] Build GUI app on Linux +13. - [ ] Test GUI app on Linux +14. - [ ] Build GUI app on Android +15. - [ ] Test GUI app on Android +16. - [ ] Finalize all releases and push to main +17. - [ ] Create Wiki + +**๐ŸŽ‰ WEBAPP CORE COMPLETE! ๐ŸŽ‰** + +**Current Status:** All core webapp functionality implemented including: +- โœ… Modular encryption engines (AES-GCM, AES-CBC, XChaCha20, RSA Hybrid) +- โœ… Complete API with documentation +- โœ… PacShare file sharing with 2FA support +- โœ… Admin panel with full management features +- โœ… Cross-platform deployment scripts +- โœ… Comprehensive documentation + +--- + +### Current Webapp Structure โœ… **COMPLETED** + +``` +PacCrypt-Webapp/ +โ”œโ”€โ”€ app.py # Main Flask application โœ… +โ”œโ”€โ”€ README.md # Updated documentation โœ… +โ”œโ”€โ”€ ROADMAP.md # This file โœ… +โ”œโ”€โ”€ API.md # API documentation โœ… *NEW* +โ”œโ”€โ”€ LICENSE # MIT License โœ… +โ”œโ”€โ”€ application_data/ โœ… # Application configuration +โ”‚ โ”œโ”€โ”€ control_scripts/ โœ… # Server management scripts +โ”‚ โ”‚ โ”œโ”€โ”€ start_dev.py โœ… # Development mode starter +โ”‚ โ”‚ โ”œโ”€โ”€ start_prod.py โœ… # Production mode starter +โ”‚ โ”‚ โ”œโ”€โ”€ restart_dev.py โœ… # Development restart +โ”‚ โ”‚ โ”œโ”€โ”€ restart_prod.py โœ… # Production restart +โ”‚ โ”‚ โ””โ”€โ”€ stop.py โœ… # Server stop script +โ”‚ โ”œโ”€โ”€ requirements.txt โœ… # Python dependencies +โ”‚ โ”œโ”€โ”€ settings.json โœ… # Application settings +โ”‚ โ”œโ”€โ”€ admin_creds.json โœ… # Encrypted admin credentials +โ”‚ โ”œโ”€โ”€ admin_key.key โœ… # Admin encryption key +โ”‚ โ””โ”€โ”€ admin_logs.enc โœ… # Encrypted audit logs +โ”œโ”€โ”€ paccrypt_algos/ โœ… # Encryption modules +โ”‚ โ”œโ”€โ”€ __init__.py โœ… # Package initialization +โ”‚ โ”œโ”€โ”€ aes_cbc.py โœ… # AES-CBC implementation +โ”‚ โ”œโ”€โ”€ aes_gcm.py โœ… # AES-GCM implementation +โ”‚ โ”œโ”€โ”€ xchacha.py โœ… # XChaCha20-Poly1305 +โ”‚ โ””โ”€โ”€ rsa_hybrid.py โœ… # RSA hybrid encryption +โ”œโ”€โ”€ pacshare/ โœ… # File upload storage +โ”‚ โ”œโ”€โ”€ *.{algorithm}.encrypted โœ… # Encrypted uploaded files +โ”‚ โ””โ”€โ”€ *.json โœ… # File metadata +โ”œโ”€โ”€ templates/ โœ… # HTML templates +โ”‚ โ”œโ”€โ”€ index.html โœ… # Main interface +โ”‚ โ”œโ”€โ”€ pickup.html โœ… # File pickup page +โ”‚ โ”œโ”€โ”€ admin*.html โœ… # Admin panel pages +โ”‚ โ””โ”€โ”€ error pages (403,404,500) โœ… +โ””โ”€โ”€ static/ โœ… # Static assets + โ”œโ”€โ”€ css/styles.css โœ… # Application styling + โ”œโ”€โ”€ js/ โœ… # JavaScript modules + โ”œโ”€โ”€ img/ โœ… # Images and icons + โ”œโ”€โ”€ fonts/ โœ… # Custom fonts + โ””โ”€โ”€ audio/ โœ… # Sound effects +``` + +**๐Ÿ† PROJECT STRUCTURE FULLY IMPLEMENTED ๐Ÿ†** 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 new file mode 100644 index 0000000..4ebe2e8 --- /dev/null +++ b/app.py @@ -0,0 +1,1518 @@ +# ===== Standard Library Imports ===== +import os +import io +import json +import html +import base64 +import hashlib +import secrets +import subprocess +import platform +from datetime import datetime +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 ( + Flask, render_template, request, jsonify, session, + redirect, url_for, flash, send_file, make_response +) +from werkzeug.utils import secure_filename +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.primitives.hashes import SHA256 +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.fernet import Fernet +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 +# Post-quantum crypto removed for simplicity + +# ===== Application Configuration ===== +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' +ADMIN_LOG_FILE = 'application_data/admin_logs.enc' +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 + "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 ===== +AVAILABLE_ALGORITHMS = { + "aes_cbc": { + "name": "AES-CBC", + "module": aes_cbc, + "supports_text": True, + "supports_file": True, + "description": "AES-256 with CBC mode and HMAC authentication" + }, + "aes_gcm": { + "name": "AES-GCM", + "module": aes_gcm, + "supports_text": True, + "supports_file": False, + "description": "AES-256 with GCM mode (authenticated encryption)" + }, + "xchacha": { + "name": "XChaCha20-Poly1305", + "module": xchacha, + "supports_text": True, + "supports_file": True, + "description": "XChaCha20 stream cipher with Poly1305 authentication" + }, + "rsa_hybrid": { + "name": "RSA Hybrid", + "module": rsa_hybrid, + "supports_text": True, + "supports_file": True, + "description": "RSA-4096 with AES hybrid encryption", + "requires_keypair": True + } +} + +# Post-quantum algorithms removed + +# ===== Settings Management ===== +def load_settings(): + """Load application settings from file or create with defaults.""" + if not os.path.exists(SETTINGS_FILE): + with open(SETTINGS_FILE, 'w') as f: + json.dump(DEFAULT_SETTINGS, f) + with open(SETTINGS_FILE, 'r') as f: + return json.load(f) + +settings = load_settings() +UPLOAD_FOLDER = settings["upload_folder"] +MAX_FILE_AGE_DAYS = settings["max_file_age_days"] +MAX_FILE_SIZE_BYTES = settings["max_file_size_bytes"] + +# Ensure upload folder exists and has proper permissions +if not os.path.exists(UPLOAD_FOLDER): + try: + os.makedirs(UPLOAD_FOLDER, exist_ok=True) + # Set permissions to 755 (rwxr-xr-x) + os.chmod(UPLOAD_FOLDER, 0o755) + print(f"[INFO] Created upload directory: {UPLOAD_FOLDER}") + except Exception as e: + print(f"[ERROR] Failed to create upload directory: {str(e)}") + raise + +# ===== Cryptographic Functions ===== +def derive_key(password: str, salt: bytes) -> bytes: + """Derive a cryptographic key from password using PBKDF2.""" + return PBKDF2HMAC(algorithm=SHA256(), length=32, salt=salt, iterations=200_000).derive(password.encode()) + +def hash_password(password: str, salt: bytes) -> str: + """Hash a password with salt for secure storage.""" + return base64.urlsafe_b64encode(derive_key(password, salt)).decode() + +def advanced_encrypt(plaintext: str, password: str) -> str: + """Encrypt plaintext with AES-GCM and return base64-encoded result.""" + salt = os.urandom(16) + nonce = os.urandom(12) + key = derive_key(password, salt) + ciphertext = AESGCM(key).encrypt(nonce, plaintext.encode(), None) + return base64.b64encode(salt + nonce + ciphertext).decode() + +def advanced_decrypt(data_b64: str, password: str) -> str: + """Decrypt base64-encoded AES-GCM encrypted data.""" + try: + data = base64.b64decode(data_b64) + salt, nonce, ciphertext = data[:16], data[16:28], data[28:] + key = derive_key(password, salt) + plaintext = AESGCM(key).decrypt(nonce, ciphertext, None) + return plaintext.decode() + except Exception: + return "[Error] Invalid password or corrupted data!" + +# ===== Admin Authentication ===== +def load_admin_key(): + """Load or generate admin encryption key.""" + if not os.path.exists(ADMIN_KEY_FILE): + with open(ADMIN_KEY_FILE, 'wb') as f: + f.write(Fernet.generate_key()) + with open(ADMIN_KEY_FILE, 'rb') as f: + return f.read() + +def encrypt_creds(username, password, totp_secret=None): + """Encrypt and store admin credentials.""" + key = load_admin_key() + cipher = Fernet(key) + salt = os.urandom(16) + hashed_pw = hash_password(password, salt) + data = { + "u": username, + "p": hashed_pw, + "s": base64.b64encode(salt).decode(), + "totp_secret": totp_secret, + "2fa_enabled": totp_secret is not None + } + with open(ADMIN_CRED_FILE, 'wb') as f: + f.write(cipher.encrypt(json.dumps(data).encode())) + +def check_creds(username, password, totp_code=None): + """Verify admin credentials.""" + try: + key = load_admin_key() + cipher = Fernet(key) + with open(ADMIN_CRED_FILE, 'rb') as f: + decrypted = cipher.decrypt(f.read()) + creds = json.loads(decrypted) + salt = base64.b64decode(creds["s"]) + + # Check username and password first + if not (creds["u"] == username and creds["p"] == hash_password(password, salt)): + return False + + # Check 2FA if enabled + if creds.get("2fa_enabled", False): + if not totp_code: + return False + totp_secret = creds.get("totp_secret") + if not totp_secret: + return False + totp = pyotp.TOTP(totp_secret) + if not totp.verify(totp_code, valid_window=1): + return False + + return True + except Exception as e: + print("[ERROR] check_creds failed:", e) + return False + +def get_admin_2fa_status(): + """Check if admin has 2FA enabled.""" + try: + key = load_admin_key() + cipher = Fernet(key) + with open(ADMIN_CRED_FILE, 'rb') as f: + decrypted = cipher.decrypt(f.read()) + creds = json.loads(decrypted) + return creds.get("2fa_enabled", False) + except Exception: + return False + +def get_admin_totp_secret(): + """Get admin TOTP secret for QR code generation.""" + try: + key = load_admin_key() + cipher = Fernet(key) + with open(ADMIN_CRED_FILE, 'rb') as f: + decrypted = cipher.decrypt(f.read()) + creds = json.loads(decrypted) + return creds.get("totp_secret") + except Exception: + return None + +def log_admin_event(message: str): + """Log admin actions securely.""" + try: + key = load_admin_key() + cipher = Fernet(key) + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + encrypted = cipher.encrypt(f"[{timestamp}] {message}".encode()) + with open(ADMIN_LOG_FILE, 'ab') as f: + f.write(encrypted + b"\n") + except Exception as e: + print("[ERROR] Failed to write admin log:", e) + +# ===== 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.""" + try: + now = datetime.now() + for fname in os.listdir(UPLOAD_FOLDER): + if fname.endswith(".enc") or fname.endswith(".json"): + path = os.path.join(UPLOAD_FOLDER, fname) + try: + file_time = datetime.datetime.fromtimestamp(os.path.getmtime(path), ) + age = (now - file_time).days + if age > MAX_FILE_AGE_DAYS: + os.remove(path) + print(f"[INFO] Deleted expired file: {fname}") + except Exception as e: + print(f"[ERROR] Could not check/delete file {fname}: {e}") + except Exception as e: + print(f"[ERROR] Failed to cleanup expired files: {str(e)}") + +# ===== Route Handlers ===== +@app.route("/", methods=["GET", "POST"]) +def index(): + """Main application route handling file uploads.""" + if request.method == 'POST': + if 'file' in request.files: + return handle_file_upload(request) + else: + return jsonify(error="Use /api/encrypt or /api/decrypt endpoints for text operations"), 400 + return render_template("index.html", settings=settings) + +def handle_file_upload(request): + """Process file upload and encryption.""" + file = request.files['file'] + enc_password = request.form.get('enc_password') + pickup_password = request.form.get('pickup_password') + algorithm = request.form.get('algorithm', 'aes_cbc') # Default to AES-CBC + enable_2fa = request.form.get('enable_2fa') == 'on' # Check if 2FA checkbox is checked + + if not file or not enc_password or not pickup_password: + return jsonify({"error": "Missing fields"}), 400 + + if file.content_length and file.content_length > MAX_FILE_SIZE_BYTES: + return jsonify({"error": f"File too large! Limit: {MAX_FILE_SIZE_BYTES / (1024**3):.2f} GB"}), 400 + + # Validate algorithm + if algorithm not in AVAILABLE_ALGORITHMS: + return jsonify({"error": "Invalid algorithm"}), 400 + + algo_config = AVAILABLE_ALGORITHMS[algorithm] + if not algo_config["supports_file"]: + return jsonify({"error": "Algorithm does not support file operations"}), 400 + + filename = secure_filename(file.filename) + 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"] + + random_id = secrets.token_urlsafe(24) + encrypted_filename = f"{random_id}.{algorithm}.encrypted" + encrypted_path = os.path.join(UPLOAD_FOLDER, encrypted_filename) + + # Encrypt file using the correct API (in_path, out_path, password) + module.encrypt_file(temp_path, encrypted_path, enc_password) + os.remove(temp_path) + + meta = { + 'pickup_password': base64.urlsafe_b64encode(hashlib.sha256(pickup_password.encode()).digest()).decode(), + 'original_name': encrypt_filename(filename, enc_password), + 'algorithm': algorithm, # Store algorithm used for decryption + 'timestamp': datetime.now().isoformat(), + 'require_2fa': enable_2fa + } + + # Generate TOTP secret if 2FA is enabled + if enable_2fa: + totp_secret = pyotp.random_base32() + meta['totp_secret'] = totp_secret + meta['service_name'] = f"PacCrypt File: {filename[:20]}..." + with open(os.path.join(UPLOAD_FOLDER, f"{random_id}.json"), 'w') as f: + json.dump(meta, f) + + pickup_url = request.host_url.rstrip('/') + url_for('pickup_file', file_id=random_id) + response_data = {"success": True, "pickup_url": pickup_url} + + # If 2FA is enabled, also return QR code URL for immediate setup + if enable_2fa: + qr_url = request.host_url.rstrip('/') + url_for('generate_qr_code', file_id=random_id) + response_data["qr_code_url"] = qr_url + response_data["totp_secret"] = totp_secret + response_data["service_name"] = f"PacCrypt File: {filename[:20]}..." + + return jsonify(response_data) + except Exception as e: + # Clean up temp file if it still exists + if os.path.exists(temp_path): + os.remove(temp_path) + return jsonify({"error": f"Encryption failed: {str(e)}"}), 500 + + +def encrypt_filename(filename: str, password: str) -> str: + salt = os.urandom(16) + key = derive_key(password, salt) + nonce = os.urandom(12) + ct = AESGCM(key).encrypt(nonce, filename.encode(), None) + return base64.urlsafe_b64encode(salt + nonce + ct).decode() + +def decrypt_filename(enc_filename_b64: str, password: str) -> str: + raw = base64.urlsafe_b64decode(enc_filename_b64) + salt, nonce, ct = raw[:16], raw[16:28], raw[28:] + key = derive_key(password, salt) + return AESGCM(key).decrypt(nonce, ct, None).decode() + +# ===== File Pickup Route ===== +@app.route("/pickup/", methods=["GET", "POST"]) +def pickup_file(file_id): + """Handle file pickup and decryption.""" + meta_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.json") + + # Find the encrypted file (could have different algorithm extensions) + enc_path = None + for filename in os.listdir(UPLOAD_FOLDER): + if filename.startswith(f"{file_id}.") and filename.endswith(".encrypted"): + enc_path = os.path.join(UPLOAD_FOLDER, filename) + break + + # Fallback to old .enc format for backward compatibility + if not enc_path: + old_enc_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.enc") + if os.path.exists(old_enc_path): + enc_path = old_enc_path + + if not os.path.exists(meta_path) or not enc_path or not os.path.exists(enc_path): + flash("File not found or expired") + return redirect(url_for('index')) + + if request.method == 'POST': + return handle_file_pickup(request, meta_path, enc_path, file_id) + + # Check if 2FA is required for this file + require_2fa = False + service_name = None + if os.path.exists(meta_path): + with open(meta_path, 'r') as f: + meta = json.load(f) + require_2fa = meta.get('require_2fa', False) + if require_2fa: + service_name = meta.get('service_name', 'PacCrypt File') + + return render_template("pickup.html", file_id=file_id, require_2fa=require_2fa, service_name=service_name) + +def handle_file_pickup(request, meta_path, enc_path, file_id): + """Process file pickup and decryption.""" + pickup_password = request.form.get('pickup_password') + enc_password = request.form.get('enc_password') + totp_code = request.form.get('totp_code') + + if not pickup_password or not enc_password: + flash("Missing fields") + return redirect(request.url) + + with open(meta_path, 'r') as f: + meta = json.load(f) + + expected_hash = base64.urlsafe_b64encode(hashlib.sha256(pickup_password.encode()).digest()).decode() + if expected_hash != meta['pickup_password']: + flash("Incorrect pickup password") + return redirect(request.url) + + # Check 2FA if required + if meta.get('require_2fa', False): + if not totp_code: + flash("2FA code is required") + return redirect(request.url) + + totp_secret = meta.get('totp_secret') + if not totp_secret: + flash("2FA configuration error") + return redirect(request.url) + + totp = pyotp.TOTP(totp_secret) + if not totp.verify(totp_code, valid_window=1): # Allow 1 window tolerance for clock drift + flash("Invalid 2FA code") + return redirect(request.url) + + # Check if this is an algorithm-based encryption or legacy AESGCM + algorithm = meta.get('algorithm') + + try: + if algorithm and algorithm in AVAILABLE_ALGORITHMS: + # Use the new algorithm-based decryption + algo_config = AVAILABLE_ALGORITHMS[algorithm] + module = algo_config["module"] + + # Create temporary file for decryption + temp_dec_path = os.path.join(UPLOAD_FOLDER, f"temp_decrypt_{secrets.token_urlsafe(8)}") + try: + # Decrypt file using the correct API (in_path, out_path, password) + module.decrypt_file(enc_path, temp_dec_path, enc_password) + + # Read decrypted data + with open(temp_dec_path, 'rb') as f: + decrypted = f.read() + finally: + # Clean up temp file + if os.path.exists(temp_dec_path): + os.remove(temp_dec_path) + else: + # Legacy AESGCM decryption for backward compatibility + with open(enc_path, 'rb') as f: + enc_data = f.read() + salt, nonce, ct = enc_data[:16], enc_data[16:28], enc_data[28:] + key = derive_key(enc_password, salt) + decrypted = AESGCM(key).decrypt(nonce, ct, None) + + except Exception as e: + flash(f"Decryption failed: {str(e)}") + return redirect(request.url) + + # Clean up files after successful decryption + os.remove(meta_path) + os.remove(enc_path) + log_admin_event(f"File {file_id} downloaded and deleted.") + + try: + original_name = decrypt_filename(meta['original_name'], enc_password) + except Exception: + original_name = "retrieved_file" + + response = send_file( + io.BytesIO(decrypted), + as_attachment=True, + download_name=original_name, + mimetype='application/octet-stream' + ) + + # Add headers for better mobile compatibility + response.headers['Content-Disposition'] = f'attachment; filename="{original_name}"' + response.headers['Content-Type'] = 'application/octet-stream' + response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' + response.headers['Pragma'] = 'no-cache' + response.headers['Expires'] = '0' + + return response + +# ===== 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"): + return redirect(url_for("admin_login")) + + totp_secret = get_admin_totp_secret() + if not totp_secret: + return "2FA not enabled for admin", 400 + + # Generate TOTP URI for QR code + totp_uri = pyotp.totp.TOTP(totp_secret).provisioning_uri( + name="admin", + issuer_name="PacCrypt Admin" + ) + + # Generate QR code + qr = qrcode.QRCode(version=1, box_size=10, border=5) + qr.add_data(totp_uri) + qr.make(fit=True) + + img = qr.make_image(fill_color="black", back_color="white") + + # Save to BytesIO + img_buffer = BytesIO() + img.save(img_buffer, format='PNG') + img_buffer.seek(0) + + response = make_response(img_buffer.getvalue()) + response.headers['Content-Type'] = 'image/png' + response.headers['Content-Disposition'] = 'inline; filename="admin_2fa_qr.png"' + return response + +@app.route("/qr/") +def generate_qr_code(file_id): + """Generate QR code for 2FA setup.""" + meta_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.json") + + if not os.path.exists(meta_path): + return "File not found", 404 + + with open(meta_path, 'r') as f: + meta = json.load(f) + + if not meta.get('require_2fa', False): + return "2FA not enabled for this file", 400 + + totp_secret = meta.get('totp_secret') + service_name = meta.get('service_name', 'PacCrypt File') + + if not totp_secret: + return "TOTP secret not found", 400 + + # Generate TOTP URI for QR code + totp_uri = pyotp.totp.TOTP(totp_secret).provisioning_uri( + name=file_id, + issuer_name=service_name + ) + + # Generate QR code + qr = qrcode.QRCode(version=1, box_size=10, border=5) + qr.add_data(totp_uri) + qr.make(fit=True) + + img = qr.make_image(fill_color="black", back_color="white") + + # Save to BytesIO + img_buffer = BytesIO() + img.save(img_buffer, format='PNG') + img_buffer.seek(0) + + response = make_response(img_buffer.getvalue()) + response.headers['Content-Type'] = 'image/png' + response.headers['Content-Disposition'] = f'inline; filename="{file_id}_qr.png"' + return response + +# ===== Admin Routes ===== +@app.route("/admin-logs") +@admin_required +def admin_logs(): + """View admin activity logs.""" + if not session.get("admin_logged_in"): + return redirect(url_for("admin_login")) + + logs = [] + try: + key = load_admin_key() + cipher = Fernet(key) + if os.path.exists(ADMIN_LOG_FILE): + with open(ADMIN_LOG_FILE, 'rb') as f: + lines = f.readlines() + for line in lines[-100:]: + if line.strip(): + try: + decrypted = cipher.decrypt(line.strip()) + logs.append(decrypted.decode()) + except Exception: + logs.append("[Error] Corrupted log entry.") + except Exception as e: + logs.append(f"[Error loading logs] {str(e)}") + + return jsonify(logs=logs) + +@app.route("/admin-settings", methods=["GET", "POST"]) +@admin_required +def admin_settings(): + """Manage application settings.""" + current_settings = load_settings() + + if request.method == 'POST': + return handle_settings_update(request, current_settings) + return render_template("admin_settings.html", settings=current_settings) + +def handle_settings_update(request, current_settings): + """Process settings update request.""" + upload_folder = request.form.get('upload_folder', current_settings.get('upload_folder', 'uploads')) + max_file_age_days = int(request.form.get('max_file_age_days', current_settings.get('max_file_age_days', 14))) + max_file_size_gb = float(request.form.get('max_file_size_gb', current_settings.get('max_file_size_bytes', 25 * 1024 * 1024 * 1024) / (1024 * 1024 * 1024))) + max_file_size_bytes = int(max_file_size_gb * 1024 * 1024 * 1024) + + # 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, + "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: + json.dump(updated_settings, f) + + flash("Settings updated successfully!") + + global settings, UPLOAD_FOLDER, MAX_FILE_AGE_DAYS, MAX_FILE_SIZE_BYTES + settings = load_settings() + UPLOAD_FOLDER = settings.get('upload_folder', 'uploads') + MAX_FILE_AGE_DAYS = settings.get('max_file_age_days', 14) + MAX_FILE_SIZE_BYTES = settings.get('max_file_size_bytes', 25 * 1024 * 1024 * 1024) + + if not os.path.exists(UPLOAD_FOLDER): + os.makedirs(UPLOAD_FOLDER) + + return redirect(url_for("admin_settings")) + +@app.route("/admin-setup", methods=["GET", "POST"]) +def admin_setup(): + """Initial admin account setup.""" + if os.path.exists(ADMIN_CRED_FILE): + return redirect(url_for("admin_login")) + if request.method == "POST": + u = request.form.get("username") + p = request.form.get("password") + enable_2fa = request.form.get("enable_2fa") == "on" + if u and p: + totp_secret = pyotp.random_base32() if enable_2fa else None + encrypt_creds(u, p, totp_secret) + session["admin_logged_in"] = True + session["admin_2fa_setup"] = enable_2fa + return redirect(url_for("admin_page")) + flash("Both fields required") + return render_template("admin_setup.html") + +@app.route("/admin-login", methods=["GET", "POST"]) +def admin_login(): + """Admin login handler.""" + if request.method == "POST": + u = request.form.get("username") + p = request.form.get("password") + 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 + 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.") + flash("Incorrect credentials or 2FA code") + + # Check if 2FA is enabled for the UI + requires_2fa = get_admin_2fa_status() + return render_template("admin_login.html", requires_2fa=requires_2fa) + +@app.route("/admin-logout") +def admin_logout(): + """Admin logout handler.""" + session.pop("admin_logged_in", None) + return redirect(url_for("index")) + +@app.route("/adminpage") +@admin_required +def admin_page(): + """Admin dashboard.""" + cleanup_expired_files() + routes = [rule.rule for rule in app.url_map.iter_rules() if rule.endpoint != 'static'] + + now = datetime.now() + try: + boot_time = datetime.fromtimestamp(psutil.boot_time()) + + uptime = now - boot_time + days = uptime.days + hours, remainder = divmod(uptime.seconds, 3600) + minutes = remainder // 60 + uptime_str = f"{days} days, {hours} hours, {minutes} minutes" + except Exception as e: + print(f"[ERROR] Uptime calculation failed: {e}") + uptime_str = "Unavailable" + + server_info = { + "uptime": uptime_str, + "server_time": now.strftime("%Y-%m-%d %H:%M:%S"), + "python_version": platform.python_version(), + "debug_mode": app.debug + } + + # Get 2FA status for UI + tfa_enabled = get_admin_2fa_status() + + return render_template("admin.html", routes=routes, server_info=server_info, tfa_enabled=tfa_enabled) + + + +@app.route("/restart-server", methods=["POST"]) +def restart_server(): + """Restart the server.""" + if not session.get("admin_logged_in"): + return jsonify({"error": "Unauthorized"}), 401 + + try: + if platform.system() == "Windows": + current_pid = os.getpid() + restart_script = f""" + @echo off + timeout /t 2 /nobreak + taskkill /F /PID {current_pid} + set PRODUCTION=true + start "" "python" "app.py" + """ + with open("restart.bat", "w") as f: + f.write(restart_script) + subprocess.Popen(["restart.bat"], shell=True) + return jsonify({"message": "Server restart initiated"}), 200 + else: + current_pid = os.getpid() + python_path = sys.executable + script_path = os.path.abspath(__file__) + + # Create a safer and cleaner restart script + restart_script = """#!/bin/bash +sleep 2 +PID=$1 +kill "$PID" +while kill -0 "$PID" 2>/dev/null; do sleep 0.5; done +export PRODUCTION=true +exec "$2" "$3" +""" + + with open("restart.sh", "w") as f: + f.write(restart_script) + os.chmod("restart.sh", 0o755) + + subprocess.Popen(["./restart.sh", str(current_pid), python_path, script_path]) + return jsonify({"message": "Server restart initiated"}), 200 + + except Exception as e: + print(f"[ERROR] Failed to restart server: {str(e)}") + return jsonify({"error": f"Failed to restart server: {str(e)}"}), 500 + +@app.route("/admin-reset", methods=["POST"]) +@admin_required +def admin_reset(): + """Reset admin credentials.""" + if not session.get("admin_logged_in"): + return redirect(url_for("admin_login")) + try: + if os.path.exists(ADMIN_CRED_FILE): + os.remove(ADMIN_CRED_FILE) + if os.path.exists(ADMIN_KEY_FILE): + os.remove(ADMIN_KEY_FILE) + session.pop("admin_logged_in", None) + flash("Admin credentials reset. Please create new credentials.") + except Exception as e: + flash("Failed to reset admin credentials.") + print("[ERROR] admin_reset failed:", e) + return redirect(url_for("admin_setup")) + +@app.route("/admin-change-password", methods=["POST"]) +@admin_required +def admin_change_password(): + """Change admin password.""" + if not session.get("admin_logged_in"): + return redirect(url_for("admin_login")) + + current = request.form.get("current_password") + new = request.form.get("new_password") + + try: + key = load_admin_key() + cipher = Fernet(key) + with open(ADMIN_CRED_FILE, 'rb') as file: + decrypted = cipher.decrypt(file.read()) + creds = json.loads(decrypted) + + salt = base64.b64decode(creds["s"]) + if hash_password(current, salt) != creds["p"]: + flash("Current password is incorrect") + return redirect(url_for("admin_page")) + + creds["p"] = hash_password(new, salt) + encrypted = cipher.encrypt(json.dumps(creds).encode()) + with open(ADMIN_CRED_FILE, 'wb') as file: + file.write(encrypted) + + log_admin_event("Admin password changed.") + flash("Password updated successfully", "password-feedback") + return redirect(url_for("admin_page")) + + except Exception as e: + flash("Failed to update password") + print("[ERROR] Password change failed:", e) + return redirect(url_for("admin_page")) + +@app.route("/admin-enable-2fa", methods=["POST"]) +@admin_required +def admin_enable_2fa(): + """Enable 2FA for admin account.""" + if not session.get("admin_logged_in"): + return redirect(url_for("admin_login")) + + try: + key = load_admin_key() + cipher = Fernet(key) + with open(ADMIN_CRED_FILE, 'rb') as file: + decrypted = cipher.decrypt(file.read()) + creds = json.loads(decrypted) + + # Generate new TOTP secret + totp_secret = pyotp.random_base32() + creds["totp_secret"] = totp_secret + creds["2fa_enabled"] = True + + encrypted = cipher.encrypt(json.dumps(creds).encode()) + with open(ADMIN_CRED_FILE, 'wb') as file: + file.write(encrypted) + + log_admin_event("Admin 2FA enabled.") + flash("2FA enabled successfully. Scan the QR code with your authenticator app.", "2fa-feedback") + return redirect(url_for("admin_page")) + + except Exception as e: + flash("Failed to enable 2FA") + print("[ERROR] 2FA enable failed:", e) + 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"): + return redirect(url_for("admin_login")) + + totp_code = request.form.get("totp_code") + if not totp_code: + flash("2FA code required to disable 2FA") + return redirect(url_for("admin_page")) + + try: + key = load_admin_key() + cipher = Fernet(key) + with open(ADMIN_CRED_FILE, 'rb') as file: + decrypted = cipher.decrypt(file.read()) + creds = json.loads(decrypted) + + # Verify 2FA code before disabling + if creds.get("2fa_enabled", False): + totp_secret = creds.get("totp_secret") + if totp_secret: + totp = pyotp.TOTP(totp_secret) + if not totp.verify(totp_code, valid_window=1): + flash("Invalid 2FA code") + return redirect(url_for("admin_page")) + + creds["totp_secret"] = None + creds["2fa_enabled"] = False + + encrypted = cipher.encrypt(json.dumps(creds).encode()) + with open(ADMIN_CRED_FILE, 'wb') as file: + file.write(encrypted) + + log_admin_event("Admin 2FA disabled.") + flash("2FA disabled successfully", "2fa-feedback") + return redirect(url_for("admin_page")) + + except Exception as e: + flash("Failed to disable 2FA") + print("[ERROR] 2FA disable failed:", e) + 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"): + return redirect(url_for("admin_login")) + + deleted = 0 + for filename in os.listdir(UPLOAD_FOLDER): + if filename.endswith(".enc") or filename.endswith(".json"): + try: + os.remove(os.path.join(UPLOAD_FOLDER, filename)) + deleted += 1 + except Exception as e: + print("[ERROR] Failed to delete:", filename, e) + + flash(f"Cleared {deleted} uploaded file(s).", "clear-feedback") + return redirect(url_for("admin_page")) + +@app.route("/admin-update-server", methods=["POST"]) +@admin_required +def admin_update_server(): + """Update server from GitHub repository.""" + if not session.get("admin_logged_in"): + return jsonify({"error": "Unauthorized"}), 401 + + try: + # Get the absolute path of the current directory + current_dir = os.path.abspath(os.path.dirname(__file__)) + + # Try to find git executable + git_paths = [ + "/usr/bin/git", # Standard Debian path + "/usr/local/bin/git", + "/bin/git", + "git" # Fallback to PATH + ] + + git_cmd = None + for path in git_paths: + if os.path.exists(path) or path == "git": + try: + # Test if git is executable + subprocess.run([path, "--version"], check=True, capture_output=True) + git_cmd = path + break + except Exception: + continue + + if not git_cmd: + return jsonify({"error": "Git executable not found. Please ensure git is installed and accessible."}), 500 + + # Try to find the git repository by checking parent directories + repo_dir = current_dir + max_depth = 5 # Limit how far up we'll look + found_git = False + + for _ in range(max_depth): + git_dir = os.path.join(repo_dir, ".git") + if os.path.exists(git_dir): + found_git = True + break + parent_dir = os.path.dirname(repo_dir) + if parent_dir == repo_dir: # We've reached the root directory + break + repo_dir = parent_dir + + if not found_git: + return jsonify({ + "error": "Git repository not found. Current directory: " + current_dir, + "details": "Please ensure the application is running from within the git repository directory." + }), 400 + + # Execute git commands with proper error handling + try: + # Fetch latest changes + fetch_result = subprocess.run([git_cmd, "fetch"], cwd=repo_dir, check=True, capture_output=True, text=True) + + # Reset to origin/main + reset_result = subprocess.run([git_cmd, "reset", "--hard", "origin/main"], cwd=repo_dir, check=True, capture_output=True, text=True) + + # Pull latest changes + pull_result = subprocess.run([git_cmd, "pull"], cwd=repo_dir, check=True, capture_output=True, text=True) + + return jsonify({ + "message": "Server updated successfully from GitHub!", + "details": { + "fetch": fetch_result.stdout, + "reset": reset_result.stdout, + "pull": pull_result.stdout + } + }), 200 + except subprocess.CalledProcessError as e: + error_msg = f"Git operation failed: {e.stderr if e.stderr else e.stdout}" + print(f"[ERROR] {error_msg}") + return jsonify({"error": error_msg}), 500 + + except Exception as e: + error_msg = f"Update failed: {str(e)}" + print(f"[ERROR] {error_msg}") + return jsonify({"error": error_msg}), 500 + +@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"): + return jsonify({"error": "Unauthorized"}), 401 + + try: + script_path = os.path.join(os.path.dirname(__file__), "application_data", "control_scripts", "restart_dev.py") + + if not os.path.exists(script_path): + return jsonify({"error": "Development restart script not found"}), 404 + + # Execute the restart script + subprocess.Popen(["python", script_path]) + + return jsonify({"message": "Switching to development mode... Server will restart momentarily."}), 200 + except Exception as e: + error_msg = f"Failed to switch to dev mode: {str(e)}" + print(f"[ERROR] {error_msg}") + 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"): + return jsonify({"error": "Unauthorized"}), 401 + + try: + script_path = os.path.join(os.path.dirname(__file__), "application_data", "control_scripts", "restart_prod.py") + + if not os.path.exists(script_path): + return jsonify({"error": "Production restart script not found"}), 404 + + # Execute the restart script + subprocess.Popen(["python", script_path]) + + return jsonify({"message": "Switching to production mode... Server will restart momentarily."}), 200 + except Exception as e: + error_msg = f"Failed to switch to prod mode: {str(e)}" + print(f"[ERROR] {error_msg}") + return jsonify({"error": error_msg}), 500 + +# ===== Sitemap and Robots ===== +@app.route("/sitemap", methods=["GET"]) +def sitemap(): + """Generate sitemap.xml.""" + sitemap_xml = ''' + + https://paccrypt.unnaturalll.dev/ + https://paccrypt.unnaturalll.dev/pickup + https://paccrypt.unnaturalll.dev/adminpage + https://paccrypt.unnaturalll.dev/sitemap +''' + return sitemap_xml, 200, {'Content-Type': 'application/xml'} + +@app.route("/robots.txt") +def robots_txt(): + """Generate robots.txt.""" + lines = [ + "User-agent: *", + "Disallow: /adminpage", + "Disallow: /admin-login", + "Disallow: /admin-setup", + "Disallow: /admin-reset", + "Disallow: /admin-settings", + "Disallow: /restart-server", + "Disallow: /pickup", + "Disallow: /admin-change-password", + "Allow: /", + f"Sitemap: {url_for('sitemap', _external=True)}" + ] + return "\n".join(lines), 200, {"Content-Type": "text/plain"} + +# ===== API Endpoints ===== +@app.route("/api/algorithms", methods=["GET"]) +@limiter.limit("100 per minute") +def api_algorithms(): + """Get list of available encryption algorithms.""" + algorithms = {} + for key, config in AVAILABLE_ALGORITHMS.items(): + algorithms[key] = { + "name": config["name"], + "description": config["description"], + "supports_text": config["supports_text"], + "supports_file": config["supports_file"], + "requires_keypair": config.get("requires_keypair", False) + } + 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: + data = request.get_json() + algorithm = data.get("algorithm", "rsa_hybrid") + + if algorithm not in AVAILABLE_ALGORITHMS: + return jsonify({"error": "Invalid algorithm"}), 400 + + if not AVAILABLE_ALGORITHMS[algorithm].get("requires_keypair"): + return jsonify({"error": "Algorithm does not require key pairs"}), 400 + + module = AVAILABLE_ALGORITHMS[algorithm]["module"] + private_key, public_key = module.generate_key_pair() + + return jsonify({ + "private_key": private_key.decode() if isinstance(private_key, bytes) else private_key, + "public_key": public_key.decode() if isinstance(public_key, bytes) else public_key + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route("/api/encrypt", methods=["POST"]) +@limiter.limit("30 per minute") +def api_encrypt(): + try: + # Text encryption + if request.is_json: + data = request.get_json() + message = data.get("message", "") + password = data.get("password", "") + algorithm = data.get("algorithm", "aes_gcm") + public_key = data.get("public_key", "") + + if not message: + return jsonify({"error": "Missing message"}), 400 + + if algorithm not in AVAILABLE_ALGORITHMS: + return jsonify({"error": "Invalid algorithm"}), 400 + + algo_config = AVAILABLE_ALGORITHMS[algorithm] + if not algo_config["supports_text"]: + return jsonify({"error": "Algorithm does not support text operations"}), 400 + + module = algo_config["module"] + + if algo_config.get("requires_keypair"): + if not public_key: + return jsonify({"error": "Public key required for this algorithm"}), 400 + encrypted = module.encrypt_text(message, public_key, algorithm.replace("_hybrid", "")) + else: + if not password: + return jsonify({"error": "Password required"}), 400 + encrypted = module.encrypt_text(message, password) + + return jsonify({"result": encrypted, "algorithm": algorithm}) + + # File encryption + if "file" in request.files and "enc_password" in request.form: + uploaded_file = request.files["file"] + password = request.form["enc_password"] + algorithm = request.form.get("algorithm", "aes_cbc") + + if algorithm not in AVAILABLE_ALGORITHMS: + return jsonify({"error": "Invalid algorithm"}), 400 + + algo_config = AVAILABLE_ALGORITHMS[algorithm] + if not algo_config["supports_file"]: + return jsonify({"error": "Algorithm does not support file operations"}), 400 + + file_data = uploaded_file.read() + temp_in = f"temp_in_{secrets.token_urlsafe(8)}" + temp_out = f"temp_out_{secrets.token_urlsafe(8)}" + + try: + with open(temp_in, 'wb') as f: + f.write(file_data) + + module = algo_config["module"] + module.encrypt_file(temp_in, temp_out, password) + + with open(temp_out, 'rb') as f: + encrypted_data = f.read() + + output_filename = f"{uploaded_file.filename}.{algorithm}.encrypted" + + return send_file( + BytesIO(encrypted_data), + as_attachment=True, + download_name=output_filename, + mimetype="application/octet-stream" + ) + finally: + for temp_file in [temp_in, temp_out]: + if os.path.exists(temp_file): + os.remove(temp_file) + + return jsonify({"error": "Missing or invalid input"}), 400 + + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route("/api/decrypt", methods=["POST"]) +@limiter.limit("30 per minute") +def api_decrypt(): + try: + # Text decryption + if request.is_json: + data = request.get_json() + encrypted_b64 = data.get("message", "") + password = data.get("password", "") + algorithm = data.get("algorithm", "aes_gcm") + private_key = data.get("private_key", "") + + if not encrypted_b64: + return jsonify({"error": "Missing encrypted message"}), 400 + + if algorithm not in AVAILABLE_ALGORITHMS: + return jsonify({"error": "Invalid algorithm"}), 400 + + algo_config = AVAILABLE_ALGORITHMS[algorithm] + if not algo_config["supports_text"]: + return jsonify({"error": "Algorithm does not support text operations"}), 400 + + module = algo_config["module"] + + if algo_config.get("requires_keypair"): + if not private_key: + return jsonify({"error": "Private key required for this algorithm"}), 400 + plaintext = module.decrypt_text(encrypted_b64, private_key) + else: + if not password: + return jsonify({"error": "Password required"}), 400 + plaintext = module.decrypt_text(encrypted_b64, password) + + return jsonify({"result": plaintext}) + + # File decryption + if "file" in request.files and "enc_password" in request.form: + uploaded_file = request.files["file"] + password = request.form["enc_password"] + + # Try to determine algorithm from filename + filename = uploaded_file.filename + algorithm = "aes_cbc" # default + + for algo_name in AVAILABLE_ALGORITHMS.keys(): + if f".{algo_name}.encrypted" in filename: + algorithm = algo_name + break + + # Allow override + algorithm = request.form.get("algorithm", algorithm) + + if algorithm not in AVAILABLE_ALGORITHMS: + return jsonify({"error": "Invalid algorithm"}), 400 + + algo_config = AVAILABLE_ALGORITHMS[algorithm] + if not algo_config["supports_file"]: + return jsonify({"error": "Algorithm does not support file operations"}), 400 + + encrypted_data = uploaded_file.read() + temp_in = f"temp_in_{secrets.token_urlsafe(8)}" + temp_out = f"temp_out_{secrets.token_urlsafe(8)}" + + try: + with open(temp_in, 'wb') as f: + f.write(encrypted_data) + + module = algo_config["module"] + module.decrypt_file(temp_in, temp_out, password) + + with open(temp_out, 'rb') as f: + decrypted_data = f.read() + + # Clean up filename + if f".{algorithm}.encrypted" in filename: + filename = filename.replace(f".{algorithm}.encrypted", "") + elif filename.endswith(".encrypted"): + filename = filename[:-10] + else: + filename = f"decrypted_{filename}" + + return send_file( + BytesIO(decrypted_data), + as_attachment=True, + download_name=filename, + mimetype="application/octet-stream" + ) + finally: + for temp_file in [temp_in, temp_out]: + if os.path.exists(temp_file): + os.remove(temp_file) + + return jsonify({"error": "Missing or invalid input"}), 400 + + except Exception as e: + 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") + pickup_password = request.form.get("pickup_password") + algorithm = request.form.get("algorithm", "aes_cbc") # Default to AES-CBC + enable_2fa = request.form.get("enable_2fa") == "on" # Check if 2FA checkbox is checked + file = request.files.get("file") + + if not file or not enc_password or not pickup_password: + return jsonify({"error": "Missing file or fields"}), 400 + + # Validate algorithm + if algorithm not in AVAILABLE_ALGORITHMS: + return jsonify({"error": "Invalid algorithm"}), 400 + + algo_config = AVAILABLE_ALGORITHMS[algorithm] + if not algo_config["supports_file"]: + return jsonify({"error": "Algorithm does not support file operations"}), 400 + + filename = secure_filename(file.filename) + temp_path = os.path.join(UPLOAD_FOLDER, f"temp_{secrets.token_urlsafe(8)}_{filename}") + file.save(temp_path) + + try: + # Use the selected algorithm for encryption + module = algo_config["module"] + + file_id = secrets.token_urlsafe(24) + encrypted_filename = f"{file_id}.{algorithm}.encrypted" + enc_path = os.path.join(UPLOAD_FOLDER, encrypted_filename) + meta_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.json") + + # Encrypt file using the correct API (in_path, out_path, password) + module.encrypt_file(temp_path, enc_path, enc_password) + + encrypted_filename = encrypt_filename(filename, enc_password) + + meta = { + 'pickup_password': base64.urlsafe_b64encode( + hashlib.sha256(pickup_password.encode()).digest() + ).decode(), + 'original_name': encrypted_filename, + 'algorithm': algorithm, # Store algorithm used for decryption + 'timestamp': datetime.now().isoformat(), + 'require_2fa': enable_2fa + } + + # Generate TOTP secret if 2FA is enabled + if enable_2fa: + totp_secret = pyotp.random_base32() + meta['totp_secret'] = totp_secret + meta['service_name'] = f"PacCrypt File: {filename[:20]}..." + + with open(meta_path, "w") as f: + json.dump(meta, f) + + pickup_url = request.host_url.rstrip('/') + url_for('pickup_file', file_id=file_id) + response_data = {"pickup_url": pickup_url} + + # If 2FA is enabled, also return QR code URL for immediate setup + if enable_2fa: + qr_url = request.host_url.rstrip('/') + url_for('generate_qr_code', file_id=file_id) + response_data["qr_code_url"] = qr_url + response_data["totp_secret"] = totp_secret + response_data["service_name"] = f"PacCrypt File: {filename[:20]}..." + + return jsonify(response_data) + + finally: + # Clean up temp file + if os.path.exists(temp_path): + os.remove(temp_path) + + except Exception as e: + return jsonify({"error": str(e)}), 500 + +# ===== Error Handlers ===== +@app.errorhandler(404) +def page_not_found(e): + return render_template('404.html'), 404 + +@app.errorhandler(500) +def server_error(e): + return render_template('500.html'), 500 + +@app.errorhandler(403) +def forbidden(e): + return render_template('403.html'), 403 + +@app.errorhandler(405) +def method_not_allowed(e): + return render_template('403.html'), 403 + +@app.errorhandler(FileNotFoundError) +def handle_file_not_found(e): + if os.getenv("PRODUCTION", "false").lower() == "true": + return render_template('500.html'), 500 + else: + raise e + +# ===== Application Entry Point ===== +if __name__ == "__main__": + PRODUCTION = os.getenv("PRODUCTION", "false").lower() == "true" + if PRODUCTION: + from waitress import serve + print("[INFO] Running in PRODUCTION mode with Waitress.") + serve(app, host="0.0.0.0", port=5000) + else: + print("[INFO] Running in DEVELOPMENT mode with Flask server.") + app.run(debug=True, host="0.0.0.0", port=5000) + diff --git a/application_data/control_scripts/restart_dev.py b/application_data/control_scripts/restart_dev.py new file mode 100644 index 0000000..3b00d56 --- /dev/null +++ b/application_data/control_scripts/restart_dev.py @@ -0,0 +1,72 @@ +import os +import subprocess +import signal +import time +import sys +import psutil +import platform + +APP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../app.py")) + +DEBUG = True + +def log(msg): + if DEBUG: + print(msg) + +def start_dev(): + env = os.environ.copy() + env["PRODUCTION"] = "false" + + if platform.system() == "Windows": + return subprocess.Popen( + ["python", APP_PATH], + env=env, + stdout=sys.stdout, + stderr=sys.stderr + ) + else: + return subprocess.Popen( + ["python3", APP_PATH], + env=env, + preexec_fn=os.setsid, + stdout=sys.stdout, + stderr=sys.stderr + ) + +def stop_by_port(port=5000): + for proc in psutil.process_iter(["pid", "name"]): + try: + for conn in proc.net_connections(kind="inet"): + if conn.laddr.port == port: + log(f"[*] Killing process {proc.pid} using port {port}") + if platform.system() == "Windows": + proc.terminate() + try: + proc.wait(timeout=5) + except psutil.TimeoutExpired: + proc.kill() + else: + os.killpg(os.getpgid(proc.pid), signal.SIGTERM) + return + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + continue + log(f"[!] No process found using port {port}") + +def main(): + log("[*] Restarting PacCrypt in DEVELOPMENT mode...") + stop_by_port() + time.sleep(2) + proc = start_dev() + if proc: + log(f"[*] Started development server with PID {proc.pid}") + try: + proc.wait() + except KeyboardInterrupt: + log("[*] Interrupted, stopping server...") + stop_by_port() + else: + log("[!] Failed to start development server") + +if __name__ == "__main__": + main() diff --git a/application_data/control_scripts/restart_prod.py b/application_data/control_scripts/restart_prod.py new file mode 100644 index 0000000..f0704b1 --- /dev/null +++ b/application_data/control_scripts/restart_prod.py @@ -0,0 +1,66 @@ +import os +import subprocess +import signal +import time +import sys +import psutil +import platform + +APP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../app.py")) + +def start_prod(): + env = os.environ.copy() + env["PRODUCTION"] = "true" + + if platform.system() == "Windows": + return subprocess.Popen( + ["python", APP_PATH], + env=env, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + else: + return subprocess.Popen( + ["python3", APP_PATH], + env=env, + preexec_fn=os.setsid, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + +def stop_by_port(port=5000): + for proc in psutil.process_iter(["pid", "name"]): + try: + for conn in proc.net_connections(kind="inet"): + if conn.laddr.port == port: + print(f"[*] Killing process {proc.pid} using port {port}") + if platform.system() == "Windows": + proc.terminate() + try: + proc.wait(timeout=5) + except psutil.TimeoutExpired: + proc.kill() + else: + os.killpg(os.getpgid(proc.pid), signal.SIGTERM) + return + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + continue + print(f"[!] No process found using port {port}") + +def main(): + print("[*] Restarting PacCrypt in PRODUCTION mode with Waitress...") + stop_by_port() + time.sleep(2) + proc = start_prod() + if proc: + print(f"[*] Started production server with PID {proc.pid}") + try: + proc.wait() + except KeyboardInterrupt: + print("[*] Interrupted, stopping server...") + stop_by_port() + else: + print("[!] Failed to start production server") + +if __name__ == "__main__": + main() diff --git a/application_data/control_scripts/start_dev.py b/application_data/control_scripts/start_dev.py new file mode 100644 index 0000000..6caef6c --- /dev/null +++ b/application_data/control_scripts/start_dev.py @@ -0,0 +1,40 @@ +import os +import subprocess +import time +import sys +import platform + +APP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../app.py")) + +DEBUG = True + +def log(msg): + if DEBUG: + print(msg) + +def start_dev(): + env = os.environ.copy() + env["PRODUCTION"] = "false" + + if platform.system() == "Windows": + return subprocess.Popen( + ["python", APP_PATH], + env=env, + stdout=sys.stdout, + stderr=sys.stderr + ) + else: + return subprocess.Popen( + ["python3", APP_PATH], + env=env, + preexec_fn=os.setsid, + stdout=sys.stdout, + stderr=sys.stderr + ) + +def main(): + log("[*] Starting PacCrypt in DEVELOPMENT mode...") + start_dev() + +if __name__ == "__main__": + main() diff --git a/application_data/control_scripts/start_prod.py b/application_data/control_scripts/start_prod.py new file mode 100644 index 0000000..ec388f7 --- /dev/null +++ b/application_data/control_scripts/start_prod.py @@ -0,0 +1,34 @@ +import os +import subprocess +import time +import sys +import platform + +APP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../app.py")) + +def start_prod(): + env = os.environ.copy() + env["PRODUCTION"] = "true" + + if platform.system() == "Windows": + return subprocess.Popen( + ["python", APP_PATH], + env=env, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + else: + return subprocess.Popen( + ["python3", APP_PATH], + env=env, + preexec_fn=os.setsid, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + +def main(): + print("[*] Starting PacCrypt in PRODUCTION mode with Waitress...") + start_prod() + +if __name__ == "__main__": + main() diff --git a/application_data/control_scripts/stop.py b/application_data/control_scripts/stop.py new file mode 100644 index 0000000..425e148 --- /dev/null +++ b/application_data/control_scripts/stop.py @@ -0,0 +1,35 @@ +import psutil +import os +import signal +import platform + +DEBUG = True + +def log(msg): + if DEBUG: + print(msg) + +def stop_by_port(port=5000): + for proc in psutil.process_iter(["pid", "name"]): + try: + for conn in proc.net_connections(kind="inet"): + if conn.laddr.port == port: + log(f"[*] Killing process {proc.pid} using port {port}") + if platform.system() == "Windows": + proc.terminate() + try: + proc.wait(timeout=5) + except psutil.TimeoutExpired: + proc.kill() + else: + os.killpg(os.getpgid(proc.pid), signal.SIGTERM) + return + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + continue + log(f"[!] No process found using port {port}") + +def main(): + stop_by_port() + +if __name__ == "__main__": + main() diff --git a/application_data/requirements.txt b/application_data/requirements.txt new file mode 100644 index 0000000..af86a7a --- /dev/null +++ b/application_data/requirements.txt @@ -0,0 +1,26 @@ +### **requirements.txt** + +# Core Flask stack +flask +flask-cors +waitress +werkzeug + +# Encryption engines +cryptography +pycryptodome +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/paccrypt_algos/__init__.py b/paccrypt_algos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/paccrypt_algos/aes_cbc.py b/paccrypt_algos/aes_cbc.py new file mode 100644 index 0000000..a36f2e4 --- /dev/null +++ b/paccrypt_algos/aes_cbc.py @@ -0,0 +1,136 @@ +import os +import base64 +from typing import Optional + +from cryptography.hazmat.primitives import padding, hashes, hmac +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.backends import default_backend +from cryptography.exceptions import InvalidSignature + +# === Constants === +SALT_LENGTH = 16 +IV_LENGTH = 16 +PBKDF2_ITERATIONS = 200_000 +KEY_LENGTH = 32 +HMAC_KEY_LENGTH = 32 # For HMAC-SHA256 +HMAC_LENGTH = 32 # Output size of SHA256 + +# === Base64 Helpers === +def b64encode(data: bytes) -> str: + return base64.b64encode(data).decode('utf-8') + +def b64decode(data: str) -> bytes: + return base64.b64decode(data.encode('utf-8')) + +# === Key Derivation === +def derive_key(password: str, salt: bytes) -> bytes: + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=KEY_LENGTH + HMAC_KEY_LENGTH, + salt=salt, + iterations=PBKDF2_ITERATIONS, + backend=default_backend() + ) + full_key = kdf.derive(password.encode('utf-8')) + return full_key[:KEY_LENGTH], full_key[KEY_LENGTH:] + +# === Encrypt Text === +def encrypt_text(plaintext: str, password: str) -> str: + salt = os.urandom(SALT_LENGTH) + iv = os.urandom(IV_LENGTH) + aes_key, hmac_key = derive_key(password, salt) + + padder = padding.PKCS7(128).padder() + padded = padder.update(plaintext.encode('utf-8')) + padder.finalize() + + cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv), backend=default_backend()) + encryptor = cipher.encryptor() + ciphertext = encryptor.update(padded) + encryptor.finalize() + + payload = salt + iv + ciphertext + + h = hmac.HMAC(hmac_key, hashes.SHA256(), backend=default_backend()) + h.update(payload) + mac = h.finalize() + + return b64encode(payload + mac) + +# === Decrypt Text === +def decrypt_text(encrypted_b64: str, password: str) -> str: + raw = b64decode(encrypted_b64) + + salt = raw[:SALT_LENGTH] + iv = raw[SALT_LENGTH:SALT_LENGTH + IV_LENGTH] + ciphertext = raw[SALT_LENGTH + IV_LENGTH:-HMAC_LENGTH] + mac = raw[-HMAC_LENGTH:] + + aes_key, hmac_key = derive_key(password, salt) + + h = hmac.HMAC(hmac_key, hashes.SHA256(), backend=default_backend()) + h.update(raw[:-HMAC_LENGTH]) + h.verify(mac) + + cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv), backend=default_backend()) + decryptor = cipher.decryptor() + padded = decryptor.update(ciphertext) + decryptor.finalize() + + unpadder = padding.PKCS7(128).unpadder() + plaintext = unpadder.update(padded) + unpadder.finalize() + + return plaintext.decode('utf-8') + +# === Encrypt File === +def encrypt_file(in_path, out_path, password: str, metadata: Optional[dict] = None): + with open(in_path, 'rb') as f: + plaintext = f.read() + + salt = os.urandom(SALT_LENGTH) + iv = os.urandom(IV_LENGTH) + aes_key, hmac_key = derive_key(password, salt) + + padder = padding.PKCS7(128).padder() + padded = padder.update(plaintext) + padder.finalize() + + cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv), backend=default_backend()) + encryptor = cipher.encryptor() + ciphertext = encryptor.update(padded) + encryptor.finalize() + + payload = salt + iv + ciphertext + + h = hmac.HMAC(hmac_key, hashes.SHA256(), backend=default_backend()) + h.update(payload) + mac = h.finalize() + + with open(out_path, 'wb') as f: + f.write(payload + mac) + +# === Decrypt File === +def decrypt_file(in_path, out_path, password: str, metadata: Optional[dict] = None): + with open(in_path, 'rb') as f: + raw = f.read() + + salt = raw[:SALT_LENGTH] + iv = raw[SALT_LENGTH:SALT_LENGTH + IV_LENGTH] + ciphertext = raw[SALT_LENGTH + IV_LENGTH:-HMAC_LENGTH] + mac = raw[-HMAC_LENGTH:] + + aes_key, hmac_key = derive_key(password, salt) + + h = hmac.HMAC(hmac_key, hashes.SHA256(), backend=default_backend()) + h.update(raw[:-HMAC_LENGTH]) + h.verify(mac) + + cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv), backend=default_backend()) + decryptor = cipher.decryptor() + padded = decryptor.update(ciphertext) + decryptor.finalize() + + unpadder = padding.PKCS7(128).unpadder() + plaintext = unpadder.update(padded) + unpadder.finalize() + + with open(out_path, 'wb') as f: + f.write(plaintext) + +# === Algo Name === +def get_name(): + return "AES-CBC" diff --git a/paccrypt_algos/aes_gcm.py b/paccrypt_algos/aes_gcm.py new file mode 100644 index 0000000..c68f49f --- /dev/null +++ b/paccrypt_algos/aes_gcm.py @@ -0,0 +1,68 @@ +import os +import base64 +import json +from typing import Optional + +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.backends import default_backend + +# === Constants === +SALT_LENGTH = 16 +IV_LENGTH = 12 +PBKDF2_ITERATIONS = 200_000 +KEY_LENGTH = 32 # 256 bits + +# === Base64 Helpers === +def b64encode(data: bytes) -> str: + return base64.b64encode(data).decode('utf-8') + +def b64decode(data: str) -> bytes: + return base64.b64decode(data.encode('utf-8')) + +# === Key Derivation === +def derive_key(password: str, salt: bytes) -> bytes: + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=KEY_LENGTH, + salt=salt, + iterations=PBKDF2_ITERATIONS, + backend=default_backend() + ) + return kdf.derive(password.encode('utf-8')) + +# === Encrypt Text === +def encrypt_text(plaintext: str, password: str) -> str: + salt = os.urandom(SALT_LENGTH) + iv = os.urandom(IV_LENGTH) + key = derive_key(password, salt) + + aesgcm = AESGCM(key) + ciphertext = aesgcm.encrypt(iv, plaintext.encode('utf-8'), None) + + payload = salt + iv + ciphertext + return b64encode(payload) + +# === Decrypt Text === +def decrypt_text(encrypted_b64: str, password: str) -> str: + raw = b64decode(encrypted_b64) + salt = raw[:SALT_LENGTH] + iv = raw[SALT_LENGTH:SALT_LENGTH + IV_LENGTH] + ciphertext = raw[SALT_LENGTH + IV_LENGTH:] + + key = derive_key(password, salt) + aesgcm = AESGCM(key) + plaintext = aesgcm.decrypt(iv, ciphertext, None) + return plaintext.decode('utf-8') + +# === Metadata-less file interface (optional placeholders) === +def encrypt_file(in_path, out_path, key, metadata: Optional[dict] = None): + raise NotImplementedError("File encryption not implemented yet.") + +def decrypt_file(in_path, out_path, key, metadata: Optional[dict] = None): + raise NotImplementedError("File decryption not implemented yet.") + +# === Engine Name === +def get_name(): + return "AES-GCM" diff --git a/paccrypt_algos/rsa_hybrid.py b/paccrypt_algos/rsa_hybrid.py new file mode 100644 index 0000000..43ceb9d --- /dev/null +++ b/paccrypt_algos/rsa_hybrid.py @@ -0,0 +1,151 @@ +import os +import base64 +import json +import importlib +from typing import Optional, Tuple +import sys +from pathlib import Path + +from cryptography.hazmat.primitives.asymmetric import rsa, padding +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.backends import default_backend + +PARENT_DIR = Path(__file__).resolve().parent.parent +if str(PARENT_DIR) not in sys.path: + sys.path.append(str(PARENT_DIR)) + +# === Constants === +RSA_KEY_SIZE = 4096 +AES_KEY_SIZE = 32 # 256-bit + +# === Base64 Helpers === +def b64encode(data: bytes) -> str: + return base64.b64encode(data).decode("utf-8") + +def b64decode(data: str) -> bytes: + return base64.b64decode(data.encode("utf-8")) + +# === RSA Key Generation === +def generate_key_pair() -> Tuple[bytes, bytes]: + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=RSA_KEY_SIZE, + backend=default_backend() + ) + private_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption() + ) + public_pem = private_key.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + return private_pem, public_pem + +# === Dynamic Engine Loader === +def load_engine(engine_name: str): + try: + return importlib.import_module(f'paccrypt_algos.{engine_name}') + except ModuleNotFoundError: + raise ValueError(f"Encryption engine '{engine_name}' not found.") + +# === Encrypt Text === +def encrypt_text(plaintext: str, public_key_pem: str, engine_name: str = "aes_gcm") -> str: + engine = load_engine(engine_name) + aes_key = os.urandom(AES_KEY_SIZE) + + public_key = serialization.load_pem_public_key(public_key_pem.encode(), backend=default_backend()) + encrypted_key = public_key.encrypt( + aes_key, + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA256()), + algorithm=hashes.SHA256(), + label=None + ) + ) + + encrypted_data = engine.encrypt_text(plaintext, aes_key.hex()) + header = json.dumps({"alg": engine_name}).encode() + payload = len(encrypted_key).to_bytes(2, 'big') + encrypted_key + header + b'\0' + encrypted_data.encode() + return b64encode(payload) + +# === Decrypt Text === +def decrypt_text(encrypted_b64: str, private_key_pem: str) -> str: + private_key = serialization.load_pem_private_key(private_key_pem.encode(), password=None, backend=default_backend()) + raw = b64decode(encrypted_b64) + + enc_key_len = int.from_bytes(raw[:2], 'big') + enc_key = raw[2:2 + enc_key_len] + rest = raw[2 + enc_key_len:] + header_data, encrypted_data = rest.split(b'\0', 1) + engine_name = json.loads(header_data.decode()).get("alg") + + aes_key = private_key.decrypt( + enc_key, + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA256()), + algorithm=hashes.SHA256(), + label=None + ) + ) + + engine = load_engine(engine_name) + return engine.decrypt_text(encrypted_data.decode(), aes_key.hex()) + +# === Encrypt File === +def encrypt_file(in_path: str, out_path: str, public_key_pem: str, engine_name: str = "aes_gcm"): + engine = load_engine(engine_name) + aes_key = os.urandom(AES_KEY_SIZE) + + public_key = serialization.load_pem_public_key(public_key_pem.encode(), backend=default_backend()) + encrypted_key = public_key.encrypt( + aes_key, + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA256()), + algorithm=hashes.SHA256(), + label=None + ) + ) + + with open(in_path, 'rb') as f: + plaintext = f.read() + + encrypted_data = engine.encrypt_file_bytes(plaintext, aes_key.hex()) + header = json.dumps({"alg": engine_name}).encode() + payload = len(encrypted_key).to_bytes(2, 'big') + encrypted_key + header + b'\0' + encrypted_data + + with open(out_path, 'wb') as f: + f.write(payload) + +# === Decrypt File === +def decrypt_file(in_path: str, out_path: str, private_key_pem: str): + private_key = serialization.load_pem_private_key(private_key_pem.encode(), password=None, backend=default_backend()) + + with open(in_path, 'rb') as f: + raw = f.read() + + enc_key_len = int.from_bytes(raw[:2], 'big') + enc_key = raw[2:2 + enc_key_len] + rest = raw[2 + enc_key_len:] + header_data, encrypted_data = rest.split(b'\0', 1) + engine_name = json.loads(header_data.decode()).get("alg") + + aes_key = private_key.decrypt( + enc_key, + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA256()), + algorithm=hashes.SHA256(), + label=None + ) + ) + + engine = load_engine(engine_name) + plaintext = engine.decrypt_file_bytes(encrypted_data, aes_key.hex()) + + with open(out_path, 'wb') as f: + f.write(plaintext) + +# === Engine Name === +def get_name(): + return "RSA Hybrid" diff --git a/paccrypt_algos/xchacha.py b/paccrypt_algos/xchacha.py new file mode 100644 index 0000000..0f4a933 --- /dev/null +++ b/paccrypt_algos/xchacha.py @@ -0,0 +1,92 @@ +import os +import base64 +from typing import Optional +from Crypto.Cipher import ChaCha20_Poly1305 +from Crypto.Random import get_random_bytes +from Crypto.Protocol.KDF import PBKDF2 +from Crypto.Hash import SHA256 + +# === Constants === +SALT_LENGTH = 16 +NONCE_LENGTH = 24 +KEY_LENGTH = 32 +PBKDF2_ITERATIONS = 200_000 +TAG_LENGTH = 16 + +# === Base64 Helpers === +def b64encode(data: bytes) -> str: + return base64.b64encode(data).decode('utf-8') + +def b64decode(data: str) -> bytes: + return base64.b64decode(data.encode('utf-8')) + +# === Key Derivation === +def derive_key(password: str, salt: bytes) -> bytes: + return PBKDF2(password, salt, dkLen=KEY_LENGTH, count=PBKDF2_ITERATIONS, hmac_hash_module=SHA256) + +# === Encrypt Text === +def encrypt_text(plaintext: str, password: str) -> str: + salt = get_random_bytes(SALT_LENGTH) + nonce = get_random_bytes(NONCE_LENGTH) + key = derive_key(password, salt) + + cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce) + ciphertext, tag = cipher.encrypt_and_digest(plaintext.encode('utf-8')) + + final = salt + nonce + ciphertext + tag + return b64encode(final) + +# === Decrypt Text === +def decrypt_text(encrypted_b64: str, password: str) -> str: + raw = b64decode(encrypted_b64) + salt = raw[:SALT_LENGTH] + nonce = raw[SALT_LENGTH:SALT_LENGTH + NONCE_LENGTH] + tag = raw[-TAG_LENGTH:] + ciphertext = raw[SALT_LENGTH + NONCE_LENGTH:-TAG_LENGTH] + + key = derive_key(password, salt) + cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce) + plaintext = cipher.decrypt_and_verify(ciphertext, tag) + + return plaintext.decode('utf-8') + +# === Encrypt File === +def encrypt_file(in_path, out_path, password: str, metadata: Optional[dict] = None): + with open(in_path, 'rb') as f: + plaintext = f.read() + + salt = get_random_bytes(SALT_LENGTH) + nonce = get_random_bytes(NONCE_LENGTH) + key = derive_key(password, salt) + + cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce) + ciphertext, tag = cipher.encrypt_and_digest(plaintext) + + with open(out_path, 'wb') as f: + f.write(salt + nonce + ciphertext + tag) + +# === Decrypt File === +def decrypt_file(in_path, out_path, password: str, metadata: Optional[dict] = None): + with open(in_path, 'rb') as f: + raw = f.read() + + salt = raw[:SALT_LENGTH] + nonce = raw[SALT_LENGTH:SALT_LENGTH + NONCE_LENGTH] + tag = raw[-TAG_LENGTH:] + ciphertext = raw[SALT_LENGTH + NONCE_LENGTH:-TAG_LENGTH] + + key = derive_key(password, salt) + cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce) + plaintext = cipher.decrypt_and_verify(ciphertext, tag) + + with open(out_path, 'wb') as f: + f.write(plaintext) + +# === Engine Name === +def get_name(): + return "XChaCha20-Poly1305" + + +if __name__ == "__main__": + from Crypto.Cipher.ChaCha20_Poly1305 import ChaCha20Poly1305Cipher as _test # Force import to validate availability + from cryptography.exceptions import InvalidTag # Still catchable for consistency diff --git a/static/audio/chomp.mp3 b/static/audio/chomp.mp3 new file mode 100644 index 0000000..f55a949 Binary files /dev/null and b/static/audio/chomp.mp3 differ diff --git a/static/css/styles.css b/static/css/styles.css new file mode 100644 index 0000000..126cb54 --- /dev/null +++ b/static/css/styles.css @@ -0,0 +1,1560 @@ +/* ===== Global Reset ===== */ +* { + box-sizing: border-box; + gap: 6px !important; + 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; + background-color: #0e0e0e; + color: #28E060; + font-size: 13px; + line-height: 1.6; + display: flex; + flex-direction: column; + min-height: 100vh; + justify-content: center; + align-items: center; + padding: 20px; +} + +@media (max-width: 600px) { + #sitemap-section, + #password-change-section, + #server-update-section, + #server-status-section, + #server-logs-section, + #system-settings-section { + padding: 20px; + margin-bottom: 20px; + } + + #sitemap-section li, + #server-status-section li { + font-size: 0.9em; + padding: 6px; + } + + #logContainer { + font-size: 0.9em; + padding: 10px; + } + + body { + font-size: 11px; + padding: 10px; + } + + .button-group, + .admin-button-grid { + flex-direction: column; + align-items: center; + } + + .button-group button, + .admin-button-grid button { + min-width: 75%; + max-width: 75%; + } + + header { + flex-direction: column; + height: auto; + padding-inline: 15px; + padding-block: 20px; + } + + .logo-container { + flex-direction: column; + align-items: center; + } + + .logo-container img { + height: 100px !important; + margin-top: -15px !important; + } + + .logo-text { + margin-left: 0 !important; + text-align: center; + } + + .logo-text h1 { + font-size: 1.4em; + margin-top: -30px !important; + margin-left: 0 !important; + text-align: center !important; + } + + .logo-text p { + font-size: 0.8em; + margin-left: 0 !important; + text-align: center !important; + } + + .admin-button-grid { + grid-template-columns: 1fr; + } + + .status-list { + width: 100%; + max-width: 400px; + padding-left: 0; + list-style: none; + word-wrap: break-word; + overflow-wrap: break-word; + } +} + +/* ===== Header ===== */ +header { + display: flex; + justify-content: center; + align-items: center; + background-color: #111; + border-radius: 12px; + box-shadow: 0 0 15px #28E060; + width: 100%; + max-width: 800px; + margin-bottom: 25px; + padding: 25px; + height: 200px; +} + +.logo-container { + display: flex; + align-items: center; +} + +.logo-container img { + height: 200px; + width: auto; +} + +.logo-text h1 { + font-size: clamp(1.4em, 6vw, 2.8em); + word-break: break-word; + overflow-wrap: break-word; + color: #28E060; + margin: 0; + margin-left: -30px; /* overlap effect */ + text-align: left; +} + +.logo-text p { + font-size: 1.2em; + color: #28E060; + margin: 0; + margin-left: -30px; + text-align: left; +} + + +/* ===== Main Layout ===== */ +main { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + max-width: 800px; + padding: 0; +} + +/* ===== Card Styling ===== */ +.card { + background-color: #1e1e1e; + padding: 25px; + width: 100%; + border-radius: 12px; + box-shadow: 0 0 15px #28E060; + text-align: center; +} + +/* ===== Form Group Styling ===== */ +.form-group { + display: flex !important; + flex-direction: column; + align-items: center; + max-width: 725px; + width: 100%; +} + +.status-list { + width: 100%; + max-width: 400px; + padding-left: 0; + list-style: none; + word-wrap: break-word; + overflow-wrap: break-word; +} + + +/* ===== Inputs, Textareas, Selects ===== */ + +button, +select, +input, +textarea { + font-family: 'Press Start 2P', monospace; + font-size: 12px !important; + letter-spacing: 0.5px; +} + +input, +textarea, +select, +input[type="file"] { + width: 80%; + max-width: 500px; + padding-inline: 20px; + padding-block: 12px; + border: 1px solid #28E060; + border-radius: 8px; + background-color: #2c2f33; + color: #28E060; + text-align: left; + transition: 0.3s; + min-height: 50px; +} + +select { + text-align: center; +} + +textarea { + min-height: 140px; + resize: none; +} + +/* ===== File Input Customization ===== */ +input[type="file"] { + border: 2px dashed #28E060; + cursor: pointer; + color: #28E060; + background-color: #2c2f33; +} + +input[type="file"]::file-selector-button { + font-family: 'Press Start 2P', monospace; + font-size: 12px; + background-color: #2c2f33; + color: #28E060; + border: 2px solid #28E060; + padding-inline: 10px; + padding-block: 8px; + margin-right: 10px; + border-radius: 6px; + text-transform: uppercase; + cursor: pointer; + transition: 0.3s ease; +} + +input[type="file"]::file-selector-button:hover { + background-color: #28E060; + color: #000; + box-shadow: 0 0 10px #28E060; +} + +/* ===== Focus Effects ===== */ +input:focus, +textarea:focus, +select:focus { + outline: none; + box-shadow: 0 0 10px #28E060; +} + +/* ===== Textareas Specific Widths ===== */ +#input-text, +#output-text { + width: 80%; + max-width: 500px; + height: 140px; +} + +/* ===== Button Group Styling ===== */ +.button-group { + display: flex; + flex-wrap: nowrap; + justify-content: center; + width: 100%; +} + +button { + padding-inline: 20px; + padding-block: 10px; + border: none; + border-radius: 8px; + background-color: #2c2f33; + color: #28E060; + font-size: 1em; + cursor: pointer; + transition: 0.3s; + width: auto; + min-width: 225px; + max-width: 300px; + height: 45px; +} + + button:hover { + background-color: #28E060; + color: #121212; + box-shadow: 0 0 10px #28E060; + } + +.danger-button { + background-color: #5f3131; + box-shadow: 0 0 10px #991717; +} + +.danger-button:hover { + background-color: #af0000; + color: #121212; + box-shadow: 0 0 40px #ff0000; +} + +.admin-button-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + justify-items: center; + width: 100%; + max-width: 640px; + margin: 0 auto; + } + + .admin-button-grid button { + width: 100%; + max-width: 280px; + font-family: 'Press Start 2P', monospace; + font-size: 12px; + } + + + +/* ===== Toggle Switch Styling ===== */ +.toggle-container { + display: flex; + align-items: center; + justify-content: center; + } + + .toggle-label { + font-family: 'Press Start 2P', monospace; + font-size: 12px; + color: #28E060; + } + + .material-switch { + position: relative; + display: inline-block; + width: 60px; + height: 34px; + } + + .material-switch input { + opacity: 0; + width: 0; + height: 0; + } + + .material-slider { + position: absolute; + cursor: pointer; + top: 0; left: 0; right: 0; bottom: 0; + background-color: #222; + border: 2px solid #28E060; + border-radius: 34px; + transition: 0.4s; + margin: unset; + } + + .material-slider::before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 2px; + bottom: 2px; + background-color: #28E060; + border-radius: 50%; + transition: 0.4s; + box-shadow: 0 0 6px #28E060; + } + + .material-switch input:checked + .material-slider { + background-color: #28E060; + } + + .material-switch input:checked + .material-slider::before { + transform: translateX(26px); + background-color: #000; + } + + + /* Label beside switch */ + #toggle-label { + font-family: 'Press Start 2P', monospace; + color: #28E060; + margin-left: 20px; + font-size: 12px; + } + +.toggle-container { + display: flex; + align-items: center; + justify-content: center; + width: 100%; +} + +/* Make sure the switch aligns well */ +.switch { + position: relative; + display: flex; + align-items: center; /* <-- Ensures vertical centering */ + justify-content: center; + width: 70px; + height: 34px; +} + + /* Hide the checkbox */ + .switch input { + opacity: 0; + width: 0; + height: 0; + } + +/* The slider */ +.slider { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #2c2f33; + border: 2px solid #28E060; + border-radius: 34px; + transition: .4s; + display: flex; + align-items: center; +} + + /* The circle knob */ + .slider::before { + content: ""; + height: 22px; + width: 22px; + background-color: #28E060; + border-radius: 50%; + transition: .4s; + transform: translateX(2px); + position: absolute; + left: auto; + bottom: auto; + } + +input:checked + .slider::before { + transform: translateX(36px); +} + +/* Toggle Labels */ +.labels { + position: relative; + width: 100px; + display: flex; + justify-content: space-between; + font-size: 0.9em; + color: #28E060; + margin-top: 5px; +} + + .labels::before, + .labels::after { + content: attr(data-on); + width: 50%; + text-align: center; + } + + .labels::after { + content: attr(data-off); + } + +/* ===== Toast Notifications ===== */ +.toast { + visibility: hidden; + width: 80%; + max-width: 500px; + min-height: 50px; + background-color: #333; + color: #28E060; + text-align: center; + border-radius: 8px; + padding: 14px; + margin: 10px auto 0 auto; + font-size: 1em; + display: flex; + align-items: center; + justify-content: center; +} + + .toast.show { + visibility: visible; + animation: fadein 0.5s, fadeout 0.5s 2.5s; + } + +@keyframes fadein { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes fadeout { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} + +/* ===== Footer ===== */ +footer { + text-align: center; + padding: 25px; + background-color: #1c1c1c; + color: #28E060; + border-radius: 12px; + box-shadow: 0 0 15px #28E060; + width: 100%; + max-width: 800px; + margin-top: 25px; +} + + footer a { + color: #28E060; + text-decoration: none; + } + + footer a:hover { + color: #ff0066; + } + +/* ===== Responsive Design ===== */ +@media (max-width: 600px) { + input, + textarea, + select, + #input-text, + #output-text { + width: 100% !important; + max-width: 90% !important; + } +} + +/* ===== Copy Feedback Message ===== */ +.copy-feedback, #shared-link-feedback { + background-color: #2c2f33; + padding-inline: 12px; + padding-block: 6px; + margin-top: 6px; + border-radius: 6px; + color: #28E060; + font-size: 0.9em; + display: none; + opacity: 0; + text-align: center; + max-width: 500px; + margin-left: auto; + margin-right: auto; + transition: opacity 0.3s ease; +} + +.copy-feedback.show, #shared-link-feedback.show { + display: block; + opacity: 1; +} + +.share-link-container { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 12px; + margin-bottom: 12px; +} + +#share-link { + display: block; + background-color: #2c2f33; + padding-inline: 16px; + padding-block: 8px; + border-radius: 6px; + color: #28E060; + font-size: 0.9em; + text-align: center; + max-width: 720px; + width: 100%; + word-break: break-all; + text-decoration: none; + transition: all 0.3s ease; +} + +#share-link:hover { + color: #00cc77; + background-color: #36393f; +} + +/* ===== Form Styling ===== */ +form { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; +} + + +/* ===== Section Card Styling ===== */ +section.card { + display: flex; + flex-direction: column; + align-items: center; +} + +/* ===== Pacman Game Styling ===== */ +#pacmanCanvas { + background-color: black; + display: block; + border: 2px solid #28E060; + border-radius: 12px; + max-width: 700px; + width: 100%; + aspect-ratio: 4/3; + object-fit: contain; +} + +#pacman-section { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 25px; + max-width: 725px; + width: 100%; +} + +.pacman-wrapper { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + padding: 0; + margin: 0; +} + +/* ===== Utility Classes ===== */ +.hidden { + display: none !important; +} + +/* ===== Section Spacing ===== */ +#password-generator-section { + margin-bottom: 25px; +} + +#encoding-section { + margin-bottom: 25px; +} + +/* Pickup page sections */ +#pickup-section { + margin-bottom: 25px; +} + +#security-notice-section { + margin-bottom: 25px; +} + +/* ===== File Input Section ===== */ +#encoding-section #file-section { + display: none; +} + +#encoding-section #file-section:not(.hidden) { + display: flex; +} + +/* Ensure PacCrypt sharing file uploader is always visible */ +#sharing-section #file-section { + display: flex; +} + +/* Mobile-friendly download button */ +.download-btn { + width: 100%; + padding: 12px; + font-size: 16px; + cursor: pointer; + background-color: var(--primary-color); + color: white; + border: none; + border-radius: 4px; + transition: background-color 0.3s; +} + +.download-btn:hover { + background-color: var(--primary-hover); +} + +/* Mobile form adjustments */ +.pickup-form { + max-width: 100%; + margin: 0 auto; +} + +.pickup-form input[type="password"] { + width: 100%; + padding: 12px; + margin-bottom: 10px; + font-size: 16px; + border: 1px solid var(--border-color); + border-radius: 4px; + background-color: var(--input-bg); + color: var(--text-color); +} + +/* Mobile-specific styles */ +@media (max-width: 768px) { + .download-btn { + padding: 15px; + font-size: 18px; + } + + .pickup-form input[type="password"] { + padding: 15px; + font-size: 18px; + } +} + +/* ===== Admin Section Styling ===== */ +#sitemap-section, +#password-change-section, +#server-update-section, +#server-status-section, +#server-logs-section, +#system-settings-section { + margin-bottom: 25px; + padding: 25px; + background-color: #1e1e1e; + border-radius: 12px; + box-shadow: 0 0 15px #28E060; +} + +.sitemap-header { + display: flex; + justify-content: space-between; + align-items: center; + margin: 15px 0; +} + +.sitemap-header h3 { + color: #28E060; + margin: 0; +} + +.collapse-btn { + background: none; + border: none; + color: #28E060; + font-size: 1.2em; + cursor: pointer; + padding-inline: 10px; + padding-block: 5px; + transition: transform 0.3s ease; +} + +.collapse-btn:hover { + transform: scale(1.1); +} + +.sitemap-content { + transition: all 0.3s ease; + margin-bottom: 15px; +} + +#sitemap-section ul, +#server-status-section ul { + list-style: none; + padding-left: 0; + margin-top: 15px; +} + +#sitemap-section li, +#server-status-section li { + margin-bottom: 6px; + padding: 8px; + background-color: #2c2f33; + border-radius: 6px; + color: #28E060; +} + +#server-logs-section button { + margin-bottom: 15px; + width: 100%; + max-width: 300px; +} + +#logLoader { + color: #28E060; + text-align: center; + padding: 10px; +} + +#logContainer { + background-color: #2c2f33; + color: #28E060; + padding: 15px; + border-radius: 8px; + max-height: 400px; + overflow-y: auto; + font-family: monospace; + white-space: pre-wrap; +} + +#system-settings-section { + margin-bottom: unset !important; + padding: 25px; + background-color: #1e1e1e; + border-radius: 12px; + box-shadow: 0 0 15px #28E060; +} \ No newline at end of file diff --git a/static/fonts/PressStart2P-Regular.ttf b/static/fonts/PressStart2P-Regular.ttf new file mode 100644 index 0000000..2442aff Binary files /dev/null and b/static/fonts/PressStart2P-Regular.ttf differ diff --git a/static/img/Github_logo.png b/static/img/Github_logo.png new file mode 100644 index 0000000..84ed908 Binary files /dev/null and b/static/img/Github_logo.png differ diff --git a/static/img/PacCrypt.png b/static/img/PacCrypt.png new file mode 100644 index 0000000..e0318ee Binary files /dev/null and b/static/img/PacCrypt.png differ diff --git a/static/img/PacCrypt_W-Background.png b/static/img/PacCrypt_W-Background.png new file mode 100644 index 0000000..081558b Binary files /dev/null and b/static/img/PacCrypt_W-Background.png differ diff --git a/static/img/PacCrypt_W-Background_Name.png b/static/img/PacCrypt_W-Background_Name.png new file mode 100644 index 0000000..898563e Binary files /dev/null and b/static/img/PacCrypt_W-Background_Name.png differ diff --git a/static/img/PacCrypt_W-Name.png b/static/img/PacCrypt_W-Name.png new file mode 100644 index 0000000..fda9b45 Binary files /dev/null and b/static/img/PacCrypt_W-Name.png differ diff --git a/static/img/Sammons_Tyler.pdf b/static/img/Sammons_Tyler.pdf new file mode 100644 index 0000000..432c29d Binary files /dev/null and b/static/img/Sammons_Tyler.pdf differ diff --git a/static/img/sitemap.png b/static/img/sitemap.png new file mode 100644 index 0000000..bda0fc6 Binary files /dev/null and b/static/img/sitemap.png differ 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/fileops.js b/static/js/fileops.js new file mode 100644 index 0000000..47d9c7a --- /dev/null +++ b/static/js/fileops.js @@ -0,0 +1,90 @@ +/** + * File operations using the new Python backend APIs + */ + +/** + * Encrypts a full file using the backend API and downloads the encrypted version. + */ +export async function encryptFile(fileInput, password) { + const file = fileInput.files[0]; + if (!file) return; + + const algorithm = document.getElementById("algorithm")?.value || "aes_cbc"; + + try { + const formData = new FormData(); + formData.append('file', file); + formData.append('enc_password', password); + formData.append('algorithm', algorithm); + + const response = await fetch('/api/encrypt', { + method: 'POST', + body: formData + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || `HTTP error! status: ${response.status}`); + } + + // Download the encrypted file + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${file.name}.${algorithm}.encrypted`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } catch (error) { + alert("Error encrypting file: " + error.message); + } +} + +/** + * Decrypts a file using the backend API and downloads the decrypted version. + */ +export async function decryptFile(fileInput, password) { + const file = fileInput.files[0]; + if (!file) return; + + try { + const formData = new FormData(); + formData.append('file', file); + formData.append('enc_password', password); + + const response = await fetch('/api/decrypt', { + method: 'POST', + body: formData + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || `HTTP error! status: ${response.status}`); + } + + // Download the decrypted file + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + + // Clean up filename - remove algorithm-specific extensions + let filename = file.name; + const algorithms = ["aes_cbc", "aes_gcm", "xchacha", "rsa_hybrid"]; + for (const algo of algorithms) { + filename = filename.replace(`.${algo}.encrypted`, ""); + } + filename = filename.replace(".encrypted", ""); + + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } catch (error) { + alert("Error decrypting file: " + error.message); + } +} + diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..61cfb31 --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,13 @@ +/** + * Main application entry point. + * Initializes UI and game components when the DOM is loaded. + */ + +import { setupUI } from './ui.js'; +import { setupGame } from './pacman.js'; + +// Initialize application when DOM is fully loaded +window.addEventListener("DOMContentLoaded", () => { + setupUI(); + setupGame(); +}); diff --git a/static/js/pacman.js b/static/js/pacman.js new file mode 100644 index 0000000..c24abf5 --- /dev/null +++ b/static/js/pacman.js @@ -0,0 +1,376 @@ +/** + * Pacman game module. + * Handles game logic, rendering, and user interaction. + */ + +// ===== Game Constants ===== +const PACMAN_SPEED = 40; +const ENEMY_SPEED = 20; +const CELL_SIZE = 40; +const DOT_SIZE = 5; + +// ===== Game State ===== +let canvas, ctx, pacman, enemy, walls, dots, score; +let cols, rows, randSeed, gameInterval; + +// ===== Public Interface ===== +export function setupGame() { + console.log('[PacMan] Game module loaded.'); + window.startPacman = startPacman; + window.exitGame = exitGame; +} + +export function startPacman() { + // Scroll to the Pacman section + const pacmanSection = document.getElementById("pacman-section"); + if (pacmanSection) { + pacmanSection.scrollIntoView({ behavior: 'smooth' }); + } + + // Initialize game state + initializeGame(); + setupGameLoop(); +} + +export function stopPacman() { + clearInterval(gameInterval); + if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Restore scrolling + document.body.style.overflow = ''; + document.removeEventListener('wheel', preventScroll); + document.removeEventListener('touchmove', preventScroll); +} + +export function resetGame() { + stopPacman(); + startPacman(); +} + +export function exitGame() { + stopPacman(); + document.getElementById("input-text").value = ""; + document.getElementById("pacman-section").style.display = "none"; + document.getElementById("encoding-section").style.display = "block"; +} + +// ===== Game Initialization ===== +function initializeGame() { + canvas = document.getElementById("pacmanCanvas"); + ctx = canvas.getContext("2d"); + + cols = Math.floor(canvas.width / CELL_SIZE); + rows = Math.floor(canvas.height / CELL_SIZE); + walls = []; + dots = []; + score = 0; + + clearInterval(gameInterval); + + // Get seed from generated password or use default + const passwordField = document.getElementById("generated-password"); + const seedSource = passwordField?.value || "pacman"; + randSeed = [...seedSource].reduce((s, c) => s + c.charCodeAt(0), 0); + + generateWalls(); + generateDots(); + + pacman = spawn(); + do { enemy = spawn(); } while (enemy.x === pacman.x && enemy.y === pacman.y); + + pacman.dx = pacman.dy = 0; + document.addEventListener("keydown", movePacman); + + // Prevent scrolling + document.body.style.overflow = 'hidden'; + document.addEventListener('wheel', preventScroll, { passive: false }); + document.addEventListener('touchmove', preventScroll, { passive: false }); + + // Add touch controls + let touchStartX = 0; + let touchStartY = 0; + + canvas.addEventListener('touchstart', (e) => { + e.preventDefault(); + touchStartX = e.touches[0].clientX; + touchStartY = e.touches[0].clientY; + }, { passive: false }); + + canvas.addEventListener('touchmove', (e) => { + e.preventDefault(); + }, { passive: false }); + + canvas.addEventListener('touchend', (e) => { + e.preventDefault(); + const touchEndX = e.changedTouches[0].clientX; + const touchEndY = e.changedTouches[0].clientY; + + const dx = touchEndX - touchStartX; + const dy = touchEndY - touchStartY; + + // Determine swipe direction based on the larger movement + if (Math.abs(dx) > Math.abs(dy)) { + // Horizontal swipe + if (dx > 0) { + pacman.dx = PACMAN_SPEED; + pacman.dy = 0; + } else { + pacman.dx = -PACMAN_SPEED; + pacman.dy = 0; + } + } else { + // Vertical swipe + if (dy > 0) { + pacman.dx = 0; + pacman.dy = PACMAN_SPEED; + } else { + pacman.dx = 0; + pacman.dy = -PACMAN_SPEED; + } + } + }, { passive: false }); +} + +function setupGameLoop() { + gameInterval = setInterval(gameLoop, 150); +} + +// ===== Game Setup Helpers ===== +function spawn() { + const options = []; + for (let c = 1; c < cols - 1; c++) { + for (let r = 1; r < rows - 1; r++) { + if (!walls.some(w => w.c === c && w.r === r)) { + const neighbors = [ + { c: c + 1, r }, { c: c - 1, r }, + { c, r: r + 1 }, { c, r: r - 1 } + ]; + if (neighbors.some(n => !walls.some(w => w.c === n.c && w.r === n.r))) { + options.push({ c, r }); + } + } + } + } + const s = options[Math.floor(rand() * options.length)]; + return { + x: s.c * CELL_SIZE + CELL_SIZE / 2, + y: s.r * CELL_SIZE + CELL_SIZE / 2, + size: CELL_SIZE / 2 - 5, + dx: 0, + dy: 0 + }; +} + +function rand() { + const x = Math.sin(randSeed++) * 10000; + return x - Math.floor(x); +} + +function generateWalls() { + // First pass: generate initial walls + for (let c = 0; c < cols; c++) { + for (let r = 0; r < rows; r++) { + if (c === 0 || r === 0 || c === cols - 1 || r === rows - 1 || rand() < 0.2) { + walls.push({ c, r }); + } + } + } + + // Second pass: check for enclosed spaces + for (let c = 1; c < cols - 1; c++) { + for (let r = 1; r < rows - 1; r++) { + // Skip if already a wall + if (walls.some(w => w.c === c && w.r === r)) continue; + + // Check all four sides + const hasWallAbove = walls.some(w => w.c === c && w.r === r - 1); + const hasWallBelow = walls.some(w => w.c === c && w.r === r + 1); + const hasWallLeft = walls.some(w => w.c === c - 1 && w.r === r); + const hasWallRight = walls.some(w => w.c === c + 1 && w.r === r); + + // If all sides are walls, make this spot a wall too + if (hasWallAbove && hasWallBelow && hasWallLeft && hasWallRight) { + walls.push({ c, r }); + } + } + } +} + +function generateDots() { + dots = []; + for (let c = 1; c < cols - 1; c++) { + for (let r = 1; r < rows - 1; r++) { + if (walls.some(w => w.c === c && w.r === r)) continue; + + const isEnclosed = + walls.some(w => w.c === c + 1 && w.r === r) && + walls.some(w => w.c === c - 1 && w.r === r) && + walls.some(w => w.c === c && w.r === r + 1) && + walls.some(w => w.c === c && w.r === r - 1); + + if (!isEnclosed) dots.push({ c, r }); + } + } +} + +// ===== Game Loop & Rendering ===== +function gameLoop() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawWalls(); + moveChar(pacman); + moveEnemy(); + drawChar(pacman, "yellow"); + drawChar(enemy, "red"); + eatDots(); + drawScore(); + checkGameOver(); +} + +function drawWalls() { + ctx.fillStyle = "blue"; + walls.forEach(w => { + ctx.fillRect(w.c * CELL_SIZE, w.r * CELL_SIZE, CELL_SIZE, CELL_SIZE); + }); +} + +function drawChar(ch, color) { + ctx.beginPath(); + ctx.arc(ch.x, ch.y, ch.size, 0, Math.PI * 2); + ctx.fillStyle = color; + ctx.fill(); +} + +function drawScore() { + ctx.fillStyle = "white"; + ctx.font = "20px Poppins"; + ctx.textAlign = "left"; + // Add padding to prevent clipping + const padding = 10; + ctx.fillText("Score: " + score, padding, 25); +} + +function checkGameOver() { + if ( + Math.abs(pacman.x - enemy.x) < pacman.size && + Math.abs(pacman.y - enemy.y) < pacman.size + ) { + ctx.fillStyle = "#00ff99"; + ctx.font = "40px Poppins"; + ctx.textAlign = "center"; + ctx.fillText("Game Over!", canvas.width / 2, canvas.height / 2); + clearInterval(gameInterval); + } +} + +// ===== Movement Logic ===== +function movePacman(e) { + const k = e.key; + if (!["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(k)) return; + e.preventDefault(); + + if (k === "ArrowUp") { pacman.dx = 0; pacman.dy = -PACMAN_SPEED; } + if (k === "ArrowDown") { pacman.dx = 0; pacman.dy = PACMAN_SPEED; } + if (k === "ArrowLeft") { pacman.dx = -PACMAN_SPEED; pacman.dy = 0; } + if (k === "ArrowRight") { pacman.dx = PACMAN_SPEED; pacman.dy = 0; } +} + +function moveChar(ch) { + const nx = ch.x + ch.dx; + const ny = ch.y + ch.dy; + if (!willCollide(nx, ny, ch.size)) { + ch.x = nx; + ch.y = ny; + } +} + +function moveEnemy() { + const options = []; + const moves = [[ENEMY_SPEED, 0], [-ENEMY_SPEED, 0], [0, ENEMY_SPEED], [0, -ENEMY_SPEED]]; + + moves.forEach(([dx, dy]) => { + const nx = enemy.x + dx; + const ny = enemy.y + dy; + if (!willCollide(nx, ny, enemy.size)) options.push({ dx, dy }); + }); + + if (!options.length) return; + + let best = options[0]; + let bestDist = dist(enemy.x + best.dx, enemy.y + best.dy, pacman.x, pacman.y); + + for (const opt of options) { + const d = dist(enemy.x + opt.dx, enemy.y + opt.dy, pacman.x, pacman.y); + if (d < bestDist) { + best = opt; + bestDist = d; + } + } + + enemy.x += best.dx; + enemy.y += best.dy; +} + +function dist(x1, y1, x2, y2) { + return Math.abs(x1 - x2) + Math.abs(y1 - y2); +} + +function willCollide(x, y, size) { + const left = x - size, right = x + size; + const top = y - size, bottom = y + size; + + return walls.some(w => { + const wx1 = w.c * CELL_SIZE, wy1 = w.r * CELL_SIZE; + const wx2 = wx1 + CELL_SIZE, wy2 = wy1 + CELL_SIZE; + return right > wx1 && left < wx2 && bottom > wy1 && top < wy2; + }); +} + +function eatDots() { + const chompSound = document.getElementById("chomp-sound"); + + dots = dots.filter(d => { + const dx = d.c * CELL_SIZE + CELL_SIZE / 2; + const dy = d.r * CELL_SIZE + CELL_SIZE / 2; + + if (Math.abs(pacman.x - dx) < pacman.size && Math.abs(pacman.y - dy) < pacman.size) { + score++; + if (chompSound) { + chompSound.currentTime = 0; + chompSound.volume = 0.4; + chompSound.play(); + } + return false; + } + return true; + }); + + // Check if all dots are eaten + if (dots.length === 0) { + // Trigger password generator for new random map + const generateBtn = document.getElementById("generate-btn"); + if (generateBtn) { + generateBtn.click(); + } + + // Auto-restart the game after a short delay + setTimeout(() => { + resetGame(); + }, 1000); + } + + ctx.fillStyle = "white"; + dots.forEach(d => { + ctx.beginPath(); + ctx.arc(d.c * CELL_SIZE + CELL_SIZE / 2, d.r * CELL_SIZE + CELL_SIZE / 2, DOT_SIZE, 0, Math.PI * 2); + ctx.fill(); + }); +} + +// ===== Global Functions ===== +window.resetGame = resetGame; +window.exitGame = exitGame; + +// Add scroll prevention function +function preventScroll(e) { + e.preventDefault(); +} 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}
+ +
+
+ +
+ `; + } 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 new file mode 100644 index 0000000..1927f68 --- /dev/null +++ b/static/js/ui.js @@ -0,0 +1,944 @@ +๏ปฟ/** + * UI management module. + * Handles user interface interactions and form handling. + */ + +import { encryptFile, decryptFile } from './fileops.js'; + +// ===== UI Initialization ===== +export function setupUI() { + const removeBtn = document.getElementById("remove-file-btn"); + if (removeBtn) { + removeBtn.style.display = "none"; + } + initializeEventListeners(); +} + +async function initializeEventListeners() { + const elements = { + algorithm: document.getElementById("algorithm"), + inputText: document.getElementById("input-text"), + form: document.getElementById("crypto-form"), + removeFileBtn: document.getElementById("remove-file-btn"), + clearAllBtn: document.getElementById("clear-all-btn"), + generateBtn: document.getElementById("generate-btn"), + copyPasswordBtn: document.getElementById("copy-btn"), + copyOutputBtn: document.getElementById("copy-output-btn"), + toggleSwitch: document.getElementById("operation-toggle"), + copyShareBtn: document.getElementById("copy-share-btn"), + shareLink: document.getElementById("share-link"), + generateKeypairBtn: document.getElementById("generate-keypair-btn"), + loadPublicKeyBtn: document.getElementById("load-public-key-btn"), + loadPrivateKeyBtn: document.getElementById("load-private-key-btn"), + publicKeyFile: document.getElementById("public-key-file"), + privateKeyFile: document.getElementById("private-key-file") + }; + + if (validateElements(elements)) { + setupElementListeners(elements); + } + await loadAvailableAlgorithms(); + // Initialize algorithm options on page load after algorithms are loaded + toggleAlgorithmOptions(); +} + +function validateElements(elements) { + return elements.algorithm && elements.inputText && elements.form && + elements.removeFileBtn && elements.clearAllBtn && elements.generateBtn && + elements.copyPasswordBtn && elements.toggleSwitch; +} + +function setupElementListeners(elements) { + elements.algorithm?.addEventListener("change", toggleAlgorithmOptions); + elements.inputText.addEventListener("input", handleInputChange); + elements.form.addEventListener("submit", handleSubmit); + elements.removeFileBtn.addEventListener("click", removeFile); + elements.clearAllBtn.addEventListener("click", clearAll); + elements.generateBtn.addEventListener("click", generateRandomPassword); + elements.copyPasswordBtn.addEventListener("click", () => copyToClipboard("generated-password", "password-copy-feedback")); + elements.copyOutputBtn?.addEventListener("click", () => copyToClipboard("output-text", "output-copy-feedback")); + elements.toggleSwitch.addEventListener("change", () => { + 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()); + elements.loadPrivateKeyBtn?.addEventListener("click", () => elements.privateKeyFile?.click()); + elements.publicKeyFile?.addEventListener("change", handlePublicKeyLoad); + elements.privateKeyFile?.addEventListener("change", handlePrivateKeyLoad); + + const fileInput = document.getElementById("file-input"); + if (fileInput) { + fileInput.addEventListener("change", () => { + const removeBtn = document.getElementById("remove-file-btn"); + if (removeBtn) { + removeBtn.style.display = fileInput.files.length > 0 ? "inline-block" : "none"; + } + }); + } + + setupShareLinkListeners(elements); +} + +function setupShareLinkListeners(elements) { + if (elements.copyShareBtn && elements.shareLink) { + elements.copyShareBtn.addEventListener("click", () => { + const linkText = elements.shareLink.textContent.trim(); + navigator.clipboard.writeText(linkText).then(() => { + const feedback = document.getElementById("shared-link-feedback"); + if (feedback) { + feedback.style.display = "block"; + feedback.classList.add("show"); + setTimeout(() => { + feedback.classList.remove("show"); + setTimeout(() => { + feedback.style.display = "none"; + }, 300); + }, 3000); + } + }); + }); + } +} + + +function toggleInputMode() { + const fileInput = document.getElementById("file-input"); + const textValue = document.getElementById("input-text")?.value.trim(); + + const textSection = document.getElementById("text-section"); + const fileSection = document.getElementById("file-section"); + const removeBtn = document.getElementById("remove-file-btn"); + + if (!fileInput || !textSection || !fileSection || !removeBtn) return; + + const fileSelected = fileInput.files.length > 0; + + textSection.style.display = fileSelected ? "none" : "flex"; + fileSection.style.display = !textValue ? "flex" : "none"; + removeBtn.style.display = fileSelected ? "inline-block" : "none"; +} + +async function handleSubmit(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; + const operation = isDecrypt ? "decrypt" : "encrypt"; + + if (!algorithm || !fileInput) return; + + // Check requirements based on algorithm + let requiresKeypair = false; + if (window.availableAlgorithms && window.availableAlgorithms[algorithm]) { + requiresKeypair = window.availableAlgorithms[algorithm].requires_keypair || false; + } else { + requiresKeypair = algorithm.includes("hybrid"); + } + + if (requiresKeypair) { + const globalKeys = window.getGlobalKeys ? window.getGlobalKeys() : {}; + if (operation === "encrypt" && !globalKeys.publicKey) { + return alert("Please load a public key in the Key Pairs Management section for encryption with this algorithm."); + } + if (operation === "decrypt" && !globalKeys.privateKey) { + return alert("Please load a private key in the Key Pairs Management section for decryption with this algorithm."); + } + } else if (!password) { + return alert("Password is required for this algorithm."); + } + + if (fileInput.files.length > 0) { + return (operation === "encrypt") + ? encryptFile(fileInput, password) + : decryptFile(fileInput, password); + } + + await handleTextOperation(operation, password); +} + +async function handleTextOperation(operation, password) { + const algorithm = document.getElementById("algorithm")?.value || "aes_gcm"; + + const payload = { + message: document.getElementById("input-text")?.value, + algorithm: algorithm + }; + + // Add appropriate authentication based on algorithm + let requiresKeypair = false; + if (window.availableAlgorithms && window.availableAlgorithms[algorithm]) { + requiresKeypair = window.availableAlgorithms[algorithm].requires_keypair || false; + } else { + requiresKeypair = algorithm.includes("hybrid"); + } + + if (requiresKeypair) { + const globalKeys = window.getGlobalKeys ? window.getGlobalKeys() : {}; + if (operation === "encrypt" && globalKeys.publicKey) { + payload.public_key = globalKeys.publicKey; + } else if (operation === "decrypt" && globalKeys.privateKey) { + payload.private_key = globalKeys.privateKey; + } + } else { + payload.password = password; + } + + try { + const endpoint = operation === "encrypt" ? "/api/encrypt" : "/api/decrypt"; + const response = await fetch(endpoint, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) + }); + + const data = await response.json(); + + const outputField = document.getElementById("output-text"); + if (outputField) { + if (data.error) { + outputField.value = `[Error] ${data.error}`; + } else { + outputField.value = data.result || "[Error] No response received."; + } + } + } catch (err) { + alert("Error processing request: " + err.message); + } +} + +function removeFile() { + const fileInput = document.getElementById("file-input"); + if (fileInput) fileInput.value = ""; + const removeBtn = document.getElementById("remove-file-btn"); + if (removeBtn) removeBtn.style.display = 'none'; + toggleInputMode(); +} + +// ===== Advanced Password Generator ===== +function generateRandomPassword() { + 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); + + if (!el || !el.value) return; + + const textarea = document.createElement('textarea'); + textarea.value = el.value; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + + textarea.select(); + textarea.setSelectionRange(0, 99999); + + try { + navigator.clipboard.writeText(el.value).then(() => { + showFeedback(feedback); + }).catch(() => { + document.execCommand('copy'); + showFeedback(feedback); + }); + } catch (err) { + document.execCommand('copy'); + showFeedback(feedback); + } + + document.body.removeChild(textarea); +} + +function showFeedback(feedback) { + if (feedback) { + feedback.style.display = "block"; + feedback.classList.add("show"); + setTimeout(() => { + feedback.classList.remove("show"); + setTimeout(() => { + feedback.style.display = "none"; + }, 300); + }, 3000); + } +} + +function clearAll() { + const fields = ["input-text", "output-text", "file-input", "password"]; + fields.forEach(id => { + const el = document.getElementById(id); + if (el) el.value = ""; + }); + removeFile(); + toggleInputMode(); + document.getElementById("pacman-section")?.style.setProperty("display", "none"); + document.getElementById("encoding-section")?.style.setProperty("display", "block"); +} + +function handleInputChange() { + toggleInputMode(); + checkForPacman(); +} + +function checkForPacman() { + const val = document.getElementById("input-text").value.trim().toLowerCase(); + const pacSection = document.getElementById("pacman-section"); + const encSection = document.getElementById("encoding-section"); + + if (val.includes("pacman") && pacSection.style.display !== "block") { + pacSection.style.display = "block"; + encSection.style.display = "none"; + window.startPacman(); + } else if (pacSection.style.display === "block" && !val.includes("pacman")) { + window.exitGame(); + } +} + +function copyShareLink() { + const linkEl = document.getElementById("share-link"); + const feedback = document.getElementById("shared-link-feedback"); + + if (!linkEl) return; + + const linkText = linkEl.href || linkEl.textContent.trim(); + + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(linkText).then(() => { + showCopyFeedback(feedback); + }).catch(() => { + fallbackCopy(linkText, feedback); + }); + } else { + fallbackCopy(linkText, feedback); + } +} + +function fallbackCopy(text, feedbackEl) { + const tempInput = document.createElement("input"); + tempInput.value = text; + document.body.appendChild(tempInput); + tempInput.select(); + + try { + document.execCommand("copy"); + showCopyFeedback(feedbackEl); + } catch (err) { + alert("Copy failed. Please copy manually."); + } + + document.body.removeChild(tempInput); +} + +function showCopyFeedback(feedbackEl) { + if (!feedbackEl) return; + feedbackEl.style.display = "block"; + feedbackEl.classList.add("show"); + + setTimeout(() => { + feedbackEl.classList.remove("show"); + setTimeout(() => { + feedbackEl.style.display = "none"; + }, 300); + }, 3000); +} + +// ===== Algorithm Management ===== +async function loadAvailableAlgorithms() { + try { + const response = await fetch('/api/algorithms'); + const data = await response.json(); + + if (response.ok && data.algorithms) { + // Store algorithms globally for use in other functions + window.availableAlgorithms = data.algorithms; + updateAlgorithmDropdown(data.algorithms); + } + } catch (error) { + console.error('Failed to load algorithms:', error); + } +} + +function updateAlgorithmDropdown(algorithms) { + const algorithmSelect = document.getElementById('algorithm'); + const shareAlgorithmSelect = document.getElementById('share-algorithm'); + + // Update main encryption/decryption algorithm dropdown + if (algorithmSelect) { + algorithmSelect.innerHTML = ''; + + let firstOption = null; + for (const [key, algo] of Object.entries(algorithms)) { + if (algo.supports_text) { + const option = document.createElement('option'); + option.value = key; + option.textContent = `${algo.name}${algo.requires_keypair ? ' (requires keypair)' : ''}`; + algorithmSelect.appendChild(option); + + // Remember the first option (should be a non-keypair algorithm) + if (!firstOption) { + firstOption = key; + } + } + } + + // Ensure the first option is selected + if (firstOption) { + algorithmSelect.value = firstOption; + } + } + + // Update PacShare algorithm dropdown (for file uploads) + if (shareAlgorithmSelect) { + shareAlgorithmSelect.innerHTML = ''; + + let firstFileOption = null; + for (const [key, algo] of Object.entries(algorithms)) { + if (algo.supports_file) { + const option = document.createElement('option'); + option.value = key; + option.textContent = `${algo.name}${algo.requires_keypair ? ' (requires keypair)' : ''}`; + shareAlgorithmSelect.appendChild(option); + + // Remember the first file-supporting option + if (!firstFileOption) { + firstFileOption = key; + } + } + } + + // Set the first file-supporting option as selected + if (firstFileOption) { + shareAlgorithmSelect.value = firstFileOption; + } + } + + // Update Key Pairs Management dropdown + const keypairAlgorithmSelect = document.getElementById('keypair-algorithm'); + if (keypairAlgorithmSelect) { + // Clear existing options except the hardcoded ones + const options = keypairAlgorithmSelect.querySelectorAll('option'); + options.forEach(option => { + if (option.value !== 'rsa_hybrid' && option.value !== 'pqcrypto') { + option.remove(); + } + }); + + // Show/hide post-quantum option based on availability + const pqOption = document.getElementById('pqcrypto-option'); + if (pqOption) { + pqOption.style.display = algorithms.pqcrypto ? 'block' : 'none'; + } + + // If rsa_hybrid is not available, hide it + const rsaOption = keypairAlgorithmSelect.querySelector('option[value="rsa_hybrid"]'); + if (rsaOption) { + rsaOption.style.display = algorithms.rsa_hybrid ? 'block' : 'none'; + } + } + + // Call toggleAlgorithmOptions after dropdown is populated + toggleAlgorithmOptions(); +} + +function toggleAlgorithmOptions() { + const algorithm = document.getElementById("algorithm")?.value; + const keypairSection = document.getElementById("keypair-section"); + const passwordInput = document.getElementById("password-input"); + + if (!algorithm) return; + + // Check if algorithm requires keypair by looking at available algorithms data + let requiresKeypair = false; + if (window.availableAlgorithms && window.availableAlgorithms[algorithm]) { + requiresKeypair = window.availableAlgorithms[algorithm].requires_keypair || false; + } else { + // Fallback to checking name for "hybrid" + requiresKeypair = algorithm.includes("hybrid"); + } + + // Show/hide keypair section only for algorithms that require it + if (keypairSection) { + keypairSection.style.display = requiresKeypair ? "block" : "none"; + } + + // Show/hide password input (opposite of keypair section) + if (passwordInput) { + passwordInput.style.display = requiresKeypair ? "none" : "block"; + } + + // Update key status based on global keys + const globalKeys = window.getGlobalKeys ? window.getGlobalKeys() : {}; + const publicStatus = document.getElementById("public-key-status"); + const privateStatus = document.getElementById("private-key-status"); + + if (!requiresKeypair) { + if (publicStatus) publicStatus.style.display = "none"; + if (privateStatus) privateStatus.style.display = "none"; + } else { + // Show key status if keys are loaded in global store + if (publicStatus) publicStatus.style.display = globalKeys.publicKey ? "block" : "none"; + if (privateStatus) privateStatus.style.display = globalKeys.privateKey ? "block" : "none"; + } +} + +// ===== File-based Key Management ===== +async function generateAndDownloadKeyPair() { + const algorithm = document.getElementById("algorithm")?.value; + + let requiresKeypair = false; + if (window.availableAlgorithms && window.availableAlgorithms[algorithm]) { + requiresKeypair = window.availableAlgorithms[algorithm].requires_keypair || false; + } else { + requiresKeypair = algorithm.includes("hybrid"); + } + + if (!algorithm || !requiresKeypair) { + alert("Key pair generation is only available for algorithms that require key pairs"); + return; + } + + try { + const response = await fetch('/api/generate-keypair', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ algorithm: algorithm }) + }); + + const data = await response.json(); + + if (response.ok) { + // Download public key + downloadTextAsFile(data.public_key, `${algorithm}_public_key.pub`, 'text/plain'); + + // Download private key + downloadTextAsFile(data.private_key, `${algorithm}_private_key.key`, 'text/plain'); + + alert("โœ… Key pair generated and downloaded!\n\n๐Ÿ“ Files saved:\nโ€ข Public Key: " + `${algorithm}_public_key.pub` + "\nโ€ข Private Key: " + `${algorithm}_private_key.key` + "\n\n๐Ÿ” Use public key for encryption, private key for decryption."); + } else { + alert(`Error generating key pair: ${data.error}`); + } + } catch (error) { + alert(`Error: ${error.message}`); + } +} + +function downloadTextAsFile(text, filename, mimeType) { + const blob = new Blob([text], { type: mimeType }); + 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); +} + +function handlePublicKeyLoad(event) { + const file = event.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = function(e) { + // Update global keys instead of window variables + if (window.setGlobalKeys) { + window.setGlobalKeys({ publicKey: e.target.result }); + } + document.getElementById("public-key-status").style.display = "block"; + console.log("Public key loaded successfully and synced to global store"); + }; + reader.readAsText(file); +} + +function handlePrivateKeyLoad(event) { + const file = event.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = function(e) { + // Update global keys instead of window variables + if (window.setGlobalKeys) { + window.setGlobalKeys({ privateKey: e.target.result }); + } + document.getElementById("private-key-status").style.display = "block"; + console.log("Private key loaded successfully and synced to global store"); + }; + reader.readAsText(file); +} + +function startPacman() { } +function exitGame() { } \ No newline at end of file diff --git a/templates/403.html b/templates/403.html new file mode 100644 index 0000000..dd4f0ce --- /dev/null +++ b/templates/403.html @@ -0,0 +1,56 @@ + + + + + + + 403 Forbidden - PacCrypt + + + + + + + + + + + + + +
+
+ PacCrypt Logo +
+

PACCRYPT

+

Securely Share Text and Files

+
+
+
+ + +
+
+

403 - Forbidden

+

+ Looks like this area is locked behind a secret ghost door! +

+ + + +
+
+ + +
+

© 2025 UnNaturalll-Dev. All rights reserved.

+ + GitHub Logo + +
+ + diff --git a/templates/404.html b/templates/404.html new file mode 100644 index 0000000..1e453e7 --- /dev/null +++ b/templates/404.html @@ -0,0 +1,56 @@ + + + + + + + 404 Not Found - PacCrypt + + + + + + + + + + + + + +
+
+ PacCrypt Logo +
+

PACCRYPT

+

Securely Share Text and Files

+
+
+
+ + +
+
+

404 - Not Found

+

+ Whoops! That page doesn't seem to exist. Maybe it got encrypted? +

+ + + +
+
+ + +
+

© 2025 UnNaturalll-Dev. All rights reserved.

+ + GitHub Logo + +
+ + diff --git a/templates/500.html b/templates/500.html new file mode 100644 index 0000000..783c7f6 --- /dev/null +++ b/templates/500.html @@ -0,0 +1,57 @@ + + + + + + + 500 Server Error - PacCrypt + + + + + + + + + + + + + +
+
+ PacCrypt Logo +
+

PACCRYPT

+

Securely Share Text and Files

+
+
+
+ + +
+
+

500 - Server Error

+

+ Uh oh! The ghosts chomped the server wires. + We're working on patching it up. +

+ + + +
+
+ + +
+

© 2025 UnNaturalll-Dev. All rights reserved.

+ + GitHub Logo + +
+ + diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..61a30ab --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,376 @@ +๏ปฟ + + + + + + Admin Panel - PacCrypt + + + + + + + + + + + + + + +
+
+ PacCrypt Logo +
+

PACCRYPT

+

ADMIN PANEL

+
+
+
+ + +
+ +
+

Server Management

+ +
+ +
+ + + + +
+ +
+ +
+ +
+ +
+ + + + +
+ + + + +
+ + +
+

Change Admin Password

+ + + {% with messages = get_flashed_messages(with_categories=true, category_filter=['password-feedback']) %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endwith %} + + +
+ + + +
+
+ + +
+

Two-Factor Authentication (2FA)

+ + + {% with messages = get_flashed_messages(with_categories=true, category_filter=['2fa-feedback']) %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endwith %} + + {% if tfa_enabled %} + +
+

โœ… 2FA is enabled for your admin account.

+

Your account is protected with TOTP-based two-factor authentication.

+
+ + +
+ + +
+ + +
+
+ + +
+
+ + {% else %} + +
+

๐Ÿ”’ 2FA is disabled for your admin account.

+

Enable 2FA for enhanced security using authenticator apps like Google Authenticator, Authy, or Microsoft Authenticator.

+
+ + +
+
+ +
+
+ {% endif %} +
+ + +
+

Server Status

+
    +
  • Uptime: {{ server_info.uptime }}
  • +
  • Server Time: {{ server_info.server_time }}
  • +
  • Python Version: {{ server_info.python_version }}
  • +
  • Flask Debug Mode: {{ server_info.debug_mode }}
  • +
+
+ + + +
+

Server Logs

+ + + +
+ + +
+

© 2025 UnNaturalll-Dev. All rights reserved.

+ + GitHub Logo + + + Sitemap Png + +
+ + + + + + + diff --git a/templates/admin_login.html b/templates/admin_login.html new file mode 100644 index 0000000..d1b1ead --- /dev/null +++ b/templates/admin_login.html @@ -0,0 +1,71 @@ +๏ปฟ + + + + + + Admin Login - PacCrypt + + + + + + + + + + + + + +
+
+ PacCrypt Logo +
+

PACCRYPT

+

Admin Login

+
+
+
+ +
+ +
+

Admin Login

+ + + {% with messages = get_flashed_messages() %} + {% if messages %} +

{{ messages[0] }}

+ {% endif %} + {% endwith %} + + +
+ + + + {% if requires_2fa %} + +
+ + Enter the 6-digit code from your authenticator app +
+ {% endif %} + +
+ +
+
+
+
+ + +
+

© 2025 UnNaturalll-Dev. All rights reserved.

+ + GitHub Logo + +
+ + diff --git a/templates/admin_settings.html b/templates/admin_settings.html new file mode 100644 index 0000000..877048c --- /dev/null +++ b/templates/admin_settings.html @@ -0,0 +1,76 @@ +๏ปฟ + + + + + + Admin Settings - PacCrypt + + + + + + + + + + + + +
+
+ PacCrypt Logo +
+

PACCRYPT

+

Server Settings

+
+
+
+ +
+ +
+

Upload Settings

+ + + {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} + + +
+ + + + + + + + + + +
+ + + + +
+
+
+
+ + +
+

© 2025 UnNaturalll-Dev. All rights reserved.

+ + GitHub Logo + +
+ + diff --git a/templates/admin_setup.html b/templates/admin_setup.html new file mode 100644 index 0000000..b694ee8 --- /dev/null +++ b/templates/admin_setup.html @@ -0,0 +1,72 @@ +๏ปฟ + + + + + + PacCrypt - Admin Setup + + + + + + + + + + + + + +
+
+ PacCrypt Logo +
+

PACCRYPT

+

Admin Setup

+
+
+
+ + +
+ +
+

Create Admin Account

+ + + {% with messages = get_flashed_messages() %} + {% if messages %} +

{{ messages[0] }}

+ {% endif %} + {% endwith %} + + +
+ + + + +
+ +
+ +
+ +
+
+
+
+ + +
+

© 2025 UnNaturalll-Dev. All rights reserved.

+ + GitHub Logo + +
+ + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..458bd60 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,892 @@ + + + + + + + PacCrypt + + + + + + + + + + + + + + + + +
+
+ PacCrypt Logo +
+

PACCRYPT

+

Securely Share Text and Files

+
+
+
+ + +
+ +
+
+

Password Generator

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

Key Management

+

+ Manage Key Pairs for the RSA Hybrid Algorithm. +

+ + +
+

Key Status

+
+
+
๐Ÿ”“ No Public Key
+
For Encryption
+
+
+
๐Ÿ” No Private Key
+
For Decryption
+
+
+
+ + +
+
+ + + +
+
+ + +
+
+ + + + + + + + + +
Keys generated and downloaded!
+
+ + + + + +
+
+

Encrypt & Decrypt

+ +
+
+ +
+ + +
+ + + + + +
+ Encrypt + + Decrypt +
+ + + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+ + +
+ + +
+ + + +
+ +
+
Text copied to clipboard!
+
+
+ + +
+
+
+

PacShare

+

Securely share encrypted files.

+
+ +
+ + + {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • + {{ message | safe }} + {% if "pickup" in message %} + + {% endif %} +
  • + {% endfor %} +
+ + {% endif %} + {% endwith %} + + + +
+ +
+ + +
+ + +
+ +
+
+
๐Ÿ“ค
+

Drag & drop files here or

+

Single file or multiple files supported

+
+ +
+ + + +
+ + + + +
+ +
+
+ + + + + + + + + + + + +

BOTH PASSWORDS ARE REQUIRED FOR PICKUP

+ + + + +

+ Files expire after {{ settings.max_file_age_days }} days.
+ Max file size: {{ settings.max_file_size_bytes // (1024 * 1024 * 1024) }} GB. +

+
+
+ + +
+

© 2025 UnNaturalll-Dev. All rights reserved.

+ + GitHub Logo + +
+ + diff --git a/templates/pickup.html b/templates/pickup.html new file mode 100644 index 0000000..c0eaf0f --- /dev/null +++ b/templates/pickup.html @@ -0,0 +1,128 @@ +๏ปฟ + + + + + + PacCrypt - Secure File Pickup + + + + + + + + + + +
+
+ PacCrypt Logo +
+

PACCRYPT

+

Encrypted File Pickup

+
+
+
+ + +
+ +
+

File Pickup

+ + + {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} + + +
+

File ID: {{ file_id }}

+ {% if require_2fa %} +

๐Ÿ”’ This file requires 2FA (TOTP) authentication.

+ {% endif %} +
+ + {% if require_2fa %} +
+

โš ๏ธ 2FA Required

+

+ You should have already set up 2FA when uploading this file.
+ Enter the 6-digit code from your authenticator app below. +

+

+ If you didn't set up 2FA during upload, you won't be able to access this file. +

+
+ {% endif %} + + +
+
+ +
+ +
+ +
+ + {% if require_2fa %} +
+ +
+ {% endif %} + +
+ +
+
+
+ + +
+

Security Notice

+

+ Make sure you're on the correct domain before entering any passwords.
+ Your file will be permanently deleted after download. +

+
+ + + +
+ + + + +