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.
|
||||
Now with an admin control panel, GitHub updater, and a built-in Pac-Man easter egg! 🕹️
|
||||
**PacCrypt** is a modern, secure web application for encrypting and decrypting text and files using multiple encryption algorithms. Built with Flask and featuring a comprehensive REST API, modular encryption engines, and advanced security features including 2FA support.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This document contains AI generated pieces that have not been reviewed yet.
|
||||
> Next push will contain human oversite on the documentation.
|
||||
|
||||
**🌐 Official Instance**: [paccrypt.unnaturalll.dev](https://paccrypt.unnaturalll.dev)
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 🔒 Basic and Advanced Encryption for Text & Files
|
||||
- 📁 Secure File Uploads with Pickup Passwords
|
||||
- 🔑 Random Password Generator
|
||||
- 🎮 Hidden Pac-Man Game — type `pacman` to play
|
||||
- 🧠 Smart UI: Auto-switches input sections, toggles encryption labels
|
||||
- 📋 Clipboard Copy Feedback with styled status boxes
|
||||
- 🧾 Admin Panel:
|
||||
- Site map with live route list
|
||||
- Server restart & GitHub update button
|
||||
- Secure admin credential management
|
||||
- Server logs & upload cleanup
|
||||
- 🧩 System Settings Page for upload config
|
||||
- 📜 Custom 403, 404, and 500 Error Pages
|
||||
- 🤖 robots.txt and /sitemap for crawlers
|
||||
- 📱 Mobile-Responsive UI
|
||||
### 🔒 **Multi-Algorithm Encryption**
|
||||
- **AES-GCM**: Text encryption with authenticated encryption
|
||||
- **AES-CBC**: Text and file encryption with HMAC authentication
|
||||
- **XChaCha20-Poly1305**: Modern stream cipher for text and files
|
||||
- **RSA Hybrid**: RSA-4096 with AES hybrid encryption for text and files
|
||||
|
||||
### 🌐 **Comprehensive API**
|
||||
- RESTful API endpoints for all encryption operations
|
||||
- Text and file encryption/decryption
|
||||
- Key pair generation for RSA hybrid
|
||||
- PacShare file sharing with secure pickup URLs
|
||||
- Full API documentation (see [API.md](API.md))
|
||||
|
||||
### 📁 **PacShare - Secure File Sharing**
|
||||
- End-to-end encrypted file uploads
|
||||
- Dual-password system (pickup + encryption)
|
||||
- Optional 2FA with TOTP codes
|
||||
- QR code generation for 2FA setup
|
||||
- Automatic file expiration
|
||||
- Secure pickup URLs with one-time download
|
||||
|
||||
### 🛡️ **Advanced Security**
|
||||
- Admin panel with 2FA support
|
||||
- Encrypted admin credentials and logs
|
||||
- Secure session management
|
||||
- PBKDF2 key derivation with 200,000 iterations
|
||||
- Cryptographically secure random ID generation
|
||||
|
||||
### 🎮 **Built-in Entertainment**
|
||||
- Hidden Pac-Man game (type `pacman` to play)
|
||||
- Arrow key and swipe controls
|
||||
- Retro gaming experience with authentic sounds
|
||||
|
||||
### 🧾 **Admin Control Panel**
|
||||
- Real-time server monitoring and statistics
|
||||
- GitHub auto-update functionality
|
||||
- Upload management and cleanup
|
||||
- Server restart capabilities
|
||||
- Development/Production mode switching
|
||||
- Comprehensive audit logging
|
||||
|
||||
### 📱 **Modern UI/UX**
|
||||
- Fully responsive mobile design
|
||||
- Smart UI state management
|
||||
- Clipboard integration
|
||||
- Visual feedback for all operations
|
||||
- Custom error pages (403, 404, 500)
|
||||
- SEO-optimized with sitemap and robots.txt
|
||||
|
||||
---
|
||||
|
||||
## 👨💻 Installation
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 📋 Prerequisites
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.7+
|
||||
- Flask 3+
|
||||
- Cryptography 42+
|
||||
- Waitress 2.1+
|
||||
- Git (For update feature)
|
||||
- Nginx (Recommended)
|
||||
- Cockpit (Recommended if hosted on **Linux**)
|
||||
- **Python 3.8+** (3.10+ recommended)
|
||||
- **Git** (for updates and installation)
|
||||
- **pip** package manager
|
||||
|
||||
---
|
||||
|
||||
### ⚡ Quick Setup
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
git clone https://github.com/TySP-Dev/PacCrypt.git
|
||||
cd paccrypt-webapp-final
|
||||
# Clone the repository
|
||||
git clone https://github.com/TySP-Dev/PacCrypt-Webapp.git
|
||||
cd PacCrypt-Webapp
|
||||
|
||||
# Create virtual environment
|
||||
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
|
||||
./start_dev.sh #<-- start_dev.bat (Windows)
|
||||
# Linux/macOS
|
||||
python application_data/control_scripts/start_dev.py
|
||||
|
||||
# Windows
|
||||
python application_data\control_scripts\start_dev.py
|
||||
```
|
||||
|
||||
- Production Mode:
|
||||
#### Production Mode
|
||||
```bash
|
||||
./start_prod.sh #<-- start_prod.bat (Windows)
|
||||
# Linux/macOS
|
||||
python application_data/control_scripts/start_prod.py
|
||||
|
||||
# Windows
|
||||
python application_data\control_scripts\start_prod.py
|
||||
```
|
||||
|
||||
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
|
||||
Visit http://hosts_private_ip - *If* you are **not** on the host system
|
||||
### 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
|
||||
- Then hit `📋 Copy Password`
|
||||
- **Note:** This is also used as a seed generator for the Pac-Man *like* game
|
||||
1. **Select Algorithm**: Choose from AES-GCM, AES-CBC, XChaCha20, or RSA Hybrid
|
||||
2. **Enter Text**: Type or paste your message
|
||||
3. **Set Password**: Enter a strong encryption password
|
||||
4. **For RSA**: Generate key pair first if using RSA Hybrid
|
||||
5. **Execute**: Click Encrypt/Decrypt
|
||||
6. **Copy Result**: Use the copy button for easy sharing
|
||||
|
||||
### 🔐 Encrypt & Decrypt
|
||||
### 📁 File Operations
|
||||
|
||||
- Choose between Basic Cipher or Advanced AES
|
||||
- Select mode using toggle (Encrypt/Decrypt)
|
||||
- Type your message or upload a file
|
||||
- Enter password (Advanced AES)
|
||||
- Hit Execute
|
||||
- Then hit `📋 Copy Output`
|
||||
1. **Upload File**: Select file using the file picker
|
||||
2. **Choose Algorithm**: Pick AES-CBC, XChaCha20, or RSA Hybrid (AES-GCM not supported for files)
|
||||
3. **Set Password**: Enter encryption password
|
||||
4. **Process**: File will be encrypted/decrypted and downloaded automatically
|
||||
|
||||
### 📤 Share Files
|
||||
### 📤 PacShare - Secure File Sharing
|
||||
|
||||
- Upload a file with two passwords:
|
||||
- Encryption password
|
||||
- Pickup password
|
||||
- Get a shareable URL and click `📋 Copy Link`
|
||||
1. **Upload File**: Select file to share
|
||||
2. **Set Passwords**:
|
||||
- **Encryption Password**: Encrypts the file content
|
||||
- **Pickup Password**: Required to access the download page
|
||||
3. **Optional 2FA**: Enable for additional security
|
||||
4. **Share URL**: Copy the generated pickup URL
|
||||
5. **Recipient Access**: They need both passwords (and 2FA code if enabled)
|
||||
|
||||
### 🎮 Pac-Man *like* Game
|
||||
### 🎮 Hidden Pac-Man Game
|
||||
|
||||
- Type `pacman` in the input box
|
||||
- Game appears with `Restart` and `Exit` buttons
|
||||
- Arrow key and Swipe controls 🕹️
|
||||
- Game restarts and a new seed is generated once all dots are eaten
|
||||
- Type `pacman` in any text input
|
||||
- Use arrow keys or swipe gestures to play
|
||||
- Authentic retro gaming experience with sound effects
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Admin Panel
|
||||
|
||||
Visit `/adminpage` after setting up credentials at `/admin-setup`.
|
||||
Access the admin panel at `/adminpage` after initial setup at `/admin-setup`.
|
||||
|
||||
Features:
|
||||
- 🔄 Restart server
|
||||
- 🔃 Update from GitHub (git pull)
|
||||
- 🧽 Clear uploads
|
||||
- 🔐 Change admin password
|
||||
- 📝 View logs
|
||||
- ⚙️ Adjust upload settings
|
||||
### 🔑 Setup Process
|
||||
1. Visit `/admin-setup` on first run
|
||||
2. Create admin username and password
|
||||
3. Optionally enable 2FA for enhanced security
|
||||
4. Login at `/admin-login`
|
||||
|
||||
### 🎛️ Admin Features
|
||||
- **📊 Server Monitoring**: Real-time statistics and uptime
|
||||
- **🔄 Server Control**: Restart, switch dev/prod modes
|
||||
- **📋 Route Management**: View all available endpoints
|
||||
- **🔃 GitHub Integration**: Auto-update from repository
|
||||
- **🧹 File Management**: Clear uploads and expired files
|
||||
- **🔐 Security**: Change password, manage 2FA
|
||||
- **📝 Audit Logs**: View encrypted activity logs
|
||||
- **⚙️ Settings**: Configure upload limits and file retention
|
||||
|
||||
### 🔒 Security Features
|
||||
- Encrypted credential storage
|
||||
- TOTP-based 2FA support
|
||||
- QR code generation for authenticator apps
|
||||
- Secure session management
|
||||
- Encrypted audit logging
|
||||
|
||||
---
|
||||
|
||||
@@ -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
|
||||
|
||||
```
|
||||
PacCrypt/
|
||||
├── app.py
|
||||
├── requirements.txt
|
||||
├── README.md
|
||||
├── templates/
|
||||
│ ├── index.html
|
||||
│ ├── 404.html
|
||||
│ └── 403.html
|
||||
│ └── 500.html
|
||||
│ └── admin.html
|
||||
│ └── admin_login.html
|
||||
│ └── admin_settings.html
|
||||
│ └── admin_setup.html
|
||||
│ └── pickup.html
|
||||
├── static/
|
||||
│ ├── css/
|
||||
│ │ └── styles.css
|
||||
│ ├── js/
|
||||
│ │ └── ui.js
|
||||
│ │ └── pacman.js
|
||||
│ │ └── main.js
|
||||
│ │ └── fileops.js
|
||||
│ │ └── encryption.js
|
||||
│ ├── img/
|
||||
│ │ └── PacCrypt.png
|
||||
│ │ └── Github_logo.png
|
||||
│ │ └── sitemap.png
|
||||
│ ├── fonts/
|
||||
│ │ └── PressStart2P-Regular.ttf
|
||||
│ └── audio/
|
||||
│ └── chomp.mp3
|
||||
├── start_dev.bat
|
||||
├── start_prod.bat
|
||||
├── start_dev.sh
|
||||
├── start_prod.sh
|
||||
PacCrypt-Webapp/
|
||||
├── app.py # Main Flask application
|
||||
├── README.md # This file
|
||||
├── ROADMAP.md # Development roadmap
|
||||
├── API.md # API documentation
|
||||
├── application_data/ # Application configuration
|
||||
│ ├── control_scripts/ # Server management scripts
|
||||
│ │ ├── start_dev.py # Development mode starter
|
||||
│ │ ├── start_prod.py # Production mode starter
|
||||
│ │ ├── restart_dev.py # Development restart
|
||||
│ │ ├── restart_prod.py # Production restart
|
||||
│ │ └── stop.py # Server stop script
|
||||
│ ├── requirements.txt # Python dependencies
|
||||
│ ├── settings.json # Application settings
|
||||
│ ├── admin_creds.json # Encrypted admin credentials
|
||||
│ ├── admin_key.key # Admin encryption key
|
||||
│ └── admin_logs.enc # Encrypted audit logs
|
||||
├── paccrypt_algos/ # Encryption modules
|
||||
│ ├── __init__.py # Package initialization
|
||||
│ ├── aes_cbc.py # AES-CBC implementation
|
||||
│ ├── aes_gcm.py # AES-GCM implementation
|
||||
│ ├── xchacha.py # XChaCha20-Poly1305
|
||||
│ └── rsa_hybrid.py # RSA hybrid encryption
|
||||
├── pacshare/ # File upload storage
|
||||
│ ├── *.encrypted # Encrypted uploaded files
|
||||
│ └── *.json # File metadata
|
||||
├── templates/ # HTML templates
|
||||
│ ├── index.html # Main interface
|
||||
│ ├── pickup.html # File pickup page
|
||||
│ ├── admin*.html # Admin panel pages
|
||||
│ └── error pages (403,404,500)
|
||||
└── static/ # Static assets
|
||||
├── css/styles.css # Application styling
|
||||
├── js/ # JavaScript modules
|
||||
├── img/ # Images and icons
|
||||
├── fonts/ # Custom fonts
|
||||
└── audio/ # Sound effects
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Security Considerations
|
||||
|
||||
### ⚠️ Important Security Notes
|
||||
|
||||
- **Password Strength**: Use strong, unique passwords for all operations
|
||||
- **2FA Recommended**: Enable 2FA for admin accounts and sensitive file shares
|
||||
- **HTTPS Required**: Always use HTTPS in production environments
|
||||
- **Regular Updates**: Keep dependencies updated for security patches
|
||||
- **Backup Strategy**: Implement regular backups of encrypted data
|
||||
|
||||
### 🛡️ Encryption Details
|
||||
|
||||
- **AES-256**: Industry standard symmetric encryption
|
||||
- **RSA-4096**: Strong asymmetric encryption for key exchange
|
||||
- **PBKDF2**: 200,000 iterations for key derivation
|
||||
- **Authenticated Encryption**: GCM and Poly1305 modes prevent tampering
|
||||
- **Secure Random**: Cryptographically secure random number generation
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [ROADMAP.md](ROADMAP.md) for planned features and development priorities.
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Add tests if applicable
|
||||
5. Submit a pull request
|
||||
|
||||
## 📞 Support
|
||||
|
||||
- **Documentation**: See [API.md](API.md) for API details
|
||||
- **Issues**: Report bugs via GitHub Issues
|
||||
- **Discussions**: Use GitHub Discussions for questions
|
||||
- **Official Instance**: [paccrypt.unnaturalll.dev](https://paccrypt.unnaturalll.dev)
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT © [TySP-Dev](https://github.com/TySP-Dev)
|
||||
|
||||
**🔐 Secure by design. Simple by choice. Powerful by nature.**
|
||||
|
||||
|
||||
+135
-126
@@ -5,7 +5,9 @@
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -17,34 +19,34 @@
|
||||
|
||||
- [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
|
||||
|
||||
##### app.py Responsibilities
|
||||
|
||||
- [ ] Flask app + routing
|
||||
- [x] Flask app + routing
|
||||
|
||||
- [ ] Handle:
|
||||
- /encrypt
|
||||
- /decrypt
|
||||
- /pickup/<file_id>
|
||||
- [x] Handle:
|
||||
- [x] /encrypt (via API endpoints)
|
||||
- [x] /decrypt (via API endpoints)
|
||||
- [x] /pickup/<file_id>
|
||||
|
||||
- [ ] Receive:
|
||||
- File or text
|
||||
- pickup_password (required)
|
||||
- encryption_password (required)
|
||||
- encryption_mode
|
||||
- [x] Receive:
|
||||
- [x] File or text
|
||||
- [x] pickup_password (required)
|
||||
- [x] encryption_password (required)
|
||||
- [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)"
|
||||
|
||||
@@ -56,7 +58,7 @@
|
||||
|
||||
- [x] Create folder + interface
|
||||
|
||||
- [ ] Remove basic cypher
|
||||
- [x] Remove basic cypher
|
||||
|
||||
Implement engines:
|
||||
|
||||
@@ -68,17 +70,18 @@ Implement engines:
|
||||
|
||||
- [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:
|
||||
|
||||
```
|
||||
def encrypt\_text(text, key, metadata): ...
|
||||
def decrypt\_text(ciphertext, key, metadata): ...
|
||||
def encrypt\_file(in\_path, out\_path, key, metadata): ...
|
||||
def decrypt\_file(in\_path, out\_path, key, metadata): ...
|
||||
def get\_name(): return "AES-GCM"
|
||||
def encrypt_text(text, key): ...
|
||||
def decrypt_text(ciphertext, key): ...
|
||||
def encrypt_file(in_path, out_path, key): ...
|
||||
def decrypt_file(in_path, out_path, key): ...
|
||||
def generate_key_pair(): ... (for RSA hybrid)
|
||||
```
|
||||
**COMPLETED: All modules implemented with correct API**
|
||||
|
||||
---
|
||||
|
||||
@@ -86,21 +89,21 @@ def get\_name(): return "AES-GCM"
|
||||
|
||||
/encrypt Route Flow
|
||||
|
||||
- [ ] JS submits (PacShare "Form"):
|
||||
- File
|
||||
- pickup_password (for metadata)
|
||||
- encryption_password (for file)
|
||||
- encryption_mode
|
||||
- 2FA token code / Yubi/Passkey set up
|
||||
- [x] JS submits (PacShare "Form"):
|
||||
- [x] File
|
||||
- [x] pickup_password (for metadata)
|
||||
- [x] encryption_password (for file)
|
||||
- [x] encryption_mode
|
||||
- [x] 2FA TOTP setup (Yubi/Passkey not implemented)
|
||||
|
||||
- [ ] Python logic:
|
||||
- Encrypt file using selected algo + encryption_password
|
||||
- Generate metadata dict:
|
||||
- filename, enc_mode, pickup_hash, timestamp, optional 2FA
|
||||
- Encrypt metadata using AES-GCM derived from pickup_password
|
||||
- Save .paccrypt and .meta files
|
||||
- Generate random file_id
|
||||
- Return /pickup/<file_id> link
|
||||
- [x] Python logic:
|
||||
- [x] Encrypt file using selected algo + encryption_password
|
||||
- [x] Generate metadata dict:
|
||||
- [x] filename, enc_mode, pickup_hash, timestamp, optional 2FA
|
||||
- [x] Encrypt metadata using AES-GCM derived from pickup_password
|
||||
- [x] Save .{algorithm}.encrypted and .json files
|
||||
- [x] Generate random file_id
|
||||
- [x] Return /pickup/<file_id> link
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Both passwords are required. One reveals the mode + metadata, the other decrypts the file.
|
||||
@@ -109,15 +112,15 @@ def get\_name(): return "AES-GCM"
|
||||
|
||||
##### /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",
|
||||
"enc\_mode": "aes\_gcm",
|
||||
"pickup\_hash": "<argon2>",
|
||||
"created\_at": "2025-08-05T18:00Z",
|
||||
"2fa\_seed": "base32string", // optional
|
||||
"yubi\_token\_hash": "sha256", // optional
|
||||
"algorithm": "aes_cbc",
|
||||
"pickup_password": "<sha256>",
|
||||
"created_at": "2025-08-05T18:00Z",
|
||||
"require_2fa": true, // optional
|
||||
"totp_secret": "base32string", // optional
|
||||
"service_name": "PacCrypt File: report.pdf..." // optional
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Stored as .meta
|
||||
> Encrypted with AES-GCM using key from pickup\_password
|
||||
> Stored as .json
|
||||
> 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
|
||||
|
||||
```
|
||||
POST /api/encrypt Local-only file/text encryption (returns file/meta)
|
||||
POST /api/ps-send Upload + encrypt + return pickup link (JSON)
|
||||
POST /api/ps-pickup Provide pickup ID + passwords, return decrypted file
|
||||
POST /api/decrypt Decrypt local .enc + .meta bundle
|
||||
GET /api/version Return current version tag
|
||||
✅ GET /api/algorithms List available encryption algorithms
|
||||
✅ POST /api/generate-keypair Generate RSA key pairs
|
||||
✅ POST /api/encrypt File/text encryption (returns encrypted data)
|
||||
✅ POST /api/decrypt File/text decryption
|
||||
✅ POST /api/pacshare Upload + encrypt + return pickup link (JSON)
|
||||
❌ POST /api/ps-pickup Provide pickup ID + passwords, return decrypted file (Use web interface)
|
||||
❌ GET /api/version Return current version tag (Not implemented)
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> 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/
|
||||
├── <file_id>pdf/jpeg/etc.paccrypt # Encrypted binary file
|
||||
└── <file_id>meta.paccrypt # Encrypted metadata
|
||||
├── <file_id>.<algorithm>.encrypted # Encrypted binary file
|
||||
└── <file_id>.json # Encrypted metadata (JSON)
|
||||
```
|
||||
|
||||
**Current Implementation:**
|
||||
- Files are stored as `.{algorithm}.encrypted` (e.g., `.aes_cbc.encrypted`)
|
||||
- Metadata stored as `.json` files with encrypted content
|
||||
- Algorithm info embedded in filename for automatic detection
|
||||
|
||||
---
|
||||
|
||||
### Development Order
|
||||
|
||||
0. - [ ] Phase 0 Tasks
|
||||
1. - [ ] paccrypt_algos/ + aes_gcm.py
|
||||
2. - [ ] app.py routes: /encrypt, /pickup/<id>
|
||||
3. - [ ] Add /decrypt route
|
||||
4. - [ ] Build metadata encryption helpers
|
||||
5. - [ ] Finish other engine modules
|
||||
6. - [ ] Build /api/* equivalents
|
||||
7. - [ ] Update README.md with all changed to the webapp.
|
||||
8. - [ ] Create a new installation guide.
|
||||
9. - [ ] Build CLI
|
||||
0. - [x] **Phase 0 Tasks** ✅
|
||||
1. - [x] **paccrypt_algos/ + aes_gcm.py** ✅
|
||||
2. - [x] **app.py routes: /encrypt, /pickup/<id>** ✅
|
||||
3. - [x] **Add /decrypt route** ✅
|
||||
4. - [x] **Build metadata encryption helpers** ✅
|
||||
5. - [x] **Finish other engine modules** ✅
|
||||
6. - [x] **Build /api/* equivalents** ✅
|
||||
7. - [x] **Update README.md with all changes to the webapp** ✅
|
||||
8. - [x] **Create a new installation guide** ✅ (Included in README.md)
|
||||
9. - [ ] Build CLI ⏳ *Next Priority*
|
||||
10. - [ ] Test CLI with --pickup + --share
|
||||
12. - [ ] Build GUI app on Linux
|
||||
13. - [ ] Test GUI app on Linux
|
||||
14. - [ ] Build GUI app on Android
|
||||
15. - [ ] Test GUI app on Android
|
||||
16. - [ ] Finilize all releases and push to main.
|
||||
16. - [ ] Finalize all releases and push to main
|
||||
17. - [ ] Create Wiki
|
||||
|
||||
**🎉 WEBAPP CORE COMPLETE! 🎉**
|
||||
|
||||
**Current Status:** All core webapp functionality implemented including:
|
||||
- ✅ Modular encryption engines (AES-GCM, AES-CBC, XChaCha20, RSA Hybrid)
|
||||
- ✅ Complete API with documentation
|
||||
- ✅ PacShare file sharing with 2FA support
|
||||
- ✅ Admin panel with full management features
|
||||
- ✅ Cross-platform deployment scripts
|
||||
- ✅ Comprehensive documentation
|
||||
|
||||
---
|
||||
|
||||
### Draft tree for webapp
|
||||
### Current Webapp Structure ✅ **COMPLETED**
|
||||
|
||||
```
|
||||
paccrypt-webapp/
|
||||
├── static/
|
||||
│ ├── audio/
|
||||
│ │ └── chomp.mp3
|
||||
│ ├── css/
|
||||
│ │ └── styles.css
|
||||
│ ├── fonts/
|
||||
│ │ └── PressStart2P-Regular.ttf
|
||||
│ ├── img/
|
||||
│ │ ├── Github_logo.png
|
||||
│ │ ├── PacCrypt.png
|
||||
│ │ ├── PacCrypt_W-Background.png
|
||||
│ │ ├── PacCrypt_W-Backgroud_Name.png
|
||||
│ │ ├── PacCrypt_W-Name.png
|
||||
│ │ └── sitemap.png <-- **Change img**
|
||||
│ └── js/ <-- **Pending changes**
|
||||
│ ├── encryption.js
|
||||
│ ├── fileops.js
|
||||
│ ├── main.js
|
||||
│ ├── pacman.js
|
||||
│ └── ui.js
|
||||
├── templates/
|
||||
│ ├── 403.html
|
||||
│ ├── 404.html
|
||||
│ ├── 500.html
|
||||
│ ├── admin.html
|
||||
│ ├── admin_login.html
|
||||
│ ├── admin_settings.html
|
||||
│ ├── admin_setup.html
|
||||
│ ├── index.html
|
||||
│ └── pickup.html
|
||||
├── application_data/ <-- *New*
|
||||
│ ├── scripts/ <-- *New*
|
||||
│ │ ├── start_dev <-- *Moved*
|
||||
│ │ ├── start_prod <-- *Moved*
|
||||
│ │ ├── restart_dev <-- *New*
|
||||
│ │ ├── restart_prod <-- *New*
|
||||
│ │ └── stop <-- *New*
|
||||
│ ├── 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
|
||||
PacCrypt-Webapp/
|
||||
├── app.py # Main Flask application ✅
|
||||
├── README.md # Updated documentation ✅
|
||||
├── ROADMAP.md # This file ✅
|
||||
├── API.md # API documentation ✅ *NEW*
|
||||
├── LICENSE # MIT License ✅
|
||||
├── application_data/ ✅ # Application configuration
|
||||
│ ├── control_scripts/ ✅ # Server management scripts
|
||||
│ │ ├── start_dev.py ✅ # Development mode starter
|
||||
│ │ ├── start_prod.py ✅ # Production mode starter
|
||||
│ │ ├── restart_dev.py ✅ # Development restart
|
||||
│ │ ├── restart_prod.py ✅ # Production restart
|
||||
│ │ └── stop.py ✅ # Server stop script
|
||||
│ ├── requirements.txt ✅ # Python dependencies
|
||||
│ ├── settings.json ✅ # Application settings
|
||||
│ ├── admin_creds.json ✅ # Encrypted admin credentials
|
||||
│ ├── admin_key.key ✅ # Admin encryption key
|
||||
│ └── admin_logs.enc ✅ # Encrypted audit logs
|
||||
├── paccrypt_algos/ ✅ # Encryption modules
|
||||
│ ├── __init__.py ✅ # Package initialization
|
||||
│ ├── aes_cbc.py ✅ # AES-CBC implementation
|
||||
│ ├── aes_gcm.py ✅ # AES-GCM implementation
|
||||
│ ├── xchacha.py ✅ # XChaCha20-Poly1305
|
||||
│ └── rsa_hybrid.py ✅ # RSA hybrid encryption
|
||||
├── pacshare/ ✅ # File upload storage
|
||||
│ ├── *.{algorithm}.encrypted ✅ # Encrypted uploaded files
|
||||
│ └── *.json ✅ # File metadata
|
||||
├── templates/ ✅ # HTML templates
|
||||
│ ├── index.html ✅ # Main interface
|
||||
│ ├── pickup.html ✅ # File pickup page
|
||||
│ ├── admin*.html ✅ # Admin panel pages
|
||||
│ └── error pages (403,404,500) ✅
|
||||
└── static/ ✅ # Static assets
|
||||
├── css/styles.css ✅ # Application styling
|
||||
├── js/ ✅ # JavaScript modules
|
||||
├── img/ ✅ # Images and icons
|
||||
├── fonts/ ✅ # Custom fonts
|
||||
└── audio/ ✅ # Sound effects
|
||||
```
|
||||
|
||||
**🏆 PROJECT STRUCTURE FULLY IMPLEMENTED 🏆**
|
||||
|
||||
@@ -24,6 +24,13 @@ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from cryptography.hazmat.primitives.hashes import SHA256
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
from cryptography.fernet import Fernet
|
||||
import pyotp
|
||||
import qrcode
|
||||
from io import BytesIO
|
||||
|
||||
# ===== PacCrypt Algorithm Imports =====
|
||||
from paccrypt_algos import aes_cbc, aes_gcm, xchacha, rsa_hybrid
|
||||
# Post-quantum crypto removed for simplicity
|
||||
|
||||
# ===== Application Configuration =====
|
||||
app = Flask(__name__)
|
||||
@@ -35,7 +42,6 @@ ADMIN_CRED_FILE = 'application_data/admin_creds.json'
|
||||
ADMIN_KEY_FILE = 'application_data/admin_key.key'
|
||||
ADMIN_LOG_FILE = 'application_data/admin_logs.enc'
|
||||
SETTINGS_FILE = 'application_data/settings.json'
|
||||
ALPHABET = list('abcdefghijklmnopqrstuvwxyz')
|
||||
|
||||
DEFAULT_SETTINGS = {
|
||||
"upload_folder": "pacshare",
|
||||
@@ -43,6 +49,41 @@ DEFAULT_SETTINGS = {
|
||||
"max_file_size_bytes": 25 * 1024 * 1024 * 1024 # 25GB
|
||||
}
|
||||
|
||||
# ===== Available Encryption Algorithms =====
|
||||
AVAILABLE_ALGORITHMS = {
|
||||
"aes_cbc": {
|
||||
"name": "AES-CBC",
|
||||
"module": aes_cbc,
|
||||
"supports_text": True,
|
||||
"supports_file": True,
|
||||
"description": "AES-256 with CBC mode and HMAC authentication"
|
||||
},
|
||||
"aes_gcm": {
|
||||
"name": "AES-GCM",
|
||||
"module": aes_gcm,
|
||||
"supports_text": True,
|
||||
"supports_file": False,
|
||||
"description": "AES-256 with GCM mode (authenticated encryption)"
|
||||
},
|
||||
"xchacha": {
|
||||
"name": "XChaCha20-Poly1305",
|
||||
"module": xchacha,
|
||||
"supports_text": True,
|
||||
"supports_file": True,
|
||||
"description": "XChaCha20 stream cipher with Poly1305 authentication"
|
||||
},
|
||||
"rsa_hybrid": {
|
||||
"name": "RSA Hybrid",
|
||||
"module": rsa_hybrid,
|
||||
"supports_text": True,
|
||||
"supports_file": True,
|
||||
"description": "RSA-4096 with AES hybrid encryption",
|
||||
"requires_keypair": True
|
||||
}
|
||||
}
|
||||
|
||||
# Post-quantum algorithms removed
|
||||
|
||||
# ===== Settings Management =====
|
||||
def load_settings():
|
||||
"""Load application settings from file or create with defaults."""
|
||||
@@ -77,14 +118,6 @@ def hash_password(password: str, salt: bytes) -> str:
|
||||
"""Hash a password with salt for secure storage."""
|
||||
return base64.urlsafe_b64encode(derive_key(password, salt)).decode()
|
||||
|
||||
def simple_encode(text: str) -> str:
|
||||
"""Basic Caesar cipher encryption."""
|
||||
return ''.join(ALPHABET[(ALPHABET.index(c) + 3) % 26] if c in ALPHABET else c for c in text.lower())
|
||||
|
||||
def simple_decode(text: str) -> str:
|
||||
"""Basic Caesar cipher decryption."""
|
||||
return ''.join(ALPHABET[(ALPHABET.index(c) - 3) % 26] if c in ALPHABET else c for c in text.lower())
|
||||
|
||||
def advanced_encrypt(plaintext: str, password: str) -> str:
|
||||
"""Encrypt plaintext with AES-GCM and return base64-encoded result."""
|
||||
salt = os.urandom(16)
|
||||
@@ -113,17 +146,23 @@ def load_admin_key():
|
||||
with open(ADMIN_KEY_FILE, 'rb') as f:
|
||||
return f.read()
|
||||
|
||||
def encrypt_creds(username, password):
|
||||
def encrypt_creds(username, password, totp_secret=None):
|
||||
"""Encrypt and store admin credentials."""
|
||||
key = load_admin_key()
|
||||
cipher = Fernet(key)
|
||||
salt = os.urandom(16)
|
||||
hashed_pw = hash_password(password, salt)
|
||||
data = json.dumps({"u": username, "p": hashed_pw, "s": base64.b64encode(salt).decode()}).encode()
|
||||
data = {
|
||||
"u": username,
|
||||
"p": hashed_pw,
|
||||
"s": base64.b64encode(salt).decode(),
|
||||
"totp_secret": totp_secret,
|
||||
"2fa_enabled": totp_secret is not None
|
||||
}
|
||||
with open(ADMIN_CRED_FILE, 'wb') as f:
|
||||
f.write(cipher.encrypt(data))
|
||||
f.write(cipher.encrypt(json.dumps(data).encode()))
|
||||
|
||||
def check_creds(username, password):
|
||||
def check_creds(username, password, totp_code=None):
|
||||
"""Verify admin credentials."""
|
||||
try:
|
||||
key = load_admin_key()
|
||||
@@ -132,11 +171,51 @@ def check_creds(username, password):
|
||||
decrypted = cipher.decrypt(f.read())
|
||||
creds = json.loads(decrypted)
|
||||
salt = base64.b64decode(creds["s"])
|
||||
return creds["u"] == username and creds["p"] == hash_password(password, salt)
|
||||
|
||||
# Check username and password first
|
||||
if not (creds["u"] == username and creds["p"] == hash_password(password, salt)):
|
||||
return False
|
||||
|
||||
# Check 2FA if enabled
|
||||
if creds.get("2fa_enabled", False):
|
||||
if not totp_code:
|
||||
return False
|
||||
totp_secret = creds.get("totp_secret")
|
||||
if not totp_secret:
|
||||
return False
|
||||
totp = pyotp.TOTP(totp_secret)
|
||||
if not totp.verify(totp_code, valid_window=1):
|
||||
return False
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print("[ERROR] check_creds failed:", e)
|
||||
return False
|
||||
|
||||
def get_admin_2fa_status():
|
||||
"""Check if admin has 2FA enabled."""
|
||||
try:
|
||||
key = load_admin_key()
|
||||
cipher = Fernet(key)
|
||||
with open(ADMIN_CRED_FILE, 'rb') as f:
|
||||
decrypted = cipher.decrypt(f.read())
|
||||
creds = json.loads(decrypted)
|
||||
return creds.get("2fa_enabled", False)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_admin_totp_secret():
|
||||
"""Get admin TOTP secret for QR code generation."""
|
||||
try:
|
||||
key = load_admin_key()
|
||||
cipher = Fernet(key)
|
||||
with open(ADMIN_CRED_FILE, 'rb') as f:
|
||||
decrypted = cipher.decrypt(f.read())
|
||||
creds = json.loads(decrypted)
|
||||
return creds.get("totp_secret")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def log_admin_event(message: str):
|
||||
"""Log admin actions securely."""
|
||||
try:
|
||||
@@ -171,19 +250,21 @@ def cleanup_expired_files():
|
||||
# ===== Route Handlers =====
|
||||
@app.route("/", methods=["GET", "POST"])
|
||||
def index():
|
||||
"""Main application route handling encryption/decryption and file uploads."""
|
||||
"""Main application route handling file uploads."""
|
||||
if request.method == 'POST':
|
||||
if 'file' in request.files:
|
||||
return handle_file_upload(request)
|
||||
else:
|
||||
return handle_text_operation(request)
|
||||
return render_template("index.html", result="", password="", encryption_type="advanced", settings=settings)
|
||||
return jsonify(error="Use /api/encrypt or /api/decrypt endpoints for text operations"), 400
|
||||
return render_template("index.html", settings=settings)
|
||||
|
||||
def handle_file_upload(request):
|
||||
"""Process file upload and encryption."""
|
||||
file = request.files['file']
|
||||
enc_password = request.form.get('enc_password')
|
||||
pickup_password = request.form.get('pickup_password')
|
||||
algorithm = request.form.get('algorithm', 'aes_cbc') # Default to AES-CBC
|
||||
enable_2fa = request.form.get('enable_2fa') == 'on' # Check if 2FA checkbox is checked
|
||||
|
||||
if not file or not enc_password or not pickup_password:
|
||||
return jsonify({"error": "Missing fields"}), 400
|
||||
@@ -191,52 +272,63 @@ def handle_file_upload(request):
|
||||
if file.content_length and file.content_length > MAX_FILE_SIZE_BYTES:
|
||||
return jsonify({"error": f"File too large! Limit: {MAX_FILE_SIZE_BYTES / (1024**3):.2f} GB"}), 400
|
||||
|
||||
# Validate algorithm
|
||||
if algorithm not in AVAILABLE_ALGORITHMS:
|
||||
return jsonify({"error": "Invalid algorithm"}), 400
|
||||
|
||||
algo_config = AVAILABLE_ALGORITHMS[algorithm]
|
||||
if not algo_config["supports_file"]:
|
||||
return jsonify({"error": "Algorithm does not support file operations"}), 400
|
||||
|
||||
filename = secure_filename(file.filename)
|
||||
temp_path = os.path.join(UPLOAD_FOLDER, filename)
|
||||
file.save(temp_path)
|
||||
|
||||
with open(temp_path, 'rb') as f:
|
||||
data = f.read()
|
||||
|
||||
salt = os.urandom(16)
|
||||
key = derive_key(enc_password, salt)
|
||||
nonce = os.urandom(12)
|
||||
ct = AESGCM(key).encrypt(nonce, data, None)
|
||||
try:
|
||||
# Use the selected algorithm for encryption
|
||||
module = algo_config["module"]
|
||||
|
||||
random_id = secrets.token_urlsafe(24)
|
||||
encrypted_filename = f"{random_id}.{algorithm}.encrypted"
|
||||
encrypted_path = os.path.join(UPLOAD_FOLDER, encrypted_filename)
|
||||
|
||||
with open(os.path.join(UPLOAD_FOLDER, f"{random_id}.enc"), 'wb') as f:
|
||||
f.write(salt + nonce + ct)
|
||||
# Encrypt file using the correct API (in_path, out_path, password)
|
||||
module.encrypt_file(temp_path, encrypted_path, enc_password)
|
||||
os.remove(temp_path)
|
||||
|
||||
meta = {
|
||||
'pickup_password': base64.urlsafe_b64encode(hashlib.sha256(pickup_password.encode()).digest()).decode(),
|
||||
'original_name': encrypt_filename(filename, enc_password),
|
||||
'timestamp': datetime.now().isoformat()
|
||||
'algorithm': algorithm, # Store algorithm used for decryption
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'require_2fa': enable_2fa
|
||||
}
|
||||
|
||||
# Generate TOTP secret if 2FA is enabled
|
||||
if enable_2fa:
|
||||
totp_secret = pyotp.random_base32()
|
||||
meta['totp_secret'] = totp_secret
|
||||
meta['service_name'] = f"PacCrypt File: {filename[:20]}..."
|
||||
with open(os.path.join(UPLOAD_FOLDER, f"{random_id}.json"), 'w') as f:
|
||||
json.dump(meta, f)
|
||||
|
||||
pickup_url = request.host_url.rstrip('/') + url_for('pickup_file', file_id=random_id)
|
||||
return jsonify({"success": True, "pickup_url": pickup_url})
|
||||
response_data = {"success": True, "pickup_url": pickup_url}
|
||||
|
||||
def handle_text_operation(request):
|
||||
data = request.get_json()
|
||||
encryption_type = data.get("encryption-type", "basic")
|
||||
operation = data.get("operation", "")
|
||||
message = data.get("message", "")
|
||||
password = data.get("password", "")
|
||||
# If 2FA is enabled, also return QR code URL for immediate setup
|
||||
if enable_2fa:
|
||||
qr_url = request.host_url.rstrip('/') + url_for('generate_qr_code', file_id=random_id)
|
||||
response_data["qr_code_url"] = qr_url
|
||||
response_data["totp_secret"] = totp_secret
|
||||
response_data["service_name"] = f"PacCrypt File: {filename[:20]}..."
|
||||
|
||||
if encryption_type == "basic":
|
||||
result = simple_encode(message) if operation == "encrypt" else simple_decode(message)
|
||||
return jsonify(result=html.escape(result))
|
||||
return jsonify(response_data)
|
||||
except Exception as e:
|
||||
# Clean up temp file if it still exists
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
return jsonify({"error": f"Encryption failed: {str(e)}"}), 500
|
||||
|
||||
if operation == "encrypt":
|
||||
encrypted = advanced_encrypt(message, password)
|
||||
return jsonify(result=encrypted)
|
||||
else:
|
||||
decrypted = advanced_decrypt(message, password)
|
||||
return jsonify(result=html.escape(decrypted))
|
||||
|
||||
def encrypt_filename(filename: str, password: str) -> str:
|
||||
salt = os.urandom(16)
|
||||
@@ -256,20 +348,44 @@ def decrypt_filename(enc_filename_b64: str, password: str) -> str:
|
||||
def pickup_file(file_id):
|
||||
"""Handle file pickup and decryption."""
|
||||
meta_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.json")
|
||||
enc_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.enc")
|
||||
|
||||
if not os.path.exists(meta_path) or not os.path.exists(enc_path):
|
||||
# Find the encrypted file (could have different algorithm extensions)
|
||||
enc_path = None
|
||||
for filename in os.listdir(UPLOAD_FOLDER):
|
||||
if filename.startswith(f"{file_id}.") and filename.endswith(".encrypted"):
|
||||
enc_path = os.path.join(UPLOAD_FOLDER, filename)
|
||||
break
|
||||
|
||||
# Fallback to old .enc format for backward compatibility
|
||||
if not enc_path:
|
||||
old_enc_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.enc")
|
||||
if os.path.exists(old_enc_path):
|
||||
enc_path = old_enc_path
|
||||
|
||||
if not os.path.exists(meta_path) or not enc_path or not os.path.exists(enc_path):
|
||||
flash("File not found or expired")
|
||||
return redirect(url_for('index'))
|
||||
|
||||
if request.method == 'POST':
|
||||
return handle_file_pickup(request, meta_path, enc_path, file_id)
|
||||
return render_template("pickup.html", file_id=file_id)
|
||||
|
||||
# Check if 2FA is required for this file
|
||||
require_2fa = False
|
||||
service_name = None
|
||||
if os.path.exists(meta_path):
|
||||
with open(meta_path, 'r') as f:
|
||||
meta = json.load(f)
|
||||
require_2fa = meta.get('require_2fa', False)
|
||||
if require_2fa:
|
||||
service_name = meta.get('service_name', 'PacCrypt File')
|
||||
|
||||
return render_template("pickup.html", file_id=file_id, require_2fa=require_2fa, service_name=service_name)
|
||||
|
||||
def handle_file_pickup(request, meta_path, enc_path, file_id):
|
||||
"""Process file pickup and decryption."""
|
||||
pickup_password = request.form.get('pickup_password')
|
||||
enc_password = request.form.get('enc_password')
|
||||
totp_code = request.form.get('totp_code')
|
||||
|
||||
if not pickup_password or not enc_password:
|
||||
flash("Missing fields")
|
||||
@@ -283,17 +399,57 @@ def handle_file_pickup(request, meta_path, enc_path, file_id):
|
||||
flash("Incorrect pickup password")
|
||||
return redirect(request.url)
|
||||
|
||||
# Check 2FA if required
|
||||
if meta.get('require_2fa', False):
|
||||
if not totp_code:
|
||||
flash("2FA code is required")
|
||||
return redirect(request.url)
|
||||
|
||||
totp_secret = meta.get('totp_secret')
|
||||
if not totp_secret:
|
||||
flash("2FA configuration error")
|
||||
return redirect(request.url)
|
||||
|
||||
totp = pyotp.TOTP(totp_secret)
|
||||
if not totp.verify(totp_code, valid_window=1): # Allow 1 window tolerance for clock drift
|
||||
flash("Invalid 2FA code")
|
||||
return redirect(request.url)
|
||||
|
||||
# Check if this is an algorithm-based encryption or legacy AESGCM
|
||||
algorithm = meta.get('algorithm')
|
||||
|
||||
try:
|
||||
if algorithm and algorithm in AVAILABLE_ALGORITHMS:
|
||||
# Use the new algorithm-based decryption
|
||||
algo_config = AVAILABLE_ALGORITHMS[algorithm]
|
||||
module = algo_config["module"]
|
||||
|
||||
# Create temporary file for decryption
|
||||
temp_dec_path = os.path.join(UPLOAD_FOLDER, f"temp_decrypt_{secrets.token_urlsafe(8)}")
|
||||
try:
|
||||
# Decrypt file using the correct API (in_path, out_path, password)
|
||||
module.decrypt_file(enc_path, temp_dec_path, enc_password)
|
||||
|
||||
# Read decrypted data
|
||||
with open(temp_dec_path, 'rb') as f:
|
||||
decrypted = f.read()
|
||||
finally:
|
||||
# Clean up temp file
|
||||
if os.path.exists(temp_dec_path):
|
||||
os.remove(temp_dec_path)
|
||||
else:
|
||||
# Legacy AESGCM decryption for backward compatibility
|
||||
with open(enc_path, 'rb') as f:
|
||||
enc_data = f.read()
|
||||
salt, nonce, ct = enc_data[:16], enc_data[16:28], enc_data[28:]
|
||||
key = derive_key(enc_password, salt)
|
||||
|
||||
try:
|
||||
decrypted = AESGCM(key).decrypt(nonce, ct, None)
|
||||
except Exception:
|
||||
flash("Decryption failed")
|
||||
|
||||
except Exception as e:
|
||||
flash(f"Decryption failed: {str(e)}")
|
||||
return redirect(request.url)
|
||||
|
||||
# Clean up files after successful decryption
|
||||
os.remove(meta_path)
|
||||
os.remove(enc_path)
|
||||
log_admin_event(f"File {file_id} downloaded and deleted.")
|
||||
@@ -306,13 +462,11 @@ def handle_file_pickup(request, meta_path, enc_path, file_id):
|
||||
response = send_file(
|
||||
io.BytesIO(decrypted),
|
||||
as_attachment=True,
|
||||
|
||||
download_name=original_name,
|
||||
mimetype='application/octet-stream'
|
||||
)
|
||||
|
||||
# Add headers for better mobile compatibility
|
||||
|
||||
response.headers['Content-Disposition'] = f'attachment; filename="{original_name}"'
|
||||
response.headers['Content-Type'] = 'application/octet-stream'
|
||||
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
|
||||
@@ -321,6 +475,83 @@ def handle_file_pickup(request, meta_path, enc_path, file_id):
|
||||
|
||||
return response
|
||||
|
||||
# ===== 2FA QR Code Routes =====
|
||||
@app.route("/admin-qr")
|
||||
def admin_qr_code():
|
||||
"""Generate QR code for admin 2FA setup."""
|
||||
if not session.get("admin_logged_in"):
|
||||
return redirect(url_for("admin_login"))
|
||||
|
||||
totp_secret = get_admin_totp_secret()
|
||||
if not totp_secret:
|
||||
return "2FA not enabled for admin", 400
|
||||
|
||||
# Generate TOTP URI for QR code
|
||||
totp_uri = pyotp.totp.TOTP(totp_secret).provisioning_uri(
|
||||
name="admin",
|
||||
issuer_name="PacCrypt Admin"
|
||||
)
|
||||
|
||||
# Generate QR code
|
||||
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
||||
qr.add_data(totp_uri)
|
||||
qr.make(fit=True)
|
||||
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
|
||||
# Save to BytesIO
|
||||
img_buffer = BytesIO()
|
||||
img.save(img_buffer, format='PNG')
|
||||
img_buffer.seek(0)
|
||||
|
||||
response = make_response(img_buffer.getvalue())
|
||||
response.headers['Content-Type'] = 'image/png'
|
||||
response.headers['Content-Disposition'] = 'inline; filename="admin_2fa_qr.png"'
|
||||
return response
|
||||
|
||||
@app.route("/qr/<file_id>")
|
||||
def generate_qr_code(file_id):
|
||||
"""Generate QR code for 2FA setup."""
|
||||
meta_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.json")
|
||||
|
||||
if not os.path.exists(meta_path):
|
||||
return "File not found", 404
|
||||
|
||||
with open(meta_path, 'r') as f:
|
||||
meta = json.load(f)
|
||||
|
||||
if not meta.get('require_2fa', False):
|
||||
return "2FA not enabled for this file", 400
|
||||
|
||||
totp_secret = meta.get('totp_secret')
|
||||
service_name = meta.get('service_name', 'PacCrypt File')
|
||||
|
||||
if not totp_secret:
|
||||
return "TOTP secret not found", 400
|
||||
|
||||
# Generate TOTP URI for QR code
|
||||
totp_uri = pyotp.totp.TOTP(totp_secret).provisioning_uri(
|
||||
name=file_id,
|
||||
issuer_name=service_name
|
||||
)
|
||||
|
||||
# Generate QR code
|
||||
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
||||
qr.add_data(totp_uri)
|
||||
qr.make(fit=True)
|
||||
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
|
||||
# Save to BytesIO
|
||||
img_buffer = BytesIO()
|
||||
img.save(img_buffer, format='PNG')
|
||||
img_buffer.seek(0)
|
||||
|
||||
response = make_response(img_buffer.getvalue())
|
||||
response.headers['Content-Type'] = 'image/png'
|
||||
response.headers['Content-Disposition'] = f'inline; filename="{file_id}_qr.png"'
|
||||
return response
|
||||
|
||||
# ===== Admin Routes =====
|
||||
@app.route("/admin-logs")
|
||||
def admin_logs():
|
||||
@@ -396,9 +627,12 @@ def admin_setup():
|
||||
if request.method == "POST":
|
||||
u = request.form.get("username")
|
||||
p = request.form.get("password")
|
||||
enable_2fa = request.form.get("enable_2fa") == "on"
|
||||
if u and p:
|
||||
encrypt_creds(u, p)
|
||||
totp_secret = pyotp.random_base32() if enable_2fa else None
|
||||
encrypt_creds(u, p, totp_secret)
|
||||
session["admin_logged_in"] = True
|
||||
session["admin_2fa_setup"] = enable_2fa
|
||||
return redirect(url_for("admin_page"))
|
||||
flash("Both fields required")
|
||||
return render_template("admin_setup.html")
|
||||
@@ -409,14 +643,19 @@ def admin_login():
|
||||
if request.method == "POST":
|
||||
u = request.form.get("username")
|
||||
p = request.form.get("password")
|
||||
if check_creds(u, p):
|
||||
totp_code = request.form.get("totp_code")
|
||||
|
||||
if check_creds(u, p, totp_code):
|
||||
session["admin_logged_in"] = True
|
||||
log_admin_event("Admin login successful.")
|
||||
return redirect(url_for("admin_page"))
|
||||
else:
|
||||
log_admin_event("Admin login failed.")
|
||||
flash("Incorrect credentials")
|
||||
return render_template("admin_login.html")
|
||||
flash("Incorrect credentials or 2FA code")
|
||||
|
||||
# Check if 2FA is enabled for the UI
|
||||
requires_2fa = get_admin_2fa_status()
|
||||
return render_template("admin_login.html", requires_2fa=requires_2fa)
|
||||
|
||||
@app.route("/admin-logout")
|
||||
def admin_logout():
|
||||
@@ -455,7 +694,10 @@ def admin_page():
|
||||
"debug_mode": app.debug
|
||||
}
|
||||
|
||||
return render_template("admin.html", routes=routes, server_info=server_info)
|
||||
# Get 2FA status for UI
|
||||
tfa_enabled = get_admin_2fa_status()
|
||||
|
||||
return render_template("admin.html", routes=routes, server_info=server_info, tfa_enabled=tfa_enabled)
|
||||
|
||||
|
||||
|
||||
@@ -557,6 +799,80 @@ def admin_change_password():
|
||||
print("[ERROR] Password change failed:", e)
|
||||
return redirect(url_for("admin_page"))
|
||||
|
||||
@app.route("/admin-enable-2fa", methods=["POST"])
|
||||
def admin_enable_2fa():
|
||||
"""Enable 2FA for admin account."""
|
||||
if not session.get("admin_logged_in"):
|
||||
return redirect(url_for("admin_login"))
|
||||
|
||||
try:
|
||||
key = load_admin_key()
|
||||
cipher = Fernet(key)
|
||||
with open(ADMIN_CRED_FILE, 'rb') as file:
|
||||
decrypted = cipher.decrypt(file.read())
|
||||
creds = json.loads(decrypted)
|
||||
|
||||
# Generate new TOTP secret
|
||||
totp_secret = pyotp.random_base32()
|
||||
creds["totp_secret"] = totp_secret
|
||||
creds["2fa_enabled"] = True
|
||||
|
||||
encrypted = cipher.encrypt(json.dumps(creds).encode())
|
||||
with open(ADMIN_CRED_FILE, 'wb') as file:
|
||||
file.write(encrypted)
|
||||
|
||||
log_admin_event("Admin 2FA enabled.")
|
||||
flash("2FA enabled successfully. Scan the QR code with your authenticator app.", "2fa-feedback")
|
||||
return redirect(url_for("admin_page"))
|
||||
|
||||
except Exception as e:
|
||||
flash("Failed to enable 2FA")
|
||||
print("[ERROR] 2FA enable failed:", e)
|
||||
return redirect(url_for("admin_page"))
|
||||
|
||||
@app.route("/admin-disable-2fa", methods=["POST"])
|
||||
def admin_disable_2fa():
|
||||
"""Disable 2FA for admin account."""
|
||||
if not session.get("admin_logged_in"):
|
||||
return redirect(url_for("admin_login"))
|
||||
|
||||
totp_code = request.form.get("totp_code")
|
||||
if not totp_code:
|
||||
flash("2FA code required to disable 2FA")
|
||||
return redirect(url_for("admin_page"))
|
||||
|
||||
try:
|
||||
key = load_admin_key()
|
||||
cipher = Fernet(key)
|
||||
with open(ADMIN_CRED_FILE, 'rb') as file:
|
||||
decrypted = cipher.decrypt(file.read())
|
||||
creds = json.loads(decrypted)
|
||||
|
||||
# Verify 2FA code before disabling
|
||||
if creds.get("2fa_enabled", False):
|
||||
totp_secret = creds.get("totp_secret")
|
||||
if totp_secret:
|
||||
totp = pyotp.TOTP(totp_secret)
|
||||
if not totp.verify(totp_code, valid_window=1):
|
||||
flash("Invalid 2FA code")
|
||||
return redirect(url_for("admin_page"))
|
||||
|
||||
creds["totp_secret"] = None
|
||||
creds["2fa_enabled"] = False
|
||||
|
||||
encrypted = cipher.encrypt(json.dumps(creds).encode())
|
||||
with open(ADMIN_CRED_FILE, 'wb') as file:
|
||||
file.write(encrypted)
|
||||
|
||||
log_admin_event("Admin 2FA disabled.")
|
||||
flash("2FA disabled successfully", "2fa-feedback")
|
||||
return redirect(url_for("admin_page"))
|
||||
|
||||
except Exception as e:
|
||||
flash("Failed to disable 2FA")
|
||||
print("[ERROR] 2FA disable failed:", e)
|
||||
return redirect(url_for("admin_page"))
|
||||
|
||||
@app.route("/admin-clear-uploads", methods=["POST"])
|
||||
def admin_clear_uploads():
|
||||
"""Clear all uploaded files."""
|
||||
@@ -657,6 +973,48 @@ def admin_update_server():
|
||||
print(f"[ERROR] {error_msg}")
|
||||
return jsonify({"error": error_msg}), 500
|
||||
|
||||
@app.route("/admin-switch-dev-mode", methods=["POST"])
|
||||
def admin_switch_dev_mode():
|
||||
"""Switch server to development mode."""
|
||||
if not session.get("admin_logged_in"):
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
|
||||
try:
|
||||
script_path = os.path.join(os.path.dirname(__file__), "application_data", "control_scripts", "restart_dev.py")
|
||||
|
||||
if not os.path.exists(script_path):
|
||||
return jsonify({"error": "Development restart script not found"}), 404
|
||||
|
||||
# Execute the restart script
|
||||
subprocess.Popen(["python", script_path])
|
||||
|
||||
return jsonify({"message": "Switching to development mode... Server will restart momentarily."}), 200
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to switch to dev mode: {str(e)}"
|
||||
print(f"[ERROR] {error_msg}")
|
||||
return jsonify({"error": error_msg}), 500
|
||||
|
||||
@app.route("/admin-switch-prod-mode", methods=["POST"])
|
||||
def admin_switch_prod_mode():
|
||||
"""Switch server to production mode."""
|
||||
if not session.get("admin_logged_in"):
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
|
||||
try:
|
||||
script_path = os.path.join(os.path.dirname(__file__), "application_data", "control_scripts", "restart_prod.py")
|
||||
|
||||
if not os.path.exists(script_path):
|
||||
return jsonify({"error": "Production restart script not found"}), 404
|
||||
|
||||
# Execute the restart script
|
||||
subprocess.Popen(["python", script_path])
|
||||
|
||||
return jsonify({"message": "Switching to production mode... Server will restart momentarily."}), 200
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to switch to prod mode: {str(e)}"
|
||||
print(f"[ERROR] {error_msg}")
|
||||
return jsonify({"error": error_msg}), 500
|
||||
|
||||
# ===== Sitemap and Robots =====
|
||||
@app.route("/sitemap", methods=["GET"])
|
||||
def sitemap():
|
||||
@@ -689,6 +1047,43 @@ def robots_txt():
|
||||
return "\n".join(lines), 200, {"Content-Type": "text/plain"}
|
||||
|
||||
# ===== API Endpoints =====
|
||||
@app.route("/api/algorithms", methods=["GET"])
|
||||
def api_algorithms():
|
||||
"""Get list of available encryption algorithms."""
|
||||
algorithms = {}
|
||||
for key, config in AVAILABLE_ALGORITHMS.items():
|
||||
algorithms[key] = {
|
||||
"name": config["name"],
|
||||
"description": config["description"],
|
||||
"supports_text": config["supports_text"],
|
||||
"supports_file": config["supports_file"],
|
||||
"requires_keypair": config.get("requires_keypair", False)
|
||||
}
|
||||
return jsonify(algorithms=algorithms)
|
||||
|
||||
@app.route("/api/generate-keypair", methods=["POST"])
|
||||
def api_generate_keypair():
|
||||
"""Generate RSA key pair for hybrid algorithms."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
algorithm = data.get("algorithm", "rsa_hybrid")
|
||||
|
||||
if algorithm not in AVAILABLE_ALGORITHMS:
|
||||
return jsonify({"error": "Invalid algorithm"}), 400
|
||||
|
||||
if not AVAILABLE_ALGORITHMS[algorithm].get("requires_keypair"):
|
||||
return jsonify({"error": "Algorithm does not require key pairs"}), 400
|
||||
|
||||
module = AVAILABLE_ALGORITHMS[algorithm]["module"]
|
||||
private_key, public_key = module.generate_key_pair()
|
||||
|
||||
return jsonify({
|
||||
"private_key": private_key.decode() if isinstance(private_key, bytes) else private_key,
|
||||
"public_key": public_key.decode() if isinstance(public_key, bytes) else public_key
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/api/encrypt", methods=["POST"])
|
||||
def api_encrypt():
|
||||
try:
|
||||
@@ -697,38 +1092,71 @@ def api_encrypt():
|
||||
data = request.get_json()
|
||||
message = data.get("message", "")
|
||||
password = data.get("password", "")
|
||||
if not message or not password:
|
||||
return jsonify({"error": "Missing message or password"}), 400
|
||||
algorithm = data.get("algorithm", "aes_gcm")
|
||||
public_key = data.get("public_key", "")
|
||||
|
||||
salt = os.urandom(16)
|
||||
nonce = os.urandom(12)
|
||||
key = derive_key(password, salt)
|
||||
ciphertext = AESGCM(key).encrypt(nonce, message.encode(), None)
|
||||
encrypted_combined = salt + nonce + ciphertext
|
||||
encrypted_b64 = base64.b64encode(encrypted_combined).decode()
|
||||
if not message:
|
||||
return jsonify({"error": "Missing message"}), 400
|
||||
|
||||
return jsonify({"result": encrypted_b64})
|
||||
if algorithm not in AVAILABLE_ALGORITHMS:
|
||||
return jsonify({"error": "Invalid algorithm"}), 400
|
||||
|
||||
algo_config = AVAILABLE_ALGORITHMS[algorithm]
|
||||
if not algo_config["supports_text"]:
|
||||
return jsonify({"error": "Algorithm does not support text operations"}), 400
|
||||
|
||||
module = algo_config["module"]
|
||||
|
||||
if algo_config.get("requires_keypair"):
|
||||
if not public_key:
|
||||
return jsonify({"error": "Public key required for this algorithm"}), 400
|
||||
encrypted = module.encrypt_text(message, public_key, algorithm.replace("_hybrid", ""))
|
||||
else:
|
||||
if not password:
|
||||
return jsonify({"error": "Password required"}), 400
|
||||
encrypted = module.encrypt_text(message, password)
|
||||
|
||||
return jsonify({"result": encrypted, "algorithm": algorithm})
|
||||
|
||||
# File encryption
|
||||
if "file" in request.files and "enc_password" in request.form:
|
||||
uploaded_file = request.files["file"]
|
||||
password = request.form["enc_password"]
|
||||
algorithm = request.form.get("algorithm", "aes_cbc")
|
||||
|
||||
if algorithm not in AVAILABLE_ALGORITHMS:
|
||||
return jsonify({"error": "Invalid algorithm"}), 400
|
||||
|
||||
algo_config = AVAILABLE_ALGORITHMS[algorithm]
|
||||
if not algo_config["supports_file"]:
|
||||
return jsonify({"error": "Algorithm does not support file operations"}), 400
|
||||
|
||||
file_data = uploaded_file.read()
|
||||
salt = os.urandom(16)
|
||||
nonce = os.urandom(12)
|
||||
key = derive_key(password, salt)
|
||||
ct = AESGCM(key).encrypt(nonce, file_data, None)
|
||||
encrypted_binary = salt + nonce + ct
|
||||
temp_in = f"temp_in_{secrets.token_urlsafe(8)}"
|
||||
temp_out = f"temp_out_{secrets.token_urlsafe(8)}"
|
||||
|
||||
output_filename = f"{uploaded_file.filename}.encrypted"
|
||||
try:
|
||||
with open(temp_in, 'wb') as f:
|
||||
f.write(file_data)
|
||||
|
||||
module = algo_config["module"]
|
||||
module.encrypt_file(temp_in, temp_out, password)
|
||||
|
||||
with open(temp_out, 'rb') as f:
|
||||
encrypted_data = f.read()
|
||||
|
||||
output_filename = f"{uploaded_file.filename}.{algorithm}.encrypted"
|
||||
|
||||
return send_file(
|
||||
BytesIO(encrypted_binary),
|
||||
BytesIO(encrypted_data),
|
||||
as_attachment=True,
|
||||
download_name=output_filename,
|
||||
mimetype="application/octet-stream"
|
||||
)
|
||||
finally:
|
||||
for temp_file in [temp_in, temp_out]:
|
||||
if os.path.exists(temp_file):
|
||||
os.remove(temp_file)
|
||||
|
||||
return jsonify({"error": "Missing or invalid input"}), 400
|
||||
|
||||
@@ -743,38 +1171,88 @@ def api_decrypt():
|
||||
data = request.get_json()
|
||||
encrypted_b64 = data.get("message", "")
|
||||
password = data.get("password", "")
|
||||
if not encrypted_b64 or not password:
|
||||
return jsonify({"error": "Missing message or password"}), 400
|
||||
algorithm = data.get("algorithm", "aes_gcm")
|
||||
private_key = data.get("private_key", "")
|
||||
|
||||
raw = base64.b64decode(encrypted_b64)
|
||||
salt, nonce, ct = raw[:16], raw[16:28], raw[28:]
|
||||
key = derive_key(password, salt)
|
||||
plaintext = AESGCM(key).decrypt(nonce, ct, None)
|
||||
if not encrypted_b64:
|
||||
return jsonify({"error": "Missing encrypted message"}), 400
|
||||
|
||||
return jsonify({"result": plaintext.decode()})
|
||||
if algorithm not in AVAILABLE_ALGORITHMS:
|
||||
return jsonify({"error": "Invalid algorithm"}), 400
|
||||
|
||||
algo_config = AVAILABLE_ALGORITHMS[algorithm]
|
||||
if not algo_config["supports_text"]:
|
||||
return jsonify({"error": "Algorithm does not support text operations"}), 400
|
||||
|
||||
module = algo_config["module"]
|
||||
|
||||
if algo_config.get("requires_keypair"):
|
||||
if not private_key:
|
||||
return jsonify({"error": "Private key required for this algorithm"}), 400
|
||||
plaintext = module.decrypt_text(encrypted_b64, private_key)
|
||||
else:
|
||||
if not password:
|
||||
return jsonify({"error": "Password required"}), 400
|
||||
plaintext = module.decrypt_text(encrypted_b64, password)
|
||||
|
||||
return jsonify({"result": plaintext})
|
||||
|
||||
# File decryption
|
||||
if "file" in request.files and "enc_password" in request.form:
|
||||
uploaded_file = request.files["file"]
|
||||
password = request.form["enc_password"]
|
||||
|
||||
encrypted_data = uploaded_file.read()
|
||||
salt, nonce, ct = encrypted_data[:16], encrypted_data[16:28], encrypted_data[28:]
|
||||
key = derive_key(password, salt)
|
||||
decrypted = AESGCM(key).decrypt(nonce, ct, None)
|
||||
|
||||
# Try to determine algorithm from filename
|
||||
filename = uploaded_file.filename
|
||||
if filename.endswith(".encrypted"):
|
||||
algorithm = "aes_cbc" # default
|
||||
|
||||
for algo_name in AVAILABLE_ALGORITHMS.keys():
|
||||
if f".{algo_name}.encrypted" in filename:
|
||||
algorithm = algo_name
|
||||
break
|
||||
|
||||
# Allow override
|
||||
algorithm = request.form.get("algorithm", algorithm)
|
||||
|
||||
if algorithm not in AVAILABLE_ALGORITHMS:
|
||||
return jsonify({"error": "Invalid algorithm"}), 400
|
||||
|
||||
algo_config = AVAILABLE_ALGORITHMS[algorithm]
|
||||
if not algo_config["supports_file"]:
|
||||
return jsonify({"error": "Algorithm does not support file operations"}), 400
|
||||
|
||||
encrypted_data = uploaded_file.read()
|
||||
temp_in = f"temp_in_{secrets.token_urlsafe(8)}"
|
||||
temp_out = f"temp_out_{secrets.token_urlsafe(8)}"
|
||||
|
||||
try:
|
||||
with open(temp_in, 'wb') as f:
|
||||
f.write(encrypted_data)
|
||||
|
||||
module = algo_config["module"]
|
||||
module.decrypt_file(temp_in, temp_out, password)
|
||||
|
||||
with open(temp_out, 'rb') as f:
|
||||
decrypted_data = f.read()
|
||||
|
||||
# Clean up filename
|
||||
if f".{algorithm}.encrypted" in filename:
|
||||
filename = filename.replace(f".{algorithm}.encrypted", "")
|
||||
elif filename.endswith(".encrypted"):
|
||||
filename = filename[:-10]
|
||||
else:
|
||||
filename = f"decrypted_{filename}"
|
||||
|
||||
return send_file(
|
||||
BytesIO(decrypted),
|
||||
BytesIO(decrypted_data),
|
||||
as_attachment=True,
|
||||
download_name=filename,
|
||||
mimetype="application/octet-stream"
|
||||
)
|
||||
finally:
|
||||
for temp_file in [temp_in, temp_out]:
|
||||
if os.path.exists(temp_file):
|
||||
os.remove(temp_file)
|
||||
|
||||
return jsonify({"error": "Missing or invalid input"}), 400
|
||||
|
||||
@@ -786,26 +1264,36 @@ def api_pacshare():
|
||||
try:
|
||||
enc_password = request.form.get("enc_password")
|
||||
pickup_password = request.form.get("pickup_password")
|
||||
algorithm = request.form.get("algorithm", "aes_cbc") # Default to AES-CBC
|
||||
enable_2fa = request.form.get("enable_2fa") == "on" # Check if 2FA checkbox is checked
|
||||
file = request.files.get("file")
|
||||
|
||||
if not file or not enc_password or not pickup_password:
|
||||
return jsonify({"error": "Missing file or fields"}), 400
|
||||
|
||||
file_data = file.read()
|
||||
filename = secure_filename(file.filename)
|
||||
# Validate algorithm
|
||||
if algorithm not in AVAILABLE_ALGORITHMS:
|
||||
return jsonify({"error": "Invalid algorithm"}), 400
|
||||
|
||||
salt = os.urandom(16)
|
||||
key = derive_key(enc_password, salt)
|
||||
nonce = os.urandom(12)
|
||||
ct = AESGCM(key).encrypt(nonce, file_data, None)
|
||||
encrypted = salt + nonce + ct
|
||||
algo_config = AVAILABLE_ALGORITHMS[algorithm]
|
||||
if not algo_config["supports_file"]:
|
||||
return jsonify({"error": "Algorithm does not support file operations"}), 400
|
||||
|
||||
filename = secure_filename(file.filename)
|
||||
temp_path = os.path.join(UPLOAD_FOLDER, f"temp_{secrets.token_urlsafe(8)}_{filename}")
|
||||
file.save(temp_path)
|
||||
|
||||
try:
|
||||
# Use the selected algorithm for encryption
|
||||
module = algo_config["module"]
|
||||
|
||||
file_id = secrets.token_urlsafe(24)
|
||||
enc_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.enc")
|
||||
encrypted_filename = f"{file_id}.{algorithm}.encrypted"
|
||||
enc_path = os.path.join(UPLOAD_FOLDER, encrypted_filename)
|
||||
meta_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.json")
|
||||
|
||||
with open(enc_path, "wb") as f:
|
||||
f.write(encrypted)
|
||||
# Encrypt file using the correct API (in_path, out_path, password)
|
||||
module.encrypt_file(temp_path, enc_path, enc_password)
|
||||
|
||||
encrypted_filename = encrypt_filename(filename, enc_password)
|
||||
|
||||
@@ -814,14 +1302,36 @@ def api_pacshare():
|
||||
hashlib.sha256(pickup_password.encode()).digest()
|
||||
).decode(),
|
||||
'original_name': encrypted_filename,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
'algorithm': algorithm, # Store algorithm used for decryption
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'require_2fa': enable_2fa
|
||||
}
|
||||
|
||||
# Generate TOTP secret if 2FA is enabled
|
||||
if enable_2fa:
|
||||
totp_secret = pyotp.random_base32()
|
||||
meta['totp_secret'] = totp_secret
|
||||
meta['service_name'] = f"PacCrypt File: {filename[:20]}..."
|
||||
|
||||
with open(meta_path, "w") as f:
|
||||
json.dump(meta, f)
|
||||
|
||||
pickup_url = request.host_url.rstrip('/') + url_for('pickup_file', file_id=file_id)
|
||||
return jsonify({"pickup_url": pickup_url})
|
||||
response_data = {"pickup_url": pickup_url}
|
||||
|
||||
# If 2FA is enabled, also return QR code URL for immediate setup
|
||||
if enable_2fa:
|
||||
qr_url = request.host_url.rstrip('/') + url_for('generate_qr_code', file_id=file_id)
|
||||
response_data["qr_code_url"] = qr_url
|
||||
response_data["totp_secret"] = totp_secret
|
||||
response_data["service_name"] = f"PacCrypt File: {filename[:20]}..."
|
||||
|
||||
return jsonify(response_data)
|
||||
|
||||
finally:
|
||||
# Clean up temp file
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@@ -4,6 +4,7 @@ import signal
|
||||
import time
|
||||
import sys
|
||||
import psutil
|
||||
import platform
|
||||
|
||||
APP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../app.py"))
|
||||
|
||||
@@ -16,6 +17,15 @@ def log(msg):
|
||||
def start_dev():
|
||||
env = os.environ.copy()
|
||||
env["PRODUCTION"] = "false"
|
||||
|
||||
if platform.system() == "Windows":
|
||||
return subprocess.Popen(
|
||||
["python", APP_PATH],
|
||||
env=env,
|
||||
stdout=sys.stdout,
|
||||
stderr=sys.stderr
|
||||
)
|
||||
else:
|
||||
return subprocess.Popen(
|
||||
["python3", APP_PATH],
|
||||
env=env,
|
||||
@@ -27,9 +37,16 @@ def start_dev():
|
||||
def stop_by_port(port=5000):
|
||||
for proc in psutil.process_iter(["pid", "name"]):
|
||||
try:
|
||||
for conn in proc.connections(kind="inet"):
|
||||
for conn in proc.net_connections(kind="inet"):
|
||||
if conn.laddr.port == port:
|
||||
log(f"[*] Killing process {proc.pid} using port {port}")
|
||||
if platform.system() == "Windows":
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except psutil.TimeoutExpired:
|
||||
proc.kill()
|
||||
else:
|
||||
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
||||
return
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||
@@ -39,8 +56,17 @@ def stop_by_port(port=5000):
|
||||
def main():
|
||||
log("[*] Restarting PacCrypt in DEVELOPMENT mode...")
|
||||
stop_by_port()
|
||||
time.sleep(1)
|
||||
start_dev()
|
||||
time.sleep(2)
|
||||
proc = start_dev()
|
||||
if proc:
|
||||
log(f"[*] Started development server with PID {proc.pid}")
|
||||
try:
|
||||
proc.wait()
|
||||
except KeyboardInterrupt:
|
||||
log("[*] Interrupted, stopping server...")
|
||||
stop_by_port()
|
||||
else:
|
||||
log("[!] Failed to start development server")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -4,12 +4,22 @@ import signal
|
||||
import time
|
||||
import sys
|
||||
import psutil
|
||||
import platform
|
||||
|
||||
APP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../app.py"))
|
||||
|
||||
def start_prod():
|
||||
env = os.environ.copy()
|
||||
env["PRODUCTION"] = "true"
|
||||
|
||||
if platform.system() == "Windows":
|
||||
return subprocess.Popen(
|
||||
["python", APP_PATH],
|
||||
env=env,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
else:
|
||||
return subprocess.Popen(
|
||||
["python3", APP_PATH],
|
||||
env=env,
|
||||
@@ -21,9 +31,16 @@ def start_prod():
|
||||
def stop_by_port(port=5000):
|
||||
for proc in psutil.process_iter(["pid", "name"]):
|
||||
try:
|
||||
for conn in proc.connections(kind="inet"):
|
||||
for conn in proc.net_connections(kind="inet"):
|
||||
if conn.laddr.port == port:
|
||||
print(f"[*] Killing process {proc.pid} using port {port}")
|
||||
if platform.system() == "Windows":
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except psutil.TimeoutExpired:
|
||||
proc.kill()
|
||||
else:
|
||||
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
||||
return
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||
@@ -33,8 +50,17 @@ def stop_by_port(port=5000):
|
||||
def main():
|
||||
print("[*] Restarting PacCrypt in PRODUCTION mode with Waitress...")
|
||||
stop_by_port()
|
||||
time.sleep(1)
|
||||
start_prod()
|
||||
time.sleep(2)
|
||||
proc = start_prod()
|
||||
if proc:
|
||||
print(f"[*] Started production server with PID {proc.pid}")
|
||||
try:
|
||||
proc.wait()
|
||||
except KeyboardInterrupt:
|
||||
print("[*] Interrupted, stopping server...")
|
||||
stop_by_port()
|
||||
else:
|
||||
print("[!] Failed to start production server")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -2,6 +2,7 @@ import os
|
||||
import subprocess
|
||||
import time
|
||||
import sys
|
||||
import platform
|
||||
|
||||
APP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../app.py"))
|
||||
|
||||
@@ -14,6 +15,15 @@ def log(msg):
|
||||
def start_dev():
|
||||
env = os.environ.copy()
|
||||
env["PRODUCTION"] = "false"
|
||||
|
||||
if platform.system() == "Windows":
|
||||
return subprocess.Popen(
|
||||
["python", APP_PATH],
|
||||
env=env,
|
||||
stdout=sys.stdout,
|
||||
stderr=sys.stderr
|
||||
)
|
||||
else:
|
||||
return subprocess.Popen(
|
||||
["python3", APP_PATH],
|
||||
env=env,
|
||||
|
||||
@@ -2,12 +2,22 @@ import os
|
||||
import subprocess
|
||||
import time
|
||||
import sys
|
||||
import platform
|
||||
|
||||
APP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../app.py"))
|
||||
|
||||
def start_prod():
|
||||
env = os.environ.copy()
|
||||
env["PRODUCTION"] = "true"
|
||||
|
||||
if platform.system() == "Windows":
|
||||
return subprocess.Popen(
|
||||
["python", APP_PATH],
|
||||
env=env,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
else:
|
||||
return subprocess.Popen(
|
||||
["python3", APP_PATH],
|
||||
env=env,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import psutil
|
||||
import os
|
||||
import signal
|
||||
import platform
|
||||
|
||||
DEBUG = True
|
||||
|
||||
@@ -11,9 +12,16 @@ def log(msg):
|
||||
def stop_by_port(port=5000):
|
||||
for proc in psutil.process_iter(["pid", "name"]):
|
||||
try:
|
||||
for conn in proc.connections(kind="inet"):
|
||||
for conn in proc.net_connections(kind="inet"):
|
||||
if conn.laddr.port == port:
|
||||
log(f"[*] Killing process {proc.pid} using port {port}")
|
||||
if platform.system() == "Windows":
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except psutil.TimeoutExpired:
|
||||
proc.kill()
|
||||
else:
|
||||
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
||||
return
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||
|
||||
@@ -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()
|
||||
|
||||
const SALT_LENGTH = 16;
|
||||
const IV_LENGTH = 12;
|
||||
const KEY_LENGTH = 256;
|
||||
/**
|
||||
* File operations using the new Python backend APIs
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
const file = fileInput.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const algorithm = document.getElementById("algorithm")?.value || "aes_cbc";
|
||||
|
||||
try {
|
||||
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
||||
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
||||
const key = await deriveKey(password, salt);
|
||||
const fileBuffer = new Uint8Array(await file.arrayBuffer());
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('enc_password', password);
|
||||
formData.append('algorithm', algorithm);
|
||||
|
||||
const ciphertext = await crypto.subtle.encrypt(
|
||||
{ name: "AES-GCM", iv },
|
||||
key,
|
||||
fileBuffer
|
||||
);
|
||||
const response = await fetch('/api/encrypt', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const ctBytes = new Uint8Array(ciphertext);
|
||||
const result = new Uint8Array(salt.length + iv.length + ctBytes.length);
|
||||
result.set(salt);
|
||||
result.set(iv, salt.length);
|
||||
result.set(ctBytes, salt.length + iv.length);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const blob = new Blob([result], { type: "application/octet-stream" });
|
||||
// Download the encrypted file
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = file.name + ".encrypted";
|
||||
a.download = `${file.name}.${algorithm}.encrypted`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
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) {
|
||||
const file = fileInput.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const data = new Uint8Array(await file.arrayBuffer());
|
||||
const salt = data.slice(0, SALT_LENGTH);
|
||||
const iv = data.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
|
||||
const ciphertext = data.slice(SALT_LENGTH + IV_LENGTH);
|
||||
const key = await deriveKey(password, salt);
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('enc_password', password);
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: "AES-GCM", iv },
|
||||
key,
|
||||
ciphertext
|
||||
);
|
||||
const response = await fetch('/api/decrypt', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
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 a = document.createElement("a");
|
||||
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);
|
||||
a.click();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+295
-46
@@ -14,9 +14,9 @@ export function setupUI() {
|
||||
initializeEventListeners();
|
||||
}
|
||||
|
||||
function initializeEventListeners() {
|
||||
async function initializeEventListeners() {
|
||||
const elements = {
|
||||
encryptionType: document.getElementById("encryption-type"),
|
||||
algorithm: document.getElementById("algorithm"),
|
||||
inputText: document.getElementById("input-text"),
|
||||
form: document.getElementById("crypto-form"),
|
||||
removeFileBtn: document.getElementById("remove-file-btn"),
|
||||
@@ -26,22 +26,30 @@ function initializeEventListeners() {
|
||||
copyOutputBtn: document.getElementById("copy-output-btn"),
|
||||
toggleSwitch: document.getElementById("operation-toggle"),
|
||||
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)) {
|
||||
setupElementListeners(elements);
|
||||
}
|
||||
await loadAvailableAlgorithms();
|
||||
// Initialize algorithm options on page load after algorithms are loaded
|
||||
toggleAlgorithmOptions();
|
||||
}
|
||||
|
||||
function validateElements(elements) {
|
||||
return elements.encryptionType && elements.inputText && elements.form &&
|
||||
return elements.algorithm && elements.inputText && elements.form &&
|
||||
elements.removeFileBtn && elements.clearAllBtn && elements.generateBtn &&
|
||||
elements.copyPasswordBtn && elements.toggleSwitch;
|
||||
}
|
||||
|
||||
function setupElementListeners(elements) {
|
||||
elements.encryptionType.addEventListener("change", toggleEncryptionOptions);
|
||||
elements.algorithm?.addEventListener("change", toggleAlgorithmOptions);
|
||||
elements.inputText.addEventListener("input", handleInputChange);
|
||||
elements.form.addEventListener("submit", handleSubmit);
|
||||
elements.removeFileBtn.addEventListener("click", removeFile);
|
||||
@@ -53,6 +61,13 @@ function setupElementListeners(elements) {
|
||||
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");
|
||||
if (fileInput) {
|
||||
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() {
|
||||
const fileInput = document.getElementById("file-input");
|
||||
const textValue = document.getElementById("input-text")?.value.trim();
|
||||
const isAdvanced = document.getElementById("encryption-type")?.value === "advanced";
|
||||
|
||||
const textSection = document.getElementById("text-section");
|
||||
const fileSection = document.getElementById("file-section");
|
||||
@@ -131,23 +116,39 @@ function toggleInputMode() {
|
||||
const fileSelected = fileInput.files.length > 0;
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
async function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const encryptionType = document.getElementById("encryption-type")?.value;
|
||||
const algorithm = document.getElementById("algorithm")?.value;
|
||||
const password = document.getElementById("password")?.value;
|
||||
const fileInput = document.getElementById("file-input");
|
||||
const isDecrypt = document.getElementById("operation-toggle").checked;
|
||||
const operation = isDecrypt ? "decrypt" : "encrypt";
|
||||
|
||||
if (!encryptionType || !fileInput) return;
|
||||
if (!algorithm || !fileInput) return;
|
||||
|
||||
if (encryptionType === "advanced" && !password) {
|
||||
return alert("Password is required for advanced encryption.");
|
||||
// Check requirements based on algorithm
|
||||
let requiresKeypair = false;
|
||||
if (window.availableAlgorithms && window.availableAlgorithms[algorithm]) {
|
||||
requiresKeypair = window.availableAlgorithms[algorithm].requires_keypair || false;
|
||||
} else {
|
||||
requiresKeypair = algorithm.includes("hybrid");
|
||||
}
|
||||
|
||||
if (requiresKeypair) {
|
||||
const globalKeys = window.getGlobalKeys ? window.getGlobalKeys() : {};
|
||||
if (operation === "encrypt" && !globalKeys.publicKey) {
|
||||
return alert("Please load a public key in the Key Pairs Management section for encryption with this algorithm.");
|
||||
}
|
||||
if (operation === "decrypt" && !globalKeys.privateKey) {
|
||||
return alert("Please load a private key in the Key Pairs Management section for decryption with this algorithm.");
|
||||
}
|
||||
} else if (!password) {
|
||||
return alert("Password is required for this algorithm.");
|
||||
}
|
||||
|
||||
if (fileInput.files.length > 0) {
|
||||
@@ -156,19 +157,39 @@ async function handleSubmit(event) {
|
||||
: 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 = {
|
||||
"encryption-type": encryptionType,
|
||||
operation: operation,
|
||||
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 {
|
||||
const response = await fetch("/", {
|
||||
const endpoint = operation === "encrypt" ? "/api/encrypt" : "/api/decrypt";
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
@@ -178,8 +199,12 @@ async function handleTextOperation(encryptionType, operation, password) {
|
||||
|
||||
const outputField = document.getElementById("output-text");
|
||||
if (outputField) {
|
||||
if (data.error) {
|
||||
outputField.value = `[Error] ${data.error}`;
|
||||
} else {
|
||||
outputField.value = data.result || "[Error] No response received.";
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
alert("Error processing request: " + err.message);
|
||||
}
|
||||
@@ -328,5 +353,229 @@ function showCopyFeedback(feedbackEl) {
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// ===== Algorithm Management =====
|
||||
async function loadAvailableAlgorithms() {
|
||||
try {
|
||||
const response = await fetch('/api/algorithms');
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.algorithms) {
|
||||
// Store algorithms globally for use in other functions
|
||||
window.availableAlgorithms = data.algorithms;
|
||||
updateAlgorithmDropdown(data.algorithms);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load algorithms:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateAlgorithmDropdown(algorithms) {
|
||||
const algorithmSelect = document.getElementById('algorithm');
|
||||
const shareAlgorithmSelect = document.getElementById('share-algorithm');
|
||||
|
||||
// Update main encryption/decryption algorithm dropdown
|
||||
if (algorithmSelect) {
|
||||
algorithmSelect.innerHTML = '';
|
||||
|
||||
let firstOption = null;
|
||||
for (const [key, algo] of Object.entries(algorithms)) {
|
||||
if (algo.supports_text) {
|
||||
const option = document.createElement('option');
|
||||
option.value = key;
|
||||
option.textContent = `${algo.name}${algo.requires_keypair ? ' (requires keypair)' : ''}`;
|
||||
algorithmSelect.appendChild(option);
|
||||
|
||||
// Remember the first option (should be a non-keypair algorithm)
|
||||
if (!firstOption) {
|
||||
firstOption = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the first option is selected
|
||||
if (firstOption) {
|
||||
algorithmSelect.value = firstOption;
|
||||
}
|
||||
}
|
||||
|
||||
// Update PacShare algorithm dropdown (for file uploads)
|
||||
if (shareAlgorithmSelect) {
|
||||
shareAlgorithmSelect.innerHTML = '';
|
||||
|
||||
let firstFileOption = null;
|
||||
for (const [key, algo] of Object.entries(algorithms)) {
|
||||
if (algo.supports_file) {
|
||||
const option = document.createElement('option');
|
||||
option.value = key;
|
||||
option.textContent = `${algo.name}${algo.requires_keypair ? ' (requires keypair)' : ''}`;
|
||||
shareAlgorithmSelect.appendChild(option);
|
||||
|
||||
// Remember the first file-supporting option
|
||||
if (!firstFileOption) {
|
||||
firstFileOption = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the first file-supporting option as selected
|
||||
if (firstFileOption) {
|
||||
shareAlgorithmSelect.value = firstFileOption;
|
||||
}
|
||||
}
|
||||
|
||||
// Update Key Pairs Management dropdown
|
||||
const keypairAlgorithmSelect = document.getElementById('keypair-algorithm');
|
||||
if (keypairAlgorithmSelect) {
|
||||
// Clear existing options except the hardcoded ones
|
||||
const options = keypairAlgorithmSelect.querySelectorAll('option');
|
||||
options.forEach(option => {
|
||||
if (option.value !== 'rsa_hybrid' && option.value !== 'pqcrypto') {
|
||||
option.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Show/hide post-quantum option based on availability
|
||||
const pqOption = document.getElementById('pqcrypto-option');
|
||||
if (pqOption) {
|
||||
pqOption.style.display = algorithms.pqcrypto ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// If rsa_hybrid is not available, hide it
|
||||
const rsaOption = keypairAlgorithmSelect.querySelector('option[value="rsa_hybrid"]');
|
||||
if (rsaOption) {
|
||||
rsaOption.style.display = algorithms.rsa_hybrid ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Call toggleAlgorithmOptions after dropdown is populated
|
||||
toggleAlgorithmOptions();
|
||||
}
|
||||
|
||||
function toggleAlgorithmOptions() {
|
||||
const algorithm = document.getElementById("algorithm")?.value;
|
||||
const keypairSection = document.getElementById("keypair-section");
|
||||
const passwordInput = document.getElementById("password-input");
|
||||
|
||||
if (!algorithm) return;
|
||||
|
||||
// Check if algorithm requires keypair by looking at available algorithms data
|
||||
let requiresKeypair = false;
|
||||
if (window.availableAlgorithms && window.availableAlgorithms[algorithm]) {
|
||||
requiresKeypair = window.availableAlgorithms[algorithm].requires_keypair || false;
|
||||
} else {
|
||||
// Fallback to checking name for "hybrid"
|
||||
requiresKeypair = algorithm.includes("hybrid");
|
||||
}
|
||||
|
||||
// Show/hide keypair section only for algorithms that require it
|
||||
if (keypairSection) {
|
||||
keypairSection.style.display = requiresKeypair ? "block" : "none";
|
||||
}
|
||||
|
||||
// Show/hide password input (opposite of keypair section)
|
||||
if (passwordInput) {
|
||||
passwordInput.style.display = requiresKeypair ? "none" : "block";
|
||||
}
|
||||
|
||||
// Update key status based on global keys
|
||||
const globalKeys = window.getGlobalKeys ? window.getGlobalKeys() : {};
|
||||
const publicStatus = document.getElementById("public-key-status");
|
||||
const privateStatus = document.getElementById("private-key-status");
|
||||
|
||||
if (!requiresKeypair) {
|
||||
if (publicStatus) publicStatus.style.display = "none";
|
||||
if (privateStatus) privateStatus.style.display = "none";
|
||||
} else {
|
||||
// Show key status if keys are loaded in global store
|
||||
if (publicStatus) publicStatus.style.display = globalKeys.publicKey ? "block" : "none";
|
||||
if (privateStatus) privateStatus.style.display = globalKeys.privateKey ? "block" : "none";
|
||||
}
|
||||
}
|
||||
|
||||
// ===== File-based Key Management =====
|
||||
async function generateAndDownloadKeyPair() {
|
||||
const algorithm = document.getElementById("algorithm")?.value;
|
||||
|
||||
let requiresKeypair = false;
|
||||
if (window.availableAlgorithms && window.availableAlgorithms[algorithm]) {
|
||||
requiresKeypair = window.availableAlgorithms[algorithm].requires_keypair || false;
|
||||
} else {
|
||||
requiresKeypair = algorithm.includes("hybrid");
|
||||
}
|
||||
|
||||
if (!algorithm || !requiresKeypair) {
|
||||
alert("Key pair generation is only available for algorithms that require key pairs");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/generate-keypair', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ algorithm: algorithm })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
// Download public key
|
||||
downloadTextAsFile(data.public_key, `${algorithm}_public_key.pub`, 'text/plain');
|
||||
|
||||
// Download private key
|
||||
downloadTextAsFile(data.private_key, `${algorithm}_private_key.key`, 'text/plain');
|
||||
|
||||
alert("✅ Key pair generated and downloaded!\n\n📁 Files saved:\n• Public Key: " + `${algorithm}_public_key.pub` + "\n• Private Key: " + `${algorithm}_private_key.key` + "\n\n🔐 Use public key for encryption, private key for decryption.");
|
||||
} else {
|
||||
alert(`Error generating key pair: ${data.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function downloadTextAsFile(text, filename, mimeType) {
|
||||
const blob = new Blob([text], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function handlePublicKeyLoad(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
// Update global keys instead of window variables
|
||||
if (window.setGlobalKeys) {
|
||||
window.setGlobalKeys({ publicKey: e.target.result });
|
||||
}
|
||||
document.getElementById("public-key-status").style.display = "block";
|
||||
console.log("Public key loaded successfully and synced to global store");
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
function handlePrivateKeyLoad(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
// Update global keys instead of window variables
|
||||
if (window.setGlobalKeys) {
|
||||
window.setGlobalKeys({ privateKey: e.target.result });
|
||||
}
|
||||
document.getElementById("private-key-status").style.display = "block";
|
||||
console.log("Private key loaded successfully and synced to global store");
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
function startPacman() { }
|
||||
function exitGame() { }
|
||||
@@ -57,6 +57,8 @@
|
||||
<form action="{{ url_for('admin_settings') }}" method="GET" style="display: inline;">
|
||||
<button type="submit">Settings</button>
|
||||
</form>
|
||||
<button onclick="switchToDevMode()" style="background: #0066cc;">Switch to Dev Mode</button>
|
||||
<button onclick="switchToProdMode()" style="background: #cc6600;">Switch to Prod Mode</button>
|
||||
<button onclick="resetAdmin()" class="danger-button">Reset Admin</button>
|
||||
<button onclick="clearUploads()" class="danger-button">Clear PacShare</button>
|
||||
</div>
|
||||
@@ -85,6 +87,58 @@
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- 2FA Management Section -->
|
||||
<section id="2fa-section" class="card form-group">
|
||||
<h2>Two-Factor Authentication (2FA)</h2>
|
||||
|
||||
<!-- 2FA Feedback -->
|
||||
{% with messages = get_flashed_messages(with_categories=true, category_filter=['2fa-feedback']) %}
|
||||
{% for category, message in messages %}
|
||||
<div class="copy-feedback show">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
{% if tfa_enabled %}
|
||||
<!-- 2FA is enabled -->
|
||||
<div class="status-info">
|
||||
<p style="color: lime;">✅ 2FA is <strong>enabled</strong> for your admin account.</p>
|
||||
<p>Your account is protected with TOTP-based two-factor authentication.</p>
|
||||
</div>
|
||||
|
||||
<!-- QR Code Display -->
|
||||
<div class="form-group">
|
||||
<button type="button" onclick="toggleQRCode()" style="margin-bottom: 10px;">Show/Hide QR Code</button>
|
||||
<div id="qr-code-container" style="display: none; text-align: center;">
|
||||
<p><strong>Scan this QR code with your authenticator app:</strong></p>
|
||||
<img src="{{ url_for('admin_qr_code') }}" alt="Admin 2FA QR Code" style="max-width: 200px;" />
|
||||
<p style="font-size: 0.85em; color: #ccc;">You can re-scan this QR code if you need to set up 2FA on a new device.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Disable 2FA Form -->
|
||||
<div class="form-group">
|
||||
<form method="POST" action="{{ url_for('admin_disable_2fa') }}">
|
||||
<input type="text" name="totp_code" placeholder="Enter current 2FA code to disable" pattern="[0-9]{6}" maxlength="6" required />
|
||||
<button type="submit" class="danger-button">Disable 2FA</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<!-- 2FA is disabled -->
|
||||
<div class="status-info">
|
||||
<p style="color: #ff6b6b;">🔒 2FA is <strong>disabled</strong> for your admin account.</p>
|
||||
<p>Enable 2FA for enhanced security using authenticator apps like Google Authenticator, Authy, or Microsoft Authenticator.</p>
|
||||
</div>
|
||||
|
||||
<!-- Enable 2FA -->
|
||||
<div class="form-group">
|
||||
<form method="POST" action="{{ url_for('admin_enable_2fa') }}">
|
||||
<button type="submit">Enable 2FA</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<!-- Server Status Section -->
|
||||
<section id="server-status-section" class="card form-group">
|
||||
<h2>Server Status</h2>
|
||||
@@ -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) {
|
||||
const feedback = document.getElementById('admin-feedback');
|
||||
feedback.textContent = message;
|
||||
@@ -252,6 +358,19 @@
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function toggleQRCode() {
|
||||
const container = document.getElementById('qr-code-container');
|
||||
const button = document.querySelector('button[onclick="toggleQRCode()"]');
|
||||
|
||||
if (container.style.display === 'none') {
|
||||
container.style.display = 'block';
|
||||
button.textContent = 'Hide QR Code';
|
||||
} else {
|
||||
container.style.display = 'none';
|
||||
button.textContent = 'Show QR Code';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -44,6 +44,15 @@
|
||||
<form method="POST">
|
||||
<input type="text" name="username" placeholder="Username" required />
|
||||
<input type="password" name="password" placeholder="Password" required />
|
||||
|
||||
{% if requires_2fa %}
|
||||
<!-- 2FA Code Input -->
|
||||
<div class="form-group">
|
||||
<input type="text" name="totp_code" placeholder="2FA Code (6 digits)" pattern="[0-9]{6}" maxlength="6" required />
|
||||
<small style="color: #ccc;">Enter the 6-digit code from your authenticator app</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="button-group mt-3">
|
||||
<button type="submit">Log In</button>
|
||||
</div>
|
||||
|
||||
@@ -45,6 +45,15 @@
|
||||
<form method="POST">
|
||||
<input type="text" name="username" placeholder="Username" required />
|
||||
<input type="password" name="password" placeholder="Password" required />
|
||||
|
||||
<!-- 2FA Option -->
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" name="enable_2fa" id="enable-2fa" />
|
||||
Enable Two-Factor Authentication (2FA) - Adds extra security using TOTP apps like Google Authenticator, Authy, etc.
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="button-group mt-3">
|
||||
<button type="submit">Set Credentials</button>
|
||||
</div>
|
||||
|
||||
+364
-7
@@ -43,6 +43,55 @@
|
||||
</div>
|
||||
</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 -->
|
||||
<section id="pacman-section" class="card" style="display: none;">
|
||||
<div class="pacman-wrapper">
|
||||
@@ -59,15 +108,34 @@
|
||||
<section id="encoding-section" class="card form-group">
|
||||
<h2>Encrypt & Decrypt</h2>
|
||||
<form id="crypto-form" class="form-group">
|
||||
<!-- Encryption Type Selection -->
|
||||
<div class="form-group">
|
||||
<label for="encryption-type">Encryption Type:</label>
|
||||
<select id="encryption-type">
|
||||
<option value="basic">Basic Cipher</option>
|
||||
<option value="advanced" selected>Advanced AES</option>
|
||||
<!-- Algorithm Selection -->
|
||||
<div class="form-group" id="algorithm-selection">
|
||||
<label for="algorithm">Encryption Algorithm:</label>
|
||||
<select id="algorithm">
|
||||
<!-- Options populated dynamically by JavaScript -->
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Key Pair Management (for RSA/PQ algorithms) -->
|
||||
<div class="form-group" id="keypair-section" style="display: none;">
|
||||
<div class="keypair-info">
|
||||
<p><strong>🔐 Key Pair Required:</strong></p>
|
||||
<p><strong>For Encryption:</strong> Use Public Key (.pub file)</p>
|
||||
<p><strong>For Decryption:</strong> Use Private Key (.key file)</p>
|
||||
</div>
|
||||
<div style="padding: 10px; border: 1px solid #ffaa00; border-radius: 5px; background-color: #221100; margin: 10px 0;">
|
||||
<p style="color: #ffaa00; margin: 0; text-align: center;">
|
||||
<strong>💡 Manage your keys in the "Key Pairs Management" section above</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Key status indicators -->
|
||||
<div id="key-status" style="margin-top: 10px;">
|
||||
<div id="public-key-status" style="display: none;">✅ Public key loaded</div>
|
||||
<div id="private-key-status" style="display: none;">✅ Private key loaded</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Operation Toggle -->
|
||||
<div class="toggle-container">
|
||||
<span class="toggle-label">Encrypt</span>
|
||||
@@ -90,7 +158,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 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" />
|
||||
<button type="button" id="remove-file-btn">Remove File</button>
|
||||
</div>
|
||||
@@ -144,10 +212,49 @@
|
||||
<button type="button" id="copy-share-btn">Copy Link</button>
|
||||
<div id="shared-link-feedback" class="copy-feedback">Link copied to clipboard!</div>
|
||||
</div>
|
||||
|
||||
<!-- 2FA Setup Container (initially hidden) -->
|
||||
<div id="tfa-setup-container" style="display: none; margin-top: 20px; padding: 15px; border: 2px solid #ffaa00; border-radius: 8px; background-color: #332200;">
|
||||
<h3 style="color: #ffaa00; margin-top: 0;">🔒 Important: Set Up 2FA Now!</h3>
|
||||
<p style="color: #ccc;">You enabled 2FA for this file. <strong>Scan this QR code NOW</strong> with your authenticator app:</p>
|
||||
<div style="text-align: center; margin: 15px 0;">
|
||||
<img id="tfa-qr-image" src="" alt="2FA QR Code" style="max-width: 200px; border: 2px solid #00ff99;" />
|
||||
</div>
|
||||
|
||||
<!-- 2FA String Container -->
|
||||
<div style="margin-top: 15px; padding: 10px; border: 1px solid #00ff99; border-radius: 5px; background-color: #001100;">
|
||||
<p style="color: #00ff99; margin: 5px 0; font-size: 0.9em;"><strong>Or manually enter this string:</strong></p>
|
||||
<div class="share-link-container" style="margin: 0;">
|
||||
<input type="text" id="tfa-string" readonly style="flex: 1; background: #111; color: #00ff99; border: 1px solid #333; padding: 8px; font-family: monospace; font-size: 0.8em;" />
|
||||
<button type="button" id="copy-tfa-string-btn">Copy String</button>
|
||||
<div id="tfa-string-feedback" class="copy-feedback">2FA string copied to clipboard!</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p style="color: #ff6b6b; font-weight: bold; margin-top: 15px;">⚠️ SAVE THIS QR CODE OR STRING NOW! It will not be shown again for security reasons.</p>
|
||||
<p style="color: #ccc; font-size: 0.9em;">Recommended apps: Google Authenticator, Authy, Microsoft Authenticator</p>
|
||||
<button type="button" onclick="closeTwoFactorSetup()">I've Saved the 2FA Information</button>
|
||||
</div>
|
||||
<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="password" name="enc_password" placeholder="Encryption/Decryption 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">
|
||||
<button type="submit">Upload and Generate Link</button>
|
||||
</div>
|
||||
@@ -178,10 +285,16 @@
|
||||
shareLink.textContent = data.pickup_url;
|
||||
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
|
||||
document.getElementById('upload-file').value = '';
|
||||
document.getElementsByName('enc_password')[0].value = '';
|
||||
document.getElementsByName('pickup_password')[0].value = '';
|
||||
document.getElementById('enable-2fa').checked = false;
|
||||
|
||||
// Scroll to the share link
|
||||
shareLinkContainer.scrollIntoView({ behavior: 'smooth' });
|
||||
@@ -190,6 +303,250 @@
|
||||
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>
|
||||
|
||||
<!-- File Limits Information -->
|
||||
|
||||
@@ -45,8 +45,24 @@
|
||||
<!-- File Info -->
|
||||
<div class="form-group">
|
||||
<p style="color: #00ff99; margin-bottom: 15px;">File ID: <code>{{ file_id }}</code></p>
|
||||
{% if require_2fa %}
|
||||
<p style="color: #ffaa00; margin-bottom: 15px;">🔒 This file requires 2FA (TOTP) authentication.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if require_2fa %}
|
||||
<div class="form-group" style="border: 2px solid #ffaa00; padding: 15px; margin-bottom: 20px; border-radius: 5px;">
|
||||
<h3 style="color: #ffaa00; margin-top: 0;">⚠️ 2FA Required</h3>
|
||||
<p style="color: #ccc;">
|
||||
<strong>You should have already set up 2FA when uploading this file.</strong><br>
|
||||
Enter the 6-digit code from your authenticator app below.
|
||||
</p>
|
||||
<p style="color: #ff6b6b; font-size: 0.9em;">
|
||||
If you didn't set up 2FA during upload, you won't be able to access this file.
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Pickup Form -->
|
||||
<form method="POST" class="form-group">
|
||||
<div class="form-group">
|
||||
@@ -65,6 +81,19 @@
|
||||
autocomplete="off" />
|
||||
</div>
|
||||
|
||||
{% if require_2fa %}
|
||||
<div class="form-group">
|
||||
<input type="text"
|
||||
name="totp_code"
|
||||
placeholder="6-Digit Authenticator Code"
|
||||
required
|
||||
maxlength="6"
|
||||
pattern="[0-9]{6}"
|
||||
autocomplete="off"
|
||||
style="text-align: center; font-size: 1.2em; letter-spacing: 0.2em;" />
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="button-group">
|
||||
<button type="submit">Decrypt and Download</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user