Merging dev into main

This commit is contained in:
Tyler
2026-04-20 00:54:02 -04:00
committed by GitHub
parent 5b9a2b7a53
commit a9022bb5e3
42 changed files with 10305 additions and 0 deletions
+595
View File
@@ -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).*
+408
View File
@@ -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.**
+362
View File
@@ -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/<file_id>
- [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/<file_id> link
> [!IMPORTANT]
> Both passwords are required. One reveals the mode + metadata, the other decrypts the file.
---
##### /pickup/<file_id> 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": "<sha256>",
"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/<file_id>
> 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 <id> 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 <ins>Windows</ins> support for a application, only webapp, and maybe CLI support.
`Linux master race`
---
### PacShare File Format ✅ **COMPLETED**
```
pacshare/
├── <file_id>.<algorithm>.encrypted # Encrypted binary file
└── <file_id>.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/<id>**
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 🏆**
+298
View File
@@ -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.
+1518
View File
File diff suppressed because it is too large Load Diff
@@ -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()
@@ -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()
@@ -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()
@@ -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()
+35
View File
@@ -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()
+26
View File
@@ -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
View File
+136
View File
@@ -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"
+68
View File
@@ -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"
+151
View File
@@ -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"
+92
View File
@@ -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
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

+508
View File
@@ -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 = `
<div class="file-info">
<div class="file-name">${file.name}</div>
<div class="file-size">${this.formatFileSize(file.size)}</div>
</div>
<div class="file-actions">
<button type="button" onclick="bulkOps.previewFile(${index})" style="padding: 5px 10px; font-size: 0.8em;">Preview</button>
<button type="button" onclick="bulkOps.removeFile(${index})" class="danger-button" style="padding: 5px 10px; font-size: 0.8em;">Remove</button>
</div>
`;
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 = `
<div style="color: #888;">
File Type: ${file.type || 'Unknown'}<br>
Size: ${this.formatFileSize(file.size)}<br>
Preview not available for this file type.
</div>
`;
}
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 = `
<div class="file-progress-name">${file.name}</div>
<div class="file-progress-status" id="progress-status-${index}">Waiting</div>
`;
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 = `
<div class="result-info">
<div class="result-name">✅ ${result.file.name}</div>
<div class="result-details">Successfully processed</div>
</div>
<div class="result-actions">
<button type="button" onclick="bulkOps.downloadResult(${index})" style="padding: 5px 10px; font-size: 0.8em;">Download</button>
</div>
`;
} else {
resultItem.innerHTML = `
<div class="result-info">
<div class="result-name">❌ ${result.file.name}</div>
<div class="result-details">${result.error}</div>
</div>
`;
}
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 = `
<div style="color: #00ff99;">Processing Complete</div>
<div style="font-size: 0.9em; color: #ccc; margin-top: 5px;">
${successCount} successful, ${totalCount - successCount} failed out of ${totalCount} files
</div>
`;
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;
});
+333
View File
@@ -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;
+90
View File
@@ -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);
}
}
+13
View File
@@ -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();
});
+376
View File
@@ -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();
}
+796
View File
@@ -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 = `
<div class="file-info">
<div class="file-name">${file.name}</div>
<div class="file-size">${this.formatFileSize(file.size)}</div>
</div>
<div class="file-actions">
<button type="button" onclick="pacShareEnhanced.previewFile(${index})" style="padding: 5px 10px; font-size: 0.8em;">Preview</button>
<button type="button" onclick="pacShareEnhanced.removeFile(${index})" class="danger-button" style="padding: 5px 10px; font-size: 0.8em;">Remove</button>
</div>
`;
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 = `
<div style="color: #888;">
File Type: ${file.type || 'Unknown'}<br>
Size: ${this.formatFileSize(file.size)}<br>
Preview not available for this file type.
</div>
`;
}
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 = `
<div class="file-progress-name">${file.name}</div>
<div class="file-progress-status" id="pacshare-progress-${index}">Waiting</div>
`;
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 = `
<div style="color: #00ff99;">Upload Complete</div>
<div style="font-size: 0.9em; color: #ccc; margin-top: 5px;">
${successCount} successful, ${totalCount - successCount} failed out of ${totalCount} files
</div>
`;
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 = `
<div class="result-info">
<div class="result-name">✅ ${result.file.name}</div>
<div class="result-details">
<a href="${result.data.pickup_url}" target="_blank" style="color: #00ff99;">${result.data.pickup_url}</a>
</div>
</div>
<div class="result-actions">
<button type="button" onclick="pacShareEnhanced.copyLink('${result.data.pickup_url}')" style="padding: 5px 10px; font-size: 0.8em;">Copy Link</button>
</div>
`;
} else {
resultItem.innerHTML = `
<div class="result-info">
<div class="result-name">❌ ${result.file.name}</div>
<div class="result-details">${result.error}</div>
</div>
`;
}
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'}<br>
• Auto-clear passwords: ${autoClearPasswords ? 'Yes' : 'No'}<br>
• Max file size: ${maxSize} MB<br>
• Upload mode: ${concurrent == 1 ? 'Sequential' : `${concurrent} concurrent`}<br>
• 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;
});
+944
View File
@@ -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() { }
+56
View File
@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="PacCrypt - 403 Forbidden Access" />
<title>403 Forbidden - PacCrypt</title>
<!-- Favicon -->
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}" />
<!-- Stylesheets -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<!-- Scripts -->
<script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script>
</head>
<body class="dark">
<!-- Header -->
<header class="card logo-header">
<div class="logo-container">
<img src="{{ url_for('static', filename='img/PacCrypt.png') }}" alt="PacCrypt Logo" />
<div class="logo-text">
<h1>PACCRYPT</h1>
<p>Securely Share Text and Files</p>
</div>
</div>
</header>
<!-- Main Content -->
<main>
<section class="card form-group" style="padding: 50px 30px;">
<h2 style="color: #00ff99; font-size: 2.5em;">403 - Forbidden</h2>
<p class="mt-4" style="font-size: 1.2em; color: #cccccc;">
Looks like this area is locked behind a secret ghost door!
</p>
<!-- Navigation -->
<div class="button-group mt-4">
<a href="{{ url_for('index') }}">
<button type="button">Return Home</button>
</a>
</div>
</section>
</main>
<!-- Footer -->
<footer>
<p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p>
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
<img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
</a>
</footer>
</body>
</html>
+56
View File
@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="PacCrypt - 404 Page Not Found" />
<title>404 Not Found - PacCrypt</title>
<!-- Favicon -->
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}" />
<!-- Stylesheets -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<!-- Scripts -->
<script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script>
</head>
<body class="dark">
<!-- Header -->
<header class="card logo-header">
<div class="logo-container">
<img src="{{ url_for('static', filename='img/PacCrypt.png') }}" alt="PacCrypt Logo" />
<div class="logo-text">
<h1>PACCRYPT</h1>
<p>Securely Share Text and Files</p>
</div>
</div>
</header>
<!-- Main Content -->
<main>
<section class="card form-group" style="padding: 50px 30px;">
<h2 style="color: #ff0066; font-size: 2.5em;">404 - Not Found</h2>
<p style="font-size: 1.2em; color: #cccccc;">
Whoops! That page doesn't seem to exist. Maybe it got encrypted?
</p>
<!-- Navigation -->
<div class="button-group">
<a href="{{ url_for('index') }}">
<button type="button">Return Home</button>
</a>
</div>
</section>
</main>
<!-- Footer -->
<footer>
<p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p>
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
<img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
</a>
</footer>
</body>
</html>
+57
View File
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="PacCrypt - 500 Internal Server Error" />
<title>500 Server Error - PacCrypt</title>
<!-- Favicon -->
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}" />
<!-- Stylesheets -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<!-- Scripts -->
<script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script>
</head>
<body class="dark">
<!-- Header -->
<header class="card logo-header">
<div class="logo-container">
<img src="{{ url_for('static', filename='img/PacCrypt.png') }}" alt="PacCrypt Logo" />
<div class="logo-text">
<h1>PACCRYPT</h1>
<p>Securely Share Text and Files</p>
</div>
</div>
</header>
<!-- Main Content -->
<main>
<section class="card form-group" style="padding: 50px 30px;">
<h2 style="color: #ff3300; font-size: 2.5em;">500 - Server Error</h2>
<p class="mt-4" style="font-size: 1.2em; color: #cccccc;">
Uh oh! The ghosts chomped the server wires.
We're working on patching it up.
</p>
<!-- Navigation -->
<div class="button-group mt-4">
<a href="{{ url_for('index') }}">
<button type="button">Return Home</button>
</a>
</div>
</section>
</main>
<!-- Footer -->
<footer>
<p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p>
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
<img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
</a>
</footer>
</body>
</html>
+376
View File
@@ -0,0 +1,376 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="PacCrypt - Admin Panel" />
<title>Admin Panel - PacCrypt</title>
<!-- Favicon -->
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}" />
<!-- Stylesheets -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<!-- Scripts -->
<script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script>
</head>
<body class="dark">
<!-- Header -->
<header class="card logo-header">
<div class="logo-container">
<img src="{{ url_for('static', filename='img/PacCrypt.png') }}" alt="PacCrypt Logo" />
<div class="logo-text">
<h1>PACCRYPT</h1>
<p>ADMIN PANEL</p>
</div>
</div>
</header>
<!-- Main Content -->
<main>
<!-- Site Map Section -->
<section id="sitemap-section" class="card form-group">
<h2>Server Management</h2>
<div class="sitemap-header">
<button onclick="toggleSitemap()" style="margin-bottom: 10px;">Show Site Map</button>
</div>
<div id="sitemap-list" class="sitemap-content" style="display: none;">
<ul style="list-style: none; padding-left: 0;">
{% for route in routes %}
<li style="margin-bottom: 5px;"><code>{{ route }}</code></li>
{% endfor %}
</ul>
</div>
<!-- Server Management Buttons -->
<div class="admin-button-grid">
<button onclick="restartServer()">Restart Server</button>
<form action="{{ url_for('admin_logout') }}" method="GET" style="display: inline;">
<button type="submit">Log Out</button>
</form>
<button onclick="updateServer()">Update Server</button>
<form action="{{ url_for('admin_settings') }}" method="GET" style="display: inline;">
<button type="submit">Settings</button>
</form>
<button onclick="switchToDevMode()" style="background: #0066cc;">Switch to Dev Mode</button>
<button onclick="switchToProdMode()" style="background: #cc6600;">Switch to Prod Mode</button>
<button onclick="resetAdmin()" class="danger-button">Reset Admin</button>
<button onclick="clearUploads()" class="danger-button">Clear PacShare</button>
</div>
<!-- Flash Messages -->
<div id="admin-feedback" class="copy-feedback" style="display: none;"></div>
</section>
<!-- Password Change Section -->
<section id="password-change-section" class="card form-group">
<h2>Change Admin Password</h2>
<!-- Password Feedback -->
{% with messages = get_flashed_messages(with_categories=true, category_filter=['password-feedback']) %}
{% for category, message in messages %}
<div class="copy-feedback show">{{ message }}</div>
{% endfor %}
{% endwith %}
<!-- Password Change Form -->
<form method="POST" action="{{ url_for('admin_change_password') }}">
<input type="password" name="current_password" placeholder="Current Password" required />
<input type="password" name="new_password" placeholder="New Password" required />
<button type="submit">Update Password</button>
</form>
</section>
<!-- 2FA Management Section -->
<section id="2fa-section" class="card form-group">
<h2>Two-Factor Authentication (2FA)</h2>
<!-- 2FA Feedback -->
{% with messages = get_flashed_messages(with_categories=true, category_filter=['2fa-feedback']) %}
{% for category, message in messages %}
<div class="copy-feedback show">{{ message }}</div>
{% endfor %}
{% endwith %}
{% if tfa_enabled %}
<!-- 2FA is enabled -->
<div class="status-info">
<p style="color: lime;">✅ 2FA is <strong>enabled</strong> for your admin account.</p>
<p>Your account is protected with TOTP-based two-factor authentication.</p>
</div>
<!-- QR Code Display -->
<div class="form-group">
<button type="button" onclick="toggleQRCode()" style="margin-bottom: 10px;">Show/Hide QR Code</button>
<div id="qr-code-container" style="display: none; text-align: center;">
<p><strong>Scan this QR code with your authenticator app:</strong></p>
<img src="{{ url_for('admin_qr_code') }}" alt="Admin 2FA QR Code" style="max-width: 200px;" />
<p style="font-size: 0.85em; color: #ccc;">You can re-scan this QR code if you need to set up 2FA on a new device.</p>
</div>
</div>
<!-- Disable 2FA Form -->
<div class="form-group">
<form method="POST" action="{{ url_for('admin_disable_2fa') }}">
<input type="text" name="totp_code" placeholder="Enter current 2FA code to disable" pattern="[0-9]{6}" maxlength="6" required />
<button type="submit" class="danger-button">Disable 2FA</button>
</form>
</div>
{% else %}
<!-- 2FA is disabled -->
<div class="status-info">
<p style="color: #ff6b6b;">🔒 2FA is <strong>disabled</strong> for your admin account.</p>
<p>Enable 2FA for enhanced security using authenticator apps like Google Authenticator, Authy, or Microsoft Authenticator.</p>
</div>
<!-- Enable 2FA -->
<div class="form-group">
<form method="POST" action="{{ url_for('admin_enable_2fa') }}">
<button type="submit">Enable 2FA</button>
</form>
</div>
{% endif %}
</section>
<!-- Server Status Section -->
<section id="server-status-section" class="card form-group">
<h2>Server Status</h2>
<ul class="status-list">
<li>Uptime: <code>{{ server_info.uptime }}</code></li>
<li>Server Time: <code>{{ server_info.server_time }}</code></li>
<li>Python Version: <code>{{ server_info.python_version }}</code></li>
<li>Flask Debug Mode: <code>{{ server_info.debug_mode }}</code></li>
</ul>
</section>
<!-- Server Logs Section -->
<section id="server-logs-section" class="card form-group">
<h2>Server Logs</h2>
<button onclick="toggleLogs()" style="margin-bottom: 10px;">Show/Hide Logs</button>
<div id="logLoader" style="display: none; margin-bottom: 10px;">Loading logs...</div>
<pre id="logContainer" style="display: none;"></pre>
</section>
<!-- Footer -->
<footer>
<p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p>
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
<img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
</a>
<a href="{{ url_for('sitemap') }}">
<img src="\static\img\sitemap.png" alt="Sitemap Png" width="55" />
</a>
</footer>
<!-- Log Viewer Script -->
<script>
async function toggleLogs() {
const logContainer = document.getElementById('logContainer');
const logLoader = document.getElementById('logLoader');
if (logContainer.style.display === 'none') {
logLoader.style.display = 'block';
const response = await fetch("{{ url_for('admin_logs') }}");
const data = await response.json();
logLoader.style.display = 'none';
logContainer.innerText = data.logs.join('\n');
logContainer.style.display = 'block';
} else {
logContainer.style.display = 'none';
}
}
</script>
<script>
function toggleSitemap() {
const list = document.getElementById('sitemap-list');
const button = document.querySelector('.sitemap-header button');
if (list.style.display === 'none') {
list.style.display = 'block';
button.textContent = 'Hide Site Map';
} else {
list.style.display = 'none';
button.textContent = 'Show Site Map';
}
}
async function restartServer() {
if (!confirm('Are you sure you want to restart the server? This will temporarily disconnect all users.')) return;
try {
const response = await fetch('{{ url_for("restart_server") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.ok) {
showFeedback(data.message);
// Add a small delay before redirecting to allow the server to restart
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
showFeedback(data.error || 'Failed to restart server.');
}
} catch (error) {
showFeedback('Failed to restart server.');
}
}
async function updateServer() {
if (!confirm('Are you sure you want to pull the latest changes from GitHub?')) return;
try {
const response = await fetch('{{ url_for("admin_update_server") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.ok) {
showFeedback(data.message);
} else {
showFeedback(data.error || 'Failed to update server from GitHub.');
}
} catch (error) {
showFeedback('Failed to update server from GitHub.');
}
}
async function resetAdmin() {
if (!confirm('Are you sure you want to reset admin credentials?')) return;
try {
const response = await fetch('{{ url_for("admin_reset") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
showFeedback('Admin credentials reset. Please create new credentials.');
setTimeout(() => {
window.location.href = '{{ url_for("admin_setup") }}';
}, 2000);
}
} catch (error) {
showFeedback('Failed to reset admin credentials.');
}
}
async function clearUploads() {
if (!confirm('Are you sure you want to delete ALL uploaded files?')) return;
try {
const response = await fetch('{{ url_for("admin_clear_uploads") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
showFeedback('All uploaded files have been cleared.');
}
} catch (error) {
showFeedback('Failed to clear uploaded files.');
}
}
async function switchToDevMode() {
if (!confirm('Are you sure you want to switch to Development mode? This will restart the server.')) return;
try {
const response = await fetch('{{ url_for("admin_switch_dev_mode") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.ok) {
showFeedback(data.message);
setTimeout(() => {
window.location.reload();
}, 3000);
} else {
showFeedback(data.error || 'Failed to switch to dev mode.');
}
} catch (error) {
showFeedback('Failed to switch to dev mode.');
}
}
async function switchToProdMode() {
if (!confirm('Are you sure you want to switch to Production mode? This will restart the server.')) return;
try {
const response = await fetch('{{ url_for("admin_switch_prod_mode") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.ok) {
showFeedback(data.message);
setTimeout(() => {
window.location.reload();
}, 3000);
} else {
showFeedback(data.error || 'Failed to switch to prod mode.');
}
} catch (error) {
showFeedback('Failed to switch to prod mode.');
}
}
function showFeedback(message) {
const feedback = document.getElementById('admin-feedback');
feedback.textContent = message;
feedback.style.display = 'block';
feedback.classList.add('show');
setTimeout(() => {
feedback.classList.remove('show');
setTimeout(() => {
feedback.style.display = 'none';
}, 300);
}, 3000);
}
function toggleQRCode() {
const container = document.getElementById('qr-code-container');
const button = document.querySelector('button[onclick="toggleQRCode()"]');
if (container.style.display === 'none') {
container.style.display = 'block';
button.textContent = 'Hide QR Code';
} else {
container.style.display = 'none';
button.textContent = 'Show QR Code';
}
}
</script>
</body>
</html>
+71
View File
@@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="PacCrypt - Admin Login" />
<title>Admin Login - PacCrypt</title>
<!-- Favicon -->
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}" />
<!-- Stylesheets -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<!-- Scripts -->
<script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script>
</head>
<body class="dark">
<!-- Header -->
<header class="card logo-header">
<div class="logo-container">
<img src="{{ url_for('static', filename='img/PacCrypt.png') }}" alt="PacCrypt Logo" />
<div class="logo-text">
<h1>PACCRYPT</h1>
<p>Admin Login</p>
</div>
</div>
</header>
<!-- Main Content -->
<main>
<!-- Login Form Section -->
<section class="card form-group">
<h2>Admin Login</h2>
<!-- Flash Messages -->
{% with messages = get_flashed_messages() %}
{% if messages %}
<p style="color: red;">{{ messages[0] }}</p>
{% endif %}
{% endwith %}
<!-- Login Form -->
<form method="POST">
<input type="text" name="username" placeholder="Username" required />
<input type="password" name="password" placeholder="Password" required />
{% if requires_2fa %}
<!-- 2FA Code Input -->
<div class="form-group">
<input type="text" name="totp_code" placeholder="2FA Code (6 digits)" pattern="[0-9]{6}" maxlength="6" required />
<small style="color: #ccc;">Enter the 6-digit code from your authenticator app</small>
</div>
{% endif %}
<div class="button-group mt-3">
<button type="submit">Log In</button>
</div>
</form>
</section>
</main>
<!-- Footer -->
<footer>
<p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p>
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
<img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
</a>
</footer>
</body>
</html>
+76
View File
@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="PacCrypt - Admin Settings" />
<title>Admin Settings - PacCrypt</title>
<!-- Favicon -->
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}" />
<!-- Stylesheets -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
</head>
<body class="dark">
<!-- Header -->
<!-- Header -->
<header class="card logo-header">
<div class="logo-container">
<img src="{{ url_for('static', filename='img/PacCrypt.png') }}" alt="PacCrypt Logo" />
<div class="logo-text">
<h1>PACCRYPT</h1>
<p>Server Settings</p>
</div>
</div>
</header>
<!-- Main Content -->
<main>
<!-- Settings Form Section -->
<section class="card form-group">
<h2>Upload Settings</h2>
<!-- Flash Messages -->
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul style="color: lime;">
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<!-- Settings Form -->
<form method="POST">
<label for="upload_folder">Upload Folder Path:</label>
<input type="text" name="upload_folder" id="upload_folder" value="{{ settings.upload_folder }}" required />
<label for="max_file_age_days">Max File Age (Days):</label>
<input type="number" name="max_file_age_days" id="max_file_age_days" value="{{ settings.max_file_age_days }}" min="1" required />
<label for="max_file_size_gb">Max File Size (GB):</label>
<input type="number" name="max_file_size_gb" id="max_file_size_gb" value="{{ settings.max_file_size_bytes // (1024 * 1024 * 1024) }}" step="0.1" min="0.1" required />
<!-- Action Buttons -->
<div class="button-group mt-4">
<button type="submit">Save Settings</button>
<a href="{{ url_for('admin_page') }}">
<button type="button">Back to Admin Panel</button>
</a>
</div>
</form>
</section>
</main>
<!-- Footer -->
<footer>
<p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p>
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
<img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
</a>
</footer>
</body>
</html>
+72
View File
@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="PacCrypt - Admin Setup" />
<title>PacCrypt - Admin Setup</title>
<!-- Favicon -->
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}" />
<!-- Stylesheets -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<!-- Scripts -->
<script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script>
</head>
<body class="dark">
<!-- Header -->
<header class="card logo-header">
<div class="logo-container">
<img src="{{ url_for('static', filename='img/PacCrypt.png') }}" alt="PacCrypt Logo" />
<div class="logo-text">
<h1>PACCRYPT</h1>
<p>Admin Setup</p>
</div>
</div>
</header>
<!-- Main Content -->
<main>
<!-- Setup Form Section -->
<section class="card form-group">
<h2>Create Admin Account</h2>
<!-- Flash Messages -->
{% with messages = get_flashed_messages() %}
{% if messages %}
<p style="color: red;">{{ messages[0] }}</p>
{% endif %}
{% endwith %}
<!-- Setup Form -->
<form method="POST">
<input type="text" name="username" placeholder="Username" required />
<input type="password" name="password" placeholder="Password" required />
<!-- 2FA Option -->
<div class="form-group">
<label>
<input type="checkbox" name="enable_2fa" id="enable-2fa" />
Enable Two-Factor Authentication (2FA) - Adds extra security using TOTP apps like Google Authenticator, Authy, etc.
</label>
</div>
<div class="button-group mt-3">
<button type="submit">Set Credentials</button>
</div>
</form>
</section>
</main>
<!-- Footer -->
<footer>
<p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p>
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
<img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
</a>
</footer>
</body>
</html>
+892
View File
@@ -0,0 +1,892 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="PacCrypt - Secure text and file encryption with password generation" />
<title>PacCrypt</title>
<!-- Favicon -->
<link rel="icon" href="{{ url_for('static', filename='img/PacCrypt.png') }}" type="image/png" />
<!-- Stylesheets -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<!-- Scripts -->
<script type="module" src="{{ url_for('static', filename='js/main.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/bulk-operations.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/crypto-settings.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/pacshare-enhanced.js') }}" defer></script>
</head>
<body class="dark">
<!-- Header -->
<header class="card logo-header">
<div class="logo-container">
<img src="{{ url_for('static', filename='img/PacCrypt.png') }}" alt="PacCrypt Logo" />
<div class="logo-text">
<h1>PACCRYPT</h1>
<p>Securely Share Text and Files</p>
</div>
</div>
</header>
<!-- Main Content -->
<main>
<!-- Password Generator Section -->
<section id="password-generator-section" class="card form-group">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px;">
<h2 style="margin: 0;">Password Generator</h2>
<button type="button" id="password-settings-btn" class="settings-button" title="Password Settings">
⚙️
</button>
</div>
<!-- Generated Password Display -->
<div class="form-group">
<label for="generated-password">Generated Password:</label>
<div style="position: relative;">
<input type="text" id="generated-password" readonly style="font-family: monospace;" />
<button type="button" id="toggle-password-visibility" style="position: absolute; right: 5px; top: 50%; transform: translateY(-50%); background: none; border: none; color: #00ff99; cursor: pointer;">👁️</button>
</div>
<!-- Password Strength Meter -->
<div id="password-strength-container" style="margin-top: 10px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
<label style="margin: 0;">Password Strength:</label>
<span id="password-strength-text" style="font-weight: bold; color: #ff6b6b;">No Password</span>
</div>
<div id="password-strength-bar" style="width: 100%; height: 10px; background-color: #333; border-radius: 5px; overflow: hidden;">
<div id="password-strength-fill" style="height: 100%; width: 0%; background-color: #ff6b6b; transition: all 0.3s ease;"></div>
</div>
<div id="password-strength-details" style="margin-top: 8px; font-size: 0.9em; color: #ccc;">
<div id="strength-score" style="margin-bottom: 3px;">Score: 0/100</div>
<div id="strength-feedback"></div>
</div>
</div>
<div class="button-group" style="margin-top: 15px;">
<button type="button" id="generate-btn">Generate Password</button>
<button type="button" id="copy-btn">Copy Password</button>
<button type="button" id="use-password-btn">Use in Form</button>
</div>
<div id="password-copy-feedback" class="copy-feedback">Password copied to clipboard!</div>
</div>
</section>
<!-- Password Settings Modal -->
<div id="password-settings-modal" class="modal-overlay" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>Password Generator Settings</h3>
<button type="button" id="close-password-settings" class="close-button"></button>
</div>
<div class="modal-body">
<!-- Password Length -->
<div class="setting-group">
<div class="length-header">
<label for="password-length">Length:</label>
<div class="length-input-container">
<input type="number" id="password-length-input" min="8" max="128" value="16" class="length-number-input" />
<span class="length-unit">characters</span>
</div>
</div>
<input type="range" id="password-length" min="8" max="128" value="16" class="length-slider" />
<div class="length-labels">
<span>8</span>
<span>32</span>
<span>64</span>
<span>128</span>
</div>
</div>
<!-- Character Set Options -->
<div class="setting-group">
<h4>Character Types</h4>
<div class="checkbox-grid">
<label class="checkbox-item">
<input type="checkbox" id="include-uppercase" checked />
<span class="checkmark"></span>
Uppercase (A-Z)
</label>
<label class="checkbox-item">
<input type="checkbox" id="include-lowercase" checked />
<span class="checkmark"></span>
Lowercase (a-z)
</label>
<label class="checkbox-item">
<input type="checkbox" id="include-numbers" checked />
<span class="checkmark"></span>
Numbers (0-9)
</label>
<label class="checkbox-item">
<input type="checkbox" id="include-special" checked />
<span class="checkmark"></span>
Special Characters
</label>
</div>
</div>
<!-- Advanced Options -->
<div class="setting-group">
<h4>Advanced Options</h4>
<label class="checkbox-item">
<input type="checkbox" id="exclude-ambiguous" />
<span class="checkmark"></span>
Exclude ambiguous characters (0, O, l, 1, I)
</label>
</div>
<!-- Custom Characters -->
<div class="setting-group">
<label for="custom-characters">Custom Characters</label>
<input type="text" id="custom-characters" placeholder="Add custom characters..." class="custom-input" />
<div class="setting-hint">Add any additional characters you want to include</div>
</div>
<!-- Preview -->
<div class="setting-group">
<h4>Character Set Preview</h4>
<div id="charset-preview" class="charset-preview">Loading...</div>
</div>
</div>
<div class="modal-footer">
<button type="button" id="reset-password-settings" class="secondary-button">Reset to Defaults</button>
<button type="button" id="apply-password-settings" class="primary-button">Apply Settings</button>
</div>
</div>
</div>
<!-- Crypto Settings Modal -->
<div id="crypto-settings-modal" class="modal-overlay" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>Encryption Settings</h3>
<button type="button" id="close-crypto-settings" class="close-button"></button>
</div>
<div class="modal-body">
<!-- Processing Mode -->
<div class="setting-group">
<h4>Processing Mode</h4>
<div class="mode-selection">
<label class="radio-item">
<input type="radio" name="processing-mode" id="single-file-mode-radio" value="single" checked />
<span class="radiomark"></span>
<div class="radio-content">
<div class="radio-title">Single File</div>
<div class="radio-description">Process one file at a time</div>
</div>
</label>
<label class="radio-item">
<input type="radio" name="processing-mode" id="bulk-file-mode-radio" value="bulk" />
<span class="radiomark"></span>
<div class="radio-content">
<div class="radio-title">Bulk Processing</div>
<div class="radio-description">Process multiple files with drag & drop</div>
</div>
</label>
</div>
</div>
<!-- File Preview Options -->
<div class="setting-group">
<h4>File Preview</h4>
<label class="checkbox-item">
<input type="checkbox" id="enable-file-preview" checked />
<span class="checkmark"></span>
Enable file preview before processing
</label>
<label class="checkbox-item">
<input type="checkbox" id="auto-download-results" checked />
<span class="checkmark"></span>
Automatically download processed files
</label>
</div>
<!-- Bulk Processing Options -->
<div class="setting-group" id="bulk-options" style="display: none;">
<h4>Bulk Processing Options</h4>
<label class="checkbox-item">
<input type="checkbox" id="sequential-processing" checked />
<span class="checkmark"></span>
Process files sequentially (prevents server overload)
</label>
<label class="checkbox-item">
<input type="checkbox" id="show-detailed-progress" checked />
<span class="checkmark"></span>
Show detailed progress for each file
</label>
<label class="checkbox-item">
<input type="checkbox" id="stop-on-error" />
<span class="checkmark"></span>
Stop processing if any file fails
</label>
</div>
<!-- Performance Settings -->
<div class="setting-group">
<h4>Performance</h4>
<div class="setting-item">
<label for="max-file-size">Maximum file size (MB):</label>
<div class="size-input-container">
<input type="number" id="max-file-size-input" min="1" max="1000" value="100" class="size-number-input" />
<span class="size-unit">MB</span>
</div>
</div>
<div class="setting-hint">Files larger than this will show a warning</div>
</div>
</div>
<div class="modal-footer">
<button type="button" id="reset-crypto-settings" class="secondary-button">Reset to Defaults</button>
<button type="button" id="apply-crypto-settings" class="primary-button">Apply Settings</button>
</div>
</div>
</div>
<!-- PacShare Settings Modal -->
<div id="pacshare-settings-modal" class="modal-overlay" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>PacShare Settings</h3>
<button type="button" id="close-pacshare-settings" class="close-button"></button>
</div>
<div class="modal-body">
<!-- Security Options -->
<div class="setting-group">
<h4>Security</h4>
<label class="checkbox-item">
<input type="checkbox" id="enable-2fa-setting" />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Enable 2FA (TOTP)</div>
<div class="checkbox-description">Adds extra security with Google Authenticator, Authy, etc.</div>
</div>
</label>
<label class="checkbox-item">
<input type="checkbox" id="auto-clear-passwords" checked />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Auto-clear passwords after upload</div>
<div class="checkbox-description">Automatically clear password fields for security</div>
</div>
</label>
</div>
<!-- Upload Behavior -->
<div class="setting-group">
<h4>Upload Behavior</h4>
<label class="checkbox-item">
<input type="checkbox" id="auto-copy-links" checked />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Auto-copy share links</div>
<div class="checkbox-description">Automatically copy links to clipboard after upload</div>
</div>
</label>
<label class="checkbox-item">
<input type="checkbox" id="show-upload-progress" checked />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Show detailed upload progress</div>
<div class="checkbox-description">Display progress bars for each file</div>
</div>
</label>
<label class="checkbox-item">
<input type="checkbox" id="scroll-to-results" checked />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Auto-scroll to results</div>
<div class="checkbox-description">Automatically scroll to results after upload</div>
</div>
</label>
</div>
<!-- File Validation -->
<div class="setting-group">
<h4>File Validation</h4>
<div class="setting-item">
<label for="max-upload-size">Maximum file size per upload (MB):</label>
<div class="size-input-container">
<input type="number" id="max-upload-size-input" min="1" max="1000" value="25" class="size-number-input" />
<span class="size-unit">MB</span>
</div>
<div class="setting-hint">Files larger than this will show a warning</div>
</div>
<label class="checkbox-item">
<input type="checkbox" id="validate-file-types" />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Validate file types</div>
<div class="checkbox-description">Show warnings for potentially unsafe file types</div>
</div>
</label>
</div>
<!-- Advanced Options -->
<div class="setting-group">
<h4>Advanced</h4>
<div class="setting-item">
<label for="concurrent-uploads">Maximum concurrent uploads:</label>
<div class="size-input-container">
<input type="number" id="concurrent-uploads-input" min="1" max="10" value="1" class="size-number-input" />
<span class="size-unit">files</span>
</div>
<div class="setting-hint">1 = sequential uploads (recommended), higher = parallel uploads</div>
</div>
<label class="checkbox-item">
<input type="checkbox" id="enable-file-preview" checked />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Enable file preview</div>
<div class="checkbox-description">Allow previewing files before upload</div>
</div>
</label>
<label class="checkbox-item">
<input type="checkbox" id="remember-algorithm" checked />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Remember encryption algorithm</div>
<div class="checkbox-description">Remember the last selected algorithm</div>
</div>
</label>
</div>
<!-- Current Settings Summary -->
<div class="setting-group">
<h4>Current Configuration</h4>
<div id="pacshare-settings-summary" class="charset-preview">
Loading current settings...
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" id="reset-pacshare-settings" class="secondary-button">Reset to Defaults</button>
<button type="button" id="apply-pacshare-settings" class="primary-button">Apply Settings</button>
</div>
</div>
</div>
<!-- Key Management Section -->
<section id="key-pairs-section" class="card form-group">
<h2>Key Management</h2>
<p style="color: #ccc; font-size: 0.9em; margin-bottom: 15px;">
Manage Key Pairs for the RSA Hybrid Algorithm.
</p>
<!-- Key Status Indicators -->
<div class="form-group">
<h3 style="margin-bottom: 10px; color: #00ff99;">Key Status</h3>
<div id="key-status-indicators" style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 15px;">
<div style="padding: 10px; border: 2px solid #333; border-radius: 5px; text-align: center;">
<div id="public-key-indicator" style="color: #ff6b6b; font-weight: bold;">🔓 No Public Key</div>
<div style="font-size: 0.8em; color: #888;">For Encryption</div>
</div>
<div style="padding: 10px; border: 2px solid #333; border-radius: 5px; text-align: center;">
<div id="private-key-indicator" style="color: #ff6b6b; font-weight: bold;">🔐 No Private Key</div>
<div style="font-size: 0.8em; color: #888;">For Decryption</div>
</div>
</div>
</div>
<!-- Key Management Buttons -->
<div class="form-group">
<div class="button-group">
<button type="button" id="generate-keypair-main-btn">Generate & Download Key Pair</button>
<button type="button" id="load-public-main-btn">Load Public Key</button>
<button type="button" id="load-private-main-btn">Load Private Key</button>
</div>
<div class="button-group" style="margin-top: 10px;">
<button type="button" id="clear-keys-btn" class="danger-button">Clear All Keys</button>
<button type="button" id="download-keys-btn" style="display: none;">Download Current Keys</button>
</div>
</div>
<!-- Hidden File Inputs -->
<input type="file" id="public-key-main-input" accept=".pub,.pem" style="display: none;">
<input type="file" id="private-key-main-input" accept=".key,.pem" style="display: none;">
<!-- Key Information Display -->
<div id="key-info-display" style="display: none; margin-top: 15px; padding: 10px; border: 1px solid #00ff99; border-radius: 5px; background-color: #001100;">
<h4 style="color: #00ff99; margin-top: 0;">Loaded Keys Information</h4>
<div id="key-info-content" style="font-family: monospace; font-size: 0.8em; color: #ccc;"></div>
</div>
<!-- Copy Feedback -->
<div id="keypair-feedback" class="copy-feedback">Keys generated and downloaded!</div>
</section>
<!-- Pacman Game Section -->
<section id="pacman-section" class="card" style="display: none;">
<div class="pacman-wrapper">
<canvas id="pacmanCanvas" width="800" height="600"></canvas>
</div>
<audio id="chomp-sound" src="{{ url_for('static', filename='audio/chomp.mp3') }}"></audio>
<div class="button-group" style="margin-top: 6px;">
<button type="button" onclick="resetGame()">Restart Game</button>
<button type="button" onclick="exitGame()">Exit Game</button>
</div>
</section>
<!-- Encryption/Decryption Section -->
<section id="encoding-section" class="card form-group">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px;">
<h2 style="margin: 0;">Encrypt & Decrypt</h2>
<button type="button" id="crypto-settings-btn" class="settings-button" title="Encryption Settings">
⚙️
</button>
</div>
<form id="crypto-form" class="form-group">
<!-- Algorithm Selection -->
<div class="form-group" id="algorithm-selection">
<label for="algorithm">Encryption Algorithm:</label>
<select id="algorithm">
<!-- Options populated dynamically by JavaScript -->
</select>
</div>
<!-- Key Pair Management (for RSA/PQ algorithms) -->
<div class="form-group" id="keypair-section" style="display: none;">
<div class="keypair-info">
<p><strong>🔐 Key Pair Required:</strong></p>
<p><strong>For Encryption:</strong> Use Public Key (.pub file)</p>
<p><strong>For Decryption:</strong> Use Private Key (.key file)</p>
</div>
<div style="padding: 10px; border: 1px solid #ffaa00; border-radius: 5px; background-color: #221100; margin: 10px 0;">
<p style="color: #ffaa00; margin: 0; text-align: center;">
<strong>💡 Manage your keys in the "Key Pairs Management" section above</strong>
</p>
</div>
<!-- Key status indicators -->
<div id="key-status" style="margin-top: 10px;">
<div id="public-key-status" style="display: none;">✅ Public key loaded</div>
<div id="private-key-status" style="display: none;">✅ Private key loaded</div>
</div>
</div>
<!-- Operation Toggle -->
<div class="toggle-container">
<span class="toggle-label">Encrypt</span>
<label class="material-switch">
<input type="checkbox" id="operation-toggle">
<span class="material-slider"></span>
</label>
<span class="toggle-label">Decrypt</span>
</div>
<!-- Text Input Section -->
<div id="text-section" class="form-group">
<textarea id="input-text" placeholder="Enter your message..."></textarea>
</div>
<!-- Password Input -->
<div id="password-input" class="form-group">
<input type="password" id="password" placeholder="Encryption/Decryption Password" />
</div>
<!-- File Input Section -->
<div id="file-section" class="form-group">
<!-- Single File Input (default) -->
<div id="single-file-mode">
<input type="file" id="file-input" />
<button type="button" id="remove-file-btn">Remove File</button>
</div>
<!-- Bulk File Mode (hidden by default) -->
<div id="bulk-file-mode" style="display: none;">
<!-- Drag & Drop Zone -->
<div id="bulk-drop-zone" class="drop-zone">
<div class="drop-zone-content">
<div class="drop-zone-icon">📁</div>
<p>Drag & drop files here or <button type="button" id="bulk-file-select">select files</button></p>
<p style="font-size: 0.9em; color: #888;">Supports multiple files</p>
</div>
<input type="file" id="bulk-file-input" multiple style="display: none;" />
</div>
<!-- File Preview Section -->
<div id="bulk-file-preview" style="display: none; margin-top: 15px;">
<h3>Selected Files</h3>
<div id="bulk-file-list" class="file-list"></div>
<div class="button-group" style="margin-top: 15px;">
<button type="button" id="bulk-clear-btn" class="danger-button">Clear All</button>
</div>
</div>
<!-- Progress Section -->
<div id="bulk-progress-section" style="display: none; margin-top: 15px;">
<h3>Processing Progress</h3>
<div id="bulk-overall-progress" class="progress-container">
<div class="progress-bar">
<div id="bulk-overall-bar" class="progress-fill"></div>
</div>
<span id="bulk-overall-text">0 / 0 files processed</span>
</div>
<div id="bulk-file-progress-list" class="file-progress-list"></div>
</div>
<!-- Results Section -->
<div id="bulk-results-section" style="display: none; margin-top: 15px;">
<h3>Results</h3>
<div id="bulk-results-list" class="results-list"></div>
<div class="button-group" style="margin-top: 15px;">
<button type="button" id="bulk-download-all">Download All Results</button>
<button type="button" id="bulk-reset">Start Over</button>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="button-group">
<button type="submit">Execute</button>
<button type="button" id="copy-output-btn">Copy Output</button>
</div>
<!-- Output Section -->
<textarea id="output-text" readonly placeholder="Encrypted/Decrypted Output"></textarea>
<div class="button-group">
<button type="button" id="clear-all-btn" class="danger-button">Clear All</button>
</div>
<div id="output-copy-feedback" class="copy-feedback">Text copied to clipboard!</div>
</form>
</section>
<!-- File Sharing Section -->
<section id="sharing-section" class="card form-group">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 15px;">
<div>
<h2 style="margin: 0;">PacShare</h2>
<p style="margin: 5px 0 0 0;">Securely share encrypted files.</p>
</div>
<button type="button" id="pacshare-settings-btn" class="settings-button" title="PacShare Settings">
⚙️
</button>
</div>
<!-- Flash Messages -->
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul style="color: lime; list-style: none; padding-left: 0;">
{% for message in messages %}
<li>
{{ message | safe }}
{% if "pickup" in message %}
<div class="share-link-container">
<a id="share-link" href="{{ message.split(' at ')[1] }}" target="_blank">{{ message.split(" at ")[1] }}</a>
<button type="button" onclick="copyShareLink()">Copy Link</button>
<div id="shared-link-feedback" class="copy-feedback">Link copied to clipboard!</div>
</div>
{% endif %}
</li>
{% endfor %}
</ul>
<script>window.onload = () => window.scrollTo(0, document.body.scrollHeight);</script>
{% endif %}
{% endwith %}
<!-- File Upload Form -->
<form method="POST" enctype="multipart/form-data" class="form-group" id="upload-form">
<!-- Algorithm Selection for PacShare -->
<div class="form-group">
<label for="share-algorithm">Encryption Algorithm:</label>
<select id="share-algorithm" name="algorithm">
<!-- Options populated dynamically by JavaScript -->
</select>
</div>
<!-- Enhanced File Upload Section -->
<div class="form-group">
<!-- Drag & Drop Zone for PacShare -->
<div id="pacshare-drop-zone" class="drop-zone">
<div class="drop-zone-content">
<div class="drop-zone-icon">📤</div>
<p>Drag & drop files here or <button type="button" id="pacshare-file-select">select files</button></p>
<p style="font-size: 0.9em; color: #888;">Single file or multiple files supported</p>
</div>
<input type="file" name="file" id="upload-file" multiple style="display: none;" />
</div>
<!-- Selected Files Display -->
<div id="pacshare-file-list" style="display: none; margin-top: 15px;">
<h4 style="color: #00ff99; margin-bottom: 10px;">Selected Files</h4>
<div id="pacshare-files-container" class="file-list"></div>
<div class="button-group" style="margin-top: 10px;">
<button type="button" id="pacshare-clear-files" class="danger-button">Clear All</button>
</div>
</div>
</div>
<input type="password" name="enc_password" placeholder="Encryption/Decryption Password" required />
<input type="password" name="pickup_password" placeholder="Pickup Password" required />
<div class="button-group">
<button type="submit" id="pacshare-upload-btn">Upload and Generate Links</button>
</div>
</form>
<!-- Results Section -->
<div id="pacshare-results" style="display: none; margin-top: 20px;">
<h3 style="color: #00ff99;">Upload Results</h3>
<div id="pacshare-results-list" class="results-list"></div>
</div>
<!-- Progress Section -->
<div id="pacshare-progress" style="display: none; margin-top: 20px;">
<h3>Upload Progress</h3>
<div id="pacshare-overall-progress" class="progress-container">
<div class="progress-bar">
<div id="pacshare-overall-bar" class="progress-fill"></div>
</div>
<span id="pacshare-overall-text">0 / 0 files uploaded</span>
</div>
<div id="pacshare-file-progress" class="file-progress-list"></div>
</div>
<!-- Share Link Container (initially hidden) -->
<div class="share-link-container" id="share-link-container" style="display: none;">
<a id="share-link" href="#" target="_blank"></a>
<button type="button" id="copy-share-btn">Copy Link</button>
<div id="shared-link-feedback" class="copy-feedback">Link copied to clipboard!</div>
</div>
<!-- 2FA Setup Container (initially hidden) -->
<div id="tfa-setup-container" style="display: none; margin-top: 20px; padding: 15px; border: 2px solid #ffaa00; border-radius: 8px; background-color: #332200;">
<h3 style="color: #ffaa00; margin-top: 0;">🔒 Important: Set Up 2FA Now!</h3>
<p style="color: #ccc;">You enabled 2FA for this file. <strong>Scan this QR code NOW</strong> with your authenticator app:</p>
<div style="text-align: center; margin: 15px 0;">
<img id="tfa-qr-image" src="" alt="2FA QR Code" style="max-width: 200px; border: 2px solid #00ff99;" />
</div>
<!-- 2FA String Container -->
<div style="margin-top: 15px; padding: 10px; border: 1px solid #00ff99; border-radius: 5px; background-color: #001100;">
<p style="color: #00ff99; margin: 5px 0; font-size: 0.9em;"><strong>Or manually enter this string:</strong></p>
<div class="share-link-container" style="margin: 0;">
<input type="text" id="tfa-string" readonly style="flex: 1; background: #111; color: #00ff99; border: 1px solid #333; padding: 8px; font-family: monospace; font-size: 0.8em;" />
<button type="button" id="copy-tfa-string-btn">Copy String</button>
<div id="tfa-string-feedback" class="copy-feedback">2FA string copied to clipboard!</div>
</div>
</div>
<p style="color: #ff6b6b; font-weight: bold; margin-top: 15px;">⚠️ SAVE THIS QR CODE OR STRING NOW! It will not be shown again for security reasons.</p>
<p style="color: #ccc; font-size: 0.9em;">Recommended apps: Google Authenticator, Authy, Microsoft Authenticator</p>
<button type="button" onclick="pacShareEnhanced && pacShareEnhanced.closeTwoFactorSetup ? pacShareEnhanced.closeTwoFactorSetup() : (document.getElementById('tfa-setup-container').style.display = 'none')">I've Saved the 2FA Information</button>
</div>
<p style="color: #9c0000;">BOTH PASSWORDS ARE REQUIRED FOR PICKUP</p>
<script>
// Centralized Key Pairs Management
let globalKeys = {
publicKey: null,
privateKey: null,
algorithm: 'rsa_hybrid'
};
function updateKeyStatusIndicators() {
const publicIndicator = document.getElementById('public-key-indicator');
const privateIndicator = document.getElementById('private-key-indicator');
const keyInfoDisplay = document.getElementById('key-info-display');
const keyInfoContent = document.getElementById('key-info-content');
const downloadKeysBtn = document.getElementById('download-keys-btn');
// Update public key indicator
if (globalKeys.publicKey) {
publicIndicator.style.color = '#00ff99';
publicIndicator.textContent = '🔓 Public Key Loaded';
document.getElementById('public-key-status').style.display = 'block';
} else {
publicIndicator.style.color = '#ff6b6b';
publicIndicator.textContent = '🔓 No Public Key';
document.getElementById('public-key-status').style.display = 'none';
}
// Update private key indicator
if (globalKeys.privateKey) {
privateIndicator.style.color = '#00ff99';
privateIndicator.textContent = '🔐 Private Key Loaded';
document.getElementById('private-key-status').style.display = 'block';
} else {
privateIndicator.style.color = '#ff6b6b';
privateIndicator.textContent = '🔐 No Private Key';
document.getElementById('private-key-status').style.display = 'none';
}
// Show/hide key info and download button
if (globalKeys.publicKey || globalKeys.privateKey) {
keyInfoDisplay.style.display = 'block';
downloadKeysBtn.style.display = 'inline-block';
let info = `Algorithm: ${globalKeys.algorithm.toUpperCase()}\n`;
if (globalKeys.publicKey) {
const pubPreview = globalKeys.publicKey.substring(0, 50) + '...';
info += `Public Key: ${pubPreview}\n`;
}
if (globalKeys.privateKey) {
const privPreview = globalKeys.privateKey.substring(0, 50) + '...';
info += `Private Key: ${privPreview}\n`;
}
keyInfoContent.textContent = info;
} else {
keyInfoDisplay.style.display = 'none';
downloadKeysBtn.style.display = 'none';
}
}
// Generate key pair
document.getElementById('generate-keypair-main-btn').addEventListener('click', async () => {
const algorithm = 'rsa_hybrid';
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) {
globalKeys.publicKey = data.public_key;
globalKeys.privateKey = data.private_key;
globalKeys.algorithm = algorithm;
// Download keys
downloadKeyPair(data.public_key, data.private_key, algorithm);
updateKeyStatusIndicators();
showKeypairFeedback('Keys generated and downloaded!');
} else {
alert('Error: ' + data.error);
}
} catch (error) {
alert('Failed to generate key pair: ' + error.message);
}
});
// Load public key
document.getElementById('load-public-main-btn').addEventListener('click', () => {
document.getElementById('public-key-main-input').click();
});
document.getElementById('public-key-main-input').addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
globalKeys.publicKey = event.target.result;
updateKeyStatusIndicators();
showKeypairFeedback('Public key loaded!');
};
reader.readAsText(file);
}
});
// Load private key
document.getElementById('load-private-main-btn').addEventListener('click', () => {
document.getElementById('private-key-main-input').click();
});
document.getElementById('private-key-main-input').addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
globalKeys.privateKey = event.target.result;
updateKeyStatusIndicators();
showKeypairFeedback('Private key loaded!');
};
reader.readAsText(file);
}
});
// Clear keys
document.getElementById('clear-keys-btn').addEventListener('click', () => {
if (confirm('Are you sure you want to clear all loaded keys?')) {
globalKeys.publicKey = null;
globalKeys.privateKey = null;
updateKeyStatusIndicators();
showKeypairFeedback('All keys cleared!');
}
});
// Download current keys
document.getElementById('download-keys-btn').addEventListener('click', () => {
if (globalKeys.publicKey || globalKeys.privateKey) {
downloadKeyPair(globalKeys.publicKey, globalKeys.privateKey, globalKeys.algorithm);
showKeypairFeedback('Keys downloaded!');
}
});
function downloadKeyPair(publicKey, privateKey, algorithm) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
if (publicKey) {
const pubBlob = new Blob([publicKey], { type: 'text/plain' });
const pubUrl = URL.createObjectURL(pubBlob);
const pubLink = document.createElement('a');
pubLink.href = pubUrl;
pubLink.download = `${algorithm}_public_key_${timestamp}.pub`;
pubLink.click();
URL.revokeObjectURL(pubUrl);
}
if (privateKey) {
const privBlob = new Blob([privateKey], { type: 'text/plain' });
const privUrl = URL.createObjectURL(privBlob);
const privLink = document.createElement('a');
privLink.href = privUrl;
privLink.download = `${algorithm}_private_key_${timestamp}.key`;
privLink.click();
URL.revokeObjectURL(privUrl);
}
}
function showKeypairFeedback(message) {
const feedback = document.getElementById('keypair-feedback');
feedback.textContent = message;
feedback.style.display = 'block';
feedback.classList.add('show');
setTimeout(() => {
feedback.classList.remove('show');
setTimeout(() => {
feedback.style.display = 'none';
}, 300);
}, 2000);
}
// Make global keys available to other scripts
window.getGlobalKeys = () => globalKeys;
window.setGlobalKeys = (keys) => {
globalKeys = { ...globalKeys, ...keys };
updateKeyStatusIndicators();
};
// Initialize key status
updateKeyStatusIndicators();
</script>
<!-- File Limits Information -->
<p class="text-muted mt-3" style="font-size: 0.85em;">
Files expire after {{ settings.max_file_age_days }} days.<br />
Max file size: {{ settings.max_file_size_bytes // (1024 * 1024 * 1024) }} GB.
</p>
</section>
</main>
<!-- Footer -->
<footer>
<p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p>
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
<img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
</a>
</footer>
</body>
</html>
+128
View File
@@ -0,0 +1,128 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="PacCrypt - Secure file pickup and decryption" />
<title>PacCrypt - Secure File Pickup</title>
<!-- Favicon -->
<link rel="icon" href="{{ url_for('static', filename='img/PacCrypt.png') }}" type="image/png" />
<!-- Stylesheets -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
</head>
<body class="dark">
<!-- Header -->
<header class="card logo-header">
<div class="logo-container">
<img src="{{ url_for('static', filename='img/PacCrypt.png') }}" alt="PacCrypt Logo" />
<div class="logo-text">
<h1>PACCRYPT</h1>
<p>Encrypted File Pickup</p>
</div>
</div>
</header>
<!-- Main Content -->
<main>
<!-- File Pickup Section -->
<section id="pickup-section" class="card form-group">
<h2>File Pickup</h2>
<!-- Flash Messages -->
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul style="color: lime; list-style: none; padding-left: 0;">
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<!-- File Info -->
<div class="form-group">
<p style="color: #00ff99; margin-bottom: 15px;">File ID: <code>{{ file_id }}</code></p>
{% if require_2fa %}
<p style="color: #ffaa00; margin-bottom: 15px;">🔒 This file requires 2FA (TOTP) authentication.</p>
{% endif %}
</div>
{% if require_2fa %}
<div class="form-group" style="border: 2px solid #ffaa00; padding: 15px; margin-bottom: 20px; border-radius: 5px;">
<h3 style="color: #ffaa00; margin-top: 0;">⚠️ 2FA Required</h3>
<p style="color: #ccc;">
<strong>You should have already set up 2FA when uploading this file.</strong><br>
Enter the 6-digit code from your authenticator app below.
</p>
<p style="color: #ff6b6b; font-size: 0.9em;">
If you didn't set up 2FA during upload, you won't be able to access this file.
</p>
</div>
{% endif %}
<!-- Pickup Form -->
<form method="POST" class="form-group">
<div class="form-group">
<input type="password"
name="pickup_password"
placeholder="Pickup Password"
required
autocomplete="off" />
</div>
<div class="form-group">
<input type="password"
name="enc_password"
placeholder="Encryption Password"
required
autocomplete="off" />
</div>
{% if require_2fa %}
<div class="form-group">
<input type="text"
name="totp_code"
placeholder="6-Digit Authenticator Code"
required
maxlength="6"
pattern="[0-9]{6}"
autocomplete="off"
style="text-align: center; font-size: 1.2em; letter-spacing: 0.2em;" />
</div>
{% endif %}
<div class="button-group">
<button type="submit">Decrypt and Download</button>
</div>
</form>
</section>
<!-- Security Notice Section -->
<section id="security-notice-section" class="card form-group">
<h2>Security Notice</h2>
<p style="color: #00ff99; text-align: center;">
Make sure you're on the correct domain before entering any passwords.<br>
Your file will be permanently deleted after download.
</p>
</section>
<!-- Link ID Section -->
<section id="link-id-section" class="card form-group">
<p style="color: #00ff99; text-align: center; font-family: monospace; font-size: 1.1em;">
Link ID: <code>{{ file_id }}</code>
</p>
</section>
</main>
<!-- Footer -->
<footer>
<p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p>
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
<img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
</a>
</footer>
</body>
</html>