Add Two-Factor Authentication (2FA) support and key management features
- Implemented 2FA management in admin panel with enable/disable options. - Added QR code display for 2FA setup and input for TOTP codes in login and pickup forms. - Introduced key management section for generating, loading, and clearing RSA key pairs. - Enhanced file upload and sharing functionality with optional 2FA. - Added buttons for switching between development and production modes in admin panel. - Updated API documentation to reflect new 2FA and key management features.
This commit is contained in:
@@ -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).*
|
||||||
@@ -1,115 +1,186 @@
|
|||||||
# PacCrypt
|
# PacCrypt 🔐
|
||||||
|
|
||||||
**PacCrypt** is a secure, feature-rich web app for encrypting and decrypting text and files — built with Flask, JavaScript, and AES-GCM encryption.
|
**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.
|
||||||
Now with an admin control panel, GitHub updater, and a built-in Pac-Man easter egg! 🕹️
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> This document contains AI generated pieces that have not been reviewed yet.
|
||||||
|
> Next push will contain human oversite on the documentation.
|
||||||
|
|
||||||
|
**🌐 Official Instance**: [paccrypt.unnaturalll.dev](https://paccrypt.unnaturalll.dev)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
- 🔒 Basic and Advanced Encryption for Text & Files
|
### 🔒 **Multi-Algorithm Encryption**
|
||||||
- 📁 Secure File Uploads with Pickup Passwords
|
- **AES-GCM**: Text encryption with authenticated encryption
|
||||||
- 🔑 Random Password Generator
|
- **AES-CBC**: Text and file encryption with HMAC authentication
|
||||||
- 🎮 Hidden Pac-Man Game — type `pacman` to play
|
- **XChaCha20-Poly1305**: Modern stream cipher for text and files
|
||||||
- 🧠 Smart UI: Auto-switches input sections, toggles encryption labels
|
- **RSA Hybrid**: RSA-4096 with AES hybrid encryption for text and files
|
||||||
- 📋 Clipboard Copy Feedback with styled status boxes
|
|
||||||
- 🧾 Admin Panel:
|
### 🌐 **Comprehensive API**
|
||||||
- Site map with live route list
|
- RESTful API endpoints for all encryption operations
|
||||||
- Server restart & GitHub update button
|
- Text and file encryption/decryption
|
||||||
- Secure admin credential management
|
- Key pair generation for RSA hybrid
|
||||||
- Server logs & upload cleanup
|
- PacShare file sharing with secure pickup URLs
|
||||||
- 🧩 System Settings Page for upload config
|
- Full API documentation (see [API.md](API.md))
|
||||||
- 📜 Custom 403, 404, and 500 Error Pages
|
|
||||||
- 🤖 robots.txt and /sitemap for crawlers
|
### 📁 **PacShare - Secure File Sharing**
|
||||||
- 📱 Mobile-Responsive UI
|
- 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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 👨💻 Installation
|
## 🚀 Quick Start
|
||||||
|
|
||||||
### 📋 Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Python 3.7+
|
- **Python 3.8+** (3.10+ recommended)
|
||||||
- Flask 3+
|
- **Git** (for updates and installation)
|
||||||
- Cryptography 42+
|
- **pip** package manager
|
||||||
- Waitress 2.1+
|
|
||||||
- Git (For update feature)
|
|
||||||
- Nginx (Recommended)
|
|
||||||
- Cockpit (Recommended if hosted on **Linux**)
|
|
||||||
|
|
||||||
---
|
### Installation
|
||||||
|
|
||||||
### ⚡ Quick Setup
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/TySP-Dev/PacCrypt.git
|
# Clone the repository
|
||||||
cd paccrypt-webapp-final
|
git clone https://github.com/TySP-Dev/PacCrypt-Webapp.git
|
||||||
|
cd PacCrypt-Webapp
|
||||||
|
|
||||||
|
# Create virtual environment
|
||||||
python -m venv venv
|
python -m venv venv
|
||||||
source venv/bin/activate # or venv\Scripts\activate on Windows
|
|
||||||
pip install -r requirements.txt
|
# Activate virtual environment
|
||||||
|
# On Linux/macOS:
|
||||||
|
source venv/bin/activate
|
||||||
|
# On Windows:
|
||||||
|
venv\Scripts\activate
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pip install -r application_data/requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
Then run:
|
### Running the Application
|
||||||
|
|
||||||
- Development Mode:
|
#### Development Mode
|
||||||
```bash
|
```bash
|
||||||
./start_dev.sh #<-- start_dev.bat (Windows)
|
# Linux/macOS
|
||||||
```
|
python application_data/control_scripts/start_dev.py
|
||||||
|
|
||||||
- Production Mode:
|
# Windows
|
||||||
```bash
|
python application_data\control_scripts\start_dev.py
|
||||||
./start_prod.sh #<-- start_prod.bat (Windows)
|
```
|
||||||
```
|
|
||||||
|
|
||||||
Visit [http://127.0.0.1:5000](http://127.0.0.1:5000) or [http://localhost:5000](http://localhost:5000) - *If* you **are** on the host system
|
#### Production Mode
|
||||||
Visit http://hosts_private_ip - *If* you are **not** on the host system
|
```bash
|
||||||
|
# Linux/macOS
|
||||||
|
python application_data/control_scripts/start_prod.py
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
python application_data\control_scripts\start_prod.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Access the Application
|
||||||
|
|
||||||
|
- **Local access**: http://127.0.0.1:5000
|
||||||
|
- **Network access**: http://YOUR_IP_ADDRESS:5000
|
||||||
|
- **Admin setup**: http://127.0.0.1:5000/admin-setup (first-time only)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🧭 Navigation & Usage
|
## 📖 Usage Guide
|
||||||
|
|
||||||
### 🔑 Generate Passwords
|
### 🔐 Text Encryption/Decryption
|
||||||
|
|
||||||
- Click Generate
|
1. **Select Algorithm**: Choose from AES-GCM, AES-CBC, XChaCha20, or RSA Hybrid
|
||||||
- Then hit `📋 Copy Password`
|
2. **Enter Text**: Type or paste your message
|
||||||
- **Note:** This is also used as a seed generator for the Pac-Man *like* game
|
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
|
||||||
|
|
||||||
### 🔐 Encrypt & Decrypt
|
### 📁 File Operations
|
||||||
|
|
||||||
- Choose between Basic Cipher or Advanced AES
|
1. **Upload File**: Select file using the file picker
|
||||||
- Select mode using toggle (Encrypt/Decrypt)
|
2. **Choose Algorithm**: Pick AES-CBC, XChaCha20, or RSA Hybrid (AES-GCM not supported for files)
|
||||||
- Type your message or upload a file
|
3. **Set Password**: Enter encryption password
|
||||||
- Enter password (Advanced AES)
|
4. **Process**: File will be encrypted/decrypted and downloaded automatically
|
||||||
- Hit Execute
|
|
||||||
- Then hit `📋 Copy Output`
|
|
||||||
|
|
||||||
### 📤 Share Files
|
### 📤 PacShare - Secure File Sharing
|
||||||
|
|
||||||
- Upload a file with two passwords:
|
1. **Upload File**: Select file to share
|
||||||
- Encryption password
|
2. **Set Passwords**:
|
||||||
- Pickup password
|
- **Encryption Password**: Encrypts the file content
|
||||||
- Get a shareable URL and click `📋 Copy Link`
|
- **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 *like* Game
|
### 🎮 Hidden Pac-Man Game
|
||||||
|
|
||||||
- Type `pacman` in the input box
|
- Type `pacman` in any text input
|
||||||
- Game appears with `Restart` and `Exit` buttons
|
- Use arrow keys or swipe gestures to play
|
||||||
- Arrow key and Swipe controls 🕹️
|
- Authentic retro gaming experience with sound effects
|
||||||
- Game restarts and a new seed is generated once all dots are eaten
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🛠️ Admin Panel
|
## 🛠️ Admin Panel
|
||||||
|
|
||||||
Visit `/adminpage` after setting up credentials at `/admin-setup`.
|
Access the admin panel at `/adminpage` after initial setup at `/admin-setup`.
|
||||||
|
|
||||||
Features:
|
### 🔑 Setup Process
|
||||||
- 🔄 Restart server
|
1. Visit `/admin-setup` on first run
|
||||||
- 🔃 Update from GitHub (git pull)
|
2. Create admin username and password
|
||||||
- 🧽 Clear uploads
|
3. Optionally enable 2FA for enhanced security
|
||||||
- 🔐 Change admin password
|
4. Login at `/admin-login`
|
||||||
- 📝 View logs
|
|
||||||
- ⚙️ Adjust upload settings
|
### 🎛️ 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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -221,49 +292,115 @@ server {
|
|||||||
```
|
```
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 📋 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
|
## 🗂️ Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
PacCrypt/
|
PacCrypt-Webapp/
|
||||||
├── app.py
|
├── app.py # Main Flask application
|
||||||
├── requirements.txt
|
├── README.md # This file
|
||||||
├── README.md
|
├── ROADMAP.md # Development roadmap
|
||||||
├── templates/
|
├── API.md # API documentation
|
||||||
│ ├── index.html
|
├── application_data/ # Application configuration
|
||||||
│ ├── 404.html
|
│ ├── control_scripts/ # Server management scripts
|
||||||
│ └── 403.html
|
│ │ ├── start_dev.py # Development mode starter
|
||||||
│ └── 500.html
|
│ │ ├── start_prod.py # Production mode starter
|
||||||
│ └── admin.html
|
│ │ ├── restart_dev.py # Development restart
|
||||||
│ └── admin_login.html
|
│ │ ├── restart_prod.py # Production restart
|
||||||
│ └── admin_settings.html
|
│ │ └── stop.py # Server stop script
|
||||||
│ └── admin_setup.html
|
│ ├── requirements.txt # Python dependencies
|
||||||
│ └── pickup.html
|
│ ├── settings.json # Application settings
|
||||||
├── static/
|
│ ├── admin_creds.json # Encrypted admin credentials
|
||||||
│ ├── css/
|
│ ├── admin_key.key # Admin encryption key
|
||||||
│ │ └── styles.css
|
│ └── admin_logs.enc # Encrypted audit logs
|
||||||
│ ├── js/
|
├── paccrypt_algos/ # Encryption modules
|
||||||
│ │ └── ui.js
|
│ ├── __init__.py # Package initialization
|
||||||
│ │ └── pacman.js
|
│ ├── aes_cbc.py # AES-CBC implementation
|
||||||
│ │ └── main.js
|
│ ├── aes_gcm.py # AES-GCM implementation
|
||||||
│ │ └── fileops.js
|
│ ├── xchacha.py # XChaCha20-Poly1305
|
||||||
│ │ └── encryption.js
|
│ └── rsa_hybrid.py # RSA hybrid encryption
|
||||||
│ ├── img/
|
├── pacshare/ # File upload storage
|
||||||
│ │ └── PacCrypt.png
|
│ ├── *.encrypted # Encrypted uploaded files
|
||||||
│ │ └── Github_logo.png
|
│ └── *.json # File metadata
|
||||||
│ │ └── sitemap.png
|
├── templates/ # HTML templates
|
||||||
│ ├── fonts/
|
│ ├── index.html # Main interface
|
||||||
│ │ └── PressStart2P-Regular.ttf
|
│ ├── pickup.html # File pickup page
|
||||||
│ └── audio/
|
│ ├── admin*.html # Admin panel pages
|
||||||
│ └── chomp.mp3
|
│ └── error pages (403,404,500)
|
||||||
├── start_dev.bat
|
└── static/ # Static assets
|
||||||
├── start_prod.bat
|
├── css/styles.css # Application styling
|
||||||
├── start_dev.sh
|
├── js/ # JavaScript modules
|
||||||
├── start_prod.sh
|
├── img/ # Images and icons
|
||||||
|
├── fonts/ # Custom fonts
|
||||||
|
└── audio/ # Sound effects
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🔒 Security Considerations
|
||||||
|
|
||||||
|
### ⚠️ Important Security Notes
|
||||||
|
|
||||||
|
- **Password Strength**: Use strong, unique passwords for all operations
|
||||||
|
- **2FA Recommended**: Enable 2FA for admin accounts and sensitive file shares
|
||||||
|
- **HTTPS Required**: Always use HTTPS in production environments
|
||||||
|
- **Regular Updates**: Keep dependencies updated for security patches
|
||||||
|
- **Backup Strategy**: Implement regular backups of encrypted data
|
||||||
|
|
||||||
|
### 🛡️ Encryption Details
|
||||||
|
|
||||||
|
- **AES-256**: Industry standard symmetric encryption
|
||||||
|
- **RSA-4096**: Strong asymmetric encryption for key exchange
|
||||||
|
- **PBKDF2**: 200,000 iterations for key derivation
|
||||||
|
- **Authenticated Encryption**: GCM and Poly1305 modes prevent tampering
|
||||||
|
- **Secure Random**: Cryptographically secure random number generation
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
We welcome contributions! Please see our [ROADMAP.md](ROADMAP.md) for planned features and development priorities.
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Make your changes
|
||||||
|
4. Add tests if applicable
|
||||||
|
5. Submit a pull request
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
- **Documentation**: See [API.md](API.md) for API details
|
||||||
|
- **Issues**: Report bugs via GitHub Issues
|
||||||
|
- **Discussions**: Use GitHub Discussions for questions
|
||||||
|
- **Official Instance**: [paccrypt.unnaturalll.dev](https://paccrypt.unnaturalll.dev)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 📄 License
|
## 📄 License
|
||||||
|
|
||||||
MIT © [TySP-Dev](https://github.com/TySP-Dev)
|
MIT © [TySP-Dev](https://github.com/TySP-Dev)
|
||||||
|
|
||||||
|
**🔐 Secure by design. Simple by choice. Powerful by nature.**
|
||||||
|
|
||||||
|
|||||||
+136
-127
@@ -5,7 +5,9 @@
|
|||||||
|
|
||||||
### Phase 0
|
### Phase 0
|
||||||
|
|
||||||
- [x] Remove docker files (Dropping official docker support)
|
- [x] ~~Remove docker files (Dropping official docker support)~~
|
||||||
|
|
||||||
|
- [ ] Readd docker support
|
||||||
|
|
||||||
- [x] Update README.md to be current.
|
- [x] Update README.md to be current.
|
||||||
|
|
||||||
@@ -17,34 +19,34 @@
|
|||||||
|
|
||||||
- [x] Create /paccrypt_algos/ folder
|
- [x] Create /paccrypt_algos/ folder
|
||||||
|
|
||||||
- [x] Builder better start, stop and restart scripts both prod and dev (Linux Only)
|
- [x] Builder better start, stop and restart scripts both prod and dev (Cross-platform: Windows & Linux)
|
||||||
|
|
||||||
- [ ] Add a button in the admin panel to switch to and from prod and dev modes - **Saving for UI Revamp**
|
- [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
|
### Phase 1: app.py - Modular Python Web App
|
||||||
|
|
||||||
##### app.py Responsibilities
|
##### app.py Responsibilities
|
||||||
|
|
||||||
- [ ] Flask app + routing
|
- [x] Flask app + routing
|
||||||
|
|
||||||
- [ ] Handle:
|
- [x] Handle:
|
||||||
- /encrypt
|
- [x] /encrypt (via API endpoints)
|
||||||
- /decrypt
|
- [x] /decrypt (via API endpoints)
|
||||||
- /pickup/<file_id>
|
- [x] /pickup/<file_id>
|
||||||
|
|
||||||
- [ ] Receive:
|
- [x] Receive:
|
||||||
- File or text
|
- [x] File or text
|
||||||
- pickup_password (required)
|
- [x] pickup_password (required)
|
||||||
- encryption_password (required)
|
- [x] encryption_password (required)
|
||||||
- encryption_mode
|
- [x] encryption_mode (algorithm selection implemented)
|
||||||
|
|
||||||
- [ ] Encrypt metadata using pickup password
|
- [x] Encrypt metadata using pickup password
|
||||||
|
|
||||||
- [ ] Encrypt file using encryption password
|
- [x] Encrypt file using encryption password
|
||||||
|
|
||||||
- [ ] Dynamically load correct engine via decrypted metadata
|
- [x] Dynamically load correct engine via decrypted metadata
|
||||||
|
|
||||||
- [ ] Save .enc + .meta, return pickup link
|
- [x] Save .encrypted + .json metadata, return pickup link
|
||||||
|
|
||||||
- [ ] Update PacMan like mini game logic revamp "(LOW PRIORITY)"
|
- [ ] Update PacMan like mini game logic revamp "(LOW PRIORITY)"
|
||||||
|
|
||||||
@@ -56,7 +58,7 @@
|
|||||||
|
|
||||||
- [x] Create folder + interface
|
- [x] Create folder + interface
|
||||||
|
|
||||||
- [ ] Remove basic cypher
|
- [x] Remove basic cypher
|
||||||
|
|
||||||
Implement engines:
|
Implement engines:
|
||||||
|
|
||||||
@@ -68,17 +70,18 @@ Implement engines:
|
|||||||
|
|
||||||
- [x] rsa_hybrid.py
|
- [x] rsa_hybrid.py
|
||||||
|
|
||||||
- [x] PQCrypt_hybrid.py (Testing)
|
- [x] ~~PQCrypt_hybrid.py (Testing)~~ **REMOVED: Post-quantum crypto removed for simplicity**
|
||||||
|
|
||||||
- [x] Each must expose:
|
- [x] Each must expose:
|
||||||
|
|
||||||
```
|
```
|
||||||
def encrypt\_text(text, key, metadata): ...
|
def encrypt_text(text, key): ...
|
||||||
def decrypt\_text(ciphertext, key, metadata): ...
|
def decrypt_text(ciphertext, key): ...
|
||||||
def encrypt\_file(in\_path, out\_path, key, metadata): ...
|
def encrypt_file(in_path, out_path, key): ...
|
||||||
def decrypt\_file(in\_path, out\_path, key, metadata): ...
|
def decrypt_file(in_path, out_path, key): ...
|
||||||
def get\_name(): return "AES-GCM"
|
def generate_key_pair(): ... (for RSA hybrid)
|
||||||
```
|
```
|
||||||
|
**COMPLETED: All modules implemented with correct API**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -86,21 +89,21 @@ def get\_name(): return "AES-GCM"
|
|||||||
|
|
||||||
/encrypt Route Flow
|
/encrypt Route Flow
|
||||||
|
|
||||||
- [ ] JS submits (PacShare "Form"):
|
- [x] JS submits (PacShare "Form"):
|
||||||
- File
|
- [x] File
|
||||||
- pickup_password (for metadata)
|
- [x] pickup_password (for metadata)
|
||||||
- encryption_password (for file)
|
- [x] encryption_password (for file)
|
||||||
- encryption_mode
|
- [x] encryption_mode
|
||||||
- 2FA token code / Yubi/Passkey set up
|
- [x] 2FA TOTP setup (Yubi/Passkey not implemented)
|
||||||
|
|
||||||
- [ ] Python logic:
|
- [x] Python logic:
|
||||||
- Encrypt file using selected algo + encryption_password
|
- [x] Encrypt file using selected algo + encryption_password
|
||||||
- Generate metadata dict:
|
- [x] Generate metadata dict:
|
||||||
- filename, enc_mode, pickup_hash, timestamp, optional 2FA
|
- [x] filename, enc_mode, pickup_hash, timestamp, optional 2FA
|
||||||
- Encrypt metadata using AES-GCM derived from pickup_password
|
- [x] Encrypt metadata using AES-GCM derived from pickup_password
|
||||||
- Save .paccrypt and .meta files
|
- [x] Save .{algorithm}.encrypted and .json files
|
||||||
- Generate random file_id
|
- [x] Generate random file_id
|
||||||
- Return /pickup/<file_id> link
|
- [x] Return /pickup/<file_id> link
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> Both passwords are required. One reveals the mode + metadata, the other decrypts the file.
|
> Both passwords are required. One reveals the mode + metadata, the other decrypts the file.
|
||||||
@@ -109,15 +112,15 @@ def get\_name(): return "AES-GCM"
|
|||||||
|
|
||||||
##### /pickup/<file_id> Route Flow
|
##### /pickup/<file_id> Route Flow
|
||||||
|
|
||||||
- [ ] Prompt for pickup_password
|
- [x] Prompt for pickup_password
|
||||||
|
|
||||||
- [ ] Decrypt .meta and validate hash
|
- [x] Decrypt .json metadata and validate hash
|
||||||
|
|
||||||
- [ ] Show original filename, prompt for encryption_password
|
- [x] Show original filename, prompt for encryption_password
|
||||||
|
|
||||||
- [ ] Load correct module, decrypt file
|
- [x] Load correct module, decrypt file
|
||||||
|
|
||||||
- [ ] Offer file download
|
- [x] Offer file download
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -125,16 +128,18 @@ def get\_name(): return "AES-GCM"
|
|||||||
|
|
||||||
```
|
```
|
||||||
"filename": "report.pdf",
|
"filename": "report.pdf",
|
||||||
"enc\_mode": "aes\_gcm",
|
"algorithm": "aes_cbc",
|
||||||
"pickup\_hash": "<argon2>",
|
"pickup_password": "<sha256>",
|
||||||
"created\_at": "2025-08-05T18:00Z",
|
"created_at": "2025-08-05T18:00Z",
|
||||||
"2fa\_seed": "base32string", // optional
|
"require_2fa": true, // optional
|
||||||
"yubi\_token\_hash": "sha256", // optional
|
"totp_secret": "base32string", // optional
|
||||||
|
"service_name": "PacCrypt File: report.pdf..." // optional
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> Stored as .meta
|
> Stored as .json
|
||||||
> Encrypted with AES-GCM using key from pickup\_password
|
> Encrypted with AES-GCM using key derived from pickup_password
|
||||||
|
> **COMPLETED: Metadata encryption implemented**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -143,15 +148,19 @@ def get\_name(): return "AES-GCM"
|
|||||||
##### Endpoint Description
|
##### Endpoint Description
|
||||||
|
|
||||||
```
|
```
|
||||||
POST /api/encrypt Local-only file/text encryption (returns file/meta)
|
✅ GET /api/algorithms List available encryption algorithms
|
||||||
POST /api/ps-send Upload + encrypt + return pickup link (JSON)
|
✅ POST /api/generate-keypair Generate RSA key pairs
|
||||||
POST /api/ps-pickup Provide pickup ID + passwords, return decrypted file
|
✅ POST /api/encrypt File/text encryption (returns encrypted data)
|
||||||
POST /api/decrypt Decrypt local .enc + .meta bundle
|
✅ POST /api/decrypt File/text decryption
|
||||||
GET /api/version Return current version tag
|
✅ 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]
|
> [!NOTE]
|
||||||
> These endpoints must receive both passwords. Encryption password is never saved.
|
> **COMPLETED: Core API endpoints implemented**
|
||||||
|
> Pickup is handled via web interface at /pickup/<file_id>
|
||||||
|
> Encryption password is never saved server-side
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -260,94 +269,94 @@ Optional (Send + Pickup)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### PacShare File Format
|
### PacShare File Format ✅ **COMPLETED**
|
||||||
|
|
||||||
```
|
```
|
||||||
pacshare/
|
pacshare/
|
||||||
├── <file_id>pdf/jpeg/etc.paccrypt # Encrypted binary file
|
├── <file_id>.<algorithm>.encrypted # Encrypted binary file
|
||||||
└── <file_id>meta.paccrypt # Encrypted metadata
|
└── <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
|
### Development Order
|
||||||
|
|
||||||
0. - [ ] Phase 0 Tasks
|
0. - [x] **Phase 0 Tasks** ✅
|
||||||
1. - [ ] paccrypt_algos/ + aes_gcm.py
|
1. - [x] **paccrypt_algos/ + aes_gcm.py** ✅
|
||||||
2. - [ ] app.py routes: /encrypt, /pickup/<id>
|
2. - [x] **app.py routes: /encrypt, /pickup/<id>** ✅
|
||||||
3. - [ ] Add /decrypt route
|
3. - [x] **Add /decrypt route** ✅
|
||||||
4. - [ ] Build metadata encryption helpers
|
4. - [x] **Build metadata encryption helpers** ✅
|
||||||
5. - [ ] Finish other engine modules
|
5. - [x] **Finish other engine modules** ✅
|
||||||
6. - [ ] Build /api/* equivalents
|
6. - [x] **Build /api/* equivalents** ✅
|
||||||
7. - [ ] Update README.md with all changed to the webapp.
|
7. - [x] **Update README.md with all changes to the webapp** ✅
|
||||||
8. - [ ] Create a new installation guide.
|
8. - [x] **Create a new installation guide** ✅ (Included in README.md)
|
||||||
9. - [ ] Build CLI
|
9. - [ ] Build CLI ⏳ *Next Priority*
|
||||||
10. - [ ] Test CLI with --pickup + --share
|
10. - [ ] Test CLI with --pickup + --share
|
||||||
12. - [ ] Build GUI app on Linux
|
12. - [ ] Build GUI app on Linux
|
||||||
13. - [ ] Test GUI app on Linux
|
13. - [ ] Test GUI app on Linux
|
||||||
14. - [ ] Build GUI app on Android
|
14. - [ ] Build GUI app on Android
|
||||||
15. - [ ] Test GUI app on Android
|
15. - [ ] Test GUI app on Android
|
||||||
16. - [ ] Finilize all releases and push to main.
|
16. - [ ] Finalize all releases and push to main
|
||||||
17. - [ ] Create Wiki
|
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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Draft tree for webapp
|
### Current Webapp Structure ✅ **COMPLETED**
|
||||||
|
|
||||||
```
|
```
|
||||||
paccrypt-webapp/
|
PacCrypt-Webapp/
|
||||||
├── static/
|
├── app.py # Main Flask application ✅
|
||||||
│ ├── audio/
|
├── README.md # Updated documentation ✅
|
||||||
│ │ └── chomp.mp3
|
├── ROADMAP.md # This file ✅
|
||||||
│ ├── css/
|
├── API.md # API documentation ✅ *NEW*
|
||||||
│ │ └── styles.css
|
├── LICENSE # MIT License ✅
|
||||||
│ ├── fonts/
|
├── application_data/ ✅ # Application configuration
|
||||||
│ │ └── PressStart2P-Regular.ttf
|
│ ├── control_scripts/ ✅ # Server management scripts
|
||||||
│ ├── img/
|
│ │ ├── start_dev.py ✅ # Development mode starter
|
||||||
│ │ ├── Github_logo.png
|
│ │ ├── start_prod.py ✅ # Production mode starter
|
||||||
│ │ ├── PacCrypt.png
|
│ │ ├── restart_dev.py ✅ # Development restart
|
||||||
│ │ ├── PacCrypt_W-Background.png
|
│ │ ├── restart_prod.py ✅ # Production restart
|
||||||
│ │ ├── PacCrypt_W-Backgroud_Name.png
|
│ │ └── stop.py ✅ # Server stop script
|
||||||
│ │ ├── PacCrypt_W-Name.png
|
│ ├── requirements.txt ✅ # Python dependencies
|
||||||
│ │ └── sitemap.png <-- **Change img**
|
│ ├── settings.json ✅ # Application settings
|
||||||
│ └── js/ <-- **Pending changes**
|
│ ├── admin_creds.json ✅ # Encrypted admin credentials
|
||||||
│ ├── encryption.js
|
│ ├── admin_key.key ✅ # Admin encryption key
|
||||||
│ ├── fileops.js
|
│ └── admin_logs.enc ✅ # Encrypted audit logs
|
||||||
│ ├── main.js
|
├── paccrypt_algos/ ✅ # Encryption modules
|
||||||
│ ├── pacman.js
|
│ ├── __init__.py ✅ # Package initialization
|
||||||
│ └── ui.js
|
│ ├── aes_cbc.py ✅ # AES-CBC implementation
|
||||||
├── templates/
|
│ ├── aes_gcm.py ✅ # AES-GCM implementation
|
||||||
│ ├── 403.html
|
│ ├── xchacha.py ✅ # XChaCha20-Poly1305
|
||||||
│ ├── 404.html
|
│ └── rsa_hybrid.py ✅ # RSA hybrid encryption
|
||||||
│ ├── 500.html
|
├── pacshare/ ✅ # File upload storage
|
||||||
│ ├── admin.html
|
│ ├── *.{algorithm}.encrypted ✅ # Encrypted uploaded files
|
||||||
│ ├── admin_login.html
|
│ └── *.json ✅ # File metadata
|
||||||
│ ├── admin_settings.html
|
├── templates/ ✅ # HTML templates
|
||||||
│ ├── admin_setup.html
|
│ ├── index.html ✅ # Main interface
|
||||||
│ ├── index.html
|
│ ├── pickup.html ✅ # File pickup page
|
||||||
│ └── pickup.html
|
│ ├── admin*.html ✅ # Admin panel pages
|
||||||
├── application_data/ <-- *New*
|
│ └── error pages (403,404,500) ✅
|
||||||
│ ├── scripts/ <-- *New*
|
└── static/ ✅ # Static assets
|
||||||
│ │ ├── start_dev <-- *Moved*
|
├── css/styles.css ✅ # Application styling
|
||||||
│ │ ├── start_prod <-- *Moved*
|
├── js/ ✅ # JavaScript modules
|
||||||
│ │ ├── restart_dev <-- *New*
|
├── img/ ✅ # Images and icons
|
||||||
│ │ ├── restart_prod <-- *New*
|
├── fonts/ ✅ # Custom fonts
|
||||||
│ │ └── stop <-- *New*
|
└── audio/ ✅ # Sound effects
|
||||||
│ ├── settings.json <-- *Moved*
|
|
||||||
│ ├── requirements.txt <-- *Moved*
|
|
||||||
│ ├── admin_cred <-- **Generated once admin is setup** / *Moved*
|
|
||||||
│ └── admin_hash <-- **Generated once admin is setup** / *Moved*
|
|
||||||
├── paccrypt_algos/ <-- *New*
|
|
||||||
│ ├── aes_gcm.py <-- *New*
|
|
||||||
│ ├── aes_cbc.py <-- *New*
|
|
||||||
│ ├── xchacha.py <-- *New*
|
|
||||||
│ ├── rsa_hybrid.py <-- *New*
|
|
||||||
│ └── kyber_hybrid.py <-- *New*
|
|
||||||
├── pacshare/ <-- **Generated at time of first PacShare upload, location customizable** / *New*
|
|
||||||
│ ├── <file_id>pdf/jpeg/etc.paccrypt <-- **Encrypted binary file** / *Moved*
|
|
||||||
│ └── <file_id>meta.paccrypt <-- **Encrypted metadata** / *Moved*
|
|
||||||
├── README.md <-- **Needs Updated**
|
|
||||||
├── ROADMAP.md
|
|
||||||
├── LICENSE <-- *New*
|
|
||||||
└── app.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**🏆 PROJECT STRUCTURE FULLY IMPLEMENTED 🏆**
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import signal
|
|||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
import psutil
|
import psutil
|
||||||
|
import platform
|
||||||
|
|
||||||
APP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../app.py"))
|
APP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../app.py"))
|
||||||
|
|
||||||
@@ -16,21 +17,37 @@ def log(msg):
|
|||||||
def start_dev():
|
def start_dev():
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["PRODUCTION"] = "false"
|
env["PRODUCTION"] = "false"
|
||||||
return subprocess.Popen(
|
|
||||||
["python3", APP_PATH],
|
if platform.system() == "Windows":
|
||||||
env=env,
|
return subprocess.Popen(
|
||||||
preexec_fn=os.setsid,
|
["python", APP_PATH],
|
||||||
stdout=sys.stdout,
|
env=env,
|
||||||
stderr=sys.stderr
|
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):
|
def stop_by_port(port=5000):
|
||||||
for proc in psutil.process_iter(["pid", "name"]):
|
for proc in psutil.process_iter(["pid", "name"]):
|
||||||
try:
|
try:
|
||||||
for conn in proc.connections(kind="inet"):
|
for conn in proc.net_connections(kind="inet"):
|
||||||
if conn.laddr.port == port:
|
if conn.laddr.port == port:
|
||||||
log(f"[*] Killing process {proc.pid} using port {port}")
|
log(f"[*] Killing process {proc.pid} using port {port}")
|
||||||
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
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
|
return
|
||||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||||
continue
|
continue
|
||||||
@@ -39,8 +56,17 @@ def stop_by_port(port=5000):
|
|||||||
def main():
|
def main():
|
||||||
log("[*] Restarting PacCrypt in DEVELOPMENT mode...")
|
log("[*] Restarting PacCrypt in DEVELOPMENT mode...")
|
||||||
stop_by_port()
|
stop_by_port()
|
||||||
time.sleep(1)
|
time.sleep(2)
|
||||||
start_dev()
|
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__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -4,27 +4,44 @@ import signal
|
|||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
import psutil
|
import psutil
|
||||||
|
import platform
|
||||||
|
|
||||||
APP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../app.py"))
|
APP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../app.py"))
|
||||||
|
|
||||||
def start_prod():
|
def start_prod():
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["PRODUCTION"] = "true"
|
env["PRODUCTION"] = "true"
|
||||||
return subprocess.Popen(
|
|
||||||
["python3", APP_PATH],
|
if platform.system() == "Windows":
|
||||||
env=env,
|
return subprocess.Popen(
|
||||||
preexec_fn=os.setsid,
|
["python", APP_PATH],
|
||||||
stdout=subprocess.DEVNULL,
|
env=env,
|
||||||
stderr=subprocess.DEVNULL
|
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):
|
def stop_by_port(port=5000):
|
||||||
for proc in psutil.process_iter(["pid", "name"]):
|
for proc in psutil.process_iter(["pid", "name"]):
|
||||||
try:
|
try:
|
||||||
for conn in proc.connections(kind="inet"):
|
for conn in proc.net_connections(kind="inet"):
|
||||||
if conn.laddr.port == port:
|
if conn.laddr.port == port:
|
||||||
print(f"[*] Killing process {proc.pid} using port {port}")
|
print(f"[*] Killing process {proc.pid} using port {port}")
|
||||||
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
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
|
return
|
||||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||||
continue
|
continue
|
||||||
@@ -33,8 +50,17 @@ def stop_by_port(port=5000):
|
|||||||
def main():
|
def main():
|
||||||
print("[*] Restarting PacCrypt in PRODUCTION mode with Waitress...")
|
print("[*] Restarting PacCrypt in PRODUCTION mode with Waitress...")
|
||||||
stop_by_port()
|
stop_by_port()
|
||||||
time.sleep(1)
|
time.sleep(2)
|
||||||
start_prod()
|
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__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
|
import platform
|
||||||
|
|
||||||
APP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../app.py"))
|
APP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../app.py"))
|
||||||
|
|
||||||
@@ -14,13 +15,22 @@ def log(msg):
|
|||||||
def start_dev():
|
def start_dev():
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["PRODUCTION"] = "false"
|
env["PRODUCTION"] = "false"
|
||||||
return subprocess.Popen(
|
|
||||||
["python3", APP_PATH],
|
if platform.system() == "Windows":
|
||||||
env=env,
|
return subprocess.Popen(
|
||||||
preexec_fn=os.setsid,
|
["python", APP_PATH],
|
||||||
stdout=sys.stdout,
|
env=env,
|
||||||
stderr=sys.stderr
|
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():
|
def main():
|
||||||
log("[*] Starting PacCrypt in DEVELOPMENT mode...")
|
log("[*] Starting PacCrypt in DEVELOPMENT mode...")
|
||||||
|
|||||||
@@ -2,19 +2,29 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
|
import platform
|
||||||
|
|
||||||
APP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../app.py"))
|
APP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../app.py"))
|
||||||
|
|
||||||
def start_prod():
|
def start_prod():
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["PRODUCTION"] = "true"
|
env["PRODUCTION"] = "true"
|
||||||
return subprocess.Popen(
|
|
||||||
["python3", APP_PATH],
|
if platform.system() == "Windows":
|
||||||
env=env,
|
return subprocess.Popen(
|
||||||
preexec_fn=os.setsid,
|
["python", APP_PATH],
|
||||||
stdout=subprocess.DEVNULL,
|
env=env,
|
||||||
stderr=subprocess.DEVNULL
|
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():
|
def main():
|
||||||
print("[*] Starting PacCrypt in PRODUCTION mode with Waitress...")
|
print("[*] Starting PacCrypt in PRODUCTION mode with Waitress...")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import psutil
|
import psutil
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
|
import platform
|
||||||
|
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|
||||||
@@ -11,10 +12,17 @@ def log(msg):
|
|||||||
def stop_by_port(port=5000):
|
def stop_by_port(port=5000):
|
||||||
for proc in psutil.process_iter(["pid", "name"]):
|
for proc in psutil.process_iter(["pid", "name"]):
|
||||||
try:
|
try:
|
||||||
for conn in proc.connections(kind="inet"):
|
for conn in proc.net_connections(kind="inet"):
|
||||||
if conn.laddr.port == port:
|
if conn.laddr.port == port:
|
||||||
log(f"[*] Killing process {proc.pid} using port {port}")
|
log(f"[*] Killing process {proc.pid} using port {port}")
|
||||||
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
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
|
return
|
||||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
import os
|
|
||||||
import base64
|
|
||||||
import json
|
|
||||||
import importlib
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from pqcrypto.kem.ml_kem_768 import generate_keypair, encrypt as kem_encapsulate, decrypt as kem_decapsulate
|
|
||||||
|
|
||||||
# === Allow Hybrid Selector ===
|
|
||||||
PARENT_DIR = Path(__file__).resolve().parent.parent
|
|
||||||
if str(PARENT_DIR) not in sys.path:
|
|
||||||
sys.path.append(str(PARENT_DIR))
|
|
||||||
|
|
||||||
# === Constants ===
|
|
||||||
KEM_ALG = "ML-KEM-768"
|
|
||||||
AES_KEY_SIZE = 32 # 256-bit
|
|
||||||
SYMMETRIC_DEFAULT = "aes_gcm"
|
|
||||||
|
|
||||||
# === 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"))
|
|
||||||
|
|
||||||
# === 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: bytes, engine_name: str = SYMMETRIC_DEFAULT) -> str:
|
|
||||||
engine = load_engine(engine_name)
|
|
||||||
kem_ciphertext, shared_secret = kem_encapsulate(public_key)
|
|
||||||
aes_key = shared_secret[:AES_KEY_SIZE]
|
|
||||||
|
|
||||||
encrypted_data = engine.encrypt_text(plaintext, aes_key.hex())
|
|
||||||
header = json.dumps({"alg": engine_name}).encode()
|
|
||||||
payload = len(kem_ciphertext).to_bytes(2, 'big') + kem_ciphertext + header + b'\0' + encrypted_data.encode()
|
|
||||||
return b64encode(payload)
|
|
||||||
|
|
||||||
# === Decrypt Text ===
|
|
||||||
def decrypt_text(encrypted_b64: str, private_key: bytes) -> str:
|
|
||||||
raw = b64decode(encrypted_b64)
|
|
||||||
kem_len = int.from_bytes(raw[:2], 'big')
|
|
||||||
kem_ct = raw[2:2 + kem_len]
|
|
||||||
rest = raw[2 + kem_len:]
|
|
||||||
header_data, encrypted_data = rest.split(b'\0', 1)
|
|
||||||
engine_name = json.loads(header_data.decode()).get("alg")
|
|
||||||
|
|
||||||
shared_secret = kem_decapsulate(private_key, kem_ct)
|
|
||||||
aes_key = shared_secret[:AES_KEY_SIZE]
|
|
||||||
|
|
||||||
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: bytes, engine_name: str = SYMMETRIC_DEFAULT):
|
|
||||||
engine = load_engine(engine_name)
|
|
||||||
kem_ciphertext, shared_secret = kem_encapsulate(public_key)
|
|
||||||
aes_key = shared_secret[:AES_KEY_SIZE]
|
|
||||||
|
|
||||||
with open(in_path, 'rb') as f:
|
|
||||||
plaintext = f.read()
|
|
||||||
|
|
||||||
encrypted = engine.encrypt_file_bytes(plaintext, aes_key.hex())
|
|
||||||
header = json.dumps({"alg": engine_name}).encode()
|
|
||||||
payload = len(kem_ciphertext).to_bytes(2, 'big') + kem_ciphertext + header + b'\0' + encrypted
|
|
||||||
|
|
||||||
with open(out_path, 'wb') as f:
|
|
||||||
f.write(payload)
|
|
||||||
|
|
||||||
# === Decrypt File ===
|
|
||||||
def decrypt_file(in_path: str, out_path: str, private_key: bytes):
|
|
||||||
with open(in_path, 'rb') as f:
|
|
||||||
raw = f.read()
|
|
||||||
|
|
||||||
kem_len = int.from_bytes(raw[:2], 'big')
|
|
||||||
kem_ct = raw[2:2 + kem_len]
|
|
||||||
rest = raw[2 + kem_len:]
|
|
||||||
header_data, encrypted_data = rest.split(b'\0', 1)
|
|
||||||
engine_name = json.loads(header_data.decode()).get("alg")
|
|
||||||
|
|
||||||
shared_secret = kem_decapsulate(private_key, kem_ct)
|
|
||||||
aes_key = shared_secret[:AES_KEY_SIZE]
|
|
||||||
|
|
||||||
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 "PQCrypto Hybrid"
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from aes_gcm import encrypt_text as aesgcm_encrypt_text, decrypt_text as aesgcm_decrypt_text, \
|
|
||||||
encrypt_file as aesgcm_encrypt_file, decrypt_file as aesgcm_decrypt_file
|
|
||||||
from aes_cbc import encrypt_text as aescbc_encrypt_text, decrypt_text as aescbc_decrypt_text, \
|
|
||||||
encrypt_file as aescbc_encrypt_file, decrypt_file as aescbc_decrypt_file
|
|
||||||
from xchacha import encrypt_text as xchacha_encrypt_text, decrypt_text as xchacha_decrypt_text, \
|
|
||||||
encrypt_file as xchacha_encrypt_file, decrypt_file as xchacha_decrypt_file
|
|
||||||
import rsa_hybrid
|
|
||||||
import pqcrypto_hybrid
|
|
||||||
|
|
||||||
def load_text(path, binary=False):
|
|
||||||
with open(path, 'rb' if binary else 'r') as f:
|
|
||||||
return f.read()
|
|
||||||
|
|
||||||
def save_text(path, data, binary=False):
|
|
||||||
with open(path, 'wb' if binary else 'w') as f:
|
|
||||||
f.write(data)
|
|
||||||
|
|
||||||
def select_symmetric():
|
|
||||||
print("\n🔀 Select symmetric engine:")
|
|
||||||
choices = ["aes_gcm", "aes_cbc", "xchacha"]
|
|
||||||
for i, c in enumerate(choices):
|
|
||||||
print(f" [{i}] {c}")
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
choice = int(input("Choice: "))
|
|
||||||
return choices[choice]
|
|
||||||
except (ValueError, IndexError):
|
|
||||||
print("❌ Invalid choice. Try again.")
|
|
||||||
|
|
||||||
def hybrid_cli(name, module, key_ext, symmetric_engine, is_pem=False):
|
|
||||||
while True:
|
|
||||||
print(f"\n=== PacCrypt {name} Debug Mode ({symmetric_engine.upper()}) ===")
|
|
||||||
print("Choose:")
|
|
||||||
print(" [g] Generate keypair")
|
|
||||||
print(" [e] Encrypt text")
|
|
||||||
print(" [d] Decrypt text")
|
|
||||||
print(" [ef] Encrypt file")
|
|
||||||
print(" [df] Decrypt file")
|
|
||||||
print(" [b] Back to engine menu")
|
|
||||||
print(" [q] Quit script")
|
|
||||||
|
|
||||||
mode = input("\nMode (g/e/d/ef/df/b/q): ").strip().lower()
|
|
||||||
|
|
||||||
if mode == 'q':
|
|
||||||
return 'quit'
|
|
||||||
elif mode == 'b':
|
|
||||||
return 'back'
|
|
||||||
|
|
||||||
try:
|
|
||||||
if mode == 'g':
|
|
||||||
priv, pub = module.generate_key_pair() if hasattr(module, 'generate_key_pair') else module.generate_keypair()
|
|
||||||
save_text(f"{name}_public.{key_ext}", pub, binary=True)
|
|
||||||
save_text(f"{name}_private.{key_ext}", priv, binary=True)
|
|
||||||
print(f"✅ Keypair saved to {name}_public.{key_ext} / {name}_private.{key_ext}")
|
|
||||||
|
|
||||||
elif mode == 'e':
|
|
||||||
plaintext = input("Text to encrypt: ")
|
|
||||||
pub_path = input("Public key path: ").strip()
|
|
||||||
pub = load_text(pub_path, binary=not is_pem)
|
|
||||||
result = module.encrypt_text(plaintext, pub, symmetric_engine)
|
|
||||||
print(f"\n🔐 Encrypted Base64:\n{result}")
|
|
||||||
|
|
||||||
elif mode == 'd':
|
|
||||||
encrypted = input("Encrypted Base64 input: ")
|
|
||||||
priv_path = input("Private key path: ").strip()
|
|
||||||
priv = load_text(priv_path, binary=not is_pem)
|
|
||||||
result = module.decrypt_text(encrypted, priv)
|
|
||||||
print(f"\n📝 Decrypted:\n{result}")
|
|
||||||
|
|
||||||
elif mode == 'ef':
|
|
||||||
in_path = input("Input file path: ").strip()
|
|
||||||
out_path = in_path + ".paccrypt"
|
|
||||||
pub_path = input("Public key path: ").strip()
|
|
||||||
pub = load_text(pub_path, binary=not is_pem)
|
|
||||||
module.encrypt_file(in_path, out_path, pub, symmetric_engine)
|
|
||||||
print(f"✅ File encrypted and saved to: {out_path}")
|
|
||||||
|
|
||||||
elif mode == 'df':
|
|
||||||
in_path = input("Encrypted file path: ").strip()
|
|
||||||
out_path = in_path.replace(".paccrypt", "")
|
|
||||||
priv_path = input("Private key path: ").strip()
|
|
||||||
priv = load_text(priv_path, binary=not is_pem)
|
|
||||||
module.decrypt_file(in_path, out_path, priv)
|
|
||||||
print(f"✅ File decrypted and saved to: {out_path}")
|
|
||||||
else:
|
|
||||||
print("❌ Invalid option.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Error: {e}")
|
|
||||||
|
|
||||||
def simple_cli(name, encrypt_text, decrypt_text, encrypt_file, decrypt_file):
|
|
||||||
while True:
|
|
||||||
print(f"\n=== PacCrypt {name} Debug Mode ===")
|
|
||||||
print("Choose:")
|
|
||||||
print(" [e] Encrypt text")
|
|
||||||
print(" [d] Decrypt text")
|
|
||||||
print(" [ef] Encrypt file")
|
|
||||||
print(" [df] Decrypt file")
|
|
||||||
print(" [b] Back to engine menu")
|
|
||||||
print(" [q] Quit script")
|
|
||||||
|
|
||||||
mode = input("\nMode (e/d/ef/df/b/q): ").strip().lower()
|
|
||||||
|
|
||||||
if mode == 'q':
|
|
||||||
return 'quit'
|
|
||||||
elif mode == 'b':
|
|
||||||
return 'back'
|
|
||||||
|
|
||||||
try:
|
|
||||||
if mode == 'e':
|
|
||||||
plaintext = input("Plaintext to encrypt: ")
|
|
||||||
password = input("Password: ")
|
|
||||||
result = encrypt_text(plaintext, password)
|
|
||||||
print(f"\n🔐 Encrypted Base64:\n{result}")
|
|
||||||
|
|
||||||
elif mode == 'd':
|
|
||||||
encrypted = input("Encrypted Base64 input: ")
|
|
||||||
password = input("Password: ")
|
|
||||||
result = decrypt_text(encrypted, password)
|
|
||||||
print(f"\n📝 Decrypted:\n{result}")
|
|
||||||
|
|
||||||
elif mode == 'ef':
|
|
||||||
in_path = input("Input file path: ").strip()
|
|
||||||
out_path = in_path + ".paccrypt"
|
|
||||||
password = input("Password: ")
|
|
||||||
encrypt_file(in_path, out_path, password)
|
|
||||||
print(f"✅ File encrypted and saved to: {out_path}")
|
|
||||||
|
|
||||||
elif mode == 'df':
|
|
||||||
in_path = input("Encrypted file path: ").strip()
|
|
||||||
out_path = in_path.replace(".paccrypt", "")
|
|
||||||
password = input("Password: ")
|
|
||||||
decrypt_file(in_path, out_path, password)
|
|
||||||
print(f"✅ File decrypted and saved to: {out_path}")
|
|
||||||
else:
|
|
||||||
print("❌ Invalid option.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Error: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
# === PacCrypt CLI Entry ===
|
|
||||||
while True:
|
|
||||||
print("\n=== PacCrypt Hardcoded CLI ===")
|
|
||||||
print("Pick an engine:")
|
|
||||||
print(" [0] AES-GCM")
|
|
||||||
print(" [1] AES-CBC")
|
|
||||||
print(" [2] XChaCha20-Poly1305")
|
|
||||||
print(" [3] RSA Hybrid (with selectable symmetric)")
|
|
||||||
print(" [4] PQCrypto Hybrid (with selectable symmetric)")
|
|
||||||
print(" [q] Quit")
|
|
||||||
|
|
||||||
choice = input("Choice: ").strip().lower()
|
|
||||||
if choice == 'q':
|
|
||||||
print("👋 Bye.")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
symmetric_engine = None
|
|
||||||
if choice in ['3', '4']:
|
|
||||||
symmetric_engine = select_symmetric()
|
|
||||||
|
|
||||||
engines = {
|
|
||||||
'0': lambda: simple_cli("AES-GCM", aesgcm_encrypt_text, aesgcm_decrypt_text, aesgcm_encrypt_file, aesgcm_decrypt_file),
|
|
||||||
'1': lambda: simple_cli("AES-CBC", aescbc_encrypt_text, aescbc_decrypt_text, aescbc_encrypt_file, aescbc_decrypt_file),
|
|
||||||
'2': lambda: simple_cli("XChaCha20-Poly1305", xchacha_encrypt_text, xchacha_decrypt_text, xchacha_encrypt_file, xchacha_decrypt_file),
|
|
||||||
'3': lambda: hybrid_cli("RSA_Hybrid", rsa_hybrid, "pem", symmetric_engine, is_pem=True),
|
|
||||||
'4': lambda: hybrid_cli("PQCrypto_Hybrid", pqcrypto_hybrid, "bin", symmetric_engine),
|
|
||||||
}
|
|
||||||
|
|
||||||
if choice in engines:
|
|
||||||
result = engines[choice]()
|
|
||||||
if result == 'quit':
|
|
||||||
print("👋 Quitting.")
|
|
||||||
sys.exit(0)
|
|
||||||
# If 'back', just loops again to show engine menu
|
|
||||||
else:
|
|
||||||
print("❌ Invalid choice.")
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
/**
|
|
||||||
* Encryption module.
|
|
||||||
* Handles cryptographic operations using Web Crypto API.
|
|
||||||
* Implements AES-GCM encryption with PBKDF2 key derivation.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// ===== Constants =====
|
|
||||||
const SALT_LENGTH = 16;
|
|
||||||
const IV_LENGTH = 12;
|
|
||||||
const PBKDF2_ITERATIONS = 200_000;
|
|
||||||
const KEY_LENGTH = 256;
|
|
||||||
|
|
||||||
// ===== Binary-safe Base64 Helpers =====
|
|
||||||
function base64Encode(buffer) {
|
|
||||||
const binary = Array.from(new Uint8Array(buffer))
|
|
||||||
.map(byte => String.fromCharCode(byte))
|
|
||||||
.join('');
|
|
||||||
return btoa(binary);
|
|
||||||
}
|
|
||||||
|
|
||||||
function base64Decode(b64str) {
|
|
||||||
const binary = atob(b64str);
|
|
||||||
const bytes = new Uint8Array(binary.length);
|
|
||||||
for (let i = 0; i < binary.length; i++) {
|
|
||||||
bytes[i] = binary.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Key Derivation =====
|
|
||||||
/**
|
|
||||||
* Derives an AES-GCM key from a password using PBKDF2.
|
|
||||||
* @param {string} password - User-supplied password.
|
|
||||||
* @param {Uint8Array} salt - Randomly generated salt.
|
|
||||||
* @returns {Promise<CryptoKey>} - Derived cryptographic key.
|
|
||||||
*/
|
|
||||||
export async function deriveKey(password, salt) {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const keyMaterial = await crypto.subtle.importKey(
|
|
||||||
'raw',
|
|
||||||
encoder.encode(password),
|
|
||||||
{ name: 'PBKDF2' },
|
|
||||||
false,
|
|
||||||
['deriveKey']
|
|
||||||
);
|
|
||||||
|
|
||||||
return crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: 'PBKDF2',
|
|
||||||
salt,
|
|
||||||
iterations: PBKDF2_ITERATIONS,
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
keyMaterial,
|
|
||||||
{ name: 'AES-GCM', length: KEY_LENGTH },
|
|
||||||
false,
|
|
||||||
['encrypt', 'decrypt']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Encryption =====
|
|
||||||
/**
|
|
||||||
* Encrypts a message using AES-GCM with a derived key.
|
|
||||||
* @param {string} message - Plaintext message to encrypt.
|
|
||||||
* @param {string} password - User password for key derivation.
|
|
||||||
* @returns {Promise<string>} - Base64-encoded encrypted string.
|
|
||||||
*/
|
|
||||||
export async function encryptAdvanced(message, password) {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
|
||||||
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
|
||||||
const key = await deriveKey(password, salt);
|
|
||||||
const encoded = encoder.encode(message);
|
|
||||||
|
|
||||||
const ciphertext = await crypto.subtle.encrypt(
|
|
||||||
{ name: 'AES-GCM', iv },
|
|
||||||
key,
|
|
||||||
encoded
|
|
||||||
);
|
|
||||||
|
|
||||||
const output = new Uint8Array(salt.length + iv.length + ciphertext.byteLength);
|
|
||||||
output.set(salt);
|
|
||||||
output.set(iv, salt.length);
|
|
||||||
output.set(new Uint8Array(ciphertext), salt.length + iv.length);
|
|
||||||
|
|
||||||
return base64Encode(output.buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Decryption =====
|
|
||||||
/**
|
|
||||||
* Decrypts an AES-GCM encrypted string.
|
|
||||||
* @param {string} encryptedData - Base64-encoded ciphertext.
|
|
||||||
* @param {string} password - Password used to derive the decryption key.
|
|
||||||
* @returns {Promise<string>} - Decrypted plaintext.
|
|
||||||
*/
|
|
||||||
export async function decryptAdvanced(encryptedData, password) {
|
|
||||||
const encrypted = base64Decode(encryptedData);
|
|
||||||
|
|
||||||
const salt = encrypted.slice(0, SALT_LENGTH);
|
|
||||||
const iv = encrypted.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
|
|
||||||
const ciphertext = encrypted.slice(SALT_LENGTH + IV_LENGTH);
|
|
||||||
const key = await deriveKey(password, salt);
|
|
||||||
|
|
||||||
const decrypted = await crypto.subtle.decrypt(
|
|
||||||
{ name: 'AES-GCM', iv },
|
|
||||||
key,
|
|
||||||
ciphertext
|
|
||||||
);
|
|
||||||
|
|
||||||
return new TextDecoder().decode(decrypted);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Module Initialization =====
|
|
||||||
/**
|
|
||||||
* Initializes the encryption module and logs its status.
|
|
||||||
*/
|
|
||||||
export function setupEncryption() {
|
|
||||||
console.log('[Encryption] Module loaded');
|
|
||||||
}
|
|
||||||
+48
-120
@@ -1,39 +1,38 @@
|
|||||||
import { deriveKey } from "./encryption.js"; // assuming shared deriveKey()
|
/**
|
||||||
|
* File operations using the new Python backend APIs
|
||||||
const SALT_LENGTH = 16;
|
*/
|
||||||
const IV_LENGTH = 12;
|
|
||||||
const KEY_LENGTH = 256;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypts a full file and downloads the encrypted version.
|
* Encrypts a full file using the backend API and downloads the encrypted version.
|
||||||
*/
|
*/
|
||||||
export async function encryptFile(fileInput, password) {
|
export async function encryptFile(fileInput, password) {
|
||||||
const file = fileInput.files[0];
|
const file = fileInput.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
|
const algorithm = document.getElementById("algorithm")?.value || "aes_cbc";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
const formData = new FormData();
|
||||||
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
formData.append('file', file);
|
||||||
const key = await deriveKey(password, salt);
|
formData.append('enc_password', password);
|
||||||
const fileBuffer = new Uint8Array(await file.arrayBuffer());
|
formData.append('algorithm', algorithm);
|
||||||
|
|
||||||
const ciphertext = await crypto.subtle.encrypt(
|
const response = await fetch('/api/encrypt', {
|
||||||
{ name: "AES-GCM", iv },
|
method: 'POST',
|
||||||
key,
|
body: formData
|
||||||
fileBuffer
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const ctBytes = new Uint8Array(ciphertext);
|
if (!response.ok) {
|
||||||
const result = new Uint8Array(salt.length + iv.length + ctBytes.length);
|
const errorData = await response.json();
|
||||||
result.set(salt);
|
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||||
result.set(iv, salt.length);
|
}
|
||||||
result.set(ctBytes, salt.length + iv.length);
|
|
||||||
|
|
||||||
const blob = new Blob([result], { type: "application/octet-stream" });
|
// Download the encrypted file
|
||||||
|
const blob = await response.blob();
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = file.name + ".encrypted";
|
a.download = `${file.name}.${algorithm}.encrypted`;
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
@@ -43,28 +42,43 @@ export async function encryptFile(fileInput, password) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts a file using the backend API and downloads the decrypted version.
|
||||||
|
*/
|
||||||
export async function decryptFile(fileInput, password) {
|
export async function decryptFile(fileInput, password) {
|
||||||
const file = fileInput.files[0];
|
const file = fileInput.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = new Uint8Array(await file.arrayBuffer());
|
const formData = new FormData();
|
||||||
const salt = data.slice(0, SALT_LENGTH);
|
formData.append('file', file);
|
||||||
const iv = data.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
|
formData.append('enc_password', password);
|
||||||
const ciphertext = data.slice(SALT_LENGTH + IV_LENGTH);
|
|
||||||
const key = await deriveKey(password, salt);
|
|
||||||
|
|
||||||
const decrypted = await crypto.subtle.decrypt(
|
const response = await fetch('/api/decrypt', {
|
||||||
{ name: "AES-GCM", iv },
|
method: 'POST',
|
||||||
key,
|
body: formData
|
||||||
ciphertext
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const blob = new Blob([decrypted], { type: "application/octet-stream" });
|
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 url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = file.name.replace(".encrypted", "");
|
|
||||||
|
// 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);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
@@ -74,89 +88,3 @@ export async function decryptFile(fileInput, password) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== File Processing =====
|
|
||||||
async function processFile(file, password, isEncrypt) {
|
|
||||||
const chunks = [];
|
|
||||||
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
|
|
||||||
let processedChunks = 0;
|
|
||||||
|
|
||||||
for (let start = 0; start < file.size; start += CHUNK_SIZE) {
|
|
||||||
const chunk = file.slice(start, start + CHUNK_SIZE);
|
|
||||||
const arrayBuffer = await chunk.arrayBuffer();
|
|
||||||
const uint8Array = new Uint8Array(arrayBuffer);
|
|
||||||
|
|
||||||
const processedChunk = await processChunk(uint8Array, password, isEncrypt);
|
|
||||||
chunks.push(processedChunk);
|
|
||||||
|
|
||||||
processedChunks++;
|
|
||||||
updateProgress(processedChunks, totalChunks);
|
|
||||||
}
|
|
||||||
|
|
||||||
return chunks;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processChunk(data, password, isEncrypt) {
|
|
||||||
const payload = {
|
|
||||||
"encryption-type": "advanced",
|
|
||||||
operation: isEncrypt ? "encrypt" : "decrypt",
|
|
||||||
message: Array.from(data).join(','),
|
|
||||||
password: password
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch("/", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
return new Uint8Array(result.result.split(',').map(Number));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== File Download =====
|
|
||||||
function downloadEncryptedFile(chunks, originalName) {
|
|
||||||
const blob = new Blob(chunks, { type: 'application/octet-stream' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = originalName + '.encrypted';
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadDecryptedFile(chunks, originalName) {
|
|
||||||
const blob = new Blob(chunks, { type: 'application/octet-stream' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = originalName.replace('.encrypted', '');
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Progress Tracking =====
|
|
||||||
function updateProgress(processed, total) {
|
|
||||||
const progressBar = document.getElementById("file-progress");
|
|
||||||
const progressText = document.getElementById("file-progress-text");
|
|
||||||
|
|
||||||
if (progressBar && progressText) {
|
|
||||||
const percent = Math.round((processed / total) * 100);
|
|
||||||
progressBar.style.width = percent + "%";
|
|
||||||
progressText.textContent = `Processing: ${percent}%`;
|
|
||||||
|
|
||||||
if (processed === total) {
|
|
||||||
setTimeout(() => {
|
|
||||||
progressBar.style.width = "0%";
|
|
||||||
progressText.textContent = "";
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+296
-47
@@ -14,9 +14,9 @@ export function setupUI() {
|
|||||||
initializeEventListeners();
|
initializeEventListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeEventListeners() {
|
async function initializeEventListeners() {
|
||||||
const elements = {
|
const elements = {
|
||||||
encryptionType: document.getElementById("encryption-type"),
|
algorithm: document.getElementById("algorithm"),
|
||||||
inputText: document.getElementById("input-text"),
|
inputText: document.getElementById("input-text"),
|
||||||
form: document.getElementById("crypto-form"),
|
form: document.getElementById("crypto-form"),
|
||||||
removeFileBtn: document.getElementById("remove-file-btn"),
|
removeFileBtn: document.getElementById("remove-file-btn"),
|
||||||
@@ -26,22 +26,30 @@ function initializeEventListeners() {
|
|||||||
copyOutputBtn: document.getElementById("copy-output-btn"),
|
copyOutputBtn: document.getElementById("copy-output-btn"),
|
||||||
toggleSwitch: document.getElementById("operation-toggle"),
|
toggleSwitch: document.getElementById("operation-toggle"),
|
||||||
copyShareBtn: document.getElementById("copy-share-btn"),
|
copyShareBtn: document.getElementById("copy-share-btn"),
|
||||||
shareLink: document.getElementById("share-link")
|
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)) {
|
if (validateElements(elements)) {
|
||||||
setupElementListeners(elements);
|
setupElementListeners(elements);
|
||||||
}
|
}
|
||||||
|
await loadAvailableAlgorithms();
|
||||||
|
// Initialize algorithm options on page load after algorithms are loaded
|
||||||
|
toggleAlgorithmOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateElements(elements) {
|
function validateElements(elements) {
|
||||||
return elements.encryptionType && elements.inputText && elements.form &&
|
return elements.algorithm && elements.inputText && elements.form &&
|
||||||
elements.removeFileBtn && elements.clearAllBtn && elements.generateBtn &&
|
elements.removeFileBtn && elements.clearAllBtn && elements.generateBtn &&
|
||||||
elements.copyPasswordBtn && elements.toggleSwitch;
|
elements.copyPasswordBtn && elements.toggleSwitch;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupElementListeners(elements) {
|
function setupElementListeners(elements) {
|
||||||
elements.encryptionType.addEventListener("change", toggleEncryptionOptions);
|
elements.algorithm?.addEventListener("change", toggleAlgorithmOptions);
|
||||||
elements.inputText.addEventListener("input", handleInputChange);
|
elements.inputText.addEventListener("input", handleInputChange);
|
||||||
elements.form.addEventListener("submit", handleSubmit);
|
elements.form.addEventListener("submit", handleSubmit);
|
||||||
elements.removeFileBtn.addEventListener("click", removeFile);
|
elements.removeFileBtn.addEventListener("click", removeFile);
|
||||||
@@ -53,6 +61,13 @@ function setupElementListeners(elements) {
|
|||||||
console.log("Mode:", elements.toggleSwitch.checked ? "Decrypt" : "Encrypt");
|
console.log("Mode:", elements.toggleSwitch.checked ? "Decrypt" : "Encrypt");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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");
|
const fileInput = document.getElementById("file-input");
|
||||||
if (fileInput) {
|
if (fileInput) {
|
||||||
fileInput.addEventListener("change", () => {
|
fileInput.addEventListener("change", () => {
|
||||||
@@ -87,40 +102,10 @@ function setupShareLinkListeners(elements) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleEncryptionOptions() {
|
|
||||||
const type = document.getElementById("encryption-type").value.trim().toLowerCase();
|
|
||||||
const passwordInputWrapper = document.getElementById("password-input");
|
|
||||||
const fileSection = document.querySelector("#encoding-section #file-section");
|
|
||||||
const isAdvanced = type.includes("advanced");
|
|
||||||
|
|
||||||
if (passwordInputWrapper) {
|
|
||||||
passwordInputWrapper.classList.toggle("hidden", !isAdvanced);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileSection) {
|
|
||||||
fileSection.classList.toggle("hidden", !isAdvanced);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateToggleLabels();
|
|
||||||
toggleInputMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateToggleLabels() {
|
|
||||||
const type = document.getElementById("encryption-type")?.value;
|
|
||||||
const leftLabel = document.getElementById("toggle-left-label");
|
|
||||||
const rightLabel = document.getElementById("toggle-right-label");
|
|
||||||
|
|
||||||
if (!type || !leftLabel || !rightLabel) return;
|
|
||||||
|
|
||||||
const isAdvanced = type.toLowerCase().includes("advanced");
|
|
||||||
leftLabel.textContent = isAdvanced ? "Encrypt" : "Encode";
|
|
||||||
rightLabel.textContent = isAdvanced ? "Decrypt" : "Decode";
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleInputMode() {
|
function toggleInputMode() {
|
||||||
const fileInput = document.getElementById("file-input");
|
const fileInput = document.getElementById("file-input");
|
||||||
const textValue = document.getElementById("input-text")?.value.trim();
|
const textValue = document.getElementById("input-text")?.value.trim();
|
||||||
const isAdvanced = document.getElementById("encryption-type")?.value === "advanced";
|
|
||||||
|
|
||||||
const textSection = document.getElementById("text-section");
|
const textSection = document.getElementById("text-section");
|
||||||
const fileSection = document.getElementById("file-section");
|
const fileSection = document.getElementById("file-section");
|
||||||
@@ -131,23 +116,39 @@ function toggleInputMode() {
|
|||||||
const fileSelected = fileInput.files.length > 0;
|
const fileSelected = fileInput.files.length > 0;
|
||||||
|
|
||||||
textSection.style.display = fileSelected ? "none" : "flex";
|
textSection.style.display = fileSelected ? "none" : "flex";
|
||||||
fileSection.style.display = (isAdvanced && !textValue) ? "flex" : "none";
|
fileSection.style.display = !textValue ? "flex" : "none";
|
||||||
removeBtn.style.display = fileSelected ? "inline-block" : "none";
|
removeBtn.style.display = fileSelected ? "inline-block" : "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit(event) {
|
async function handleSubmit(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const encryptionType = document.getElementById("encryption-type")?.value;
|
const algorithm = document.getElementById("algorithm")?.value;
|
||||||
const password = document.getElementById("password")?.value;
|
const password = document.getElementById("password")?.value;
|
||||||
const fileInput = document.getElementById("file-input");
|
const fileInput = document.getElementById("file-input");
|
||||||
const isDecrypt = document.getElementById("operation-toggle").checked;
|
const isDecrypt = document.getElementById("operation-toggle").checked;
|
||||||
const operation = isDecrypt ? "decrypt" : "encrypt";
|
const operation = isDecrypt ? "decrypt" : "encrypt";
|
||||||
|
|
||||||
if (!encryptionType || !fileInput) return;
|
if (!algorithm || !fileInput) return;
|
||||||
|
|
||||||
if (encryptionType === "advanced" && !password) {
|
// Check requirements based on algorithm
|
||||||
return alert("Password is required for advanced encryption.");
|
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) {
|
if (fileInput.files.length > 0) {
|
||||||
@@ -156,19 +157,39 @@ async function handleSubmit(event) {
|
|||||||
: decryptFile(fileInput, password);
|
: decryptFile(fileInput, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
await handleTextOperation(encryptionType, operation, password);
|
await handleTextOperation(operation, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleTextOperation(encryptionType, operation, password) {
|
async function handleTextOperation(operation, password) {
|
||||||
|
const algorithm = document.getElementById("algorithm")?.value || "aes_gcm";
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
"encryption-type": encryptionType,
|
|
||||||
operation: operation,
|
|
||||||
message: document.getElementById("input-text")?.value,
|
message: document.getElementById("input-text")?.value,
|
||||||
password: password
|
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 {
|
try {
|
||||||
const response = await fetch("/", {
|
const endpoint = operation === "encrypt" ? "/api/encrypt" : "/api/decrypt";
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
@@ -178,7 +199,11 @@ async function handleTextOperation(encryptionType, operation, password) {
|
|||||||
|
|
||||||
const outputField = document.getElementById("output-text");
|
const outputField = document.getElementById("output-text");
|
||||||
if (outputField) {
|
if (outputField) {
|
||||||
outputField.value = data.result || "[Error] No response received.";
|
if (data.error) {
|
||||||
|
outputField.value = `[Error] ${data.error}`;
|
||||||
|
} else {
|
||||||
|
outputField.value = data.result || "[Error] No response received.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("Error processing request: " + err.message);
|
alert("Error processing request: " + err.message);
|
||||||
@@ -328,5 +353,229 @@ function showCopyFeedback(feedbackEl) {
|
|||||||
}, 3000);
|
}, 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 startPacman() { }
|
||||||
function exitGame() { }
|
function exitGame() { }
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
<form action="{{ url_for('admin_settings') }}" method="GET" style="display: inline;">
|
<form action="{{ url_for('admin_settings') }}" method="GET" style="display: inline;">
|
||||||
<button type="submit">Settings</button>
|
<button type="submit">Settings</button>
|
||||||
</form>
|
</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="resetAdmin()" class="danger-button">Reset Admin</button>
|
||||||
<button onclick="clearUploads()" class="danger-button">Clear PacShare</button>
|
<button onclick="clearUploads()" class="danger-button">Clear PacShare</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,6 +87,58 @@
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</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 -->
|
<!-- Server Status Section -->
|
||||||
<section id="server-status-section" class="card form-group">
|
<section id="server-status-section" class="card form-group">
|
||||||
<h2>Server Status</h2>
|
<h2>Server Status</h2>
|
||||||
@@ -239,6 +293,58 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function showFeedback(message) {
|
||||||
const feedback = document.getElementById('admin-feedback');
|
const feedback = document.getElementById('admin-feedback');
|
||||||
feedback.textContent = message;
|
feedback.textContent = message;
|
||||||
@@ -252,6 +358,19 @@
|
|||||||
}, 300);
|
}, 300);
|
||||||
}, 3000);
|
}, 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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -44,6 +44,15 @@
|
|||||||
<form method="POST">
|
<form method="POST">
|
||||||
<input type="text" name="username" placeholder="Username" required />
|
<input type="text" name="username" placeholder="Username" required />
|
||||||
<input type="password" name="password" placeholder="Password" 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">
|
<div class="button-group mt-3">
|
||||||
<button type="submit">Log In</button>
|
<button type="submit">Log In</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -45,6 +45,15 @@
|
|||||||
<form method="POST">
|
<form method="POST">
|
||||||
<input type="text" name="username" placeholder="Username" required />
|
<input type="text" name="username" placeholder="Username" required />
|
||||||
<input type="password" name="password" placeholder="Password" 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">
|
<div class="button-group mt-3">
|
||||||
<button type="submit">Set Credentials</button>
|
<button type="submit">Set Credentials</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+364
-7
@@ -43,6 +43,55 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- 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 -->
|
<!-- 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">
|
||||||
@@ -59,15 +108,34 @@
|
|||||||
<section id="encoding-section" class="card form-group">
|
<section id="encoding-section" class="card form-group">
|
||||||
<h2>Encrypt & Decrypt</h2>
|
<h2>Encrypt & Decrypt</h2>
|
||||||
<form id="crypto-form" class="form-group">
|
<form id="crypto-form" class="form-group">
|
||||||
<!-- Encryption Type Selection -->
|
<!-- Algorithm Selection -->
|
||||||
<div class="form-group">
|
<div class="form-group" id="algorithm-selection">
|
||||||
<label for="encryption-type">Encryption Type:</label>
|
<label for="algorithm">Encryption Algorithm:</label>
|
||||||
<select id="encryption-type">
|
<select id="algorithm">
|
||||||
<option value="basic">Basic Cipher</option>
|
<!-- Options populated dynamically by JavaScript -->
|
||||||
<option value="advanced" selected>Advanced AES</option>
|
|
||||||
</select>
|
</select>
|
||||||
</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 -->
|
<!-- Operation Toggle -->
|
||||||
<div class="toggle-container">
|
<div class="toggle-container">
|
||||||
<span class="toggle-label">Encrypt</span>
|
<span class="toggle-label">Encrypt</span>
|
||||||
@@ -90,7 +158,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- File Input Section -->
|
<!-- File Input Section -->
|
||||||
<div id="file-section" class="form-group" style="display: none;">
|
<div id="file-section" class="form-group">
|
||||||
<input type="file" id="file-input" />
|
<input type="file" id="file-input" />
|
||||||
<button type="button" id="remove-file-btn">Remove File</button>
|
<button type="button" id="remove-file-btn">Remove File</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -144,10 +212,49 @@
|
|||||||
<button type="button" id="copy-share-btn">Copy Link</button>
|
<button type="button" id="copy-share-btn">Copy Link</button>
|
||||||
<div id="shared-link-feedback" class="copy-feedback">Link copied to clipboard!</div>
|
<div id="shared-link-feedback" class="copy-feedback">Link copied to clipboard!</div>
|
||||||
</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="closeTwoFactorSetup()">I've Saved the 2FA Information</button>
|
||||||
|
</div>
|
||||||
<form method="POST" enctype="multipart/form-data" class="form-group" id="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>
|
||||||
<input type="file" name="file" id="upload-file" required />
|
<input type="file" name="file" id="upload-file" required />
|
||||||
<input type="password" name="enc_password" placeholder="Encryption/Decryption Password" required />
|
<input type="password" name="enc_password" placeholder="Encryption/Decryption Password" required />
|
||||||
<input type="password" name="pickup_password" placeholder="Pickup Password" required />
|
<input type="password" name="pickup_password" placeholder="Pickup Password" required />
|
||||||
|
|
||||||
|
<!-- 2FA Option -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="enable_2fa" id="enable-2fa" />
|
||||||
|
Enable 2FA (TOTP) - Adds extra security with Google Authenticator, Authy, etc.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<button type="submit">Upload and Generate Link</button>
|
<button type="submit">Upload and Generate Link</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -178,10 +285,16 @@
|
|||||||
shareLink.textContent = data.pickup_url;
|
shareLink.textContent = data.pickup_url;
|
||||||
shareLinkContainer.style.display = 'flex';
|
shareLinkContainer.style.display = 'flex';
|
||||||
|
|
||||||
|
// If 2FA is enabled, show the QR code immediately
|
||||||
|
if (data.qr_code_url) {
|
||||||
|
showTwoFactorSetup(data.qr_code_url, data.service_name, data.totp_secret);
|
||||||
|
}
|
||||||
|
|
||||||
// Clear form fields
|
// Clear form fields
|
||||||
document.getElementById('upload-file').value = '';
|
document.getElementById('upload-file').value = '';
|
||||||
document.getElementsByName('enc_password')[0].value = '';
|
document.getElementsByName('enc_password')[0].value = '';
|
||||||
document.getElementsByName('pickup_password')[0].value = '';
|
document.getElementsByName('pickup_password')[0].value = '';
|
||||||
|
document.getElementById('enable-2fa').checked = false;
|
||||||
|
|
||||||
// Scroll to the share link
|
// Scroll to the share link
|
||||||
shareLinkContainer.scrollIntoView({ behavior: 'smooth' });
|
shareLinkContainer.scrollIntoView({ behavior: 'smooth' });
|
||||||
@@ -190,6 +303,250 @@
|
|||||||
alert('Error uploading file: ' + error.message);
|
alert('Error uploading file: ' + error.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 2FA Setup Functions
|
||||||
|
function showTwoFactorSetup(qrCodeUrl, serviceName, totpSecret) {
|
||||||
|
const container = document.getElementById('tfa-setup-container');
|
||||||
|
const qrImage = document.getElementById('tfa-qr-image');
|
||||||
|
const tfaString = document.getElementById('tfa-string');
|
||||||
|
|
||||||
|
qrImage.src = qrCodeUrl;
|
||||||
|
tfaString.value = totpSecret;
|
||||||
|
container.style.display = 'block';
|
||||||
|
|
||||||
|
// Scroll to the 2FA setup
|
||||||
|
container.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTwoFactorSetup() {
|
||||||
|
const container = document.getElementById('tfa-setup-container');
|
||||||
|
container.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy share link functionality
|
||||||
|
document.getElementById('copy-share-btn').addEventListener('click', () => {
|
||||||
|
const shareLink = document.getElementById('share-link');
|
||||||
|
const feedback = document.getElementById('shared-link-feedback');
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(shareLink.href).then(() => {
|
||||||
|
feedback.style.display = 'block';
|
||||||
|
feedback.classList.add('show');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
feedback.classList.remove('show');
|
||||||
|
setTimeout(() => {
|
||||||
|
feedback.style.display = 'none';
|
||||||
|
}, 300);
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy 2FA string functionality
|
||||||
|
document.getElementById('copy-tfa-string-btn').addEventListener('click', () => {
|
||||||
|
const tfaString = document.getElementById('tfa-string');
|
||||||
|
const feedback = document.getElementById('tfa-string-feedback');
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(tfaString.value).then(() => {
|
||||||
|
feedback.style.display = 'block';
|
||||||
|
feedback.classList.add('show');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
feedback.classList.remove('show');
|
||||||
|
setTimeout(() => {
|
||||||
|
feedback.style.display = 'none';
|
||||||
|
}, 300);
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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>
|
</script>
|
||||||
|
|
||||||
<!-- File Limits Information -->
|
<!-- File Limits Information -->
|
||||||
|
|||||||
@@ -45,8 +45,24 @@
|
|||||||
<!-- File Info -->
|
<!-- File Info -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<p style="color: #00ff99; margin-bottom: 15px;">File ID: <code>{{ file_id }}</code></p>
|
<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>
|
</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 -->
|
<!-- Pickup Form -->
|
||||||
<form method="POST" class="form-group">
|
<form method="POST" class="form-group">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -65,6 +81,19 @@
|
|||||||
autocomplete="off" />
|
autocomplete="off" />
|
||||||
</div>
|
</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">
|
<div class="button-group">
|
||||||
<button type="submit">Decrypt and Download</button>
|
<button type="submit">Decrypt and Download</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user