Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7a4fc3e52 | |||
| 9ec15d62ef | |||
| 8c7f3bd8cc | |||
| f787ac1a8e | |||
| 0df78edead | |||
| 22062a7a3d | |||
| ea4dccb438 | |||
| 28e49dac94 | |||
| 94a89d3f76 | |||
| 0d26a3d84b | |||
| 12eb87779b | |||
| 87deca1a29 | |||
| 8b57d89009 | |||
| 114a48d8bf | |||
| 7a7be4d5c8 | |||
| 07ae277968 | |||
| ac385f2feb | |||
| 843485e598 | |||
| d3667700cb | |||
| b901f5240a | |||
| a1104cb24b | |||
| fe94e13b4a | |||
| 2842d92213 | |||
| ce195c9df6 | |||
| c3f638c888 | |||
| 91814845a4 | |||
| cb40ccdfa7 | |||
| 71544a8e01 | |||
| b80f6b80fd | |||
| a9022bb5e3 | |||
| 5b9a2b7a53 | |||
| 379c14781e | |||
| 67adca4d4f | |||
| 3e8c2ca097 | |||
| bede46d87f | |||
| 8fe7be4d56 | |||
| 20813d0e04 | |||
| e489c1e980 | |||
| 5aff9df9f7 | |||
| ff24e6eba8 | |||
| 179be48bfe | |||
| 3a44f6a2bd | |||
| 7a27d314a2 | |||
| 2a5eb3ff04 | |||
| d253fd4802 | |||
| 5234017129 | |||
| 0093d33a77 | |||
| ae19598750 | |||
| c6d480aa12 | |||
| 38d3b7e6c1 | |||
| 5d568f7f89 | |||
| 7a9d87b46e | |||
| 0756e66b80 | |||
| 98262746a9 | |||
| 49da1eaa3f | |||
| 36cf8f18f8 | |||
| b03316cea8 | |||
| 4ccf7afa5a | |||
| 3588bc3349 | |||
| e108d23945 | |||
| 099a5c8f18 | |||
| 2a414e62cf | |||
| 9c53a6e14f | |||
| aad43c2024 | |||
| a56ee7cefe | |||
| f042127931 | |||
| 66ed918a78 | |||
| 43f47565da | |||
| 022a1e7aaf | |||
| db8fd2ac1f | |||
| 1fd15b40f3 | |||
| da0ab0f042 | |||
| 973aa0f20f | |||
| b7a85b8d84 | |||
| 5f6a5747a6 | |||
| 03079263ec | |||
| 0b39998364 | |||
| 8f2d56c05a | |||
| bb8690b74f | |||
| ed11ccd2a1 | |||
| db00538aee | |||
| 05a9ada8d9 | |||
| 61193320d4 | |||
| 1c1fed1dd5 | |||
| 1edd1c858c | |||
| 1d55d4f4ce | |||
| 7aefd5aff8 | |||
| 271b4cdc91 | |||
| 90dcb7ecb8 | |||
| 7ec213fad0 | |||
| 766386501b | |||
| 6ad2b65aba | |||
| 265dff3329 | |||
| 9e45c34365 |
@@ -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).*
|
||||||
@@ -0,0 +1,661 @@
|
|||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
@@ -1,77 +1,421 @@
|
|||||||
# PacCrypt WebApp
|
> [!WARNING]
|
||||||
|
> PacCrypt is currently maintenance only, please submit issues for bugs. Only major bugs will be addressed right now. Development will continue in the future.
|
||||||
|
|
||||||
**PacCrypt** is a web-based application designed to provide secure encoding, encryption, and password generation. It allows users to easily encrypt and decrypt text and files, with both basic and advanced encryption options. It also features a password generator and a simple Pac-Man game as an Easter egg!
|
<div align="center">
|
||||||
|
|
||||||
## Features
|
[](https://git.tysstech.com/TySS-Dev/PacCrypt-Webapp)
|
||||||
|
[](https://github.com/TySP-Dev/PacCrypt-Webapp)
|
||||||
|
[](https://paccrypt.tysstech.com)
|
||||||
|
|
||||||
- **Basic and Advanced Encryption**: Choose between simple encryption (Caesar Cipher) or more secure AES-GCM encryption.
|
</div>
|
||||||
- **File Encryption/Decryption**: Encrypt or decrypt files with a password.
|
|
||||||
- **Password Generator**: Generate secure random passwords with customizable length and complexity.
|
|
||||||
- **Pac-Man Game**: A fun Easter egg! Play a Pac-Man game when you type "pacman" in the text area.
|
|
||||||
- **Copy to Clipboard**: Copy generated passwords or encrypted results with one click.
|
|
||||||
- **Responsive Design**: Fully responsive web design that works across different screen sizes.
|
|
||||||
|
|
||||||
## Installation
|
# 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.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Merged Dev branch into main, program is still in the development stage so no need to have multiple branches. Please submit issues for bugs. I expect a lot, I dont recall the state of the Dev branch.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> This document contains AI generated pieces that have not been reviewed yet.
|
||||||
|
> Next push will contain human oversite on the documentation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ 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
|
### Prerequisites
|
||||||
|
|
||||||
- **Python 3.7+**
|
- **Python 3.8+** (3.10+ recommended)
|
||||||
- **Nginx** (for reverse proxy and SSL configuration)
|
- **Git** (for updates and installation)
|
||||||
|
- **pip** package manager
|
||||||
|
|
||||||
Official PacCrypt website: paccrypt.unnaturalll.dev
|
### Installation
|
||||||
|
|
||||||
### Steps to Set Up Locally (Windows)
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone -b "dev-only_DO-NOT-USE" https://github.com/TySP-Dev/PacCrypt-Webapp.git
|
||||||
|
cd PacCrypt-Webapp
|
||||||
|
|
||||||
1. Clone the repository:
|
# Create virtual environment
|
||||||
git clone https://github.com/TySP-Dev/PacCrypt.git
|
python -m venv venv
|
||||||
cd paccrypt-webapp
|
|
||||||
|
|
||||||
2. Create and activate a virtual environment:
|
# Activate virtual environment
|
||||||
python3 -m venv venv
|
# On Linux/macOS:
|
||||||
source venv/bin/activate # On Windows, use `venv\Scripts\activate`
|
source venv/bin/activate
|
||||||
|
# On Windows:
|
||||||
|
venv\Scripts\activate
|
||||||
|
|
||||||
3. Install the required Python dependencies:
|
# Install dependencies
|
||||||
pip install -r requirements.txt
|
pip install -r application_data/requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
4. Run the Flask app:
|
### Running the Application
|
||||||
python app.py
|
|
||||||
|
|
||||||
5. Open http://127.0.0.1:5000 to access the app locally.
|
#### Development Mode
|
||||||
|
```bash
|
||||||
|
# Linux/macOS
|
||||||
|
python application_data/control_scripts/start_dev.py
|
||||||
|
|
||||||
## Usage
|
# Windows
|
||||||
|
python application_data\control_scripts\start_dev.py
|
||||||
|
```
|
||||||
|
|
||||||
### Encryption and Decryption
|
#### Production Mode
|
||||||
|
```bash
|
||||||
|
# Linux/macOS
|
||||||
|
python application_data/control_scripts/start_prod.py
|
||||||
|
|
||||||
Select the encryption type (Basic or Advanced).
|
# Windows
|
||||||
|
python application_data\control_scripts\start_prod.py
|
||||||
|
```
|
||||||
|
|
||||||
For text encryption/decryption:
|
### Access the Application
|
||||||
|
|
||||||
Enter text in the Input Text area.
|
- **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)
|
||||||
|
|
||||||
Choose whether to Encrypt or Decrypt.
|
---
|
||||||
|
|
||||||
Enter a password (if using advanced encryption).
|
## 📖 Usage Guide
|
||||||
|
|
||||||
For file encryption/decryption:
|
### 🔐 Text Encryption/Decryption
|
||||||
|
|
||||||
Upload a file.
|
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
|
||||||
|
|
||||||
Enter a password for encryption/decryption.
|
### 📁 File Operations
|
||||||
|
|
||||||
Click Encrypt or Decrypt.
|
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
|
||||||
|
|
||||||
### Password Generation
|
### 📤 PacShare - Secure File Sharing
|
||||||
|
|
||||||
Click the Generate button to create a random password, then use the Copy button to copy it to your clipboard.
|
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)
|
||||||
|
|
||||||
### Pac-Man Game (Easter Egg)
|
### 🎮 Hidden Pac-Man Game
|
||||||
|
|
||||||
Type the word "pacman" in the input box to unlock the Pac-Man game!
|
- Type `pacman` in any text input
|
||||||
|
- Use arrow keys or swipe gestures to play
|
||||||
|
- Authentic retro gaming experience with sound effects
|
||||||
|
|
||||||
### Contributing
|
---
|
||||||
|
|
||||||
|
## 🛠️ 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
|
||||||
|
- **Element/Matrix Chat**:
|
||||||
|
- **Official Instance**: N/A
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
**🔐 Secure by design. Simple by choice. Powerful by nature.**
|
||||||
|
|
||||||
Feel free to open an issue or submit a pull request for improvements, bug fixes, or new features!
|
|
||||||
|
|
||||||
### License
|
|
||||||
|
|
||||||
This project is open source and available under the MIT License.
|
|
||||||
|
|||||||
@@ -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 🏆**
|
||||||
@@ -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.
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
### **requirements.txt**
|
|
||||||
|
|
||||||
Flask==2.1.2
|
|
||||||
cryptography==3.4.8
|
|
||||||
nginx==1.21.0 # Only needed for Nginx integration, not installed via pip
|
|
||||||
|
After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 1006 KiB After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
@@ -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;
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
});
|
||||||
@@ -1,496 +0,0 @@
|
|||||||
// ===== AES Encryption =====
|
|
||||||
async function encryptAdvanced(message, password) {
|
|
||||||
// Create a random salt for key derivation
|
|
||||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
||||||
|
|
||||||
// Derive a key from the password using PBKDF2 and the salt
|
|
||||||
const key = await deriveKey(password, salt);
|
|
||||||
|
|
||||||
// Create a random initialization vector (IV)
|
|
||||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
||||||
|
|
||||||
// Encode the message as a Uint8Array
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const encodedMessage = encoder.encode(message);
|
|
||||||
|
|
||||||
// Encrypt the message using AES-GCM
|
|
||||||
const encryptedMessage = await crypto.subtle.encrypt(
|
|
||||||
{ name: 'AES-GCM', iv: iv },
|
|
||||||
key,
|
|
||||||
encodedMessage
|
|
||||||
);
|
|
||||||
|
|
||||||
// Combine salt, IV, and encrypted message
|
|
||||||
const encryptedArray = new Uint8Array(salt.length + iv.length + encryptedMessage.byteLength);
|
|
||||||
encryptedArray.set(salt);
|
|
||||||
encryptedArray.set(iv, salt.length);
|
|
||||||
encryptedArray.set(new Uint8Array(encryptedMessage), salt.length + iv.length);
|
|
||||||
|
|
||||||
// Convert the result to base64 to send to the server
|
|
||||||
return btoa(String.fromCharCode.apply(null, encryptedArray));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Derive a key from the password using PBKDF2
|
|
||||||
async function deriveKey(password, salt) {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const passwordBuffer = encoder.encode(password);
|
|
||||||
|
|
||||||
const key = await crypto.subtle.importKey(
|
|
||||||
'raw',
|
|
||||||
passwordBuffer,
|
|
||||||
{ name: 'PBKDF2' },
|
|
||||||
false,
|
|
||||||
['deriveKey']
|
|
||||||
);
|
|
||||||
|
|
||||||
return crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: 'PBKDF2',
|
|
||||||
salt: salt,
|
|
||||||
iterations: 100000,
|
|
||||||
hash: 'SHA-256',
|
|
||||||
},
|
|
||||||
key,
|
|
||||||
{ name: 'AES-GCM', length: 256 },
|
|
||||||
false,
|
|
||||||
['encrypt', 'decrypt']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== AES Decryption =====
|
|
||||||
async function decryptAdvanced(encryptedData, password) {
|
|
||||||
// Decode the base64-encoded encrypted data
|
|
||||||
const encryptedArray = new Uint8Array(atob(encryptedData).split("").map(char => char.charCodeAt(0)));
|
|
||||||
|
|
||||||
// Extract salt, IV, and encrypted message from the encrypted data
|
|
||||||
const salt = encryptedArray.slice(0, 16);
|
|
||||||
const iv = encryptedArray.slice(16, 28);
|
|
||||||
const encryptedMessage = encryptedArray.slice(28);
|
|
||||||
|
|
||||||
// Derive the key from the password and salt
|
|
||||||
const key = await deriveKey(password, salt);
|
|
||||||
|
|
||||||
// Decrypt the message using AES-GCM
|
|
||||||
const decryptedMessage = await crypto.subtle.decrypt(
|
|
||||||
{ name: 'AES-GCM', iv: iv },
|
|
||||||
key,
|
|
||||||
encryptedMessage
|
|
||||||
);
|
|
||||||
|
|
||||||
// Decode the decrypted message to text
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
return decoder.decode(decryptedMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== UI Toggles =====
|
|
||||||
function toggleEncryptionOptions() {
|
|
||||||
const type = document.getElementById("encryption-type").value;
|
|
||||||
const pwdContainer = document.getElementById("password-input");
|
|
||||||
pwdContainer.style.display = (type === 'advanced') ? 'flex' : 'none';
|
|
||||||
if (type === 'basic') removeFile();
|
|
||||||
toggleInputMode();
|
|
||||||
document.getElementById("encrypt-label").textContent =
|
|
||||||
(type === 'basic') ? "Encode" : "Encrypt";
|
|
||||||
document.getElementById("decrypt-label").textContent =
|
|
||||||
(type === 'basic') ? "Decode" : "Decrypt";
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Remove File Button =====
|
|
||||||
function removeFile() {
|
|
||||||
document.getElementById("file-input").value = ""; // Clear the file input
|
|
||||||
document.getElementById("remove-file-btn").style.display = 'none'; // Hide the remove file button
|
|
||||||
toggleInputMode(); // Reapply the input mode logic
|
|
||||||
document.getElementById("file-password-input").style.display = 'none'; // Hide the file password input
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Input vs. File Toggle =====
|
|
||||||
function toggleInputMode() {
|
|
||||||
const textValue = document.getElementById("input-text").value.trim();
|
|
||||||
const fileSelected = document.getElementById("file-input").files.length > 0;
|
|
||||||
const isAdvanced = document.getElementById("encryption-type").value === 'advanced';
|
|
||||||
|
|
||||||
// Show/hide text area based on file selection
|
|
||||||
document.getElementById("text-section").style.display =
|
|
||||||
fileSelected ? 'none' : 'flex';
|
|
||||||
|
|
||||||
// Show/hide file input section when in advanced mode and no text input is given
|
|
||||||
document.getElementById("file-section").style.display =
|
|
||||||
(isAdvanced && !textValue) ? 'flex' : 'none';
|
|
||||||
|
|
||||||
// Show/hide the remove file button
|
|
||||||
document.getElementById("remove-file-btn").style.display =
|
|
||||||
fileSelected ? 'inline-block' : 'none';
|
|
||||||
|
|
||||||
// ALWAYS show the password input in advanced mode
|
|
||||||
if (isAdvanced) {
|
|
||||||
document.getElementById("password-input").style.display = 'flex';
|
|
||||||
} else {
|
|
||||||
document.getElementById("password-input").style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show the dedicated password input for file encryption if a file is selected
|
|
||||||
if (fileSelected) {
|
|
||||||
document.getElementById("file-password-input").style.display = 'flex'; // Show password input for files
|
|
||||||
} else {
|
|
||||||
document.getElementById("file-password-input").style.display = 'none'; // Hide when no file is selected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Validate and Submit Form =====
|
|
||||||
async function handleSubmit(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
// If the encryption type is advanced, ensure password is provided
|
|
||||||
const password = document.getElementById("password").value;
|
|
||||||
const filePassword = document.getElementById("file-password") ? document.getElementById("file-password").value : '';
|
|
||||||
const encryptionType = document.getElementById("encryption-type").value;
|
|
||||||
|
|
||||||
if (encryptionType === 'advanced' && !password && !filePassword) {
|
|
||||||
alert("Password is required for advanced encryption.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare the form data
|
|
||||||
const payload = {
|
|
||||||
"encryption-type": encryptionType,
|
|
||||||
operation: document.querySelector('input[name="operation"]:checked').value,
|
|
||||||
message: document.getElementById("input-text").value,
|
|
||||||
password: password,
|
|
||||||
"file-password": filePassword
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle file upload encryption/decryption
|
|
||||||
const fileInput = document.getElementById("file-input");
|
|
||||||
if (fileInput.files.length > 0) {
|
|
||||||
const op = document.querySelector('input[name="operation"]:checked').value;
|
|
||||||
if (op === 'encrypt') encryptFile();
|
|
||||||
else decryptFile();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle text encryption/decryption
|
|
||||||
try {
|
|
||||||
const resp = await fetch("/", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
});
|
|
||||||
const data = await resp.json();
|
|
||||||
document.getElementById("output-text").value = data.result;
|
|
||||||
} catch (err) {
|
|
||||||
alert("Error processing request: " + err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== File Encryption / Decryption =====
|
|
||||||
function encryptFile() {
|
|
||||||
const f = document.getElementById("file-input");
|
|
||||||
const pwd = document.getElementById("file-password").value;
|
|
||||||
if (!pwd) return alert("Please enter a password!");
|
|
||||||
if (!f.files.length) return alert("Please select a file!");
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = async (e) => {
|
|
||||||
const raw = e.target.result;
|
|
||||||
let encryptedMessage = await encryptAdvanced(raw, pwd);
|
|
||||||
downloadFile(encryptedMessage, f.files[0].name + ".enc");
|
|
||||||
};
|
|
||||||
reader.readAsText(f.files[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function decryptFile() {
|
|
||||||
const f = document.getElementById("file-input");
|
|
||||||
const pwd = document.getElementById("file-password").value;
|
|
||||||
if (!pwd) return alert("Please enter a password!");
|
|
||||||
if (!f.files.length) return alert("Please select a file!");
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = async (e) => {
|
|
||||||
try {
|
|
||||||
const enc = e.target.result;
|
|
||||||
const decryptedMessage = await decryptAdvanced(enc, pwd);
|
|
||||||
downloadFile(decryptedMessage, f.files[0].name.replace(/\.enc$/, ''));
|
|
||||||
} catch {
|
|
||||||
alert("Decryption failed: wrong password or corrupted file.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsText(f.files[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadFile(content, filename) {
|
|
||||||
const blob = new Blob([content], { type: "application/octet-stream" });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement("a");
|
|
||||||
a.href = url;
|
|
||||||
a.download = filename;
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Password Generator =====
|
|
||||||
function generateRandomPassword() {
|
|
||||||
const length = 30;
|
|
||||||
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+[]{}|;:,.<>?/~";
|
|
||||||
let password = "";
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
password += charset.charAt(Math.floor(Math.random() * charset.length));
|
|
||||||
}
|
|
||||||
document.getElementById("password-field").value = password;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Copy to Clipboard =====
|
|
||||||
function copyToClipboard(elementId, toastId) {
|
|
||||||
const copyText = document.getElementById(elementId);
|
|
||||||
copyText.select();
|
|
||||||
copyText.setSelectionRange(0, 99999); // For mobile devices
|
|
||||||
document.execCommand("copy");
|
|
||||||
|
|
||||||
// Show toast notification
|
|
||||||
const toast = document.getElementById(toastId);
|
|
||||||
toast.classList.add("show");
|
|
||||||
setTimeout(() => toast.classList.remove("show"), 2000); // Remove toast after 2 seconds
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Pacman Easter Egg =====
|
|
||||||
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';
|
|
||||||
startPacman();
|
|
||||||
} else if (pacSection.style.display === 'block' && !val.includes('pacman')) {
|
|
||||||
exitGame();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Game Exit & Restart =====
|
|
||||||
function exitGame() {
|
|
||||||
stopPacman();
|
|
||||||
document.getElementById("input-text").value = "";
|
|
||||||
document.getElementById("pacman-section").style.display = 'none';
|
|
||||||
document.getElementById("encoding-section").style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetGame() {
|
|
||||||
stopPacman();
|
|
||||||
startPacman();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Pacman Game Variables & Logic =====
|
|
||||||
let canvas, ctx, pacman, enemy, walls, dots, score;
|
|
||||||
let pacmanSpeed = 40, enemySpeed = 20, cellSize = 40, dotSize = 5;
|
|
||||||
let cols, rows, randSeed, gameInterval;
|
|
||||||
|
|
||||||
function startPacman() {
|
|
||||||
canvas = document.getElementById("pacmanCanvas");
|
|
||||||
ctx = canvas.getContext("2d");
|
|
||||||
cols = Math.floor(canvas.width / cellSize);
|
|
||||||
rows = Math.floor(canvas.height / cellSize);
|
|
||||||
walls = []; dots = []; score = 0;
|
|
||||||
clearInterval(gameInterval);
|
|
||||||
|
|
||||||
randSeed = Array.from(
|
|
||||||
document.getElementById("password-field").value
|
|
||||||
).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);
|
|
||||||
gameInterval = setInterval(gameLoop, 150);
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopPacman() {
|
|
||||||
clearInterval(gameInterval);
|
|
||||||
if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
function spawn() {
|
|
||||||
const opts = [];
|
|
||||||
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)
|
|
||||||
)) {
|
|
||||||
opts.push({ c, r });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const s = opts[Math.floor(rand() * opts.length)];
|
|
||||||
return {
|
|
||||||
x: s.c * cellSize + cellSize/2,
|
|
||||||
y: s.r * cellSize + cellSize/2,
|
|
||||||
size: cellSize/2 - 5,
|
|
||||||
dx: 0,
|
|
||||||
dy: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function rand() {
|
|
||||||
const x = Math.sin(randSeed++) * 10000;
|
|
||||||
return x - Math.floor(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateWalls() {
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function movePacman(e) {
|
|
||||||
if (!["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(e.key)) return;
|
|
||||||
e.preventDefault();
|
|
||||||
if (e.key==="ArrowUp") { pacman.dx=0; pacman.dy=-pacmanSpeed; }
|
|
||||||
if (e.key==="ArrowDown") { pacman.dx=0; pacman.dy=pacmanSpeed; }
|
|
||||||
if (e.key==="ArrowLeft") { pacman.dx=-pacmanSpeed; pacman.dy=0; }
|
|
||||||
if (e.key==="ArrowRight") { pacman.dx=pacmanSpeed; pacman.dy=0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Collision Helper =====
|
|
||||||
function willCollide(x, y, size) {
|
|
||||||
const left = x - size, right = x + size;
|
|
||||||
const top = y - size, bottom = y + size;
|
|
||||||
for (let w of walls) {
|
|
||||||
const wx1 = w.c * cellSize, wy1 = w.r * cellSize;
|
|
||||||
const wx2 = wx1 + cellSize, wy2 = wy1 + cellSize;
|
|
||||||
if (right > wx1 && left < wx2 && bottom > wy1 && top < wy2) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveChar(ch) {
|
|
||||||
const nx = ch.x + ch.dx, ny = ch.y + ch.dy;
|
|
||||||
if (!willCollide(nx, ny, ch.size)) {
|
|
||||||
ch.x = nx; ch.y = ny;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveEnemy() {
|
|
||||||
const options = [];
|
|
||||||
[[enemySpeed,0],[-enemySpeed,0],[0,enemySpeed],[0,-enemySpeed]].forEach(
|
|
||||||
([dx,dy]) => {
|
|
||||||
const nx = enemy.x + dx, ny = enemy.y + dy;
|
|
||||||
if (!willCollide(nx, ny, enemy.size)) options.push({dx,dy});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!options.length) return;
|
|
||||||
let best = options[0];
|
|
||||||
let bestD = Math.abs(enemy.x+best.dx-pacman.x)+Math.abs(enemy.y+best.dy-pacman.y);
|
|
||||||
for (let opt of options) {
|
|
||||||
const d = Math.abs(enemy.x+opt.dx-pacman.x)+Math.abs(enemy.y+opt.dy-pacman.y);
|
|
||||||
if (d < bestD) { best=opt; bestD=d; }
|
|
||||||
}
|
|
||||||
enemy.x += best.dx; enemy.y += best.dy;
|
|
||||||
}
|
|
||||||
|
|
||||||
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*cellSize,w.r*cellSize,cellSize,cellSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawChar(ch,color) {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(ch.x,ch.y,ch.size,0,Math.PI*2);
|
|
||||||
ctx.fillStyle=color; ctx.fill();
|
|
||||||
}
|
|
||||||
|
|
||||||
function eatDots() {
|
|
||||||
dots = dots.filter(d=>{
|
|
||||||
const dx = d.c*cellSize+cellSize/2, dy = d.r*cellSize+cellSize/2;
|
|
||||||
if (Math.abs(pacman.x-dx)<pacman.size && Math.abs(pacman.y-dy)<pacman.size) {
|
|
||||||
score++;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
ctx.fillStyle="white";
|
|
||||||
dots.forEach(d=>{
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(d.c*cellSize+cellSize/2, d.r*cellSize+cellSize/2, dotSize,0,Math.PI*2);
|
|
||||||
ctx.fill();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawScore() {
|
|
||||||
ctx.fillStyle="white";
|
|
||||||
ctx.font="20px Poppins";
|
|
||||||
ctx.fillText("Score: "+score,10,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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Clear All Functionality =====
|
|
||||||
function clearAll() {
|
|
||||||
document.getElementById("input-text").value = "";
|
|
||||||
document.getElementById("output-text").value = "";
|
|
||||||
document.getElementById("file-input").value = "";
|
|
||||||
document.getElementById("password").value = "";
|
|
||||||
document.getElementById("file-password").value = "";
|
|
||||||
|
|
||||||
document.getElementById("pacman-section").style.display = "none";
|
|
||||||
document.getElementById("encoding-section").style.display = "block";
|
|
||||||
|
|
||||||
removeFile();
|
|
||||||
toggleInputMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Initialize =====
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
toggleEncryptionOptions();
|
|
||||||
toggleInputMode();
|
|
||||||
document.getElementById("input-text").addEventListener("input", checkForPacman);
|
|
||||||
});
|
|
||||||
@@ -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() { }
|
||||||
@@ -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>© 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>
|
||||||
@@ -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>© 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>
|
||||||
@@ -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>© 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>
|
||||||
@@ -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>© 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>
|
||||||
@@ -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>© 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>
|
||||||
@@ -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>© 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>
|
||||||
@@ -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>© 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>
|
||||||
@@ -3,109 +3,889 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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>
|
<title>PacCrypt</title>
|
||||||
<!-- Favicon Link -->
|
|
||||||
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}">
|
|
||||||
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet" />
|
<!-- 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 rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
|
||||||
<script defer src="{{ url_for('static', filename='js/script.js') }}"></script>
|
<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>
|
</head>
|
||||||
<body class="dark">
|
<body class="dark">
|
||||||
<header>
|
<!-- Header -->
|
||||||
<h1>PacCrypt</h1>
|
<header class="card logo-header">
|
||||||
<p>Secure Encoding, Encryption and Password Generation</p>
|
<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>
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
<main>
|
<main>
|
||||||
<section id="password-section" class="card">
|
<!-- Password Generator Section -->
|
||||||
<h2>Password Generator</h2>
|
<section id="password-generator-section" class="card form-group">
|
||||||
<div class="form-group">
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px;">
|
||||||
<input
|
<h2 style="margin: 0;">Password Generator</h2>
|
||||||
type="text"
|
<button type="button" id="password-settings-btn" class="settings-button" title="Password Settings">
|
||||||
id="password-field"
|
⚙️
|
||||||
placeholder="Generated password will appear here"
|
</button>
|
||||||
readonly
|
|
||||||
/>
|
|
||||||
<div class="button-group">
|
|
||||||
<button type="button" onclick="generateRandomPassword()">Generate</button>
|
|
||||||
<button type="button" onclick="copyToClipboard('password-field', 'password-toast')">Copy</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="password-toast" class="toast">Copied to Clipboard!</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>
|
</div>
|
||||||
</section>
|
</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;">
|
<section id="pacman-section" class="card" style="display: none;">
|
||||||
<div class="pacman-wrapper">
|
<div class="pacman-wrapper">
|
||||||
<canvas id="pacmanCanvas" width="800" height="600"></canvas>
|
<canvas id="pacmanCanvas" width="800" height="600"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<audio id="chomp-sound" src="{{ url_for('static', filename='audio/chomp.mp3') }}"></audio>
|
<audio id="chomp-sound" src="{{ url_for('static', filename='audio/chomp.mp3') }}"></audio>
|
||||||
<div class="button-group">
|
<div class="button-group" style="margin-top: 6px;">
|
||||||
<button type="button" onclick="resetGame()">Restart Game</button>
|
<button type="button" onclick="resetGame()">Restart Game</button>
|
||||||
<button type="button" onclick="exitGame()">Exit Game</button>
|
<button type="button" onclick="exitGame()">Exit Game</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="encoding-section" class="card">
|
<!-- Encryption/Decryption Section -->
|
||||||
<h2>Text Encoder / Decoder & File Encryption</h2>
|
<section id="encoding-section" class="card form-group">
|
||||||
<form id="main-form" class="form-group" method="POST" onsubmit="handleSubmit(event)">
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px;">
|
||||||
<label for="encryption-type">Select Encryption Type:</label>
|
<h2 style="margin: 0;">Encrypt & Decrypt</h2>
|
||||||
<select id="encryption-type" name="encryption-type" onchange="toggleEncryptionOptions()">
|
<button type="button" id="crypto-settings-btn" class="settings-button" title="Encryption Settings">
|
||||||
<option value="basic">Basic (Less Secure)</option>
|
⚙️
|
||||||
<option value="advanced" selected>Advanced (More Secure)</option>
|
</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>
|
</select>
|
||||||
|
|
||||||
<div id="encryption-options" class="radio-group">
|
|
||||||
<label class="radio-button">
|
|
||||||
<input type="radio" name="operation" value="encrypt" id="encrypt-radio" checked />
|
|
||||||
<span id="encrypt-label">Encrypt</span>
|
|
||||||
</label>
|
|
||||||
<label class="radio-button">
|
|
||||||
<input type="radio" name="operation" value="decrypt" id="decrypt-radio" />
|
|
||||||
<span id="decrypt-label">Decrypt</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</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">
|
<div id="text-section" class="form-group">
|
||||||
<textarea
|
<textarea id="input-text" placeholder="Enter your message..."></textarea>
|
||||||
id="input-text"
|
</div>
|
||||||
name="message"
|
|
||||||
placeholder="Enter text here..."
|
<!-- Password Input -->
|
||||||
oninput="toggleInputMode()"
|
<div id="password-input" class="form-group">
|
||||||
></textarea>
|
<input type="password" id="password" placeholder="Encryption/Decryption Password" />
|
||||||
<div id="password-input">
|
</div>
|
||||||
<input type="password" id="password" name="password" placeholder="Enter Password" />
|
|
||||||
|
<!-- 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="file-section" class="form-group" style="display: none;">
|
<!-- Progress Section -->
|
||||||
<input type="file" id="file-input" onchange="toggleInputMode()" />
|
<div id="bulk-progress-section" style="display: none; margin-top: 15px;">
|
||||||
<button type="button" id="remove-file-btn" onclick="removeFile()">Remove File</button>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div id="file-password-input" style="display: none;">
|
<!-- Results Section -->
|
||||||
<input type="password" id="file-password" name="file-password" placeholder="Enter Password for File" />
|
<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>
|
</div>
|
||||||
|
|
||||||
<button type="button" class="submit-button" onclick="handleSubmit(event)">Submit</button>
|
<!-- 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>
|
</form>
|
||||||
|
|
||||||
<div style="height: 20px;"></div>
|
<!-- Results Section -->
|
||||||
|
<div id="pacshare-results" style="display: none; margin-top: 20px;">
|
||||||
<textarea id="output-text" readonly placeholder="Result will appear here">{{ result }}</textarea>
|
<h3 style="color: #00ff99;">Upload Results</h3>
|
||||||
<div class="button-group">
|
<div id="pacshare-results-list" class="results-list"></div>
|
||||||
<button type="button" onclick="copyToClipboard('output-text', 'output-toast')">Copy Output</button>
|
|
||||||
<button type="button" onclick="clearAll()">Clear All</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="output-toast" class="toast">Copied to Clipboard!</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>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
<footer>
|
<footer>
|
||||||
<p>© 2025 UnNaturalll-Dev. All rights reserved.</p>
|
<p>© 2025 UnNaturalll-Dev. All rights reserved.</p>
|
||||||
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
|
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
|
||||||
<img src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Flogos-world.net%2Fwp-content%2Fuploads%2F2020%2F11%2FGitHub-Logo.png&f=1&nofb=1&ipt=b9d67651e313b2cdbeae8a7ec9320dadb278a21a2e7217810b839c233c04f265"
|
<img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
|
||||||
alt="GitHub Logo" width="100" />
|
|
||||||
</a>
|
</a>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -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>© 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>
|
||||||