77 Commits

Author SHA1 Message Date
TySS-Dev d7a4fc3e52 Typo 2026-05-25 20:55:32 -04:00
TySS-Dev 9ec15d62ef Maintenance only 2026-05-25 20:54:52 -04:00
TySS-Dev 8c7f3bd8cc Update README.md 2026-05-25 20:39:09 -04:00
TySS-Dev f787ac1a8e Update README.md 2026-05-25 20:38:57 -04:00
TySS-Dev 0df78edead Fixing typo 2026-05-25 20:38:16 -04:00
TySS-Dev 22062a7a3d Changed username, updating link 2026-05-17 03:29:10 -04:00
tyler ea4dccb438 Update README.md 2026-04-20 01:30:34 -04:00
tyler 28e49dac94 Update README.md 2026-04-20 01:29:37 -04:00
tyler 94a89d3f76 Update README.md 2026-04-20 01:28:14 -04:00
tyler 0d26a3d84b Update README.md 2026-04-20 01:27:57 -04:00
tyler 12eb87779b Update README.md 2026-04-20 01:27:15 -04:00
tyler 87deca1a29 Update README.md 2026-04-20 01:26:59 -04:00
tyler 8b57d89009 Update README.md 2026-04-20 01:26:35 -04:00
tyler 114a48d8bf Update README.md 2026-04-20 01:25:08 -04:00
tyler 7a7be4d5c8 Update README.md 2026-04-20 01:24:40 -04:00
tyler 07ae277968 Update README.md 2026-04-20 01:22:20 -04:00
tyler ac385f2feb Update README.md 2026-04-20 01:21:50 -04:00
tyler 843485e598 Update README.md 2026-04-20 01:20:58 -04:00
tyler d3667700cb Update README.md 2026-04-20 01:18:46 -04:00
tyler b901f5240a Update README.md 2026-04-20 01:18:28 -04:00
tyler a1104cb24b test 2026-04-20 01:18:00 -04:00
tyler fe94e13b4a Testing badge 2026-04-20 01:14:29 -04:00
tyler 2842d92213 Fixed repo table 2026-04-20 01:11:58 -04:00
tyler ce195c9df6 Update README.md 2026-04-20 01:10:41 -04:00
tyler c3f638c888 Update README.md 2026-04-20 01:08:42 -04:00
tyler 91814845a4 Added repos 2026-04-20 01:08:33 -04:00
tyler cb40ccdfa7 Update README.md 2026-04-20 01:07:16 -04:00
Tyler 71544a8e01 Update README with merged dev branch information
Merged development branch into main and updated README to reflect current state and features.
2026-04-20 00:57:34 -04:00
Tyler b80f6b80fd Merge branch 'dev-only_DO-NOT-USE' into main 2026-04-20 00:55:18 -04:00
Tyler a9022bb5e3 Merging dev into main 2026-04-20 00:54:02 -04:00
Tyler 5b9a2b7a53 Delete start_prod.sh 2026-04-20 00:53:40 -04:00
Tyler 379c14781e Delete start_prod.bat 2026-04-20 00:53:34 -04:00
Tyler 67adca4d4f Delete restart.sh 2026-04-20 00:53:28 -04:00
Tyler 3e8c2ca097 Delete start_dev.sh 2026-04-20 00:53:23 -04:00
Tyler bede46d87f Delete start_dev.bat 2026-04-20 00:53:17 -04:00
Tyler 8fe7be4d56 Delete settings.json 2026-04-20 00:53:11 -04:00
Tyler 20813d0e04 Delete restart.bat 2026-04-20 00:53:05 -04:00
Tyler e489c1e980 Delete requirements.txt 2026-04-20 00:53:00 -04:00
Tyler 5aff9df9f7 Delete app.py 2026-04-20 00:52:54 -04:00
Tyler ff24e6eba8 Delete ROADMAP.md 2026-04-20 00:52:46 -04:00
Tyler 179be48bfe Delete Dockerfile 2026-04-20 00:51:45 -04:00
Tyler 3a44f6a2bd Delete templates directory 2026-04-20 00:51:38 -04:00
Tyler 7a27d314a2 Cuz why not 2026-04-20 00:51:29 -04:00
Tyler 2a5eb3ff04 Delete README.md 2026-04-20 00:49:04 -04:00
Tyler d253fd4802 Delete docker-compose.yml 2026-04-20 00:48:53 -04:00
Tyler 5234017129 Revise README with updated warnings and features
Updated README to reflect current state and features of PacCrypt.
2026-04-20 00:47:20 -04:00
Tyler 0093d33a77 Revise README for clarity and development notes
Updated README to reflect development status and installation instructions.
2026-04-20 00:46:22 -04:00
Tyler ae19598750 Update README.md
Changed the clone to match the dev branch
2025-09-15 13:03:38 -10:00
Tyler c6d480aa12 Update README.md 2025-09-15 13:01:43 -10:00
Tyler Sammons 38d3b7e6c1 More towards the roadmap 2025-09-15 12:55:01 -10:00
Tyler Sammons 5d568f7f89 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.
2025-09-14 13:10:04 -10:00
Tyler 7a9d87b46e Update docker-compose.yml 2025-08-25 14:55:28 -10:00
Tyler 0756e66b80 Update README.md 2025-08-25 14:22:08 -10:00
Tyler 98262746a9 Update README.md 2025-08-25 13:19:54 -10:00
Tyler 49da1eaa3f Update README.md 2025-08-25 13:17:51 -10:00
Tyler 36cf8f18f8 Update ROADMAP.md 2025-08-06 20:15:19 -10:00
Tyler b03316cea8 Update ROADMAP.md 2025-08-06 20:14:49 -10:00
Tyler 4ccf7afa5a Delete application_data/settings.json 2025-08-06 19:16:33 -10:00
Tyler 3588bc3349 Delete paccrypt_algos/__pycache__ directory 2025-08-06 19:16:17 -10:00
TySP-Dev e108d23945 Phase 1 in progress 2025-08-06 19:15:05 -10:00
TySP-Dev 099a5c8f18 Phase 0 Complete 2025-08-06 15:52:26 -10:00
Tyler 2a414e62cf Update ROADMAP.md 2025-08-06 14:09:53 -10:00
Tyler 9c53a6e14f Update README.md 2025-08-06 13:40:43 -10:00
Tyler aad43c2024 Update ROADMAP.md 2025-08-06 12:51:12 -10:00
Tyler a56ee7cefe Update ROADMAP.md 2025-08-06 12:49:07 -10:00
Tyler f042127931 Update ROADMAP.md 2025-08-06 12:36:45 -10:00
Tyler 66ed918a78 Update ROADMAP.md 2025-08-06 12:30:35 -10:00
Tyler 43f47565da Update ROADMAP.md 2025-08-06 12:28:52 -10:00
Tyler 022a1e7aaf Create ROADMAP.md 2025-08-06 12:27:53 -10:00
Tyler db8fd2ac1f Create LICENSE 2025-08-06 12:01:34 -10:00
Tyler 1fd15b40f3 docker support 2025-05-18 00:38:15 -10:00
Tyler da0ab0f042 Working on Docker 2025-05-17 20:49:36 -10:00
Tyler 973aa0f20f More api fixes 2025-05-17 14:49:12 -10:00
Tyler b7a85b8d84 Add files via upload 2025-05-17 03:33:24 -10:00
Tyler 5f6a5747a6 Changed API handling 2025-05-17 03:08:05 -10:00
Tyler 03079263ec Add APIs 2025-05-17 02:23:03 -10:00
Tyler 0b39998364 Fixed PacShare Uploads 2025-05-16 21:30:54 -10:00
40 changed files with 7976 additions and 750 deletions
+595
View File
@@ -0,0 +1,595 @@
# PacCrypt API Documentation 🔐
> [!IMPORTANT]
> This document is fully AI generated, pending my review.
> It is only here so I can push to alpha.
> Next push will contain human oversite on the documentation.
This document provides AI slop documentation for the PacCrypt REST API, covering all endpoints for encryption, decryption, file sharing, and administrative operations.
## 🌐 Base URLs
- **Official Instance**: `https://paccrypt.unnaturalll.dev`
- **Self-hosted**: `http://YOUR_SERVER_IP:5000` or `https://your-domain.com`
## 📋 Table of Contents
- [Authentication](#authentication)
- [Encryption Operations](#encryption-operations)
- [PacShare File Sharing](#pacshare-file-sharing)
- [Administrative Endpoints](#administrative-endpoints)
- [Error Handling](#error-handling)
- [Examples](#examples)
---
## 🔑 Authentication
Most API endpoints are **public** and don't require authentication. Admin endpoints require session-based authentication.
### Admin Authentication Flow
1. **Setup Admin Account** (first-time only)
```
POST /admin-setup
```
2. **Login**
```
POST /admin-login
```
3. **Access Admin Endpoints** (with session)
---
## 🔐 Encryption Operations
### 1. Get Available Algorithms
Get list of supported encryption algorithms and their capabilities.
**Endpoint**: `GET /api/algorithms`
**Response**:
```json
{
"algorithms": {
"aes_gcm": {
"name": "AES-GCM",
"description": "AES-256 with GCM mode (authenticated encryption)",
"supports_text": true,
"supports_file": false,
"requires_keypair": false
},
"aes_cbc": {
"name": "AES-CBC",
"description": "AES-256 with CBC mode and HMAC authentication",
"supports_text": true,
"supports_file": true,
"requires_keypair": false
},
"xchacha": {
"name": "XChaCha20-Poly1305",
"description": "XChaCha20 stream cipher with Poly1305 authentication",
"supports_text": true,
"supports_file": true,
"requires_keypair": false
},
"rsa_hybrid": {
"name": "RSA Hybrid",
"description": "RSA-4096 with AES hybrid encryption",
"supports_text": true,
"supports_file": true,
"requires_keypair": true
}
}
}
```
### 2. Generate RSA Key Pair
Generate RSA key pairs for hybrid encryption algorithms.
**Endpoint**: `POST /api/generate-keypair`
**Request Body**:
```json
{
"algorithm": "rsa_hybrid"
}
```
**Response**:
```json
{
"private_key": "-----BEGIN RSA PRIVATE KEY-----\n...",
"public_key": "-----BEGIN PUBLIC KEY-----\n..."
}
```
### 3. Text Encryption
Encrypt text using various algorithms.
**Endpoint**: `POST /api/encrypt`
**Content-Type**: `application/json`
#### Password-based Algorithms (AES-GCM, AES-CBC, XChaCha20)
**Request Body**:
```json
{
"message": "Hello, World!",
"password": "your_secure_password",
"algorithm": "aes_gcm"
}
```
#### Key-based Algorithms (RSA Hybrid)
**Request Body**:
```json
{
"message": "Hello, World!",
"public_key": "-----BEGIN PUBLIC KEY-----\n...",
"algorithm": "rsa_hybrid"
}
```
**Response**:
```json
{
"result": "base64_encrypted_text",
"algorithm": "aes_gcm"
}
```
### 4. Text Decryption
Decrypt text using various algorithms.
**Endpoint**: `POST /api/decrypt`
**Content-Type**: `application/json`
#### Password-based Algorithms
**Request Body**:
```json
{
"message": "base64_encrypted_text",
"password": "your_secure_password",
"algorithm": "aes_gcm"
}
```
#### Key-based Algorithms
**Request Body**:
```json
{
"message": "base64_encrypted_text",
"private_key": "-----BEGIN RSA PRIVATE KEY-----\n...",
"algorithm": "rsa_hybrid"
}
```
**Response**:
```json
{
"result": "Hello, World!"
}
```
### 5. File Encryption
Encrypt files and download the encrypted result.
**Endpoint**: `POST /api/encrypt`
**Content-Type**: `multipart/form-data`
**Form Data**:
- `file`: File to encrypt
- `enc_password`: Encryption password
- `algorithm`: Algorithm to use (`aes_cbc`, `xchacha`, `rsa_hybrid`)
**Response**: Binary file download with `.{algorithm}.encrypted` extension
### 6. File Decryption
Decrypt files and download the decrypted result.
**Endpoint**: `POST /api/decrypt`
**Content-Type**: `multipart/form-data`
**Form Data**:
- `file`: Encrypted file
- `enc_password`: Decryption password
- `algorithm`: Algorithm used (optional, auto-detected from filename)
**Response**: Binary file download with original filename
---
## 📤 PacShare File Sharing
### 1. Upload File for Sharing
Upload and encrypt a file for secure sharing with pickup URLs.
**Endpoint**: `POST /api/pacshare`
**Content-Type**: `multipart/form-data`
**Form Data**:
- `file`: File to upload and share
- `enc_password`: Password to encrypt the file
- `pickup_password`: Password required to access pickup page
- `algorithm`: Encryption algorithm (`aes_cbc`, `xchacha`, `rsa_hybrid`)
- `enable_2fa`: Set to `"on"` to enable 2FA (optional)
**Response**:
```json
{
"pickup_url": "https://paccrypt.unnaturalll.dev/pickup/abc123def456",
"qr_code_url": "https://paccrypt.unnaturalll.dev/qr/abc123def456",
"totp_secret": "BASE32SECRET",
"service_name": "PacCrypt File: document.pdf..."
}
```
**Note**: QR code URL and TOTP fields only present if 2FA is enabled.
### 2. Generate 2FA QR Code
Get QR code for 2FA setup for a specific file.
**Endpoint**: `GET /qr/{file_id}`
**Response**: PNG image (QR code)
---
## 🛠️ Administrative Endpoints
### 1. Admin Setup
Create initial admin account (first-time setup only).
**Endpoint**: `POST /admin-setup`
**Content-Type**: `application/x-www-form-urlencoded`
**Form Data**:
- `username`: Admin username
- `password`: Admin password
- `enable_2fa`: Set to `"on"` to enable 2FA (optional)
### 2. Admin Login
Authenticate admin user and create session.
**Endpoint**: `POST /admin-login`
**Content-Type**: `application/x-www-form-urlencoded`
**Form Data**:
- `username`: Admin username
- `password`: Admin password
- `totp_code`: 2FA code (if 2FA enabled)
### 3. Admin Logout
End admin session.
**Endpoint**: `GET /admin-logout`
### 4. View Admin Logs
Get encrypted admin activity logs.
**Endpoint**: `GET /admin-logs`
**Response**:
```json
{
"logs": [
"[2025-01-15 10:30:00] Admin login successful.",
"[2025-01-15 10:31:00] File abc123 downloaded and deleted."
]
}
```
### 5. Server Control
#### Restart Server
**Endpoint**: `POST /restart-server`
#### Switch to Development Mode
**Endpoint**: `POST /admin-switch-dev-mode`
#### Switch to Production Mode
**Endpoint**: `POST /admin-switch-prod-mode`
#### Update from GitHub
**Endpoint**: `POST /admin-update-server`
### 6. File Management
#### Clear All Uploads
**Endpoint**: `POST /admin-clear-uploads`
### 7. Admin Account Management
#### Change Password
**Endpoint**: `POST /admin-change-password`
**Form Data**:
- `current_password`: Current admin password
- `new_password`: New admin password
#### Enable 2FA
**Endpoint**: `POST /admin-enable-2fa`
#### Disable 2FA
**Endpoint**: `POST /admin-disable-2fa`
**Form Data**:
- `totp_code`: Current 2FA code
#### Reset Admin Credentials
**Endpoint**: `POST /admin-reset`
---
## ❌ Error Handling
All API endpoints return appropriate HTTP status codes and error messages.
### Common HTTP Status Codes
- `200 OK`: Successful operation
- `400 Bad Request`: Invalid request parameters
- `401 Unauthorized`: Authentication required
- `404 Not Found`: Resource not found
- `500 Internal Server Error`: Server error
### Error Response Format
```json
{
"error": "Descriptive error message"
}
```
### Common Errors
- `"Missing message"`: Required message field not provided
- `"Invalid algorithm"`: Unsupported encryption algorithm
- `"Algorithm does not support text/file operations"`: Algorithm limitation
- `"Public/Private key required for this algorithm"`: Missing key for RSA
- `"Password required"`: Missing password for symmetric algorithms
- `"Encryption/Decryption failed"`: Cryptographic operation failed
---
## 🌟 Examples
### Self-Hosted Usage
Replace `localhost:5000` with your server's IP address or domain.
#### Text Encryption Example
```bash
curl -X POST "http://localhost:5000/api/encrypt" \
-H "Content-Type: application/json" \
-d '{
"message": "Confidential information",
"password": "super_secure_password",
"algorithm": "aes_gcm"
}'
```
#### File Upload Example
```bash
curl -X POST "http://localhost:5000/api/pacshare" \
-F "file=@confidential.pdf" \
-F "enc_password=file_encryption_key" \
-F "pickup_password=pickup_key_123" \
-F "algorithm=aes_cbc" \
-F "enable_2fa=on"
```
#### RSA Key Generation and Usage
```bash
# 1. Generate key pair
curl -X POST "http://localhost:5000/api/generate-keypair" \
-H "Content-Type: application/json" \
-d '{"algorithm": "rsa_hybrid"}' > keys.json
# 2. Extract public key and encrypt
PUBLIC_KEY=$(cat keys.json | jq -r '.public_key')
curl -X POST "http://localhost:5000/api/encrypt" \
-H "Content-Type: application/json" \
-d "{
\"message\": \"RSA encrypted message\",
\"public_key\": \"$PUBLIC_KEY\",
\"algorithm\": \"rsa_hybrid\"
}"
```
### Official Instance Usage
#### Text Encryption with Official API
```bash
curl -X POST "https://paccrypt.unnaturalll.dev/api/encrypt" \
-H "Content-Type: application/json" \
-d '{
"message": "Hello from the official API!",
"password": "my_password_123",
"algorithm": "xchacha"
}'
```
#### PacShare Upload to Official Instance
```bash
curl -X POST "https://paccrypt.unnaturalll.dev/api/pacshare" \
-F "file=@document.docx" \
-F "enc_password=encrypt_me_please" \
-F "pickup_password=come_get_it" \
-F "algorithm=xchacha"
```
#### File Encryption with Official API
```bash
curl -X POST "https://paccrypt.unnaturalll.dev/api/encrypt" \
-F "file=@sensitive_data.txt" \
-F "enc_password=strong_password" \
-F "algorithm=aes_cbc" \
-o encrypted_file.aes_cbc.encrypted
```
### Python Integration Example
```python
import requests
import json
# Official API base URL
BASE_URL = "https://paccrypt.unnaturalll.dev"
# Text encryption
def encrypt_text(message, password, algorithm="aes_gcm"):
response = requests.post(f"{BASE_URL}/api/encrypt",
json={
"message": message,
"password": password,
"algorithm": algorithm
})
return response.json()
# File sharing via PacShare
def share_file(file_path, enc_password, pickup_password, algorithm="aes_cbc"):
with open(file_path, 'rb') as f:
response = requests.post(f"{BASE_URL}/api/pacshare",
files={"file": f},
data={
"enc_password": enc_password,
"pickup_password": pickup_password,
"algorithm": algorithm
})
return response.json()
# Usage examples
encrypted = encrypt_text("Secret message", "my_password")
print(f"Encrypted: {encrypted['result']}")
file_share = share_file("document.pdf", "encrypt123", "pickup123")
print(f"Pickup URL: {file_share['pickup_url']}")
```
### JavaScript/Browser Example
```javascript
// Text encryption using fetch API
async function encryptText(message, password, algorithm = 'aes_gcm') {
const response = await fetch('https://paccrypt.unnaturalll.dev/api/encrypt', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: message,
password: password,
algorithm: algorithm
})
});
return await response.json();
}
// File upload for sharing
async function shareFile(fileInput, encPassword, pickupPassword) {
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('enc_password', encPassword);
formData.append('pickup_password', pickupPassword);
formData.append('algorithm', 'aes_cbc');
const response = await fetch('https://paccrypt.unnaturalll.dev/api/pacshare', {
method: 'POST',
body: formData
});
return await response.json();
}
// Usage
encryptText("Hello API!", "password123").then(result => {
console.log("Encrypted:", result.result);
});
```
---
## 🔒 Security Best Practices
### For API Consumers
1. **Always use HTTPS** in production environments
2. **Use strong passwords** with high entropy
3. **Implement proper error handling** for failed operations
4. **Don't log sensitive data** like passwords or private keys
5. **Validate inputs** before sending to the API
6. **Use 2FA** for sensitive file shares
7. **Implement rate limiting** to prevent abuse
### For Self-Hosted Instances
1. **Configure reverse proxy** (nginx/apache) with HTTPS
2. **Set up firewall rules** to restrict access
3. **Regular security updates** for all dependencies
4. **Monitor admin logs** for suspicious activity
5. **Backup encrypted data** regularly
6. **Use strong admin passwords** with 2FA enabled
7. **Configure CORS** appropriately for your use case
---
## 🚀 Advanced Usage
### Batch Operations
You can process multiple files or messages by making multiple API calls. Consider implementing:
- **Concurrent requests** for better performance
- **Progress tracking** for large batches
- **Error retry logic** for failed operations
### Integration Patterns
- **CLI Tools**: Build command-line interfaces using the API
- **Web Applications**: Integrate encryption into existing apps
- **Mobile Apps**: Use the API for mobile encryption needs
- **Automation Scripts**: Automate encryption workflows
### Performance Considerations
- **File Size Limits**: Check instance-specific upload limits
- **Rate Limiting**: Respect API rate limits if implemented
- **Caching**: Cache algorithm information to reduce API calls
- **Connection Pooling**: Reuse HTTP connections for multiple requests
---
*For more information, see the main [README.md](README.md) or visit the [official instance](https://paccrypt.unnaturalll.dev).*
+661
View File
@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
+421 -268
View File
@@ -1,268 +1,421 @@
# PacCrypt > [!WARNING]
> PacCrypt is currently maintenance only, please submit issues for bugs. Only major bugs will be addressed right now. Development will continue in the future.
**PacCrypt** is a 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! 🕹️ <div align="center">
Officially Hosted Here: [paccrypt.unnaturalll.dev](http://paccrypt.unnaturalll.dev) [![Main Repo](https://img.shields.io/badge/Main%20Repo-git.tysstech.com-blue?logo=gitea)](https://git.tysstech.com/TySS-Dev/PacCrypt-Webapp)
[![Mirror Repo](https://img.shields.io/badge/Mirror%20Repo-github.com-blue?logo=github)](https://github.com/TySP-Dev/PacCrypt-Webapp)
--- [![Official Instance](https://img.shields.io/website?url=https%3A%2F%2Fpaccrypt.tysstech.com&label=Official%20Instance)](https://paccrypt.tysstech.com)
## ✨ Features </div>
- 🔒 Basic and Advanced Encryption for Text & Files # PacCrypt 🔐
- 📁 Secure File Uploads with Pickup Passwords
- 🔑 Random Password Generator **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.
- 🎮 Hidden Pac-Man Game — type `pacman` to play
- 🧠 Smart UI: Auto-switches input sections, toggles encryption labels > [!WARNING]
- 📋 Clipboard Copy Feedback with styled status boxes > Merged Dev branch into main, program is still in the development stage so no need to have multiple branches. Please submit issues for bugs. I expect a lot, I dont recall the state of the Dev branch.
- 🧾 Admin Panel:
- Site map with live route list > [!IMPORTANT]
- Server restart & GitHub update button > This document contains AI generated pieces that have not been reviewed yet.
- Secure admin credential management > Next push will contain human oversite on the documentation.
- Server logs & upload cleanup
- 🧩 System Settings Page for upload config ---
- 📜 Custom 403, 404, and 500 Error Pages
- 🤖 robots.txt and /sitemap for crawlers ## ✨ Features
- 📱 Mobile-Responsive UI
### 🔒 **Multi-Algorithm Encryption**
--- - **AES-GCM**: Text encryption with authenticated encryption
- **AES-CBC**: Text and file encryption with HMAC authentication
## 👨‍💻 Installation - **XChaCha20-Poly1305**: Modern stream cipher for text and files
- **RSA Hybrid**: RSA-4096 with AES hybrid encryption for text and files
### 📋 Prerequisites
### 🌐 **Comprehensive API**
- Python 3.7+ - RESTful API endpoints for all encryption operations
- Flask 3+ - Text and file encryption/decryption
- Cryptography 42+ - Key pair generation for RSA hybrid
- Waitress 2.1+ - PacShare file sharing with secure pickup URLs
- Git (For update feature) - Full API documentation (see [API.md](API.md))
- Nginx (Recommended)
- Cockpit (Recommended if hosted on **Linux**) ### 📁 **PacShare - Secure File Sharing**
- End-to-end encrypted file uploads
--- - Dual-password system (pickup + encryption)
- Optional 2FA with TOTP codes
### ⚡ Quick Setup - QR code generation for 2FA setup
- Automatic file expiration
```bash - Secure pickup URLs with one-time download
git clone https://github.com/TySP-Dev/PacCrypt.git
cd paccrypt-webapp-final ### 🛡️ **Advanced Security**
python -m venv venv - Admin panel with 2FA support
source venv/bin/activate # or venv\Scripts\activate on Windows - Encrypted admin credentials and logs
pip install -r requirements.txt - Secure session management
``` - PBKDF2 key derivation with 200,000 iterations
- Cryptographically secure random ID generation
Then run:
### 🎮 **Built-in Entertainment**
- Development Mode: - Hidden Pac-Man game (type `pacman` to play)
```bash - Arrow key and swipe controls
./start_dev.sh #<-- start_dev.bat (Windows) - Retro gaming experience with authentic sounds
```
### 🧾 **Admin Control Panel**
- Production Mode: - Real-time server monitoring and statistics
```bash - GitHub auto-update functionality
./start_prod.sh #<-- start_prod.bat (Windows) - Upload management and cleanup
``` - Server restart capabilities
- Development/Production mode switching
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 - Comprehensive audit logging
Visit http://hosts_private_ip - *If* you are **not** on the host system
### 📱 **Modern UI/UX**
--- - Fully responsive mobile design
- Smart UI state management
## 🧭 Navigation & Usage - Clipboard integration
- Visual feedback for all operations
### 🔑 Generate Passwords - Custom error pages (403, 404, 500)
- SEO-optimized with sitemap and robots.txt
- Click Generate
- Then hit `📋 Copy Password` ---
- **Note:** This is also used as a seed generator for the Pac-Man *like* game
## 🚀 Quick Start
### 🔐 Encrypt & Decrypt
### Prerequisites
- Choose between Basic Cipher or Advanced AES
- Select mode using toggle (Encrypt/Decrypt) - **Python 3.8+** (3.10+ recommended)
- Type your message or upload a file - **Git** (for updates and installation)
- Enter password (Advanced AES) - **pip** package manager
- Hit Execute
- Then hit `📋 Copy Output` ### Installation
### 📤 Share Files ```bash
# Clone the repository
- Upload a file with two passwords: git clone -b "dev-only_DO-NOT-USE" https://github.com/TySP-Dev/PacCrypt-Webapp.git
- Encryption password cd PacCrypt-Webapp
- Pickup password
- Get a shareable URL and click `📋 Copy Link` # Create virtual environment
python -m venv venv
### 🎮 Pac-Man *like* Game
# Activate virtual environment
- Type `pacman` in the input box # On Linux/macOS:
- Game appears with `Restart` and `Exit` buttons source venv/bin/activate
- Arrow key and Swipe controls 🕹️ # On Windows:
- Game restarts and a new seed is generated once all dots are eaten venv\Scripts\activate
--- # Install dependencies
pip install -r application_data/requirements.txt
## 🛠️ Admin Panel ```
Visit `/adminpage` after setting up credentials at `/admin-setup`. ### Running the Application
Features: #### Development Mode
- 🔄 Restart server ```bash
- 🔃 Update from GitHub (git pull) # Linux/macOS
- 🧽 Clear uploads python application_data/control_scripts/start_dev.py
- 🔐 Change admin password
- 📝 View logs # Windows
- ⚙️ Adjust upload settings python application_data\control_scripts\start_dev.py
```
---
#### Production Mode
## 🛡️ Deployment Tips ```bash
##### I recommend using Linux as the host server, the follow confs are Linux focused # Linux/macOS
The official PacCrypt host is **Debian** minimal install. python application_data/control_scripts/start_prod.py
**HTTP** Nginx config (Not recommended): # Windows
python application_data\control_scripts\start_prod.py
```nginx ```
server {
listen 80; ### Access the Application
server_name yourdomain.com; #<-- Your URL here
- **Local access**: http://127.0.0.1:5000
# Basic Privacy-Respecting Logging - **Network access**: http://YOUR_IP_ADDRESS:5000
access_log off; #<-- set to syslog:server=unix:/dev/log; for logging - **Admin setup**: http://127.0.0.1:5000/admin-setup (first-time only)
error_log syslog:server=unix:/dev/log crit; #<-- Currently set for only critical logs, remove crit for all logs
---
# Hardened Proxy Settings
location / { ## 📖 Usage Guide
proxy_pass http://127.0.0.1:5000;
### 🔐 Text Encryption/Decryption
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; 1. **Select Algorithm**: Choose from AES-GCM, AES-CBC, XChaCha20, or RSA Hybrid
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 2. **Enter Text**: Type or paste your message
3. **Set Password**: Enter a strong encryption password
proxy_http_version 1.1; 4. **For RSA**: Generate key pair first if using RSA Hybrid
proxy_set_header Connection ""; 5. **Execute**: Click Encrypt/Decrypt
6. **Copy Result**: Use the copy button for easy sharing
# Timeouts
proxy_connect_timeout 5s; ### 📁 File Operations
proxy_send_timeout 30s;
proxy_read_timeout 30s; 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
# Basic Hardening Headers 4. **Process**: File will be encrypted/decrypted and downloaded automatically
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always; ### 📤 PacShare - Secure File Sharing
add_header Referrer-Policy "no-referrer" always;
add_header Permissions-Policy "geolocation=(), microphone=()" always; 1. **Upload File**: Select file to share
2. **Set Passwords**:
# Prevent Abuse - **Encryption Password**: Encrypts the file content
client_max_body_size 10M; - **Pickup Password**: Required to access the download page
keepalive_timeout 10; 3. **Optional 2FA**: Enable for additional security
server_tokens off; 4. **Share URL**: Copy the generated pickup URL
} 5. **Recipient Access**: They need both passwords (and 2FA code if enabled)
```
### 🎮 Hidden Pac-Man Game
**HTTPS** Nginx config (Recommended):
- Type `pacman` in any text input
```nginx - Use arrow keys or swipe gestures to play
# Redirect HTTP to HTTPS - Authentic retro gaming experience with sound effects
server {
listen 80; ---
server_name yourdomain.com; #<-- Your URL here
## 🛠️ Admin Panel
# Basic Privacy-Respecting Logging
access_log off; #<-- set to syslog:server=unix:/dev/log; for logging Access the admin panel at `/adminpage` after initial setup at `/admin-setup`.
error_log syslog:server=unix:/dev/log crit; #<-- Currently set for only critical logs, remove crit for all logs
### 🔑 Setup Process
location / { 1. Visit `/admin-setup` on first run
return 301 https://$host$request_uri; 2. Create admin username and password
} 3. Optionally enable 2FA for enhanced security
} 4. Login at `/admin-login`
# HTTPS Server Block ### 🎛️ Admin Features
server { - **📊 Server Monitoring**: Real-time statistics and uptime
listen 443 ssl http2; - **🔄 Server Control**: Restart, switch dev/prod modes
server_name yourdomain.com; - **📋 Route Management**: View all available endpoints
- **🔃 GitHub Integration**: Auto-update from repository
ssl_certificate path/to/yourdomain.com.cert; #<-- Could also be .cert.pem - **🧹 File Management**: Clear uploads and expired files
ssl_certificate_key path/to/yourdomain.com.key; #<-- Could also be .key.pem - **🔐 Security**: Change password, manage 2FA
- **📝 Audit Logs**: View encrypted activity logs
# SSL Hardening - **⚙️ Settings**: Configure upload limits and file retention
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384'; ### 🔒 Security Features
ssl_prefer_server_ciphers on; - Encrypted credential storage
ssl_session_cache shared:SSL:10m; - TOTP-based 2FA support
ssl_session_timeout 10m; - QR code generation for authenticator apps
- Secure session management
# Strong security headers (adjust as needed) - Encrypted audit logging
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options nosniff always; ---
add_header X-Frame-Options DENY always;
add_header Referrer-Policy "no-referrer" always; ## 🛡️ Deployment Tips
add_header Permissions-Policy "geolocation=(), camera=()" always; ##### I recommend using Linux as the host server, the follow confs are Linux focused
add_header X-XSS-Protection "1; mode=block" always; The official PacCrypt host is **Arch** minimal install.
# Basic Privacy-Respecting Logging **HTTP** Nginx config (Not recommended):
access_log off; #<-- set to syslog:server=unix:/dev/log; for logging
error_log syslog:server=unix:/dev/log crit; #<-- Currently set for only critical logs, remove crit for all logs ```nginx
server {
client_max_body_size xG; #<-- Change to what the max upload for PacCrypt Share listen 80;
server_name yourdomain.com; #<-- Your URL here
# Reverse proxy to Flask
location / { # Basic Privacy-Respecting Logging
proxy_pass http://127.0.0.1:5000; access_log off; #<-- set to syslog:server=unix:/dev/log; for logging
proxy_set_header Host $host; error_log syslog:server=unix:/dev/log crit; #<-- Currently set for only critical logs, remove crit for all logs
# Comment these out if you want complete anonymity between client and app # Hardened Proxy Settings
# proxy_set_header X-Real-IP $remote_addr; location / {
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://127.0.0.1:5000;
# proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
# Optional privacy: strip identifying headers proxy_set_header X-Real-IP $remote_addr;
proxy_hide_header X-Powered-By; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
} proxy_http_version 1.1;
``` proxy_set_header Connection "";
---
# Timeouts
## 🗂️ Project Structure proxy_connect_timeout 5s;
proxy_send_timeout 30s;
``` proxy_read_timeout 30s;
PacCrypt/ }
├── app.py
├── requirements.txt # Basic Hardening Headers
├── README.md add_header X-Frame-Options "DENY" always;
├── templates/ add_header X-Content-Type-Options "nosniff" always;
│ ├── index.html add_header Referrer-Policy "no-referrer" always;
│ ├── 404.html add_header Permissions-Policy "geolocation=(), microphone=()" always;
│ └── 403.html
│ └── 500.html # Prevent Abuse
│ └── admin.html client_max_body_size 10M;
│ └── admin_login.html keepalive_timeout 10;
│ └── admin_settings.html server_tokens off;
│ └── admin_setup.html }
│ └── pickup.html ```
├── static/
│ ├── css/ **HTTPS** Nginx config (Recommended):
│ │ └── styles.css
│ ├── js/ ```nginx
│ │ └── ui.js # Redirect HTTP to HTTPS
│ │ └── pacman.js server {
│ │ └── main.js listen 80;
│ │ └── fileops.js server_name yourdomain.com; #<-- Your URL here
│ │ └── encryption.js
│ ├── img/ # Basic Privacy-Respecting Logging
│ └── PacCrypt.png access_log off; #<-- set to syslog:server=unix:/dev/log; for logging
│ │ └── Github_logo.png error_log syslog:server=unix:/dev/log crit; #<-- Currently set for only critical logs, remove crit for all logs
│ │ └── sitemap.png
│ └── audio/ location / {
│ └── chomp.mp3 return 301 https://$host$request_uri;
├── start_dev.bat }
├── start_prod.bat }
├── start_dev.sh
├── start_prod.sh # HTTPS Server Block
``` server {
listen 443 ssl http2;
--- server_name yourdomain.com;
## 📄 License ssl_certificate path/to/yourdomain.com.cert; #<-- Could also be .cert.pem
ssl_certificate_key path/to/yourdomain.com.key; #<-- Could also be .key.pem
MIT © [TySP-Dev](https://github.com/TySP-Dev)
# SSL Hardening
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Strong security headers (adjust as needed)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header Referrer-Policy "no-referrer" always;
add_header Permissions-Policy "geolocation=(), camera=()" always;
add_header X-XSS-Protection "1; mode=block" always;
# Basic Privacy-Respecting Logging
access_log off; #<-- set to syslog:server=unix:/dev/log; for logging
error_log syslog:server=unix:/dev/log crit; #<-- Currently set for only critical logs, remove crit for all logs
client_max_body_size xG; #<-- Change to what the max upload for PacCrypt Share
# Reverse proxy to Flask
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
# Comment these out if you want complete anonymity between client and app
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# Optional privacy: strip identifying headers
proxy_hide_header X-Powered-By;
}
}
```
---
## 📋 API Integration
PacCrypt provides a comprehensive REST API for programmatic access. See the detailed [API Documentation](API.md) for:
- **Encryption Operations**: Text and file encryption/decryption
- **Key Management**: RSA key pair generation
- **PacShare Integration**: Programmatic file sharing
- **Algorithm Discovery**: List available encryption methods
### Quick API Example
```bash
# Encrypt text using AES-GCM
curl -X POST "https://paccrypt.unnaturalll.dev/api/encrypt" \
-H "Content-Type: application/json" \
-d '{"message": "Hello World!", "password": "secret123", "algorithm": "aes_gcm"}'
# Upload file via PacShare
curl -X POST "https://paccrypt.unnaturalll.dev/api/pacshare" \
-F "file=@document.pdf" \
-F "enc_password=encrypt123" \
-F "pickup_password=pickup123" \
-F "algorithm=aes_cbc"
```
## 🗂️ Project Structure
```
PacCrypt-Webapp/
├── app.py # Main Flask application
├── README.md # This file
├── ROADMAP.md # Development roadmap
├── API.md # API documentation
├── application_data/ # Application configuration
│ ├── control_scripts/ # Server management scripts
│ │ ├── start_dev.py # Development mode starter
│ │ ├── start_prod.py # Production mode starter
│ │ ├── restart_dev.py # Development restart
│ │ ├── restart_prod.py # Production restart
│ │ └── stop.py # Server stop script
│ ├── requirements.txt # Python dependencies
│ ├── settings.json # Application settings
│ ├── admin_creds.json # Encrypted admin credentials
│ ├── admin_key.key # Admin encryption key
│ └── admin_logs.enc # Encrypted audit logs
├── paccrypt_algos/ # Encryption modules
│ ├── __init__.py # Package initialization
│ ├── aes_cbc.py # AES-CBC implementation
│ ├── aes_gcm.py # AES-GCM implementation
│ ├── xchacha.py # XChaCha20-Poly1305
│ └── rsa_hybrid.py # RSA hybrid encryption
├── pacshare/ # File upload storage
│ ├── *.encrypted # Encrypted uploaded files
│ └── *.json # File metadata
├── templates/ # HTML templates
│ ├── index.html # Main interface
│ ├── pickup.html # File pickup page
│ ├── admin*.html # Admin panel pages
│ └── error pages (403,404,500)
└── static/ # Static assets
├── css/styles.css # Application styling
├── js/ # JavaScript modules
├── img/ # Images and icons
├── fonts/ # Custom fonts
└── audio/ # Sound effects
```
---
## 🔒 Security Considerations
### ⚠️ Important Security Notes
- **Password Strength**: Use strong, unique passwords for all operations
- **2FA Recommended**: Enable 2FA for admin accounts and sensitive file shares
- **HTTPS Required**: Always use HTTPS in production environments
- **Regular Updates**: Keep dependencies updated for security patches
- **Backup Strategy**: Implement regular backups of encrypted data
### 🛡️ Encryption Details
- **AES-256**: Industry standard symmetric encryption
- **RSA-4096**: Strong asymmetric encryption for key exchange
- **PBKDF2**: 200,000 iterations for key derivation
- **Authenticated Encryption**: GCM and Poly1305 modes prevent tampering
- **Secure Random**: Cryptographically secure random number generation
## 🤝 Contributing
We welcome contributions! Please see our [ROADMAP.md](ROADMAP.md) for planned features and development priorities.
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests if applicable
5. Submit a pull request
## 📞 Support
- **Documentation**: See [API.md](API.md) for API details
- **Issues**: Report bugs via GitHub Issues
- **Discussions**: Use GitHub Discussions for questions
- **Element/Matrix Chat**:
- **Official Instance**: N/A
---
## 📄 License
MIT
**🔐 Secure by design. Simple by choice. Powerful by nature.**
+362
View File
@@ -0,0 +1,362 @@
> [!IMPORTANT]
> Fully modular code for encryption libraries, ensure metadata is stored as encrypted hashs for PacShare, Revamp PacShares secure file send and pickup, and create a CLI and local application (Linux and Android).
---
### Phase 0
- [x] ~~Remove docker files (Dropping official docker support)~~
- [ ] Readd docker support
- [x] Update README.md to be current.
- [x] Add roadmap.md to repo
- [x] Create /application_data/ folder (for server settings, admin login and creds)
- [x] Create scripts folder in /application_data/
- [x] Create /paccrypt_algos/ folder
- [x] Builder better start, stop and restart scripts both prod and dev (Cross-platform: Windows & Linux)
- [x] Add a button in the admin panel to switch to and from prod and dev modes - **COMPLETED: `/admin-switch-dev-mode` and `/admin-switch-prod-mode` endpoints implemented**
### Phase 1: app.py - Modular Python Web App
##### app.py Responsibilities
- [x] Flask app + routing
- [x] Handle:
- [x] /encrypt (via API endpoints)
- [x] /decrypt (via API endpoints)
- [x] /pickup/<file_id>
- [x] Receive:
- [x] File or text
- [x] pickup_password (required)
- [x] encryption_password (required)
- [x] encryption_mode (algorithm selection implemented)
- [x] Encrypt metadata using pickup password
- [x] Encrypt file using encryption password
- [x] Dynamically load correct engine via decrypted metadata
- [x] Save .encrypted + .json metadata, return pickup link
- [ ] Update PacMan like mini game logic revamp "(LOW PRIORITY)"
- [ ] Update PacMan like mini game base revamp "(LOW PRIORITY)"
---
##### /paccrypt_algos/ - Modular Crypto Engines
- [x] Create folder + interface
- [x] Remove basic cypher
Implement engines:
- [x] aes_gcm.py
- [x] aes_cbc.py
- [x] xchacha.py
- [x] rsa_hybrid.py
- [x] ~~PQCrypt_hybrid.py (Testing)~~ **REMOVED: Post-quantum crypto removed for simplicity**
- [x] Each must expose:
```
def encrypt_text(text, key): ...
def decrypt_text(ciphertext, key): ...
def encrypt_file(in_path, out_path, key): ...
def decrypt_file(in_path, out_path, key): ...
def generate_key_pair(): ... (for RSA hybrid)
```
**COMPLETED: All modules implemented with correct API**
---
### Phase 2: PacShare - Reimplementation
/encrypt Route Flow
- [x] JS submits (PacShare "Form"):
- [x] File
- [x] pickup_password (for metadata)
- [x] encryption_password (for file)
- [x] encryption_mode
- [x] 2FA TOTP setup (Yubi/Passkey not implemented)
- [x] Python logic:
- [x] Encrypt file using selected algo + encryption_password
- [x] Generate metadata dict:
- [x] filename, enc_mode, pickup_hash, timestamp, optional 2FA
- [x] Encrypt metadata using AES-GCM derived from pickup_password
- [x] Save .{algorithm}.encrypted and .json files
- [x] Generate random file_id
- [x] Return /pickup/<file_id> link
> [!IMPORTANT]
> Both passwords are required. One reveals the mode + metadata, the other decrypts the file.
---
##### /pickup/<file_id> Route Flow
- [x] Prompt for pickup_password
- [x] Decrypt .json metadata and validate hash
- [x] Show original filename, prompt for encryption_password
- [x] Load correct module, decrypt file
- [x] Offer file download
---
##### Metadata Structure (Encrypted JSON)
```
"filename": "report.pdf",
"algorithm": "aes_cbc",
"pickup_password": "<sha256>",
"created_at": "2025-08-05T18:00Z",
"require_2fa": true, // optional
"totp_secret": "base32string", // optional
"service_name": "PacCrypt File: report.pdf..." // optional
```
> [!NOTE]
> Stored as .json
> Encrypted with AES-GCM using key derived from pickup_password
> **COMPLETED: Metadata encryption implemented**
---
### Phase 3: External API Access (/api/*)
##### Endpoint Description
```
✅ GET /api/algorithms List available encryption algorithms
✅ POST /api/generate-keypair Generate RSA key pairs
✅ POST /api/encrypt File/text encryption (returns encrypted data)
✅ POST /api/decrypt File/text decryption
✅ POST /api/pacshare Upload + encrypt + return pickup link (JSON)
❌ POST /api/ps-pickup Provide pickup ID + passwords, return decrypted file (Use web interface)
❌ GET /api/version Return current version tag (Not implemented)
```
> [!NOTE]
> **COMPLETED: Core API endpoints implemented**
> Pickup is handled via web interface at /pickup/<file_id>
> Encryption password is never saved server-side
---
### Phase 4: CLI Tool (Offline and API Hybrid)
- [ ] Create PacCrypt-CLI repo
- [ ] paccrypt-cli command
- [ ] Local encrypt/decrypt support
##### Support:
- [ ] --share-api to change api address (in case user is self hosting PacCrypt-Webapp)
- Default api from https://paccrypt.unnaturalll.dev/
- [ ] --share to upload via /api/ps-send
- [ ] --pickup <id> to download + decrypt via /api/ps-pickup
##### Always require (Send + Pickup)
- [ ] --method (to define encryption type)
- [ ] --pickup-password
- [ ] --encryption-password
Optional (Send + Pickup)
- [ ] 2FA Token
- No Yubi or passkey support for API calls
- [ ] --help (Shows command usage)
- [ ] CLI PacMan like mini game (LOW PRIORITY)
---
### Phase 5: Local GUI Applications
##### Linux (First)
- [ ] PyQt6 or GTK
- [ ] Same features as the Webapp
- [ ] Support for PacShare through API calls
- Default https://paccrypt.unnaturalll.dev/
- User changeable if the webapp is self hosted
- [ ] Text Encryption / Decryption mode
- [ ] Text Password
- [ ] Text input / output
- [ ] PacShare Mode selector
- [ ] PacShare File Uploader
- [ ] PacShare Pickup Password
- [ ] PacShare Encryption / Decryption password
- [ ] PacShare 2FA Token support
- No Yubi/Passkey support for API calls
- [ ] PacShare error message if devices is offline or server can't be reached
- [ ] KDE Dolphin context integration (right-click → encrypt | decrypt | share - share opens the paccrypt gui with the file already staged)
##### Android
- [ ] Kivy or BeeWare
- [ ] Same features as the Webapp
- [ ] Support for PacShare through API calls
- Default https://paccrypt.unnaturalll.dev/
- User changeable if the webapp is self hosted
- [ ] Text Encryption / Decryption mode
- [ ] Text Password
- [ ] Text input / output
- [ ] PS Mode selector
- [ ] PS File Uploader
- [ ] PS Pickup Password
- [ ] PS Encryption / Decryption password
- [ ] PS 2FA Token support
- No Yubi/Passkey support for API calls
- [ ] PS error message if devices is offline or server can't be reached
> [!IMPORTANT]
> No <ins>Windows</ins> support for a application, only webapp, and maybe CLI support.
`Linux master race`
---
### PacShare File Format ✅ **COMPLETED**
```
pacshare/
├── <file_id>.<algorithm>.encrypted # Encrypted binary file
└── <file_id>.json # Encrypted metadata (JSON)
```
**Current Implementation:**
- Files are stored as `.{algorithm}.encrypted` (e.g., `.aes_cbc.encrypted`)
- Metadata stored as `.json` files with encrypted content
- Algorithm info embedded in filename for automatic detection
---
### Development Order
0. - [x] **Phase 0 Tasks**
1. - [x] **paccrypt_algos/ + aes_gcm.py**
2. - [x] **app.py routes: /encrypt, /pickup/<id>**
3. - [x] **Add /decrypt route**
4. - [x] **Build metadata encryption helpers**
5. - [x] **Finish other engine modules**
6. - [x] **Build /api/* equivalents**
7. - [x] **Update README.md with all changes to the webapp**
8. - [x] **Create a new installation guide** ✅ (Included in README.md)
9. - [ ] Build CLI ⏳ *Next Priority*
10. - [ ] Test CLI with --pickup + --share
12. - [ ] Build GUI app on Linux
13. - [ ] Test GUI app on Linux
14. - [ ] Build GUI app on Android
15. - [ ] Test GUI app on Android
16. - [ ] Finalize all releases and push to main
17. - [ ] Create Wiki
**🎉 WEBAPP CORE COMPLETE! 🎉**
**Current Status:** All core webapp functionality implemented including:
- ✅ Modular encryption engines (AES-GCM, AES-CBC, XChaCha20, RSA Hybrid)
- ✅ Complete API with documentation
- ✅ PacShare file sharing with 2FA support
- ✅ Admin panel with full management features
- ✅ Cross-platform deployment scripts
- ✅ Comprehensive documentation
---
### Current Webapp Structure ✅ **COMPLETED**
```
PacCrypt-Webapp/
├── app.py # Main Flask application ✅
├── README.md # Updated documentation ✅
├── ROADMAP.md # This file ✅
├── API.md # API documentation ✅ *NEW*
├── LICENSE # MIT License ✅
├── application_data/ ✅ # Application configuration
│ ├── control_scripts/ ✅ # Server management scripts
│ │ ├── start_dev.py ✅ # Development mode starter
│ │ ├── start_prod.py ✅ # Production mode starter
│ │ ├── restart_dev.py ✅ # Development restart
│ │ ├── restart_prod.py ✅ # Production restart
│ │ └── stop.py ✅ # Server stop script
│ ├── requirements.txt ✅ # Python dependencies
│ ├── settings.json ✅ # Application settings
│ ├── admin_creds.json ✅ # Encrypted admin credentials
│ ├── admin_key.key ✅ # Admin encryption key
│ └── admin_logs.enc ✅ # Encrypted audit logs
├── paccrypt_algos/ ✅ # Encryption modules
│ ├── __init__.py ✅ # Package initialization
│ ├── aes_cbc.py ✅ # AES-CBC implementation
│ ├── aes_gcm.py ✅ # AES-GCM implementation
│ ├── xchacha.py ✅ # XChaCha20-Poly1305
│ └── rsa_hybrid.py ✅ # RSA hybrid encryption
├── pacshare/ ✅ # File upload storage
│ ├── *.{algorithm}.encrypted ✅ # Encrypted uploaded files
│ └── *.json ✅ # File metadata
├── templates/ ✅ # HTML templates
│ ├── index.html ✅ # Main interface
│ ├── pickup.html ✅ # File pickup page
│ ├── admin*.html ✅ # Admin panel pages
│ └── error pages (403,404,500) ✅
└── static/ ✅ # Static assets
├── css/styles.css ✅ # Application styling
├── js/ ✅ # JavaScript modules
├── img/ ✅ # Images and icons
├── fonts/ ✅ # Custom fonts
└── audio/ ✅ # Sound effects
```
**🏆 PROJECT STRUCTURE FULLY IMPLEMENTED 🏆**
+298
View File
@@ -0,0 +1,298 @@
# PacCrypt Security Features 🔒
This document outlines the security enhancements added to PacCrypt, including setup instructions and configuration options.
## 🚀 New Security Features
### 1. Rate Limiting
- **API Endpoints**: Prevents abuse with configurable rate limits
- **Default Limits**:
- `/api/algorithms`: 100 requests/minute
- `/api/encrypt`, `/api/decrypt`: 30 requests/minute
- `/api/generate-keypair`: 10 requests/minute
- `/api/pacshare`: 10 requests/minute
- Global default: 1000 requests/hour
### 2. Session Timeout
- **Admin Sessions**: Automatic timeout after configurable period (default: 30 minutes)
- **Security**: Sessions are cleared and require re-authentication
- **Logging**: Session timeouts are logged for audit purposes
### 3. File Virus Scanning
- **Integration**: ClamAV antivirus scanning before encryption
- **Automatic**: All uploaded files are scanned
- **Logging**: Scan results and virus detections are logged
- **Graceful Degradation**: If ClamAV is unavailable, scanning is skipped with warning
### 4. IP Whitelisting
- **Admin Access**: Restrict admin panel access to specific IP addresses
- **CIDR Support**: Supports both single IPs and CIDR notation (e.g., `192.168.1.0/24`)
- **Flexible**: Empty whitelist allows all IPs (default behavior)
- **Logging**: Unauthorized access attempts are logged
### 5. Enhanced Audit Logging
- **Encrypted Logs**: All admin actions are encrypted and logged
- **Comprehensive**: Login attempts, file operations, security events
- **IP Tracking**: Source IP addresses are logged for security monitoring
## 🛠️ Installation & Setup
### Prerequisites
```bash
# Update package lists
sudo apt update
# Install Python dependencies
pip install -r application_data/requirements.txt
```
### ClamAV Setup (Required for Virus Scanning)
#### Ubuntu/Debian:
```bash
# Install ClamAV
sudo apt install clamav clamav-daemon
# Update virus definitions
sudo freshclam
# Start ClamAV daemon
sudo systemctl start clamav-daemon
sudo systemctl enable clamav-daemon
# Verify installation
sudo systemctl status clamav-daemon
```
#### CentOS/RHEL:
```bash
# Install EPEL repository
sudo yum install epel-release
# Install ClamAV
sudo yum install clamav clamav-server clamav-update
# Update virus definitions
sudo freshclam
# Start services
sudo systemctl start clamd@scan
sudo systemctl enable clamd@scan
```
#### Manual Configuration:
If ClamAV fails to start, you may need to configure it manually:
```bash
# Edit configuration
sudo nano /etc/clamav/clamd.conf
# Remove or comment out the "Example" line
# Example
# Set socket permissions
sudo chown clamav:clamav /var/run/clamav/clamd.ctl
sudo chmod 666 /var/run/clamav/clamd.ctl
# Restart daemon
sudo systemctl restart clamav-daemon
```
### Testing ClamAV Integration
```bash
# Test if ClamAV is working
clamscan --version
# Test daemon connection
clamdscan --version
# Test with EICAR test file (harmless test virus)
echo 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > /tmp/eicar.txt
clamscan /tmp/eicar.txt
```
## ⚙️ Configuration
### Admin Settings Panel
Access the admin settings at `/admin-settings` to configure:
1. **Session Timeout**: Set admin session timeout (minutes)
2. **Virus Scanning**: Enable/disable ClamAV scanning
3. **IP Whitelist**: Configure allowed admin IP addresses
4. **File Limits**: Upload size and retention settings
### Manual Configuration
Edit `application_data/settings.json`:
```json
{
"upload_folder": "pacshare",
"max_file_age_days": 14,
"max_file_size_bytes": 26843545600,
"admin_ip_whitelist": [
"192.168.1.100",
"10.0.0.0/8",
"127.0.0.1"
],
"virus_scanning_enabled": true,
"session_timeout_minutes": 30,
"rate_limit_per_minute": 60,
"rate_limit_per_hour": 1000
}
```
### IP Whitelist Examples
```json
"admin_ip_whitelist": [
"127.0.0.1", // Local access only
"192.168.1.100", // Specific IP
"192.168.1.0/24", // Local network
"10.0.0.0/8", // Private network range
"203.0.113.0/24" // Public IP range
]
```
## 🔍 Security Monitoring
### Log Files
- **Admin Logs**: `application_data/admin_logs.enc` (encrypted)
- **Application Logs**: Check console output for security events
### Key Events Logged
- Admin login/logout attempts
- Session timeouts
- IP whitelist violations
- Virus scan results
- File upload/download activities
- Rate limit violations
### Viewing Admin Logs
Access encrypted logs via the admin panel at `/admin-logs` or programmatically:
```python
# Example: View recent security events
key = load_admin_key()
cipher = Fernet(key)
with open('application_data/admin_logs.enc', 'rb') as f:
for line in f:
if line.strip():
decrypted = cipher.decrypt(line.strip())
print(decrypted.decode())
```
## 🚨 Security Best Practices
### 1. Regular Updates
```bash
# Update virus definitions
sudo freshclam
# Update Python dependencies
pip install --upgrade -r application_data/requirements.txt
```
### 2. Firewall Configuration
```bash
# UFW example - restrict admin access
sudo ufw allow from 192.168.1.0/24 to any port 5000
sudo ufw deny 5000
```
### 3. HTTPS Configuration
Always use HTTPS in production. Example nginx config:
```nginx
server {
listen 443 ssl http2;
server_name your-domain.com;
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/m;
location /api/ {
limit_req zone=api burst=5 nodelay;
proxy_pass http://127.0.0.1:5000;
}
location /admin {
# Additional admin restrictions
allow 192.168.1.0/24;
deny all;
proxy_pass http://127.0.0.1:5000;
}
}
```
### 4. Regular Security Audits
- Review admin logs regularly
- Monitor rate limit violations
- Check for unauthorized access attempts
- Verify virus scan effectiveness
## 🐛 Troubleshooting
### ClamAV Issues
```bash
# Check ClamAV status
sudo systemctl status clamav-daemon
# View ClamAV logs
sudo journalctl -u clamav-daemon
# Test socket connection
sudo -u clamav clamdscan --ping
# Manual socket creation
sudo mkdir -p /var/run/clamav
sudo chown clamav:clamav /var/run/clamav
```
### Rate Limiting Issues
- Check if requests are being properly limited
- Verify Flask-Limiter configuration
- Monitor application logs for rate limit errors
### Session Timeout Issues
- Verify session configuration in settings
- Check if `session.permanent = True` is set
- Ensure proper timezone handling
### IP Whitelist Issues
- Verify IP address format (CIDR notation)
- Check if client IP is correctly detected
- Consider proxy/load balancer IP forwarding
## 📋 Security Checklist
- [ ] ClamAV installed and running
- [ ] Virus definitions up to date
- [ ] Admin IP whitelist configured
- [ ] Session timeout configured
- [ ] Rate limiting tested
- [ ] HTTPS enabled in production
- [ ] Firewall rules configured
- [ ] Regular log monitoring set up
- [ ] Backup procedures for encrypted logs
- [ ] Security update schedule established
## 🔗 Related Documentation
- [Main README](README.md) - General installation and usage
- [API Documentation](API.md) - API endpoint details
- [Roadmap](ROADMAP.md) - Future security enhancements
---
**⚠️ Important Security Notes:**
1. **Default Configuration**: By default, IP whitelisting is disabled (empty list). Configure it for production use.
2. **ClamAV Dependency**: Virus scanning requires ClamAV. If not installed, scanning is skipped with warnings.
3. **Rate Limiting**: Default limits are conservative. Adjust based on your usage patterns.
4. **Log Encryption**: Admin logs are encrypted with the same key as admin credentials. Backup this key securely.
5. **Session Security**: Sessions use Flask's built-in session management. Consider Redis for distributed deployments.
For security questions or issues, please refer to the GitHub Issues page.
+918 -97
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,72 @@
import os
import subprocess
import signal
import time
import sys
import psutil
import platform
APP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../app.py"))
DEBUG = True
def log(msg):
if DEBUG:
print(msg)
def start_dev():
env = os.environ.copy()
env["PRODUCTION"] = "false"
if platform.system() == "Windows":
return subprocess.Popen(
["python", APP_PATH],
env=env,
stdout=sys.stdout,
stderr=sys.stderr
)
else:
return subprocess.Popen(
["python3", APP_PATH],
env=env,
preexec_fn=os.setsid,
stdout=sys.stdout,
stderr=sys.stderr
)
def stop_by_port(port=5000):
for proc in psutil.process_iter(["pid", "name"]):
try:
for conn in proc.net_connections(kind="inet"):
if conn.laddr.port == port:
log(f"[*] Killing process {proc.pid} using port {port}")
if platform.system() == "Windows":
proc.terminate()
try:
proc.wait(timeout=5)
except psutil.TimeoutExpired:
proc.kill()
else:
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
return
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
continue
log(f"[!] No process found using port {port}")
def main():
log("[*] Restarting PacCrypt in DEVELOPMENT mode...")
stop_by_port()
time.sleep(2)
proc = start_dev()
if proc:
log(f"[*] Started development server with PID {proc.pid}")
try:
proc.wait()
except KeyboardInterrupt:
log("[*] Interrupted, stopping server...")
stop_by_port()
else:
log("[!] Failed to start development server")
if __name__ == "__main__":
main()
@@ -0,0 +1,66 @@
import os
import subprocess
import signal
import time
import sys
import psutil
import platform
APP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../app.py"))
def start_prod():
env = os.environ.copy()
env["PRODUCTION"] = "true"
if platform.system() == "Windows":
return subprocess.Popen(
["python", APP_PATH],
env=env,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
else:
return subprocess.Popen(
["python3", APP_PATH],
env=env,
preexec_fn=os.setsid,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
def stop_by_port(port=5000):
for proc in psutil.process_iter(["pid", "name"]):
try:
for conn in proc.net_connections(kind="inet"):
if conn.laddr.port == port:
print(f"[*] Killing process {proc.pid} using port {port}")
if platform.system() == "Windows":
proc.terminate()
try:
proc.wait(timeout=5)
except psutil.TimeoutExpired:
proc.kill()
else:
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
return
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
continue
print(f"[!] No process found using port {port}")
def main():
print("[*] Restarting PacCrypt in PRODUCTION mode with Waitress...")
stop_by_port()
time.sleep(2)
proc = start_prod()
if proc:
print(f"[*] Started production server with PID {proc.pid}")
try:
proc.wait()
except KeyboardInterrupt:
print("[*] Interrupted, stopping server...")
stop_by_port()
else:
print("[!] Failed to start production server")
if __name__ == "__main__":
main()
@@ -0,0 +1,40 @@
import os
import subprocess
import time
import sys
import platform
APP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../app.py"))
DEBUG = True
def log(msg):
if DEBUG:
print(msg)
def start_dev():
env = os.environ.copy()
env["PRODUCTION"] = "false"
if platform.system() == "Windows":
return subprocess.Popen(
["python", APP_PATH],
env=env,
stdout=sys.stdout,
stderr=sys.stderr
)
else:
return subprocess.Popen(
["python3", APP_PATH],
env=env,
preexec_fn=os.setsid,
stdout=sys.stdout,
stderr=sys.stderr
)
def main():
log("[*] Starting PacCrypt in DEVELOPMENT mode...")
start_dev()
if __name__ == "__main__":
main()
@@ -0,0 +1,34 @@
import os
import subprocess
import time
import sys
import platform
APP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../app.py"))
def start_prod():
env = os.environ.copy()
env["PRODUCTION"] = "true"
if platform.system() == "Windows":
return subprocess.Popen(
["python", APP_PATH],
env=env,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
else:
return subprocess.Popen(
["python3", APP_PATH],
env=env,
preexec_fn=os.setsid,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
def main():
print("[*] Starting PacCrypt in PRODUCTION mode with Waitress...")
start_prod()
if __name__ == "__main__":
main()
+35
View File
@@ -0,0 +1,35 @@
import psutil
import os
import signal
import platform
DEBUG = True
def log(msg):
if DEBUG:
print(msg)
def stop_by_port(port=5000):
for proc in psutil.process_iter(["pid", "name"]):
try:
for conn in proc.net_connections(kind="inet"):
if conn.laddr.port == port:
log(f"[*] Killing process {proc.pid} using port {port}")
if platform.system() == "Windows":
proc.terminate()
try:
proc.wait(timeout=5)
except psutil.TimeoutExpired:
proc.kill()
else:
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
return
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
continue
log(f"[!] No process found using port {port}")
def main():
stop_by_port()
if __name__ == "__main__":
main()
+26
View File
@@ -0,0 +1,26 @@
### **requirements.txt**
# Core Flask stack
flask
flask-cors
waitress
werkzeug
# Encryption engines
cryptography
pycryptodome
pqcrypto
# Utility
psutil
# Security and rate limiting
flask-limiter
clamd
ipaddress
# TOTP for 2FA
pyotp
qrcode
# Run pip install -r application_data/requirements.txt
View File
+136
View File
@@ -0,0 +1,136 @@
import os
import base64
from typing import Optional
from cryptography.hazmat.primitives import padding, hashes, hmac
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.exceptions import InvalidSignature
# === Constants ===
SALT_LENGTH = 16
IV_LENGTH = 16
PBKDF2_ITERATIONS = 200_000
KEY_LENGTH = 32
HMAC_KEY_LENGTH = 32 # For HMAC-SHA256
HMAC_LENGTH = 32 # Output size of SHA256
# === Base64 Helpers ===
def b64encode(data: bytes) -> str:
return base64.b64encode(data).decode('utf-8')
def b64decode(data: str) -> bytes:
return base64.b64decode(data.encode('utf-8'))
# === Key Derivation ===
def derive_key(password: str, salt: bytes) -> bytes:
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=KEY_LENGTH + HMAC_KEY_LENGTH,
salt=salt,
iterations=PBKDF2_ITERATIONS,
backend=default_backend()
)
full_key = kdf.derive(password.encode('utf-8'))
return full_key[:KEY_LENGTH], full_key[KEY_LENGTH:]
# === Encrypt Text ===
def encrypt_text(plaintext: str, password: str) -> str:
salt = os.urandom(SALT_LENGTH)
iv = os.urandom(IV_LENGTH)
aes_key, hmac_key = derive_key(password, salt)
padder = padding.PKCS7(128).padder()
padded = padder.update(plaintext.encode('utf-8')) + padder.finalize()
cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv), backend=default_backend())
encryptor = cipher.encryptor()
ciphertext = encryptor.update(padded) + encryptor.finalize()
payload = salt + iv + ciphertext
h = hmac.HMAC(hmac_key, hashes.SHA256(), backend=default_backend())
h.update(payload)
mac = h.finalize()
return b64encode(payload + mac)
# === Decrypt Text ===
def decrypt_text(encrypted_b64: str, password: str) -> str:
raw = b64decode(encrypted_b64)
salt = raw[:SALT_LENGTH]
iv = raw[SALT_LENGTH:SALT_LENGTH + IV_LENGTH]
ciphertext = raw[SALT_LENGTH + IV_LENGTH:-HMAC_LENGTH]
mac = raw[-HMAC_LENGTH:]
aes_key, hmac_key = derive_key(password, salt)
h = hmac.HMAC(hmac_key, hashes.SHA256(), backend=default_backend())
h.update(raw[:-HMAC_LENGTH])
h.verify(mac)
cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv), backend=default_backend())
decryptor = cipher.decryptor()
padded = decryptor.update(ciphertext) + decryptor.finalize()
unpadder = padding.PKCS7(128).unpadder()
plaintext = unpadder.update(padded) + unpadder.finalize()
return plaintext.decode('utf-8')
# === Encrypt File ===
def encrypt_file(in_path, out_path, password: str, metadata: Optional[dict] = None):
with open(in_path, 'rb') as f:
plaintext = f.read()
salt = os.urandom(SALT_LENGTH)
iv = os.urandom(IV_LENGTH)
aes_key, hmac_key = derive_key(password, salt)
padder = padding.PKCS7(128).padder()
padded = padder.update(plaintext) + padder.finalize()
cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv), backend=default_backend())
encryptor = cipher.encryptor()
ciphertext = encryptor.update(padded) + encryptor.finalize()
payload = salt + iv + ciphertext
h = hmac.HMAC(hmac_key, hashes.SHA256(), backend=default_backend())
h.update(payload)
mac = h.finalize()
with open(out_path, 'wb') as f:
f.write(payload + mac)
# === Decrypt File ===
def decrypt_file(in_path, out_path, password: str, metadata: Optional[dict] = None):
with open(in_path, 'rb') as f:
raw = f.read()
salt = raw[:SALT_LENGTH]
iv = raw[SALT_LENGTH:SALT_LENGTH + IV_LENGTH]
ciphertext = raw[SALT_LENGTH + IV_LENGTH:-HMAC_LENGTH]
mac = raw[-HMAC_LENGTH:]
aes_key, hmac_key = derive_key(password, salt)
h = hmac.HMAC(hmac_key, hashes.SHA256(), backend=default_backend())
h.update(raw[:-HMAC_LENGTH])
h.verify(mac)
cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv), backend=default_backend())
decryptor = cipher.decryptor()
padded = decryptor.update(ciphertext) + decryptor.finalize()
unpadder = padding.PKCS7(128).unpadder()
plaintext = unpadder.update(padded) + unpadder.finalize()
with open(out_path, 'wb') as f:
f.write(plaintext)
# === Algo Name ===
def get_name():
return "AES-CBC"
+68
View File
@@ -0,0 +1,68 @@
import os
import base64
import json
from typing import Optional
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
# === Constants ===
SALT_LENGTH = 16
IV_LENGTH = 12
PBKDF2_ITERATIONS = 200_000
KEY_LENGTH = 32 # 256 bits
# === Base64 Helpers ===
def b64encode(data: bytes) -> str:
return base64.b64encode(data).decode('utf-8')
def b64decode(data: str) -> bytes:
return base64.b64decode(data.encode('utf-8'))
# === Key Derivation ===
def derive_key(password: str, salt: bytes) -> bytes:
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=KEY_LENGTH,
salt=salt,
iterations=PBKDF2_ITERATIONS,
backend=default_backend()
)
return kdf.derive(password.encode('utf-8'))
# === Encrypt Text ===
def encrypt_text(plaintext: str, password: str) -> str:
salt = os.urandom(SALT_LENGTH)
iv = os.urandom(IV_LENGTH)
key = derive_key(password, salt)
aesgcm = AESGCM(key)
ciphertext = aesgcm.encrypt(iv, plaintext.encode('utf-8'), None)
payload = salt + iv + ciphertext
return b64encode(payload)
# === Decrypt Text ===
def decrypt_text(encrypted_b64: str, password: str) -> str:
raw = b64decode(encrypted_b64)
salt = raw[:SALT_LENGTH]
iv = raw[SALT_LENGTH:SALT_LENGTH + IV_LENGTH]
ciphertext = raw[SALT_LENGTH + IV_LENGTH:]
key = derive_key(password, salt)
aesgcm = AESGCM(key)
plaintext = aesgcm.decrypt(iv, ciphertext, None)
return plaintext.decode('utf-8')
# === Metadata-less file interface (optional placeholders) ===
def encrypt_file(in_path, out_path, key, metadata: Optional[dict] = None):
raise NotImplementedError("File encryption not implemented yet.")
def decrypt_file(in_path, out_path, key, metadata: Optional[dict] = None):
raise NotImplementedError("File decryption not implemented yet.")
# === Engine Name ===
def get_name():
return "AES-GCM"
+151
View File
@@ -0,0 +1,151 @@
import os
import base64
import json
import importlib
from typing import Optional, Tuple
import sys
from pathlib import Path
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.backends import default_backend
PARENT_DIR = Path(__file__).resolve().parent.parent
if str(PARENT_DIR) not in sys.path:
sys.path.append(str(PARENT_DIR))
# === Constants ===
RSA_KEY_SIZE = 4096
AES_KEY_SIZE = 32 # 256-bit
# === Base64 Helpers ===
def b64encode(data: bytes) -> str:
return base64.b64encode(data).decode("utf-8")
def b64decode(data: str) -> bytes:
return base64.b64decode(data.encode("utf-8"))
# === RSA Key Generation ===
def generate_key_pair() -> Tuple[bytes, bytes]:
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=RSA_KEY_SIZE,
backend=default_backend()
)
private_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
)
public_pem = private_key.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
return private_pem, public_pem
# === Dynamic Engine Loader ===
def load_engine(engine_name: str):
try:
return importlib.import_module(f'paccrypt_algos.{engine_name}')
except ModuleNotFoundError:
raise ValueError(f"Encryption engine '{engine_name}' not found.")
# === Encrypt Text ===
def encrypt_text(plaintext: str, public_key_pem: str, engine_name: str = "aes_gcm") -> str:
engine = load_engine(engine_name)
aes_key = os.urandom(AES_KEY_SIZE)
public_key = serialization.load_pem_public_key(public_key_pem.encode(), backend=default_backend())
encrypted_key = public_key.encrypt(
aes_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
encrypted_data = engine.encrypt_text(plaintext, aes_key.hex())
header = json.dumps({"alg": engine_name}).encode()
payload = len(encrypted_key).to_bytes(2, 'big') + encrypted_key + header + b'\0' + encrypted_data.encode()
return b64encode(payload)
# === Decrypt Text ===
def decrypt_text(encrypted_b64: str, private_key_pem: str) -> str:
private_key = serialization.load_pem_private_key(private_key_pem.encode(), password=None, backend=default_backend())
raw = b64decode(encrypted_b64)
enc_key_len = int.from_bytes(raw[:2], 'big')
enc_key = raw[2:2 + enc_key_len]
rest = raw[2 + enc_key_len:]
header_data, encrypted_data = rest.split(b'\0', 1)
engine_name = json.loads(header_data.decode()).get("alg")
aes_key = private_key.decrypt(
enc_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
engine = load_engine(engine_name)
return engine.decrypt_text(encrypted_data.decode(), aes_key.hex())
# === Encrypt File ===
def encrypt_file(in_path: str, out_path: str, public_key_pem: str, engine_name: str = "aes_gcm"):
engine = load_engine(engine_name)
aes_key = os.urandom(AES_KEY_SIZE)
public_key = serialization.load_pem_public_key(public_key_pem.encode(), backend=default_backend())
encrypted_key = public_key.encrypt(
aes_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
with open(in_path, 'rb') as f:
plaintext = f.read()
encrypted_data = engine.encrypt_file_bytes(plaintext, aes_key.hex())
header = json.dumps({"alg": engine_name}).encode()
payload = len(encrypted_key).to_bytes(2, 'big') + encrypted_key + header + b'\0' + encrypted_data
with open(out_path, 'wb') as f:
f.write(payload)
# === Decrypt File ===
def decrypt_file(in_path: str, out_path: str, private_key_pem: str):
private_key = serialization.load_pem_private_key(private_key_pem.encode(), password=None, backend=default_backend())
with open(in_path, 'rb') as f:
raw = f.read()
enc_key_len = int.from_bytes(raw[:2], 'big')
enc_key = raw[2:2 + enc_key_len]
rest = raw[2 + enc_key_len:]
header_data, encrypted_data = rest.split(b'\0', 1)
engine_name = json.loads(header_data.decode()).get("alg")
aes_key = private_key.decrypt(
enc_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
engine = load_engine(engine_name)
plaintext = engine.decrypt_file_bytes(encrypted_data, aes_key.hex())
with open(out_path, 'wb') as f:
f.write(plaintext)
# === Engine Name ===
def get_name():
return "RSA Hybrid"
+92
View File
@@ -0,0 +1,92 @@
import os
import base64
from typing import Optional
from Crypto.Cipher import ChaCha20_Poly1305
from Crypto.Random import get_random_bytes
from Crypto.Protocol.KDF import PBKDF2
from Crypto.Hash import SHA256
# === Constants ===
SALT_LENGTH = 16
NONCE_LENGTH = 24
KEY_LENGTH = 32
PBKDF2_ITERATIONS = 200_000
TAG_LENGTH = 16
# === Base64 Helpers ===
def b64encode(data: bytes) -> str:
return base64.b64encode(data).decode('utf-8')
def b64decode(data: str) -> bytes:
return base64.b64decode(data.encode('utf-8'))
# === Key Derivation ===
def derive_key(password: str, salt: bytes) -> bytes:
return PBKDF2(password, salt, dkLen=KEY_LENGTH, count=PBKDF2_ITERATIONS, hmac_hash_module=SHA256)
# === Encrypt Text ===
def encrypt_text(plaintext: str, password: str) -> str:
salt = get_random_bytes(SALT_LENGTH)
nonce = get_random_bytes(NONCE_LENGTH)
key = derive_key(password, salt)
cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
ciphertext, tag = cipher.encrypt_and_digest(plaintext.encode('utf-8'))
final = salt + nonce + ciphertext + tag
return b64encode(final)
# === Decrypt Text ===
def decrypt_text(encrypted_b64: str, password: str) -> str:
raw = b64decode(encrypted_b64)
salt = raw[:SALT_LENGTH]
nonce = raw[SALT_LENGTH:SALT_LENGTH + NONCE_LENGTH]
tag = raw[-TAG_LENGTH:]
ciphertext = raw[SALT_LENGTH + NONCE_LENGTH:-TAG_LENGTH]
key = derive_key(password, salt)
cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
plaintext = cipher.decrypt_and_verify(ciphertext, tag)
return plaintext.decode('utf-8')
# === Encrypt File ===
def encrypt_file(in_path, out_path, password: str, metadata: Optional[dict] = None):
with open(in_path, 'rb') as f:
plaintext = f.read()
salt = get_random_bytes(SALT_LENGTH)
nonce = get_random_bytes(NONCE_LENGTH)
key = derive_key(password, salt)
cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
ciphertext, tag = cipher.encrypt_and_digest(plaintext)
with open(out_path, 'wb') as f:
f.write(salt + nonce + ciphertext + tag)
# === Decrypt File ===
def decrypt_file(in_path, out_path, password: str, metadata: Optional[dict] = None):
with open(in_path, 'rb') as f:
raw = f.read()
salt = raw[:SALT_LENGTH]
nonce = raw[SALT_LENGTH:SALT_LENGTH + NONCE_LENGTH]
tag = raw[-TAG_LENGTH:]
ciphertext = raw[SALT_LENGTH + NONCE_LENGTH:-TAG_LENGTH]
key = derive_key(password, salt)
cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
plaintext = cipher.decrypt_and_verify(ciphertext, tag)
with open(out_path, 'wb') as f:
f.write(plaintext)
# === Engine Name ===
def get_name():
return "XChaCha20-Poly1305"
if __name__ == "__main__":
from Crypto.Cipher.ChaCha20_Poly1305 import ChaCha20Poly1305Cipher as _test # Force import to validate availability
from cryptography.exceptions import InvalidTag # Still catchable for consistency
-10
View File
@@ -1,10 +0,0 @@
### **requirements.txt**
flask==3.0.3
cryptography==42.0.5
waitress==2.1.2
werkzeug==3.0.1
psutil>=5.9.0,<6.0.0
# nginx - Only needed for Nginx integration, not installed via pip
# Run pip install -r requirements.txt
-7
View File
@@ -1,7 +0,0 @@
@echo off
timeout /t 2 /nobreak
taskkill /F /PID 15428
set PRODUCTION=true
start "" "python" "app.py"
-17
View File
@@ -1,17 +0,0 @@
#!/bin/bash
sleep 2
# Save current process PID
PID=$1
# Gracefully stop the current server
kill "$PID"
# Wait until it exits
while kill -0 "$PID" 2>/dev/null; do
sleep 0.5
done
# Restart with the same interpreter and script
export PRODUCTION=true
exec "$2" "$3"
-1
View File
@@ -1 +0,0 @@
{"upload_folder": "uploads", "max_file_age_days": 14, "max_file_size_bytes": 26843545600}
-5
View File
@@ -1,5 +0,0 @@
@echo off
echo Starting PacCrypt in DEVELOPMENT mode...
set PRODUCTION=false
python app.py
pause
-4
View File
@@ -1,4 +0,0 @@
#!/bin/bash
echo "Starting PacCrypt in DEVELOPMENT mode..."
export PRODUCTION=false
python3 app.py
-5
View File
@@ -1,5 +0,0 @@
@echo off
echo Starting PacCrypt in PRODUCTION mode...
set PRODUCTION=true
python app.py
pause
-4
View File
@@ -1,4 +0,0 @@
#!/bin/bash
echo "Starting PacCrypt in PRODUCTION mode..."
export PRODUCTION=true
python3 app.py
+727
View File
@@ -5,6 +5,733 @@
padding: 0; padding: 0;
} }
/* ===== Bulk Operations Styles ===== */
.drop-zone {
border: 2px dashed #00ff99;
border-radius: 8px;
padding: 40px 20px;
text-align: center;
background-color: #001100;
transition: all 0.3s ease;
cursor: pointer;
}
.drop-zone:hover,
.drop-zone.drag-over {
background-color: #002200;
border-color: #00ff44;
}
.drop-zone-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.drop-zone-icon {
font-size: 2em;
margin-bottom: 10px;
}
.file-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid #333;
border-radius: 5px;
background-color: #111;
}
.file-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 15px;
border-bottom: 1px solid #333;
background-color: #1a1a1a;
}
.file-item:last-child {
border-bottom: none;
}
.file-info {
display: flex;
flex-direction: column;
flex: 1;
gap: 5px;
}
.file-name {
font-weight: bold;
color: #00ff99;
}
.file-size {
font-size: 0.8em;
color: #888;
}
.file-actions {
display: flex;
gap: 10px;
}
.progress-container {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 10px;
}
.progress-bar {
flex: 1;
height: 20px;
background-color: #333;
border-radius: 10px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: #00ff99;
border-radius: 10px;
transition: width 0.3s ease;
width: 0%;
}
.file-progress-list {
max-height: 200px;
overflow-y: auto;
border: 1px solid #333;
border-radius: 5px;
background-color: #111;
}
.file-progress-item {
padding: 10px 15px;
border-bottom: 1px solid #333;
display: flex;
align-items: center;
justify-content: space-between;
}
.file-progress-item:last-child {
border-bottom: none;
}
.file-progress-name {
flex: 1;
font-size: 0.9em;
color: #ccc;
}
.file-progress-status {
font-size: 0.8em;
padding: 3px 8px;
border-radius: 3px;
}
.status-processing {
background-color: #ffaa00;
color: #000;
}
.status-completed {
background-color: #00ff99;
color: #000;
}
.status-error {
background-color: #ff4444;
color: #fff;
}
.results-list {
max-height: 400px;
overflow-y: auto;
border: 1px solid #333;
border-radius: 5px;
background-color: #111;
}
.result-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
border-bottom: 1px solid #333;
}
.result-item:last-child {
border-bottom: none;
}
.result-info {
flex: 1;
}
.result-name {
font-weight: bold;
color: #00ff99;
margin-bottom: 5px;
}
.result-details {
font-size: 0.8em;
color: #888;
}
.result-actions {
display: flex;
gap: 10px;
}
/* File Preview Styles */
.file-preview-container {
max-height: 200px;
overflow-y: auto;
border: 1px solid #333;
border-radius: 5px;
background-color: #111;
margin-top: 10px;
}
.file-preview-content {
padding: 15px;
font-family: monospace;
white-space: pre-wrap;
font-size: 0.8em;
color: #ccc;
}
.image-preview {
max-width: 100%;
max-height: 150px;
border-radius: 5px;
}
.file-preview-header {
padding: 10px 15px;
border-bottom: 1px solid #333;
background-color: #1a1a1a;
font-weight: bold;
color: #00ff99;
}
/* ===== Password Settings Modal ===== */
.settings-button {
background: none;
border: 2px solid #00ff99;
color: #00ff99;
border-radius: 50%;
width: 40px;
height: 40px;
font-size: 1.2em;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.settings-button:hover {
background-color: #00ff99;
color: #000;
transform: rotate(90deg);
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(5px);
}
.modal-content {
background-color: #1a1a1a;
border: 2px solid #00ff99;
border-radius: 10px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 10px 30px rgba(0, 255, 153, 0.3);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #333;
background-color: #222;
}
.modal-header h3 {
margin: 0;
color: #00ff99;
font-size: 1.1em;
}
.close-button {
background: none;
border: none;
color: #ff6b6b;
font-size: 1.5em;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
border-radius: 50%;
transition: all 0.3s ease;
}
.close-button:hover {
background-color: #ff6b6b;
color: #fff;
transform: rotate(90deg);
}
.modal-body {
padding: 20px;
}
.modal-footer {
display: flex;
justify-content: space-between;
padding: 20px;
border-top: 1px solid #333;
background-color: #222;
}
.setting-group {
margin-bottom: 25px;
}
.setting-group h4 {
color: #00ff99;
margin-bottom: 15px;
font-size: 1em;
border-bottom: 1px solid #333;
padding-bottom: 5px;
}
.length-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.length-input-container {
display: flex;
align-items: center;
gap: 8px;
}
.length-number-input {
width: 70px;
padding: 6px 10px;
background-color: #333;
border: 2px solid #666;
border-radius: 5px;
color: #00ff99;
font-weight: bold;
text-align: center;
font-size: 1em;
transition: all 0.3s ease;
}
.length-number-input:focus {
outline: none;
border-color: #00ff99;
box-shadow: 0 0 10px rgba(0, 255, 153, 0.3);
background-color: #222;
}
.length-number-input::-webkit-outer-spin-button,
.length-number-input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.length-number-input[type=number] {
-moz-appearance: textfield;
}
.length-unit {
font-size: 0.9em;
color: #888;
font-weight: normal;
}
.length-slider {
width: 100%;
height: 8px;
border-radius: 5px;
background: #333;
outline: none;
margin: 10px 0;
-webkit-appearance: none;
}
.length-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #00ff99;
cursor: pointer;
box-shadow: 0 0 10px rgba(0, 255, 153, 0.5);
transition: all 0.3s ease;
}
.length-slider::-webkit-slider-thumb:hover {
transform: scale(1.1);
box-shadow: 0 0 15px rgba(0, 255, 153, 0.7);
}
.length-slider::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: #00ff99;
cursor: pointer;
border: none;
box-shadow: 0 0 10px rgba(0, 255, 153, 0.5);
transition: all 0.3s ease;
}
.length-slider::-moz-range-thumb:hover {
transform: scale(1.1);
box-shadow: 0 0 15px rgba(0, 255, 153, 0.7);
}
.length-labels {
display: flex;
justify-content: space-between;
font-size: 0.8em;
color: #888;
margin-top: 5px;
}
.checkbox-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.checkbox-item {
display: flex;
align-items: center;
cursor: pointer;
padding: 10px;
border-radius: 5px;
transition: background-color 0.3s ease;
position: relative;
}
.checkbox-item:hover {
background-color: #333;
}
.checkbox-item input[type="checkbox"] {
display: none;
}
.checkmark {
width: 20px;
height: 20px;
border: 2px solid #666;
border-radius: 3px;
margin-right: 10px;
position: relative;
transition: all 0.3s ease;
}
.checkbox-item input[type="checkbox"]:checked + .checkmark {
background-color: #00ff99;
border-color: #00ff99;
}
.checkbox-item input[type="checkbox"]:checked + .checkmark::after {
content: "✓";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #000;
font-weight: bold;
font-size: 0.9em;
}
.custom-input {
width: 100%;
padding: 10px;
background-color: #333;
border: 1px solid #666;
border-radius: 5px;
color: #fff;
font-family: monospace;
margin-top: 5px;
}
.custom-input:focus {
outline: none;
border-color: #00ff99;
box-shadow: 0 0 5px rgba(0, 255, 153, 0.3);
}
.setting-hint {
font-size: 0.8em;
color: #888;
margin-top: 5px;
}
.charset-preview {
background-color: #333;
border: 1px solid #666;
border-radius: 5px;
padding: 15px;
font-family: monospace;
font-size: 0.9em;
color: #ccc;
max-height: 100px;
overflow-y: auto;
word-break: break-all;
line-height: 1.4;
}
.primary-button {
background-color: #00ff99;
color: #000;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s ease;
}
.primary-button:hover {
background-color: #00cc77;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 255, 153, 0.3);
}
.secondary-button {
background: none;
color: #ccc;
border: 1px solid #666;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s ease;
}
.secondary-button:hover {
background-color: #333;
border-color: #999;
color: #fff;
}
/* Radio Button Styles */
.mode-selection {
display: flex;
flex-direction: column;
gap: 15px;
}
.radio-item {
display: flex;
align-items: center;
cursor: pointer;
padding: 15px;
border: 2px solid #333;
border-radius: 8px;
transition: all 0.3s ease;
position: relative;
}
.radio-item:hover {
border-color: #666;
background-color: #333;
}
.radio-item input[type="radio"] {
display: none;
}
.radiomark {
width: 20px;
height: 20px;
border: 2px solid #666;
border-radius: 50%;
margin-right: 15px;
position: relative;
transition: all 0.3s ease;
flex-shrink: 0;
}
.radio-item input[type="radio"]:checked + .radiomark {
border-color: #00ff99;
background-color: #00ff99;
}
.radio-item input[type="radio"]:checked + .radiomark::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #000;
}
.radio-item input[type="radio"]:checked {
border-color: #00ff99;
background-color: #001100;
}
.radio-content {
flex: 1;
}
.radio-title {
font-weight: bold;
color: #00ff99;
margin-bottom: 5px;
}
.radio-description {
font-size: 0.8em;
color: #888;
line-height: 1.3;
}
/* Size Input Styles */
.size-input-container {
display: flex;
align-items: center;
gap: 8px;
margin-top: 5px;
}
.size-number-input {
width: 80px;
padding: 6px 10px;
background-color: #333;
border: 2px solid #666;
border-radius: 5px;
color: #00ff99;
font-weight: bold;
text-align: center;
font-size: 1em;
transition: all 0.3s ease;
}
.size-number-input:focus {
outline: none;
border-color: #00ff99;
box-shadow: 0 0 5px rgba(0, 255, 153, 0.3);
background-color: #222;
}
.size-number-input::-webkit-outer-spin-button,
.size-number-input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.size-number-input[type=number] {
-moz-appearance: textfield;
}
.size-unit {
font-size: 0.9em;
color: #888;
font-weight: normal;
}
.setting-item {
margin-bottom: 10px;
}
/* Enhanced Checkbox Styles for Descriptions */
.checkbox-content {
flex: 1;
}
.checkbox-title {
font-weight: bold;
color: #00ff99;
margin-bottom: 3px;
font-size: 0.95em;
}
.checkbox-description {
font-size: 0.8em;
color: #888;
line-height: 1.3;
}
/* Responsive adjustments */
@media (max-width: 600px) {
.drop-zone {
padding: 30px 15px;
}
.file-item,
.result-item {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.file-actions,
.result-actions {
width: 100%;
justify-content: flex-start;
}
.progress-container {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.modal-content {
width: 95%;
max-height: 90vh;
}
.checkbox-grid {
grid-template-columns: 1fr;
}
.modal-footer {
flex-direction: column;
gap: 10px;
}
.settings-button {
width: 35px;
height: 35px;
font-size: 1em;
}
}
/* ===== Body ===== */ /* ===== Body ===== */
body { body {
font-family: 'Press Start 2P', monospace; font-family: 'Press Start 2P', monospace;
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

+508
View File
@@ -0,0 +1,508 @@
/**
* Bulk Operations Module
* Handles bulk file encryption/decryption, drag & drop, and file preview
*/
class BulkOperations {
constructor() {
this.files = [];
this.results = [];
this.isProcessing = false;
this.setupEventListeners();
this.populateAlgorithmDropdown();
}
setupEventListeners() {
// Drag & Drop Zone
const dropZone = document.getElementById('bulk-drop-zone');
const fileInput = document.getElementById('bulk-file-input');
const fileSelect = document.getElementById('bulk-file-select');
console.log('Bulk setup - dropZone:', dropZone, 'fileInput:', fileInput, 'fileSelect:', fileSelect);
if (dropZone && fileInput) {
console.log('Setting up bulk drag & drop events');
// Drag & Drop Events
dropZone.addEventListener('dragover', this.handleDragOver.bind(this));
dropZone.addEventListener('dragleave', this.handleDragLeave.bind(this));
dropZone.addEventListener('drop', this.handleDrop.bind(this));
dropZone.addEventListener('click', () => {
console.log('Bulk drop zone clicked, opening file input');
fileInput.click();
});
// File Input Events
fileInput.addEventListener('change', this.handleFileSelect.bind(this));
} else {
console.error('Bulk elements not found - dropZone:', dropZone, 'fileInput:', fileInput);
}
if (fileSelect) {
console.log('Setting up bulk file select button');
fileSelect.addEventListener('click', (e) => {
console.log('Bulk file select button clicked');
e.stopPropagation();
fileInput.click();
});
} else {
console.error('Bulk file select button not found');
}
// Control Buttons
const processBtn = document.getElementById('bulk-process-btn');
const clearBtn = document.getElementById('bulk-clear-btn');
const downloadAllBtn = document.getElementById('bulk-download-all');
const resetBtn = document.getElementById('bulk-reset');
if (processBtn) processBtn.addEventListener('click', this.processFiles.bind(this));
if (clearBtn) clearBtn.addEventListener('click', this.clearFiles.bind(this));
if (downloadAllBtn) downloadAllBtn.addEventListener('click', this.downloadAllResults.bind(this));
if (resetBtn) resetBtn.addEventListener('click', this.reset.bind(this));
}
handleDragOver(e) {
e.preventDefault();
e.stopPropagation();
document.getElementById('bulk-drop-zone').classList.add('drag-over');
}
handleDragLeave(e) {
e.preventDefault();
e.stopPropagation();
document.getElementById('bulk-drop-zone').classList.remove('drag-over');
}
handleDrop(e) {
e.preventDefault();
e.stopPropagation();
document.getElementById('bulk-drop-zone').classList.remove('drag-over');
const files = Array.from(e.dataTransfer.files);
this.addFiles(files);
}
handleFileSelect(e) {
const files = Array.from(e.target.files);
this.addFiles(files);
}
addFiles(newFiles) {
// Filter out duplicates
newFiles = newFiles.filter(newFile =>
!this.files.some(existingFile =>
existingFile.name === newFile.name && existingFile.size === newFile.size
)
);
this.files.push(...newFiles);
this.updateFileList();
this.showFilePreview();
}
updateFileList() {
const fileList = document.getElementById('bulk-file-list');
if (!fileList) return;
fileList.innerHTML = '';
this.files.forEach((file, index) => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.innerHTML = `
<div class="file-info">
<div class="file-name">${file.name}</div>
<div class="file-size">${this.formatFileSize(file.size)}</div>
</div>
<div class="file-actions">
<button type="button" onclick="bulkOps.previewFile(${index})" style="padding: 5px 10px; font-size: 0.8em;">Preview</button>
<button type="button" onclick="bulkOps.removeFile(${index})" class="danger-button" style="padding: 5px 10px; font-size: 0.8em;">Remove</button>
</div>
`;
fileList.appendChild(fileItem);
});
}
showFilePreview() {
const previewSection = document.getElementById('bulk-file-preview');
if (previewSection) {
previewSection.style.display = this.files.length > 0 ? 'block' : 'none';
}
}
async previewFile(index) {
const file = this.files[index];
if (!file) return;
const previewContainer = document.createElement('div');
previewContainer.className = 'file-preview-container';
const header = document.createElement('div');
header.className = 'file-preview-header';
header.textContent = `Preview: ${file.name}`;
const content = document.createElement('div');
content.className = 'file-preview-content';
// Handle different file types
if (file.type.startsWith('text/') || this.isTextFile(file.name)) {
try {
const text = await this.readFileAsText(file);
content.textContent = text.length > 2000 ? text.substring(0, 2000) + '...' : text;
} catch (error) {
content.textContent = 'Error reading file: ' + error.message;
}
} else if (file.type.startsWith('image/')) {
const img = document.createElement('img');
img.className = 'image-preview';
img.src = URL.createObjectURL(file);
img.onload = () => URL.revokeObjectURL(img.src);
content.appendChild(img);
} else {
content.innerHTML = `
<div style="color: #888;">
File Type: ${file.type || 'Unknown'}<br>
Size: ${this.formatFileSize(file.size)}<br>
Preview not available for this file type.
</div>
`;
}
previewContainer.appendChild(header);
previewContainer.appendChild(content);
// Remove existing preview
const existingPreview = document.querySelector('.file-preview-container');
if (existingPreview) {
existingPreview.remove();
}
// Add new preview after the file list
const fileList = document.getElementById('bulk-file-list');
if (fileList) {
fileList.parentNode.insertBefore(previewContainer, fileList.nextSibling);
}
}
isTextFile(filename) {
const textExtensions = ['.txt', '.md', '.js', '.html', '.css', '.json', '.xml', '.csv', '.log', '.py', '.java', '.c', '.cpp', '.h'];
return textExtensions.some(ext => filename.toLowerCase().endsWith(ext));
}
readFileAsText(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = e => resolve(e.target.result);
reader.onerror = e => reject(new Error('Failed to read file'));
reader.readAsText(file);
});
}
removeFile(index) {
this.files.splice(index, 1);
this.updateFileList();
this.showFilePreview();
// Remove preview if it exists
const existingPreview = document.querySelector('.file-preview-container');
if (existingPreview) {
existingPreview.remove();
}
}
clearFiles() {
this.files = [];
this.updateFileList();
this.showFilePreview();
// Remove preview if it exists
const existingPreview = document.querySelector('.file-preview-container');
if (existingPreview) {
existingPreview.remove();
}
// Clear file input
const fileInput = document.getElementById('bulk-file-input');
if (fileInput) fileInput.value = '';
}
async processFiles() {
if (this.files.length === 0) {
alert('Please select files to process');
return;
}
const password = document.getElementById('bulk-password')?.value;
if (!password) {
alert('Please enter a password');
return;
}
const algorithm = document.getElementById('bulk-algorithm')?.value;
if (!algorithm) {
alert('Please select an algorithm');
return;
}
const isDecrypt = document.getElementById('bulk-operation-toggle')?.checked;
this.isProcessing = true;
this.results = [];
// Show progress section
const progressSection = document.getElementById('bulk-progress-section');
if (progressSection) progressSection.style.display = 'block';
// Initialize progress
this.updateOverallProgress(0, this.files.length);
this.initializeFileProgress();
// Process files sequentially to avoid overwhelming the server
for (let i = 0; i < this.files.length; i++) {
const file = this.files[i];
this.updateFileProgress(i, 'processing');
try {
const result = await this.processFile(file, password, algorithm, isDecrypt);
this.results.push({ file, result, success: true });
this.updateFileProgress(i, 'completed');
} catch (error) {
this.results.push({ file, error: error.message, success: false });
this.updateFileProgress(i, 'error');
}
this.updateOverallProgress(i + 1, this.files.length);
}
this.isProcessing = false;
this.showResults();
}
async processFile(file, password, algorithm, isDecrypt) {
const formData = new FormData();
formData.append('file', file);
formData.append('enc_password', password);
formData.append('algorithm', algorithm);
const endpoint = isDecrypt ? '/api/decrypt' : '/api/encrypt';
const response = await fetch(endpoint, {
method: 'POST',
body: formData
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Processing failed');
}
// Return the blob for download
return await response.blob();
}
updateOverallProgress(completed, total) {
const progressBar = document.getElementById('bulk-overall-bar');
const progressText = document.getElementById('bulk-overall-text');
if (progressBar) {
const percentage = total > 0 ? (completed / total) * 100 : 0;
progressBar.style.width = `${percentage}%`;
}
if (progressText) {
progressText.textContent = `${completed} / ${total} files processed`;
}
}
initializeFileProgress() {
const progressList = document.getElementById('bulk-file-progress-list');
if (!progressList) return;
progressList.innerHTML = '';
this.files.forEach((file, index) => {
const progressItem = document.createElement('div');
progressItem.className = 'file-progress-item';
progressItem.innerHTML = `
<div class="file-progress-name">${file.name}</div>
<div class="file-progress-status" id="progress-status-${index}">Waiting</div>
`;
progressList.appendChild(progressItem);
});
}
updateFileProgress(index, status) {
const statusElement = document.getElementById(`progress-status-${index}`);
if (!statusElement) return;
statusElement.className = `file-progress-status status-${status}`;
switch (status) {
case 'processing':
statusElement.textContent = 'Processing...';
break;
case 'completed':
statusElement.textContent = 'Completed';
break;
case 'error':
statusElement.textContent = 'Error';
break;
default:
statusElement.textContent = 'Waiting';
}
}
showResults() {
const resultsSection = document.getElementById('bulk-results-section');
if (!resultsSection) return;
resultsSection.style.display = 'block';
const resultsList = document.getElementById('bulk-results-list');
if (!resultsList) return;
resultsList.innerHTML = '';
this.results.forEach((result, index) => {
const resultItem = document.createElement('div');
resultItem.className = 'result-item';
const successCount = this.results.filter(r => r.success).length;
const totalCount = this.results.length;
if (result.success) {
resultItem.innerHTML = `
<div class="result-info">
<div class="result-name">✅ ${result.file.name}</div>
<div class="result-details">Successfully processed</div>
</div>
<div class="result-actions">
<button type="button" onclick="bulkOps.downloadResult(${index})" style="padding: 5px 10px; font-size: 0.8em;">Download</button>
</div>
`;
} else {
resultItem.innerHTML = `
<div class="result-info">
<div class="result-name">❌ ${result.file.name}</div>
<div class="result-details">${result.error}</div>
</div>
`;
}
resultsList.appendChild(resultItem);
});
// Add summary
const summary = document.createElement('div');
summary.style.cssText = 'padding: 15px; border-bottom: 1px solid #333; background-color: #1a1a1a; font-weight: bold;';
summary.innerHTML = `
<div style="color: #00ff99;">Processing Complete</div>
<div style="font-size: 0.9em; color: #ccc; margin-top: 5px;">
${successCount} successful, ${totalCount - successCount} failed out of ${totalCount} files
</div>
`;
resultsList.insertBefore(summary, resultsList.firstChild);
}
downloadResult(index) {
const result = this.results[index];
if (!result.success) return;
const isDecrypt = document.getElementById('bulk-operation-toggle')?.checked;
const algorithm = document.getElementById('bulk-algorithm')?.value;
let filename;
if (isDecrypt) {
// For decryption, try to restore original filename
filename = result.file.name.replace(/\.(aes_cbc|aes_gcm|xchacha|rsa_hybrid)\.encrypted$/, '');
} else {
// For encryption, add algorithm extension
filename = `${result.file.name}.${algorithm}.encrypted`;
}
this.downloadBlob(result.result, filename);
}
downloadAllResults() {
const successfulResults = this.results.filter(r => r.success);
if (successfulResults.length === 0) {
alert('No successful results to download');
return;
}
successfulResults.forEach((result, index) => {
setTimeout(() => {
this.downloadResult(this.results.indexOf(result));
}, index * 500); // Stagger downloads
});
}
downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
reset() {
this.clearFiles();
this.results = [];
this.isProcessing = false;
// Hide sections
const sections = ['bulk-progress-section', 'bulk-results-section'];
sections.forEach(id => {
const section = document.getElementById(id);
if (section) section.style.display = 'none';
});
// Clear password
const passwordField = document.getElementById('bulk-password');
if (passwordField) passwordField.value = '';
}
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
async populateAlgorithmDropdown() {
try {
const response = await fetch('/api/algorithms');
const data = await response.json();
if (response.ok && data.algorithms) {
const dropdown = document.getElementById('bulk-algorithm');
if (dropdown) {
dropdown.innerHTML = '';
for (const [key, algo] of Object.entries(data.algorithms)) {
if (algo.supports_file) {
const option = document.createElement('option');
option.value = key;
option.textContent = algo.name;
dropdown.appendChild(option);
}
}
}
}
} catch (error) {
console.error('Failed to load algorithms for bulk operations:', error);
}
}
}
// Initialize bulk operations when DOM is loaded
let bulkOps;
document.addEventListener('DOMContentLoaded', () => {
bulkOps = new BulkOperations();
// Make bulkOps available globally for onclick handlers
window.bulkOps = bulkOps;
});
+333
View File
@@ -0,0 +1,333 @@
/**
* Crypto Settings Module
* Handles the encryption settings modal and mode switching
*/
class CryptoSettings {
constructor() {
this.currentMode = 'single';
this.settings = {
processingMode: 'single',
enableFilePreview: true,
autoDownloadResults: true,
sequentialProcessing: true,
showDetailedProgress: true,
stopOnError: false,
maxFileSizeMB: 100
};
this.setupEventListeners();
this.loadSettings();
}
setupEventListeners() {
// Modal controls
const settingsBtn = document.getElementById("crypto-settings-btn");
const modal = document.getElementById("crypto-settings-modal");
const closeBtn = document.getElementById("close-crypto-settings");
const applyBtn = document.getElementById("apply-crypto-settings");
const resetBtn = document.getElementById("reset-crypto-settings");
if (settingsBtn && modal) {
settingsBtn.addEventListener("click", () => {
modal.style.display = "flex";
this.updateModalFromSettings();
});
}
if (closeBtn && modal) {
closeBtn.addEventListener("click", () => {
modal.style.display = "none";
});
}
// Close modal when clicking outside
if (modal) {
modal.addEventListener("click", (e) => {
if (e.target === modal) {
modal.style.display = "none";
}
});
}
if (applyBtn && modal) {
applyBtn.addEventListener("click", () => {
this.applySettings();
modal.style.display = "none";
});
}
if (resetBtn) {
resetBtn.addEventListener("click", () => {
this.resetToDefaults();
});
}
// Processing mode radio buttons
const singleModeRadio = document.getElementById("single-file-mode-radio");
const bulkModeRadio = document.getElementById("bulk-file-mode-radio");
if (singleModeRadio) {
singleModeRadio.addEventListener("change", () => {
if (singleModeRadio.checked) {
this.toggleBulkOptions(false);
}
});
}
if (bulkModeRadio) {
bulkModeRadio.addEventListener("change", () => {
if (bulkModeRadio.checked) {
this.toggleBulkOptions(true);
}
});
}
// File size input validation
const fileSizeInput = document.getElementById("max-file-size-input");
if (fileSizeInput) {
fileSizeInput.addEventListener("input", () => {
let value = parseInt(fileSizeInput.value);
if (value < 1) {
fileSizeInput.value = 1;
} else if (value > 1000) {
fileSizeInput.value = 1000;
}
});
}
}
toggleBulkOptions(show) {
const bulkOptions = document.getElementById("bulk-options");
if (bulkOptions) {
bulkOptions.style.display = show ? "block" : "none";
}
}
updateModalFromSettings() {
// Set radio buttons
const singleModeRadio = document.getElementById("single-file-mode-radio");
const bulkModeRadio = document.getElementById("bulk-file-mode-radio");
if (this.settings.processingMode === 'single') {
if (singleModeRadio) singleModeRadio.checked = true;
this.toggleBulkOptions(false);
} else {
if (bulkModeRadio) bulkModeRadio.checked = true;
this.toggleBulkOptions(true);
}
// Set checkboxes
const checkboxes = [
{ id: "enable-file-preview", setting: "enableFilePreview" },
{ id: "auto-download-results", setting: "autoDownloadResults" },
{ id: "sequential-processing", setting: "sequentialProcessing" },
{ id: "show-detailed-progress", setting: "showDetailedProgress" },
{ id: "stop-on-error", setting: "stopOnError" }
];
checkboxes.forEach(checkbox => {
const element = document.getElementById(checkbox.id);
if (element) {
element.checked = this.settings[checkbox.setting];
}
});
// Set file size
const fileSizeInput = document.getElementById("max-file-size-input");
if (fileSizeInput) {
fileSizeInput.value = this.settings.maxFileSizeMB;
}
}
applySettings() {
// Get processing mode
const singleModeRadio = document.getElementById("single-file-mode-radio");
const bulkModeRadio = document.getElementById("bulk-file-mode-radio");
if (singleModeRadio && singleModeRadio.checked) {
this.settings.processingMode = 'single';
} else if (bulkModeRadio && bulkModeRadio.checked) {
this.settings.processingMode = 'bulk';
}
// Get checkbox values
const checkboxes = [
{ id: "enable-file-preview", setting: "enableFilePreview" },
{ id: "auto-download-results", setting: "autoDownloadResults" },
{ id: "sequential-processing", setting: "sequentialProcessing" },
{ id: "show-detailed-progress", setting: "showDetailedProgress" },
{ id: "stop-on-error", setting: "stopOnError" }
];
checkboxes.forEach(checkbox => {
const element = document.getElementById(checkbox.id);
if (element) {
this.settings[checkbox.setting] = element.checked;
}
});
// Get file size
const fileSizeInput = document.getElementById("max-file-size-input");
if (fileSizeInput) {
this.settings.maxFileSizeMB = parseInt(fileSizeInput.value) || 100;
}
// Apply the mode change
this.switchMode(this.settings.processingMode);
// Save settings
this.saveSettings();
// Show feedback
this.showFeedback("Settings applied successfully!");
}
switchMode(mode) {
this.currentMode = mode;
const singleFileMode = document.getElementById("single-file-mode");
const bulkFileMode = document.getElementById("bulk-file-mode");
if (mode === 'single') {
if (singleFileMode) singleFileMode.style.display = "block";
if (bulkFileMode) bulkFileMode.style.display = "none";
} else {
if (singleFileMode) singleFileMode.style.display = "none";
if (bulkFileMode) bulkFileMode.style.display = "block";
}
// Update the form submit handler
this.updateFormHandler();
}
updateFormHandler() {
const cryptoForm = document.getElementById("crypto-form");
if (!cryptoForm) return;
// Remove existing event listeners by cloning the form
const newForm = cryptoForm.cloneNode(true);
cryptoForm.parentNode.replaceChild(newForm, cryptoForm);
// Add the appropriate event listener
if (this.currentMode === 'single') {
newForm.addEventListener("submit", this.handleSingleFileSubmit.bind(this));
} else {
newForm.addEventListener("submit", this.handleBulkFileSubmit.bind(this));
}
}
async handleSingleFileSubmit(event) {
event.preventDefault();
const algorithm = document.getElementById("algorithm")?.value;
const password = document.getElementById("password")?.value;
const fileInput = document.getElementById("file-input");
const isDecrypt = document.getElementById("operation-toggle").checked;
if (!algorithm || !fileInput) return;
// Use existing single file handling logic
if (window.handleSubmit) {
window.handleSubmit(event);
}
}
async handleBulkFileSubmit(event) {
event.preventDefault();
// Use bulk operations functionality
if (window.bulkOps && window.bulkOps.processFiles) {
await window.bulkOps.processFiles();
}
}
resetToDefaults() {
this.settings = {
processingMode: 'single',
enableFilePreview: true,
autoDownloadResults: true,
sequentialProcessing: true,
showDetailedProgress: true,
stopOnError: false,
maxFileSizeMB: 100
};
this.updateModalFromSettings();
this.showFeedback("Settings reset to defaults!");
}
loadSettings() {
try {
const saved = localStorage.getItem('paccrypt-crypto-settings');
if (saved) {
this.settings = { ...this.settings, ...JSON.parse(saved) };
}
} catch (error) {
console.warn('Failed to load crypto settings:', error);
}
// Apply the loaded settings
this.switchMode(this.settings.processingMode);
}
saveSettings() {
try {
localStorage.setItem('paccrypt-crypto-settings', JSON.stringify(this.settings));
} catch (error) {
console.warn('Failed to save crypto settings:', error);
}
}
showFeedback(message) {
// Use the existing feedback system or create a temporary one
const feedbackDiv = document.createElement('div');
feedbackDiv.className = 'copy-feedback';
feedbackDiv.textContent = message;
feedbackDiv.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background-color: #00ff99;
color: #000;
padding: 10px 20px;
border-radius: 5px;
font-weight: bold;
z-index: 10000;
display: block;
`;
document.body.appendChild(feedbackDiv);
setTimeout(() => {
feedbackDiv.style.opacity = '0';
feedbackDiv.style.transition = 'opacity 0.3s ease';
setTimeout(() => {
if (feedbackDiv.parentNode) {
feedbackDiv.parentNode.removeChild(feedbackDiv);
}
}, 300);
}, 2000);
}
// Public methods for external access
getCurrentMode() {
return this.currentMode;
}
getSettings() {
return { ...this.settings };
}
isBulkMode() {
return this.currentMode === 'bulk';
}
}
// Initialize crypto settings when DOM is loaded
let cryptoSettings;
document.addEventListener('DOMContentLoaded', () => {
cryptoSettings = new CryptoSettings();
});
// Make cryptoSettings available globally
window.cryptoSettings = cryptoSettings;
-104
View File
@@ -1,104 +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;
// ===== 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 btoa(String.fromCharCode(...output));
}
// ===== 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 = new Uint8Array(
atob(encryptedData).split('').map(c => c.charCodeAt(0))
);
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');
}
+67 -96
View File
@@ -1,119 +1,90 @@
/** /**
* File operations module. * File operations using the new Python backend APIs
* Handles file encryption and decryption operations.
*/ */
// ===== Constants ===== /**
const CHUNK_SIZE = 1024 * 1024; // 1MB chunks * Encrypts a full file using the backend API and downloads the encrypted version.
*/
// ===== Public Interface =====
export async function encryptFile(fileInput, password) { export async function encryptFile(fileInput, password) {
const file = fileInput.files[0]; const file = fileInput.files[0];
if (!file) return; if (!file) return;
const algorithm = document.getElementById("algorithm")?.value || "aes_cbc";
try { try {
const encryptedChunks = await processFile(file, password, true); const formData = new FormData();
downloadEncryptedFile(encryptedChunks, file.name); formData.append('file', file);
formData.append('enc_password', password);
formData.append('algorithm', algorithm);
const response = await fetch('/api/encrypt', {
method: 'POST',
body: formData
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
// Download the encrypted file
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${file.name}.${algorithm}.encrypted`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (error) { } catch (error) {
alert("Error encrypting file: " + error.message); alert("Error encrypting file: " + error.message);
} }
} }
/**
* Decrypts a file using the backend API and downloads the decrypted version.
*/
export async function decryptFile(fileInput, password) { export async function decryptFile(fileInput, password) {
const file = fileInput.files[0]; const file = fileInput.files[0];
if (!file) return; if (!file) return;
try { try {
const decryptedChunks = await processFile(file, password, false); const formData = new FormData();
downloadDecryptedFile(decryptedChunks, file.name); formData.append('file', file);
formData.append('enc_password', password);
const response = await fetch('/api/decrypt', {
method: 'POST',
body: formData
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
// Download the decrypted file
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
// Clean up filename - remove algorithm-specific extensions
let filename = file.name;
const algorithms = ["aes_cbc", "aes_gcm", "xchacha", "rsa_hybrid"];
for (const algo of algorithms) {
filename = filename.replace(`.${algo}.encrypted`, "");
}
filename = filename.replace(".encrypted", "");
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (error) { } catch (error) {
alert("Error decrypting file: " + error.message); alert("Error decrypting file: " + error.message);
} }
} }
// ===== 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);
}
}
}
+796
View File
@@ -0,0 +1,796 @@
/**
* Enhanced PacShare Module
* Handles bulk uploads and single file uploads seamlessly
*/
class PacShareEnhanced {
constructor() {
this.selectedFiles = [];
this.uploadResults = [];
this.settings = {
enable2FA: false,
autoClearPasswords: true,
autoCopyLinks: true,
showUploadProgress: true,
scrollToResults: true,
maxUploadSizeMB: 25,
validateFileTypes: false,
concurrentUploads: 1,
enableFilePreview: true,
rememberAlgorithm: true
};
this.setupEventListeners();
this.loadSettings();
}
setupEventListeners() {
// Drag & Drop Zone
const dropZone = document.getElementById('pacshare-drop-zone');
const fileInput = document.getElementById('upload-file');
const fileSelect = document.getElementById('pacshare-file-select');
console.log('PacShare setup - dropZone:', dropZone, 'fileInput:', fileInput, 'fileSelect:', fileSelect);
if (dropZone && fileInput) {
console.log('Setting up PacShare drag & drop events');
// Drag & Drop Events
dropZone.addEventListener('dragover', this.handleDragOver.bind(this));
dropZone.addEventListener('dragleave', this.handleDragLeave.bind(this));
dropZone.addEventListener('drop', this.handleDrop.bind(this));
dropZone.addEventListener('click', () => {
console.log('PacShare drop zone clicked, opening file input');
fileInput.click();
});
// File Input Events
fileInput.addEventListener('change', this.handleFileSelect.bind(this));
} else {
console.error('PacShare elements not found - dropZone:', dropZone, 'fileInput:', fileInput);
}
if (fileSelect) {
console.log('Setting up PacShare file select button');
fileSelect.addEventListener('click', (e) => {
console.log('PacShare file select button clicked');
e.stopPropagation();
fileInput.click();
});
} else {
console.error('PacShare file select button not found');
}
// Clear files button
const clearBtn = document.getElementById('pacshare-clear-files');
if (clearBtn) {
clearBtn.addEventListener('click', this.clearFiles.bind(this));
}
// Enhanced form submission
const uploadForm = document.getElementById('upload-form');
if (uploadForm) {
// Remove existing event listener first
uploadForm.replaceWith(uploadForm.cloneNode(true));
const newForm = document.getElementById('upload-form');
newForm.addEventListener('submit', this.handleEnhancedSubmit.bind(this));
}
// Settings modal controls
this.setupSettingsModal();
}
handleDragOver(e) {
e.preventDefault();
e.stopPropagation();
document.getElementById('pacshare-drop-zone').classList.add('drag-over');
}
handleDragLeave(e) {
e.preventDefault();
e.stopPropagation();
document.getElementById('pacshare-drop-zone').classList.remove('drag-over');
}
handleDrop(e) {
e.preventDefault();
e.stopPropagation();
document.getElementById('pacshare-drop-zone').classList.remove('drag-over');
const files = Array.from(e.dataTransfer.files);
this.addFiles(files);
}
handleFileSelect(e) {
const files = Array.from(e.target.files);
this.addFiles(files);
}
addFiles(newFiles) {
// Filter out duplicates
newFiles = newFiles.filter(newFile =>
!this.selectedFiles.some(existingFile =>
existingFile.name === newFile.name && existingFile.size === newFile.size
)
);
this.selectedFiles.push(...newFiles);
this.updateFileDisplay();
this.updateUI();
}
updateFileDisplay() {
const fileListContainer = document.getElementById('pacshare-file-list');
const filesContainer = document.getElementById('pacshare-files-container');
const uploadBtn = document.getElementById('pacshare-upload-btn');
if (!filesContainer || !fileListContainer) return;
if (this.selectedFiles.length === 0) {
fileListContainer.style.display = 'none';
if (uploadBtn) uploadBtn.textContent = 'Upload and Generate Link';
return;
}
fileListContainer.style.display = 'block';
filesContainer.innerHTML = '';
// Update button text based on file count
if (uploadBtn) {
uploadBtn.textContent = this.selectedFiles.length === 1
? 'Upload and Generate Link'
: `Upload ${this.selectedFiles.length} Files and Generate Links`;
}
this.selectedFiles.forEach((file, index) => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.innerHTML = `
<div class="file-info">
<div class="file-name">${file.name}</div>
<div class="file-size">${this.formatFileSize(file.size)}</div>
</div>
<div class="file-actions">
<button type="button" onclick="pacShareEnhanced.previewFile(${index})" style="padding: 5px 10px; font-size: 0.8em;">Preview</button>
<button type="button" onclick="pacShareEnhanced.removeFile(${index})" class="danger-button" style="padding: 5px 10px; font-size: 0.8em;">Remove</button>
</div>
`;
filesContainer.appendChild(fileItem);
});
}
async previewFile(index) {
const file = this.selectedFiles[index];
if (!file) return;
const previewContainer = document.createElement('div');
previewContainer.className = 'file-preview-container';
const header = document.createElement('div');
header.className = 'file-preview-header';
header.textContent = `Preview: ${file.name}`;
const content = document.createElement('div');
content.className = 'file-preview-content';
// Handle different file types
if (file.type.startsWith('text/') || this.isTextFile(file.name)) {
try {
const text = await this.readFileAsText(file);
content.textContent = text.length > 2000 ? text.substring(0, 2000) + '...' : text;
} catch (error) {
content.textContent = 'Error reading file: ' + error.message;
}
} else if (file.type.startsWith('image/')) {
const img = document.createElement('img');
img.className = 'image-preview';
img.src = URL.createObjectURL(file);
img.onload = () => URL.revokeObjectURL(img.src);
content.appendChild(img);
} else {
content.innerHTML = `
<div style="color: #888;">
File Type: ${file.type || 'Unknown'}<br>
Size: ${this.formatFileSize(file.size)}<br>
Preview not available for this file type.
</div>
`;
}
previewContainer.appendChild(header);
previewContainer.appendChild(content);
// Remove existing preview
const existingPreview = document.querySelector('.file-preview-container');
if (existingPreview) {
existingPreview.remove();
}
// Add new preview after the file list
const fileList = document.getElementById('pacshare-files-container');
if (fileList) {
fileList.parentNode.insertBefore(previewContainer, fileList.nextSibling);
}
}
removeFile(index) {
this.selectedFiles.splice(index, 1);
this.updateFileDisplay();
// Remove preview if it exists
const existingPreview = document.querySelector('.file-preview-container');
if (existingPreview) {
existingPreview.remove();
}
}
clearFiles() {
this.selectedFiles = [];
this.updateFileDisplay();
// Clear file input
const fileInput = document.getElementById('upload-file');
if (fileInput) fileInput.value = '';
// Remove preview if it exists
const existingPreview = document.querySelector('.file-preview-container');
if (existingPreview) {
existingPreview.remove();
}
this.hideResults();
}
async handleEnhancedSubmit(e) {
e.preventDefault();
if (this.selectedFiles.length === 0) {
alert('Please select at least one file to upload.');
return;
}
const algorithm = document.getElementById('share-algorithm')?.value;
const encPassword = document.querySelector('input[name="enc_password"]')?.value;
const pickupPassword = document.querySelector('input[name="pickup_password"]')?.value;
const enable2FA = this.settings.enable2FA;
if (!algorithm || !encPassword || !pickupPassword) {
alert('Please fill in all required fields.');
return;
}
if (this.selectedFiles.length === 1) {
// Single file - use existing logic
await this.uploadSingleFile(this.selectedFiles[0], algorithm, encPassword, pickupPassword, enable2FA);
} else {
// Multiple files - use bulk upload
await this.uploadMultipleFiles(algorithm, encPassword, pickupPassword, enable2FA);
}
}
async uploadSingleFile(file, algorithm, encPassword, pickupPassword, enable2FA) {
const formData = new FormData();
formData.append('file', file);
formData.append('algorithm', algorithm);
formData.append('enc_password', encPassword);
formData.append('pickup_password', pickupPassword);
if (enable2FA) formData.append('enable_2fa', 'on');
try {
const response = await fetch('/', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.error) {
alert(data.error);
return;
}
if (data.success && data.pickup_url) {
this.showSingleResult(data);
}
} catch (error) {
alert('Error uploading file: ' + error.message);
}
}
async uploadMultipleFiles(algorithm, encPassword, pickupPassword, enable2FA) {
this.uploadResults = [];
this.showProgress();
// Upload files sequentially to avoid overwhelming the server
for (let i = 0; i < this.selectedFiles.length; i++) {
const file = this.selectedFiles[i];
this.updateFileProgress(i, 'uploading');
try {
const formData = new FormData();
formData.append('file', file);
formData.append('algorithm', algorithm);
formData.append('enc_password', encPassword);
formData.append('pickup_password', pickupPassword);
if (enable2FA) formData.append('enable_2fa', 'on');
const response = await fetch('/', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.error) {
this.uploadResults.push({ file, error: data.error, success: false });
this.updateFileProgress(i, 'error');
} else if (data.success && data.pickup_url) {
this.uploadResults.push({ file, data, success: true });
this.updateFileProgress(i, 'completed');
}
} catch (error) {
this.uploadResults.push({ file, error: error.message, success: false });
this.updateFileProgress(i, 'error');
}
this.updateOverallProgress(i + 1, this.selectedFiles.length);
}
this.showResults();
}
showProgress() {
const progressSection = document.getElementById('pacshare-progress');
if (progressSection) {
progressSection.style.display = 'block';
}
this.updateOverallProgress(0, this.selectedFiles.length);
this.initializeFileProgress();
}
initializeFileProgress() {
const progressContainer = document.getElementById('pacshare-file-progress');
if (!progressContainer) return;
progressContainer.innerHTML = '';
this.selectedFiles.forEach((file, index) => {
const progressItem = document.createElement('div');
progressItem.className = 'file-progress-item';
progressItem.innerHTML = `
<div class="file-progress-name">${file.name}</div>
<div class="file-progress-status" id="pacshare-progress-${index}">Waiting</div>
`;
progressContainer.appendChild(progressItem);
});
}
updateFileProgress(index, status) {
const statusElement = document.getElementById(`pacshare-progress-${index}`);
if (!statusElement) return;
statusElement.className = `file-progress-status status-${status}`;
switch (status) {
case 'uploading':
statusElement.textContent = 'Uploading...';
break;
case 'completed':
statusElement.textContent = 'Completed';
break;
case 'error':
statusElement.textContent = 'Error';
break;
default:
statusElement.textContent = 'Waiting';
}
}
updateOverallProgress(completed, total) {
const progressBar = document.getElementById('pacshare-overall-bar');
const progressText = document.getElementById('pacshare-overall-text');
if (progressBar) {
const percentage = total > 0 ? (completed / total) * 100 : 0;
progressBar.style.width = `${percentage}%`;
}
if (progressText) {
progressText.textContent = `${completed} / ${total} files uploaded`;
}
}
showSingleResult(data) {
// Use existing single result display logic
const shareLink = document.getElementById('share-link');
const shareLinkContainer = document.getElementById('share-link-container');
if (shareLink && shareLinkContainer) {
shareLink.href = data.pickup_url;
shareLink.textContent = data.pickup_url;
shareLinkContainer.style.display = 'flex';
// Handle 2FA if enabled
if (data.qr_code_url) {
this.showTwoFactorSetup(data.qr_code_url, data.service_name, data.totp_secret);
}
// Clear form
this.clearForm();
// Scroll to results
shareLinkContainer.scrollIntoView({ behavior: 'smooth' });
}
}
showResults() {
const resultsSection = document.getElementById('pacshare-results');
const resultsList = document.getElementById('pacshare-results-list');
if (!resultsSection || !resultsList) return;
resultsSection.style.display = 'block';
resultsList.innerHTML = '';
const successCount = this.uploadResults.filter(r => r.success).length;
const totalCount = this.uploadResults.length;
// Add summary
const summary = document.createElement('div');
summary.style.cssText = 'padding: 15px; border-bottom: 1px solid #333; background-color: #1a1a1a; font-weight: bold;';
summary.innerHTML = `
<div style="color: #00ff99;">Upload Complete</div>
<div style="font-size: 0.9em; color: #ccc; margin-top: 5px;">
${successCount} successful, ${totalCount - successCount} failed out of ${totalCount} files
</div>
`;
resultsList.appendChild(summary);
// Add individual results
this.uploadResults.forEach((result, index) => {
const resultItem = document.createElement('div');
resultItem.className = 'result-item';
if (result.success) {
resultItem.innerHTML = `
<div class="result-info">
<div class="result-name">✅ ${result.file.name}</div>
<div class="result-details">
<a href="${result.data.pickup_url}" target="_blank" style="color: #00ff99;">${result.data.pickup_url}</a>
</div>
</div>
<div class="result-actions">
<button type="button" onclick="pacShareEnhanced.copyLink('${result.data.pickup_url}')" style="padding: 5px 10px; font-size: 0.8em;">Copy Link</button>
</div>
`;
} else {
resultItem.innerHTML = `
<div class="result-info">
<div class="result-name">❌ ${result.file.name}</div>
<div class="result-details">${result.error}</div>
</div>
`;
}
resultsList.appendChild(resultItem);
});
// Clear form and scroll to results
this.clearForm();
resultsSection.scrollIntoView({ behavior: 'smooth' });
}
copyLink(url) {
navigator.clipboard.writeText(url).then(() => {
this.showToast('Link copied to clipboard!');
}).catch(() => {
// Fallback
const textArea = document.createElement('textarea');
textArea.value = url;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
this.showToast('Link copied to clipboard!');
});
}
showToast(message) {
const toast = document.createElement('div');
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background-color: #00ff99;
color: #000;
padding: 10px 20px;
border-radius: 5px;
font-weight: bold;
z-index: 10000;
opacity: 1;
transition: opacity 0.3s ease;
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}, 2000);
}
clearForm() {
// Clear passwords but keep algorithm
const encPassword = document.querySelector('input[name="enc_password"]');
const pickupPassword = document.querySelector('input[name="pickup_password"]');
const enable2FA = document.getElementById('enable-2fa');
if (encPassword) encPassword.value = '';
if (pickupPassword) pickupPassword.value = '';
if (enable2FA) enable2FA.checked = false;
// Clear selected files
this.clearFiles();
}
hideResults() {
const sections = ['pacshare-results', 'pacshare-progress', 'share-link-container'];
sections.forEach(id => {
const section = document.getElementById(id);
if (section) section.style.display = 'none';
});
}
updateUI() {
// Hide results when new files are selected
this.hideResults();
}
// Utility methods
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
isTextFile(filename) {
const textExtensions = ['.txt', '.md', '.js', '.html', '.css', '.json', '.xml', '.csv', '.log', '.py', '.java', '.c', '.cpp', '.h'];
return textExtensions.some(ext => filename.toLowerCase().endsWith(ext));
}
readFileAsText(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = e => resolve(e.target.result);
reader.onerror = e => reject(new Error('Failed to read file'));
reader.readAsText(file);
});
}
showTwoFactorSetup(qrCodeUrl, serviceName, totpSecret) {
const container = document.getElementById('tfa-setup-container');
const qrImage = document.getElementById('tfa-qr-image');
const tfaString = document.getElementById('tfa-string');
if (container && qrImage && tfaString) {
qrImage.src = qrCodeUrl;
tfaString.value = totpSecret;
container.style.display = 'block';
container.scrollIntoView({ behavior: 'smooth' });
}
}
// Settings Modal Methods
setupSettingsModal() {
const settingsBtn = document.getElementById("pacshare-settings-btn");
const modal = document.getElementById("pacshare-settings-modal");
const closeBtn = document.getElementById("close-pacshare-settings");
const applyBtn = document.getElementById("apply-pacshare-settings");
const resetBtn = document.getElementById("reset-pacshare-settings");
if (settingsBtn && modal) {
settingsBtn.addEventListener("click", () => {
modal.style.display = "flex";
this.updateSettingsModal();
});
}
if (closeBtn && modal) {
closeBtn.addEventListener("click", () => {
modal.style.display = "none";
});
}
// Close modal when clicking outside
if (modal) {
modal.addEventListener("click", (e) => {
if (e.target === modal) {
modal.style.display = "none";
}
});
}
if (applyBtn && modal) {
applyBtn.addEventListener("click", () => {
this.applySettings();
modal.style.display = "none";
});
}
if (resetBtn) {
resetBtn.addEventListener("click", () => {
this.resetSettings();
});
}
// Input validation
const uploadSizeInput = document.getElementById("max-upload-size-input");
const concurrentInput = document.getElementById("concurrent-uploads-input");
if (uploadSizeInput) {
uploadSizeInput.addEventListener("input", () => {
let value = parseInt(uploadSizeInput.value);
if (value < 1) uploadSizeInput.value = 1;
else if (value > 1000) uploadSizeInput.value = 1000;
this.updateSettingsSummary();
});
}
if (concurrentInput) {
concurrentInput.addEventListener("input", () => {
let value = parseInt(concurrentInput.value);
if (value < 1) concurrentInput.value = 1;
else if (value > 10) concurrentInput.value = 10;
this.updateSettingsSummary();
});
}
// Update summary when checkboxes change
const checkboxIds = [
"enable-2fa-setting", "auto-clear-passwords", "auto-copy-links",
"show-upload-progress", "scroll-to-results", "validate-file-types",
"enable-file-preview", "remember-algorithm"
];
checkboxIds.forEach(id => {
const checkbox = document.getElementById(id);
if (checkbox) {
checkbox.addEventListener("change", () => {
this.updateSettingsSummary();
});
}
});
}
updateSettingsModal() {
// Set checkbox values
const checkboxMap = {
"enable-2fa-setting": "enable2FA",
"auto-clear-passwords": "autoClearPasswords",
"auto-copy-links": "autoCopyLinks",
"show-upload-progress": "showUploadProgress",
"scroll-to-results": "scrollToResults",
"validate-file-types": "validateFileTypes",
"enable-file-preview": "enableFilePreview",
"remember-algorithm": "rememberAlgorithm"
};
Object.entries(checkboxMap).forEach(([id, setting]) => {
const checkbox = document.getElementById(id);
if (checkbox) {
checkbox.checked = this.settings[setting];
}
});
// Set number inputs
const uploadSizeInput = document.getElementById("max-upload-size-input");
const concurrentInput = document.getElementById("concurrent-uploads-input");
if (uploadSizeInput) uploadSizeInput.value = this.settings.maxUploadSizeMB;
if (concurrentInput) concurrentInput.value = this.settings.concurrentUploads;
this.updateSettingsSummary();
}
updateSettingsSummary() {
const summary = document.getElementById("pacshare-settings-summary");
if (!summary) return;
const enable2FA = document.getElementById("enable-2fa-setting")?.checked || this.settings.enable2FA;
const autoClearPasswords = document.getElementById("auto-clear-passwords")?.checked || this.settings.autoClearPasswords;
const maxSize = document.getElementById("max-upload-size-input")?.value || this.settings.maxUploadSizeMB;
const concurrent = document.getElementById("concurrent-uploads-input")?.value || this.settings.concurrentUploads;
summary.innerHTML = `
• 2FA: ${enable2FA ? 'Enabled' : 'Disabled'}<br>
• Auto-clear passwords: ${autoClearPasswords ? 'Yes' : 'No'}<br>
• Max file size: ${maxSize} MB<br>
• Upload mode: ${concurrent == 1 ? 'Sequential' : `${concurrent} concurrent`}<br>
• File preview: ${this.settings.enableFilePreview ? 'Enabled' : 'Disabled'}
`;
}
applySettings() {
// Get checkbox values
const checkboxMap = {
"enable-2fa-setting": "enable2FA",
"auto-clear-passwords": "autoClearPasswords",
"auto-copy-links": "autoCopyLinks",
"show-upload-progress": "showUploadProgress",
"scroll-to-results": "scrollToResults",
"validate-file-types": "validateFileTypes",
"enable-file-preview": "enableFilePreview",
"remember-algorithm": "rememberAlgorithm"
};
Object.entries(checkboxMap).forEach(([id, setting]) => {
const checkbox = document.getElementById(id);
if (checkbox) {
this.settings[setting] = checkbox.checked;
}
});
// Get number inputs
const uploadSizeInput = document.getElementById("max-upload-size-input");
const concurrentInput = document.getElementById("concurrent-uploads-input");
if (uploadSizeInput) this.settings.maxUploadSizeMB = parseInt(uploadSizeInput.value) || 25;
if (concurrentInput) this.settings.concurrentUploads = parseInt(concurrentInput.value) || 1;
this.saveSettings();
this.showToast("PacShare settings applied successfully!");
}
resetSettings() {
this.settings = {
enable2FA: false,
autoClearPasswords: true,
autoCopyLinks: true,
showUploadProgress: true,
scrollToResults: true,
maxUploadSizeMB: 25,
validateFileTypes: false,
concurrentUploads: 1,
enableFilePreview: true,
rememberAlgorithm: true
};
this.updateSettingsModal();
this.showToast("Settings reset to defaults!");
}
loadSettings() {
try {
const saved = localStorage.getItem('paccrypt-pacshare-settings');
if (saved) {
this.settings = { ...this.settings, ...JSON.parse(saved) };
}
} catch (error) {
console.warn('Failed to load PacShare settings:', error);
}
}
saveSettings() {
try {
localStorage.setItem('paccrypt-pacshare-settings', JSON.stringify(this.settings));
} catch (error) {
console.warn('Failed to save PacShare settings:', error);
}
}
}
// Initialize enhanced PacShare when DOM is loaded
let pacShareEnhanced;
document.addEventListener('DOMContentLoaded', () => {
pacShareEnhanced = new PacShareEnhanced();
// Make available globally for onclick handlers
window.pacShareEnhanced = pacShareEnhanced;
});
+674 -83
View File
@@ -7,19 +7,16 @@ import { encryptFile, decryptFile } from './fileops.js';
// ===== UI Initialization ===== // ===== UI Initialization =====
export function setupUI() { export function setupUI() {
// Set initial state of remove button to hidden
const removeBtn = document.getElementById("remove-file-btn"); const removeBtn = document.getElementById("remove-file-btn");
if (removeBtn) { if (removeBtn) {
removeBtn.style.display = "none"; removeBtn.style.display = "none";
} }
initializeEventListeners(); initializeEventListeners();
} }
// ===== Event Listeners ===== async function initializeEventListeners() {
function initializeEventListeners() {
const elements = { const elements = {
encryptionType: document.getElementById("encryption-type"), algorithm: document.getElementById("algorithm"),
inputText: document.getElementById("input-text"), inputText: document.getElementById("input-text"),
form: document.getElementById("crypto-form"), form: document.getElementById("crypto-form"),
removeFileBtn: document.getElementById("remove-file-btn"), removeFileBtn: document.getElementById("remove-file-btn"),
@@ -29,22 +26,30 @@ function initializeEventListeners() {
copyOutputBtn: document.getElementById("copy-output-btn"), copyOutputBtn: document.getElementById("copy-output-btn"),
toggleSwitch: document.getElementById("operation-toggle"), toggleSwitch: document.getElementById("operation-toggle"),
copyShareBtn: document.getElementById("copy-share-btn"), copyShareBtn: document.getElementById("copy-share-btn"),
shareLink: document.getElementById("share-link") shareLink: document.getElementById("share-link"),
generateKeypairBtn: document.getElementById("generate-keypair-btn"),
loadPublicKeyBtn: document.getElementById("load-public-key-btn"),
loadPrivateKeyBtn: document.getElementById("load-private-key-btn"),
publicKeyFile: document.getElementById("public-key-file"),
privateKeyFile: document.getElementById("private-key-file")
}; };
if (validateElements(elements)) { if (validateElements(elements)) {
setupElementListeners(elements); setupElementListeners(elements);
} }
await loadAvailableAlgorithms();
// Initialize algorithm options on page load after algorithms are loaded
toggleAlgorithmOptions();
} }
function validateElements(elements) { function validateElements(elements) {
return elements.encryptionType && elements.inputText && elements.form && return elements.algorithm && elements.inputText && elements.form &&
elements.removeFileBtn && elements.clearAllBtn && elements.generateBtn && elements.removeFileBtn && elements.clearAllBtn && elements.generateBtn &&
elements.copyPasswordBtn && elements.toggleSwitch; elements.copyPasswordBtn && elements.toggleSwitch;
} }
function setupElementListeners(elements) { function setupElementListeners(elements) {
elements.encryptionType.addEventListener("change", toggleEncryptionOptions); elements.algorithm?.addEventListener("change", toggleAlgorithmOptions);
elements.inputText.addEventListener("input", handleInputChange); elements.inputText.addEventListener("input", handleInputChange);
elements.form.addEventListener("submit", handleSubmit); elements.form.addEventListener("submit", handleSubmit);
elements.removeFileBtn.addEventListener("click", removeFile); elements.removeFileBtn.addEventListener("click", removeFile);
@@ -55,10 +60,17 @@ function setupElementListeners(elements) {
elements.toggleSwitch.addEventListener("change", () => { elements.toggleSwitch.addEventListener("change", () => {
console.log("Mode:", elements.toggleSwitch.checked ? "Decrypt" : "Encrypt"); console.log("Mode:", elements.toggleSwitch.checked ? "Decrypt" : "Encrypt");
}); });
// Add file input change listener // Password generator controls
setupPasswordGeneratorListeners();
// Key pair management listeners
elements.generateKeypairBtn?.addEventListener("click", generateAndDownloadKeyPair);
elements.loadPublicKeyBtn?.addEventListener("click", () => elements.publicKeyFile?.click());
elements.loadPrivateKeyBtn?.addEventListener("click", () => elements.privateKeyFile?.click());
elements.publicKeyFile?.addEventListener("change", handlePublicKeyLoad);
elements.privateKeyFile?.addEventListener("change", handlePrivateKeyLoad);
const fileInput = document.getElementById("file-input"); const fileInput = document.getElementById("file-input");
if (fileInput) { if (fileInput) {
fileInput.addEventListener("change", () => { fileInput.addEventListener("change", () => {
@@ -93,49 +105,10 @@ function setupShareLinkListeners(elements) {
} }
} }
// ===== UI State Management =====
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) {
if (isAdvanced) {
passwordInputWrapper.classList.remove("hidden");
} else {
passwordInputWrapper.classList.add("hidden");
}
}
if (fileSection) {
if (isAdvanced) {
fileSection.classList.remove("hidden");
} else {
fileSection.classList.add("hidden");
}
}
updateToggleLabels();
toggleInputMode();
}
function updateToggleLabels() {
const type = document.getElementById("encryption-type")?.value;
const leftLabel = document.getElementById("toggle-left-label");
const rightLabel = document.getElementById("toggle-right-label");
if (!type || !leftLabel || !rightLabel) return;
const isAdvanced = type.toLowerCase().includes("advanced");
leftLabel.textContent = isAdvanced ? "Encrypt" : "Encode";
rightLabel.textContent = isAdvanced ? "Decrypt" : "Decode";
}
function toggleInputMode() { function toggleInputMode() {
const fileInput = document.getElementById("file-input"); const fileInput = document.getElementById("file-input");
const textValue = document.getElementById("input-text")?.value.trim(); const textValue = document.getElementById("input-text")?.value.trim();
const isAdvanced = document.getElementById("encryption-type")?.value === "advanced";
const textSection = document.getElementById("text-section"); const textSection = document.getElementById("text-section");
const fileSection = document.getElementById("file-section"); const fileSection = document.getElementById("file-section");
@@ -146,24 +119,39 @@ function toggleInputMode() {
const fileSelected = fileInput.files.length > 0; const fileSelected = fileInput.files.length > 0;
textSection.style.display = fileSelected ? "none" : "flex"; textSection.style.display = fileSelected ? "none" : "flex";
fileSection.style.display = (isAdvanced && !textValue) ? "flex" : "none"; fileSection.style.display = !textValue ? "flex" : "none";
removeBtn.style.display = fileSelected ? "inline-block" : "none"; removeBtn.style.display = fileSelected ? "inline-block" : "none";
} }
// ===== Form Handling =====
async function handleSubmit(event) { async function handleSubmit(event) {
event.preventDefault(); event.preventDefault();
const encryptionType = document.getElementById("encryption-type")?.value; const algorithm = document.getElementById("algorithm")?.value;
const password = document.getElementById("password")?.value; const password = document.getElementById("password")?.value;
const fileInput = document.getElementById("file-input"); const fileInput = document.getElementById("file-input");
const isDecrypt = document.getElementById("operation-toggle").checked; const isDecrypt = document.getElementById("operation-toggle").checked;
const operation = isDecrypt ? "decrypt" : "encrypt"; const operation = isDecrypt ? "decrypt" : "encrypt";
if (!encryptionType || !fileInput) return; if (!algorithm || !fileInput) return;
if (encryptionType === "advanced" && !password) { // Check requirements based on algorithm
return alert("Password is required for advanced encryption."); let requiresKeypair = false;
if (window.availableAlgorithms && window.availableAlgorithms[algorithm]) {
requiresKeypair = window.availableAlgorithms[algorithm].requires_keypair || false;
} else {
requiresKeypair = algorithm.includes("hybrid");
}
if (requiresKeypair) {
const globalKeys = window.getGlobalKeys ? window.getGlobalKeys() : {};
if (operation === "encrypt" && !globalKeys.publicKey) {
return alert("Please load a public key in the Key Pairs Management section for encryption with this algorithm.");
}
if (operation === "decrypt" && !globalKeys.privateKey) {
return alert("Please load a private key in the Key Pairs Management section for decryption with this algorithm.");
}
} else if (!password) {
return alert("Password is required for this algorithm.");
} }
if (fileInput.files.length > 0) { if (fileInput.files.length > 0) {
@@ -172,31 +160,59 @@ async function handleSubmit(event) {
: decryptFile(fileInput, password); : decryptFile(fileInput, password);
} }
await handleTextOperation(encryptionType, operation, password); await handleTextOperation(operation, password);
} }
async function handleTextOperation(encryptionType, operation, password) { async function handleTextOperation(operation, password) {
const algorithm = document.getElementById("algorithm")?.value || "aes_gcm";
const payload = { const payload = {
"encryption-type": encryptionType,
operation: operation,
message: document.getElementById("input-text")?.value, message: document.getElementById("input-text")?.value,
password: password algorithm: algorithm
}; };
// Add appropriate authentication based on algorithm
let requiresKeypair = false;
if (window.availableAlgorithms && window.availableAlgorithms[algorithm]) {
requiresKeypair = window.availableAlgorithms[algorithm].requires_keypair || false;
} else {
requiresKeypair = algorithm.includes("hybrid");
}
if (requiresKeypair) {
const globalKeys = window.getGlobalKeys ? window.getGlobalKeys() : {};
if (operation === "encrypt" && globalKeys.publicKey) {
payload.public_key = globalKeys.publicKey;
} else if (operation === "decrypt" && globalKeys.privateKey) {
payload.private_key = globalKeys.privateKey;
}
} else {
payload.password = password;
}
try { try {
const response = await fetch("/", { const endpoint = operation === "encrypt" ? "/api/encrypt" : "/api/decrypt";
const response = await fetch(endpoint, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
const data = await response.json(); const data = await response.json();
document.getElementById("output-text").value = data.result;
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) { } catch (err) {
alert("Error processing request: " + err.message); alert("Error processing request: " + err.message);
} }
} }
// ===== Utility Functions =====
function removeFile() { function removeFile() {
const fileInput = document.getElementById("file-input"); const fileInput = document.getElementById("file-input");
if (fileInput) fileInput.value = ""; if (fileInput) fileInput.value = "";
@@ -205,53 +221,406 @@ function removeFile() {
toggleInputMode(); toggleInputMode();
} }
// ===== Advanced Password Generator =====
function generateRandomPassword() { function generateRandomPassword() {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+[]{}|;:,.<>?/~"; const settings = getPasswordSettings();
const length = 30;
const password = Array.from({ length }, () => if (!settings.charset || settings.charset.length === 0) {
charset.charAt(Math.floor(Math.random() * charset.length)) alert("Please select at least one character type for password generation!");
).join(""); return;
}
const password = generatePassword(settings.length, settings.charset);
const passwordField = document.getElementById("generated-password"); const passwordField = document.getElementById("generated-password");
if (passwordField) { if (passwordField) {
passwordField.value = password; passwordField.value = password;
// Check if we should start Pacman updatePasswordStrength(password);
checkForPacman(); checkForPacman();
} }
} }
function getPasswordSettings() {
const length = parseInt(document.getElementById("password-length-input")?.value || 16);
const includeUppercase = document.getElementById("include-uppercase")?.checked;
const includeLowercase = document.getElementById("include-lowercase")?.checked;
const includeNumbers = document.getElementById("include-numbers")?.checked;
const includeSpecial = document.getElementById("include-special")?.checked;
const excludeAmbiguous = document.getElementById("exclude-ambiguous")?.checked;
const customCharacters = document.getElementById("custom-characters")?.value || "";
let charset = "";
// Character sets
const sets = {
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
lowercase: "abcdefghijklmnopqrstuvwxyz",
numbers: "0123456789",
special: "!@#$%^&*()_+-=[]{}|;:,.<>?/~"
};
// Ambiguous characters to exclude
const ambiguous = "0O1lI";
if (includeUppercase) charset += sets.uppercase;
if (includeLowercase) charset += sets.lowercase;
if (includeNumbers) charset += sets.numbers;
if (includeSpecial) charset += sets.special;
// Add custom characters
if (customCharacters) {
charset += customCharacters;
}
// Remove ambiguous characters if requested
if (excludeAmbiguous) {
charset = charset.split('').filter(char => !ambiguous.includes(char)).join('');
}
// Remove duplicates
charset = [...new Set(charset)].join('');
return { length, charset, settings: { includeUppercase, includeLowercase, includeNumbers, includeSpecial } };
}
function generatePassword(length, charset) {
// Use crypto.getRandomValues for cryptographically secure random generation
const array = new Uint32Array(length);
crypto.getRandomValues(array);
return Array.from(array, (x) => charset[x % charset.length]).join('');
}
function updatePasswordStrength(password) {
const score = calculatePasswordStrength(password);
const strengthText = document.getElementById("password-strength-text");
const strengthFill = document.getElementById("password-strength-fill");
const strengthScore = document.getElementById("strength-score");
const strengthFeedback = document.getElementById("strength-feedback");
if (!strengthText || !strengthFill || !strengthScore || !strengthFeedback) return;
strengthScore.textContent = `Score: ${score.score}/100`;
strengthFeedback.textContent = score.feedback;
// Update strength level and colors
let level, color, width;
if (score.score < 30) {
level = "Very Weak";
color = "#ff4444";
width = "20%";
} else if (score.score < 50) {
level = "Weak";
color = "#ff8800";
width = "40%";
} else if (score.score < 70) {
level = "Fair";
color = "#ffaa00";
width = "60%";
} else if (score.score < 85) {
level = "Strong";
color = "#88ff00";
width = "80%";
} else {
level = "Very Strong";
color = "#00ff44";
width = "100%";
}
strengthText.textContent = level;
strengthText.style.color = color;
strengthFill.style.backgroundColor = color;
strengthFill.style.width = width;
}
function calculatePasswordStrength(password) {
if (!password) return { score: 0, feedback: "Enter a password to see strength analysis" };
let score = 0;
const feedback = [];
// Length scoring
if (password.length >= 8) score += 10;
if (password.length >= 12) score += 10;
if (password.length >= 16) score += 10;
if (password.length >= 20) score += 5;
// Character variety scoring
const hasLower = /[a-z]/.test(password);
const hasUpper = /[A-Z]/.test(password);
const hasNumber = /[0-9]/.test(password);
const hasSpecial = /[^a-zA-Z0-9]/.test(password);
let varieties = 0;
if (hasLower) { score += 5; varieties++; }
if (hasUpper) { score += 5; varieties++; }
if (hasNumber) { score += 5; varieties++; }
if (hasSpecial) { score += 10; varieties++; }
// Bonus for character variety
if (varieties >= 3) score += 10;
if (varieties === 4) score += 5;
// Pattern penalties
if (/(.)\1{2,}/.test(password)) {
score -= 10;
feedback.push("Avoid repeating characters");
}
if (/123|abc|qwe|password|admin|test/i.test(password)) {
score -= 15;
feedback.push("Avoid common patterns or words");
}
// Entropy calculation
const uniqueChars = new Set(password).size;
const entropy = password.length * Math.log2(uniqueChars);
if (entropy > 60) score += 15;
else if (entropy > 40) score += 10;
else if (entropy > 30) score += 5;
// Generate specific feedback
if (password.length < 8) feedback.push("Use at least 8 characters");
if (password.length < 12) feedback.push("12+ characters recommended");
if (!hasLower) feedback.push("Add lowercase letters");
if (!hasUpper) feedback.push("Add uppercase letters");
if (!hasNumber) feedback.push("Add numbers");
if (!hasSpecial) feedback.push("Add special characters");
if (feedback.length === 0) {
feedback.push("Excellent password strength!");
}
return {
score: Math.min(100, Math.max(0, score)),
feedback: feedback.join(", ")
};
}
function setupPasswordGeneratorListeners() {
// Modal controls
const settingsBtn = document.getElementById("password-settings-btn");
const modal = document.getElementById("password-settings-modal");
const closeBtn = document.getElementById("close-password-settings");
const applyBtn = document.getElementById("apply-password-settings");
const resetBtn = document.getElementById("reset-password-settings");
if (settingsBtn && modal) {
settingsBtn.addEventListener("click", () => {
modal.style.display = "flex";
updateCharsetPreview();
});
}
if (closeBtn && modal) {
closeBtn.addEventListener("click", () => {
modal.style.display = "none";
});
}
// Close modal when clicking outside
if (modal) {
modal.addEventListener("click", (e) => {
if (e.target === modal) {
modal.style.display = "none";
}
});
}
if (applyBtn && modal) {
applyBtn.addEventListener("click", () => {
generateRandomPassword();
modal.style.display = "none";
showPasswordFeedback("Settings applied and password regenerated!");
});
}
if (resetBtn) {
resetBtn.addEventListener("click", () => {
resetPasswordSettings();
updateCharsetPreview();
showPasswordFeedback("Settings reset to defaults!");
});
}
// Length controls (slider and number input)
const lengthSlider = document.getElementById("password-length");
const lengthInput = document.getElementById("password-length-input");
if (lengthSlider && lengthInput) {
// Sync slider to number input
lengthSlider.addEventListener("input", () => {
lengthInput.value = lengthSlider.value;
updateCharsetPreview();
});
// Sync number input to slider
lengthInput.addEventListener("input", () => {
let value = parseInt(lengthInput.value);
// Validate bounds
if (value < 8) {
value = 8;
lengthInput.value = 8;
} else if (value > 128) {
value = 128;
lengthInput.value = 128;
}
lengthSlider.value = value;
updateCharsetPreview();
});
// Handle edge cases for number input
lengthInput.addEventListener("blur", () => {
if (!lengthInput.value || lengthInput.value < 8) {
lengthInput.value = 8;
lengthSlider.value = 8;
updateCharsetPreview();
}
});
// Allow Enter key to apply changes
lengthInput.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
lengthInput.blur();
}
});
}
// Character set checkboxes
const checkboxes = [
"include-uppercase",
"include-lowercase",
"include-numbers",
"include-special",
"exclude-ambiguous"
];
checkboxes.forEach(id => {
const checkbox = document.getElementById(id);
if (checkbox) {
checkbox.addEventListener("change", () => {
updateCharsetPreview();
});
}
});
// Custom characters input
const customCharsInput = document.getElementById("custom-characters");
if (customCharsInput) {
customCharsInput.addEventListener("input", () => {
updateCharsetPreview();
});
}
// Password visibility toggle
const toggleVisibilityBtn = document.getElementById("toggle-password-visibility");
const passwordField = document.getElementById("generated-password");
if (toggleVisibilityBtn && passwordField) {
toggleVisibilityBtn.addEventListener("click", () => {
if (passwordField.type === "password") {
passwordField.type = "text";
toggleVisibilityBtn.textContent = "🙈";
} else {
passwordField.type = "password";
toggleVisibilityBtn.textContent = "👁️";
}
});
}
// Use password in form button
const usePasswordBtn = document.getElementById("use-password-btn");
if (usePasswordBtn) {
usePasswordBtn.addEventListener("click", () => {
const generatedPassword = document.getElementById("generated-password")?.value;
const passwordInput = document.getElementById("password");
if (generatedPassword && passwordInput) {
passwordInput.value = generatedPassword;
showPasswordFeedback("Password applied to form!");
}
});
}
// Monitor password field for manual changes to update strength
if (passwordField) {
passwordField.addEventListener("input", () => {
updatePasswordStrength(passwordField.value);
});
}
// Generate initial password
generateRandomPassword();
}
function updateCharsetPreview() {
const settings = getPasswordSettings();
const preview = document.getElementById("charset-preview");
if (preview) {
if (settings.charset && settings.charset.length > 0) {
preview.textContent = `Characters (${settings.charset.length}): ${settings.charset}`;
} else {
preview.textContent = "⚠️ No character types selected! Please select at least one character type.";
preview.style.color = "#ff6b6b";
}
}
}
function resetPasswordSettings() {
// Reset to default values
document.getElementById("password-length").value = 16;
document.getElementById("password-length-input").value = 16;
document.getElementById("include-uppercase").checked = true;
document.getElementById("include-lowercase").checked = true;
document.getElementById("include-numbers").checked = true;
document.getElementById("include-special").checked = true;
document.getElementById("exclude-ambiguous").checked = false;
document.getElementById("custom-characters").value = "";
}
function showPasswordFeedback(message) {
const feedback = document.getElementById("password-copy-feedback");
if (feedback) {
const originalText = feedback.textContent;
feedback.textContent = message;
showFeedback(feedback);
// Reset feedback text after showing
setTimeout(() => {
feedback.textContent = originalText;
}, 3000);
}
}
function copyToClipboard(elementId, feedbackId) { function copyToClipboard(elementId, feedbackId) {
const el = document.getElementById(elementId); const el = document.getElementById(elementId);
const feedback = document.getElementById(feedbackId); const feedback = document.getElementById(feedbackId);
if (!el || !el.value) return; if (!el || !el.value) return;
// Create a temporary textarea element
const textarea = document.createElement('textarea'); const textarea = document.createElement('textarea');
textarea.value = el.value; textarea.value = el.value;
textarea.style.position = 'fixed'; textarea.style.position = 'fixed';
textarea.style.opacity = '0'; textarea.style.opacity = '0';
document.body.appendChild(textarea); document.body.appendChild(textarea);
// Select and copy the text
textarea.select(); textarea.select();
textarea.setSelectionRange(0, 99999); // For mobile devices textarea.setSelectionRange(0, 99999);
try { try {
// Try using the modern clipboard API first
navigator.clipboard.writeText(el.value).then(() => { navigator.clipboard.writeText(el.value).then(() => {
showFeedback(feedback); showFeedback(feedback);
}).catch(() => { }).catch(() => {
// Fallback to execCommand for older browsers
document.execCommand('copy'); document.execCommand('copy');
showFeedback(feedback); showFeedback(feedback);
}); });
} catch (err) { } catch (err) {
// Final fallback
document.execCommand('copy'); document.execCommand('copy');
showFeedback(feedback); showFeedback(feedback);
} }
// Clean up
document.body.removeChild(textarea); document.body.removeChild(textarea);
} }
@@ -298,6 +667,7 @@ function checkForPacman() {
window.exitGame(); window.exitGame();
} }
} }
function copyShareLink() { function copyShareLink() {
const linkEl = document.getElementById("share-link"); const linkEl = document.getElementById("share-link");
const feedback = document.getElementById("shared-link-feedback"); const feedback = document.getElementById("shared-link-feedback");
@@ -346,8 +716,229 @@ function showCopyFeedback(feedbackEl) {
}, 3000); }, 3000);
} }
// ===== Algorithm Management =====
async function loadAvailableAlgorithms() {
try {
const response = await fetch('/api/algorithms');
const data = await response.json();
if (response.ok && data.algorithms) {
// Store algorithms globally for use in other functions
window.availableAlgorithms = data.algorithms;
updateAlgorithmDropdown(data.algorithms);
}
} catch (error) {
console.error('Failed to load algorithms:', error);
}
}
function updateAlgorithmDropdown(algorithms) {
const algorithmSelect = document.getElementById('algorithm');
const shareAlgorithmSelect = document.getElementById('share-algorithm');
// Update main encryption/decryption algorithm dropdown
if (algorithmSelect) {
algorithmSelect.innerHTML = '';
let firstOption = null;
for (const [key, algo] of Object.entries(algorithms)) {
if (algo.supports_text) {
const option = document.createElement('option');
option.value = key;
option.textContent = `${algo.name}${algo.requires_keypair ? ' (requires keypair)' : ''}`;
algorithmSelect.appendChild(option);
// Remember the first option (should be a non-keypair algorithm)
if (!firstOption) {
firstOption = key;
}
}
}
// Ensure the first option is selected
if (firstOption) {
algorithmSelect.value = firstOption;
}
}
// Update PacShare algorithm dropdown (for file uploads)
if (shareAlgorithmSelect) {
shareAlgorithmSelect.innerHTML = '';
let firstFileOption = null;
for (const [key, algo] of Object.entries(algorithms)) {
if (algo.supports_file) {
const option = document.createElement('option');
option.value = key;
option.textContent = `${algo.name}${algo.requires_keypair ? ' (requires keypair)' : ''}`;
shareAlgorithmSelect.appendChild(option);
// Remember the first file-supporting option
if (!firstFileOption) {
firstFileOption = key;
}
}
}
// Set the first file-supporting option as selected
if (firstFileOption) {
shareAlgorithmSelect.value = firstFileOption;
}
}
// Update Key Pairs Management dropdown
const keypairAlgorithmSelect = document.getElementById('keypair-algorithm');
if (keypairAlgorithmSelect) {
// Clear existing options except the hardcoded ones
const options = keypairAlgorithmSelect.querySelectorAll('option');
options.forEach(option => {
if (option.value !== 'rsa_hybrid' && option.value !== 'pqcrypto') {
option.remove();
}
});
// Show/hide post-quantum option based on availability
const pqOption = document.getElementById('pqcrypto-option');
if (pqOption) {
pqOption.style.display = algorithms.pqcrypto ? 'block' : 'none';
}
// If rsa_hybrid is not available, hide it
const rsaOption = keypairAlgorithmSelect.querySelector('option[value="rsa_hybrid"]');
if (rsaOption) {
rsaOption.style.display = algorithms.rsa_hybrid ? 'block' : 'none';
}
}
// Call toggleAlgorithmOptions after dropdown is populated
toggleAlgorithmOptions();
}
function toggleAlgorithmOptions() {
const algorithm = document.getElementById("algorithm")?.value;
const keypairSection = document.getElementById("keypair-section");
const passwordInput = document.getElementById("password-input");
if (!algorithm) return;
// Check if algorithm requires keypair by looking at available algorithms data
let requiresKeypair = false;
if (window.availableAlgorithms && window.availableAlgorithms[algorithm]) {
requiresKeypair = window.availableAlgorithms[algorithm].requires_keypair || false;
} else {
// Fallback to checking name for "hybrid"
requiresKeypair = algorithm.includes("hybrid");
}
// Show/hide keypair section only for algorithms that require it
if (keypairSection) {
keypairSection.style.display = requiresKeypair ? "block" : "none";
}
// Show/hide password input (opposite of keypair section)
if (passwordInput) {
passwordInput.style.display = requiresKeypair ? "none" : "block";
}
// Update key status based on global keys
const globalKeys = window.getGlobalKeys ? window.getGlobalKeys() : {};
const publicStatus = document.getElementById("public-key-status");
const privateStatus = document.getElementById("private-key-status");
if (!requiresKeypair) {
if (publicStatus) publicStatus.style.display = "none";
if (privateStatus) privateStatus.style.display = "none";
} else {
// Show key status if keys are loaded in global store
if (publicStatus) publicStatus.style.display = globalKeys.publicKey ? "block" : "none";
if (privateStatus) privateStatus.style.display = globalKeys.privateKey ? "block" : "none";
}
}
// ===== File-based Key Management =====
async function generateAndDownloadKeyPair() {
const algorithm = document.getElementById("algorithm")?.value;
let requiresKeypair = false;
if (window.availableAlgorithms && window.availableAlgorithms[algorithm]) {
requiresKeypair = window.availableAlgorithms[algorithm].requires_keypair || false;
} else {
requiresKeypair = algorithm.includes("hybrid");
}
if (!algorithm || !requiresKeypair) {
alert("Key pair generation is only available for algorithms that require key pairs");
return;
}
try {
const response = await fetch('/api/generate-keypair', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ algorithm: algorithm })
});
const data = await response.json();
if (response.ok) {
// Download public key
downloadTextAsFile(data.public_key, `${algorithm}_public_key.pub`, 'text/plain');
// Download private key
downloadTextAsFile(data.private_key, `${algorithm}_private_key.key`, 'text/plain');
alert("✅ Key pair generated and downloaded!\n\n📁 Files saved:\n• Public Key: " + `${algorithm}_public_key.pub` + "\n• Private Key: " + `${algorithm}_private_key.key` + "\n\n🔐 Use public key for encryption, private key for decryption.");
} else {
alert(`Error generating key pair: ${data.error}`);
}
} catch (error) {
alert(`Error: ${error.message}`);
}
}
function downloadTextAsFile(text, filename, mimeType) {
const blob = new Blob([text], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function handlePublicKeyLoad(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
// Update global keys instead of window variables
if (window.setGlobalKeys) {
window.setGlobalKeys({ publicKey: e.target.result });
}
document.getElementById("public-key-status").style.display = "block";
console.log("Public key loaded successfully and synced to global store");
};
reader.readAsText(file);
}
function handlePrivateKeyLoad(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
// Update global keys instead of window variables
if (window.setGlobalKeys) {
window.setGlobalKeys({ privateKey: e.target.result });
}
document.getElementById("private-key-status").style.display = "block";
console.log("Private key loaded successfully and synced to global store");
};
reader.readAsText(file);
}
function startPacman() { } function startPacman() { }
function exitGame() { } function exitGame() { }
+119
View File
@@ -57,6 +57,8 @@
<form action="{{ url_for('admin_settings') }}" method="GET" style="display: inline;"> <form action="{{ url_for('admin_settings') }}" method="GET" style="display: inline;">
<button type="submit">Settings</button> <button type="submit">Settings</button>
</form> </form>
<button onclick="switchToDevMode()" style="background: #0066cc;">Switch to Dev Mode</button>
<button onclick="switchToProdMode()" style="background: #cc6600;">Switch to Prod Mode</button>
<button onclick="resetAdmin()" class="danger-button">Reset Admin</button> <button onclick="resetAdmin()" class="danger-button">Reset Admin</button>
<button onclick="clearUploads()" class="danger-button">Clear PacShare</button> <button onclick="clearUploads()" class="danger-button">Clear PacShare</button>
</div> </div>
@@ -85,6 +87,58 @@
</form> </form>
</section> </section>
<!-- 2FA Management Section -->
<section id="2fa-section" class="card form-group">
<h2>Two-Factor Authentication (2FA)</h2>
<!-- 2FA Feedback -->
{% with messages = get_flashed_messages(with_categories=true, category_filter=['2fa-feedback']) %}
{% for category, message in messages %}
<div class="copy-feedback show">{{ message }}</div>
{% endfor %}
{% endwith %}
{% if tfa_enabled %}
<!-- 2FA is enabled -->
<div class="status-info">
<p style="color: lime;">✅ 2FA is <strong>enabled</strong> for your admin account.</p>
<p>Your account is protected with TOTP-based two-factor authentication.</p>
</div>
<!-- QR Code Display -->
<div class="form-group">
<button type="button" onclick="toggleQRCode()" style="margin-bottom: 10px;">Show/Hide QR Code</button>
<div id="qr-code-container" style="display: none; text-align: center;">
<p><strong>Scan this QR code with your authenticator app:</strong></p>
<img src="{{ url_for('admin_qr_code') }}" alt="Admin 2FA QR Code" style="max-width: 200px;" />
<p style="font-size: 0.85em; color: #ccc;">You can re-scan this QR code if you need to set up 2FA on a new device.</p>
</div>
</div>
<!-- Disable 2FA Form -->
<div class="form-group">
<form method="POST" action="{{ url_for('admin_disable_2fa') }}">
<input type="text" name="totp_code" placeholder="Enter current 2FA code to disable" pattern="[0-9]{6}" maxlength="6" required />
<button type="submit" class="danger-button">Disable 2FA</button>
</form>
</div>
{% else %}
<!-- 2FA is disabled -->
<div class="status-info">
<p style="color: #ff6b6b;">🔒 2FA is <strong>disabled</strong> for your admin account.</p>
<p>Enable 2FA for enhanced security using authenticator apps like Google Authenticator, Authy, or Microsoft Authenticator.</p>
</div>
<!-- Enable 2FA -->
<div class="form-group">
<form method="POST" action="{{ url_for('admin_enable_2fa') }}">
<button type="submit">Enable 2FA</button>
</form>
</div>
{% endif %}
</section>
<!-- Server Status Section --> <!-- Server Status Section -->
<section id="server-status-section" class="card form-group"> <section id="server-status-section" class="card form-group">
<h2>Server Status</h2> <h2>Server Status</h2>
@@ -239,6 +293,58 @@
} }
} }
async function switchToDevMode() {
if (!confirm('Are you sure you want to switch to Development mode? This will restart the server.')) return;
try {
const response = await fetch('{{ url_for("admin_switch_dev_mode") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.ok) {
showFeedback(data.message);
setTimeout(() => {
window.location.reload();
}, 3000);
} else {
showFeedback(data.error || 'Failed to switch to dev mode.');
}
} catch (error) {
showFeedback('Failed to switch to dev mode.');
}
}
async function switchToProdMode() {
if (!confirm('Are you sure you want to switch to Production mode? This will restart the server.')) return;
try {
const response = await fetch('{{ url_for("admin_switch_prod_mode") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.ok) {
showFeedback(data.message);
setTimeout(() => {
window.location.reload();
}, 3000);
} else {
showFeedback(data.error || 'Failed to switch to prod mode.');
}
} catch (error) {
showFeedback('Failed to switch to prod mode.');
}
}
function showFeedback(message) { function showFeedback(message) {
const feedback = document.getElementById('admin-feedback'); const feedback = document.getElementById('admin-feedback');
feedback.textContent = message; feedback.textContent = message;
@@ -252,6 +358,19 @@
}, 300); }, 300);
}, 3000); }, 3000);
} }
function toggleQRCode() {
const container = document.getElementById('qr-code-container');
const button = document.querySelector('button[onclick="toggleQRCode()"]');
if (container.style.display === 'none') {
container.style.display = 'block';
button.textContent = 'Hide QR Code';
} else {
container.style.display = 'none';
button.textContent = 'Show QR Code';
}
}
</script> </script>
</body> </body>
</html> </html>
+9
View File
@@ -44,6 +44,15 @@
<form method="POST"> <form method="POST">
<input type="text" name="username" placeholder="Username" required /> <input type="text" name="username" placeholder="Username" required />
<input type="password" name="password" placeholder="Password" required /> <input type="password" name="password" placeholder="Password" required />
{% if requires_2fa %}
<!-- 2FA Code Input -->
<div class="form-group">
<input type="text" name="totp_code" placeholder="2FA Code (6 digits)" pattern="[0-9]{6}" maxlength="6" required />
<small style="color: #ccc;">Enter the 6-digit code from your authenticator app</small>
</div>
{% endif %}
<div class="button-group mt-3"> <div class="button-group mt-3">
<button type="submit">Log In</button> <button type="submit">Log In</button>
</div> </div>
+9
View File
@@ -45,6 +45,15 @@
<form method="POST"> <form method="POST">
<input type="text" name="username" placeholder="Username" required /> <input type="text" name="username" placeholder="Username" required />
<input type="password" name="password" placeholder="Password" required /> <input type="password" name="password" placeholder="Password" required />
<!-- 2FA Option -->
<div class="form-group">
<label>
<input type="checkbox" name="enable_2fa" id="enable-2fa" />
Enable Two-Factor Authentication (2FA) - Adds extra security using TOTP apps like Google Authenticator, Authy, etc.
</label>
</div>
<div class="button-group mt-3"> <div class="button-group mt-3">
<button type="submit">Set Credentials</button> <button type="submit">Set Credentials</button>
</div> </div>
+730 -49
View File
@@ -15,6 +15,9 @@
<!-- Scripts --> <!-- Scripts -->
<script type="module" src="{{ url_for('static', filename='js/main.js') }}" defer></script> <script type="module" src="{{ url_for('static', filename='js/main.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/bulk-operations.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/crypto-settings.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/pacshare-enhanced.js') }}" defer></script>
</head> </head>
<body class="dark"> <body class="dark">
<!-- Header --> <!-- Header -->
@@ -32,17 +35,392 @@
<main> <main>
<!-- Password Generator Section --> <!-- Password Generator Section -->
<section id="password-generator-section" class="card form-group"> <section id="password-generator-section" class="card form-group">
<h2>Password Generator</h2> <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px;">
<h2 style="margin: 0;">Password Generator</h2>
<button type="button" id="password-settings-btn" class="settings-button" title="Password Settings">
⚙️
</button>
</div>
<!-- Generated Password Display -->
<div class="form-group"> <div class="form-group">
<input type="text" id="generated-password" readonly /> <label for="generated-password">Generated Password:</label>
<div class="button-group"> <div style="position: relative;">
<button type="button" id="generate-btn">Generate</button> <input type="text" id="generated-password" readonly style="font-family: monospace;" />
<button type="button" id="toggle-password-visibility" style="position: absolute; right: 5px; top: 50%; transform: translateY(-50%); background: none; border: none; color: #00ff99; cursor: pointer;">👁️</button>
</div>
<!-- Password Strength Meter -->
<div id="password-strength-container" style="margin-top: 10px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
<label style="margin: 0;">Password Strength:</label>
<span id="password-strength-text" style="font-weight: bold; color: #ff6b6b;">No Password</span>
</div>
<div id="password-strength-bar" style="width: 100%; height: 10px; background-color: #333; border-radius: 5px; overflow: hidden;">
<div id="password-strength-fill" style="height: 100%; width: 0%; background-color: #ff6b6b; transition: all 0.3s ease;"></div>
</div>
<div id="password-strength-details" style="margin-top: 8px; font-size: 0.9em; color: #ccc;">
<div id="strength-score" style="margin-bottom: 3px;">Score: 0/100</div>
<div id="strength-feedback"></div>
</div>
</div>
<div class="button-group" style="margin-top: 15px;">
<button type="button" id="generate-btn">Generate Password</button>
<button type="button" id="copy-btn">Copy Password</button> <button type="button" id="copy-btn">Copy Password</button>
<button type="button" id="use-password-btn">Use in Form</button>
</div> </div>
<div id="password-copy-feedback" class="copy-feedback">Password copied to clipboard!</div> <div id="password-copy-feedback" class="copy-feedback">Password copied to clipboard!</div>
</div> </div>
</section> </section>
<!-- Password Settings Modal -->
<div id="password-settings-modal" class="modal-overlay" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>Password Generator Settings</h3>
<button type="button" id="close-password-settings" class="close-button"></button>
</div>
<div class="modal-body">
<!-- Password Length -->
<div class="setting-group">
<div class="length-header">
<label for="password-length">Length:</label>
<div class="length-input-container">
<input type="number" id="password-length-input" min="8" max="128" value="16" class="length-number-input" />
<span class="length-unit">characters</span>
</div>
</div>
<input type="range" id="password-length" min="8" max="128" value="16" class="length-slider" />
<div class="length-labels">
<span>8</span>
<span>32</span>
<span>64</span>
<span>128</span>
</div>
</div>
<!-- Character Set Options -->
<div class="setting-group">
<h4>Character Types</h4>
<div class="checkbox-grid">
<label class="checkbox-item">
<input type="checkbox" id="include-uppercase" checked />
<span class="checkmark"></span>
Uppercase (A-Z)
</label>
<label class="checkbox-item">
<input type="checkbox" id="include-lowercase" checked />
<span class="checkmark"></span>
Lowercase (a-z)
</label>
<label class="checkbox-item">
<input type="checkbox" id="include-numbers" checked />
<span class="checkmark"></span>
Numbers (0-9)
</label>
<label class="checkbox-item">
<input type="checkbox" id="include-special" checked />
<span class="checkmark"></span>
Special Characters
</label>
</div>
</div>
<!-- Advanced Options -->
<div class="setting-group">
<h4>Advanced Options</h4>
<label class="checkbox-item">
<input type="checkbox" id="exclude-ambiguous" />
<span class="checkmark"></span>
Exclude ambiguous characters (0, O, l, 1, I)
</label>
</div>
<!-- Custom Characters -->
<div class="setting-group">
<label for="custom-characters">Custom Characters</label>
<input type="text" id="custom-characters" placeholder="Add custom characters..." class="custom-input" />
<div class="setting-hint">Add any additional characters you want to include</div>
</div>
<!-- Preview -->
<div class="setting-group">
<h4>Character Set Preview</h4>
<div id="charset-preview" class="charset-preview">Loading...</div>
</div>
</div>
<div class="modal-footer">
<button type="button" id="reset-password-settings" class="secondary-button">Reset to Defaults</button>
<button type="button" id="apply-password-settings" class="primary-button">Apply Settings</button>
</div>
</div>
</div>
<!-- Crypto Settings Modal -->
<div id="crypto-settings-modal" class="modal-overlay" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>Encryption Settings</h3>
<button type="button" id="close-crypto-settings" class="close-button"></button>
</div>
<div class="modal-body">
<!-- Processing Mode -->
<div class="setting-group">
<h4>Processing Mode</h4>
<div class="mode-selection">
<label class="radio-item">
<input type="radio" name="processing-mode" id="single-file-mode-radio" value="single" checked />
<span class="radiomark"></span>
<div class="radio-content">
<div class="radio-title">Single File</div>
<div class="radio-description">Process one file at a time</div>
</div>
</label>
<label class="radio-item">
<input type="radio" name="processing-mode" id="bulk-file-mode-radio" value="bulk" />
<span class="radiomark"></span>
<div class="radio-content">
<div class="radio-title">Bulk Processing</div>
<div class="radio-description">Process multiple files with drag & drop</div>
</div>
</label>
</div>
</div>
<!-- File Preview Options -->
<div class="setting-group">
<h4>File Preview</h4>
<label class="checkbox-item">
<input type="checkbox" id="enable-file-preview" checked />
<span class="checkmark"></span>
Enable file preview before processing
</label>
<label class="checkbox-item">
<input type="checkbox" id="auto-download-results" checked />
<span class="checkmark"></span>
Automatically download processed files
</label>
</div>
<!-- Bulk Processing Options -->
<div class="setting-group" id="bulk-options" style="display: none;">
<h4>Bulk Processing Options</h4>
<label class="checkbox-item">
<input type="checkbox" id="sequential-processing" checked />
<span class="checkmark"></span>
Process files sequentially (prevents server overload)
</label>
<label class="checkbox-item">
<input type="checkbox" id="show-detailed-progress" checked />
<span class="checkmark"></span>
Show detailed progress for each file
</label>
<label class="checkbox-item">
<input type="checkbox" id="stop-on-error" />
<span class="checkmark"></span>
Stop processing if any file fails
</label>
</div>
<!-- Performance Settings -->
<div class="setting-group">
<h4>Performance</h4>
<div class="setting-item">
<label for="max-file-size">Maximum file size (MB):</label>
<div class="size-input-container">
<input type="number" id="max-file-size-input" min="1" max="1000" value="100" class="size-number-input" />
<span class="size-unit">MB</span>
</div>
</div>
<div class="setting-hint">Files larger than this will show a warning</div>
</div>
</div>
<div class="modal-footer">
<button type="button" id="reset-crypto-settings" class="secondary-button">Reset to Defaults</button>
<button type="button" id="apply-crypto-settings" class="primary-button">Apply Settings</button>
</div>
</div>
</div>
<!-- PacShare Settings Modal -->
<div id="pacshare-settings-modal" class="modal-overlay" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>PacShare Settings</h3>
<button type="button" id="close-pacshare-settings" class="close-button"></button>
</div>
<div class="modal-body">
<!-- Security Options -->
<div class="setting-group">
<h4>Security</h4>
<label class="checkbox-item">
<input type="checkbox" id="enable-2fa-setting" />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Enable 2FA (TOTP)</div>
<div class="checkbox-description">Adds extra security with Google Authenticator, Authy, etc.</div>
</div>
</label>
<label class="checkbox-item">
<input type="checkbox" id="auto-clear-passwords" checked />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Auto-clear passwords after upload</div>
<div class="checkbox-description">Automatically clear password fields for security</div>
</div>
</label>
</div>
<!-- Upload Behavior -->
<div class="setting-group">
<h4>Upload Behavior</h4>
<label class="checkbox-item">
<input type="checkbox" id="auto-copy-links" checked />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Auto-copy share links</div>
<div class="checkbox-description">Automatically copy links to clipboard after upload</div>
</div>
</label>
<label class="checkbox-item">
<input type="checkbox" id="show-upload-progress" checked />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Show detailed upload progress</div>
<div class="checkbox-description">Display progress bars for each file</div>
</div>
</label>
<label class="checkbox-item">
<input type="checkbox" id="scroll-to-results" checked />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Auto-scroll to results</div>
<div class="checkbox-description">Automatically scroll to results after upload</div>
</div>
</label>
</div>
<!-- File Validation -->
<div class="setting-group">
<h4>File Validation</h4>
<div class="setting-item">
<label for="max-upload-size">Maximum file size per upload (MB):</label>
<div class="size-input-container">
<input type="number" id="max-upload-size-input" min="1" max="1000" value="25" class="size-number-input" />
<span class="size-unit">MB</span>
</div>
<div class="setting-hint">Files larger than this will show a warning</div>
</div>
<label class="checkbox-item">
<input type="checkbox" id="validate-file-types" />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Validate file types</div>
<div class="checkbox-description">Show warnings for potentially unsafe file types</div>
</div>
</label>
</div>
<!-- Advanced Options -->
<div class="setting-group">
<h4>Advanced</h4>
<div class="setting-item">
<label for="concurrent-uploads">Maximum concurrent uploads:</label>
<div class="size-input-container">
<input type="number" id="concurrent-uploads-input" min="1" max="10" value="1" class="size-number-input" />
<span class="size-unit">files</span>
</div>
<div class="setting-hint">1 = sequential uploads (recommended), higher = parallel uploads</div>
</div>
<label class="checkbox-item">
<input type="checkbox" id="enable-file-preview" checked />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Enable file preview</div>
<div class="checkbox-description">Allow previewing files before upload</div>
</div>
</label>
<label class="checkbox-item">
<input type="checkbox" id="remember-algorithm" checked />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Remember encryption algorithm</div>
<div class="checkbox-description">Remember the last selected algorithm</div>
</div>
</label>
</div>
<!-- Current Settings Summary -->
<div class="setting-group">
<h4>Current Configuration</h4>
<div id="pacshare-settings-summary" class="charset-preview">
Loading current settings...
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" id="reset-pacshare-settings" class="secondary-button">Reset to Defaults</button>
<button type="button" id="apply-pacshare-settings" class="primary-button">Apply Settings</button>
</div>
</div>
</div>
<!-- Key Management Section -->
<section id="key-pairs-section" class="card form-group">
<h2>Key Management</h2>
<p style="color: #ccc; font-size: 0.9em; margin-bottom: 15px;">
Manage Key Pairs for the RSA Hybrid Algorithm.
</p>
<!-- Key Status Indicators -->
<div class="form-group">
<h3 style="margin-bottom: 10px; color: #00ff99;">Key Status</h3>
<div id="key-status-indicators" style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 15px;">
<div style="padding: 10px; border: 2px solid #333; border-radius: 5px; text-align: center;">
<div id="public-key-indicator" style="color: #ff6b6b; font-weight: bold;">🔓 No Public Key</div>
<div style="font-size: 0.8em; color: #888;">For Encryption</div>
</div>
<div style="padding: 10px; border: 2px solid #333; border-radius: 5px; text-align: center;">
<div id="private-key-indicator" style="color: #ff6b6b; font-weight: bold;">🔐 No Private Key</div>
<div style="font-size: 0.8em; color: #888;">For Decryption</div>
</div>
</div>
</div>
<!-- Key Management Buttons -->
<div class="form-group">
<div class="button-group">
<button type="button" id="generate-keypair-main-btn">Generate & Download Key Pair</button>
<button type="button" id="load-public-main-btn">Load Public Key</button>
<button type="button" id="load-private-main-btn">Load Private Key</button>
</div>
<div class="button-group" style="margin-top: 10px;">
<button type="button" id="clear-keys-btn" class="danger-button">Clear All Keys</button>
<button type="button" id="download-keys-btn" style="display: none;">Download Current Keys</button>
</div>
</div>
<!-- Hidden File Inputs -->
<input type="file" id="public-key-main-input" accept=".pub,.pem" style="display: none;">
<input type="file" id="private-key-main-input" accept=".key,.pem" style="display: none;">
<!-- Key Information Display -->
<div id="key-info-display" style="display: none; margin-top: 15px; padding: 10px; border: 1px solid #00ff99; border-radius: 5px; background-color: #001100;">
<h4 style="color: #00ff99; margin-top: 0;">Loaded Keys Information</h4>
<div id="key-info-content" style="font-family: monospace; font-size: 0.8em; color: #ccc;"></div>
</div>
<!-- Copy Feedback -->
<div id="keypair-feedback" class="copy-feedback">Keys generated and downloaded!</div>
</section>
<!-- Pacman Game Section --> <!-- Pacman Game Section -->
<section id="pacman-section" class="card" style="display: none;"> <section id="pacman-section" class="card" style="display: none;">
<div class="pacman-wrapper"> <div class="pacman-wrapper">
@@ -57,17 +435,41 @@
<!-- Encryption/Decryption Section --> <!-- Encryption/Decryption Section -->
<section id="encoding-section" class="card form-group"> <section id="encoding-section" class="card form-group">
<h2>Encrypt & Decrypt</h2> <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px;">
<h2 style="margin: 0;">Encrypt & Decrypt</h2>
<button type="button" id="crypto-settings-btn" class="settings-button" title="Encryption Settings">
⚙️
</button>
</div>
<form id="crypto-form" class="form-group"> <form id="crypto-form" class="form-group">
<!-- Encryption Type Selection --> <!-- Algorithm Selection -->
<div class="form-group"> <div class="form-group" id="algorithm-selection">
<label for="encryption-type">Encryption Type:</label> <label for="algorithm">Encryption Algorithm:</label>
<select id="encryption-type"> <select id="algorithm">
<option value="basic">Basic Cipher</option> <!-- Options populated dynamically by JavaScript -->
<option value="advanced" selected>Advanced AES</option>
</select> </select>
</div> </div>
<!-- Key Pair Management (for RSA/PQ algorithms) -->
<div class="form-group" id="keypair-section" style="display: none;">
<div class="keypair-info">
<p><strong>🔐 Key Pair Required:</strong></p>
<p><strong>For Encryption:</strong> Use Public Key (.pub file)</p>
<p><strong>For Decryption:</strong> Use Private Key (.key file)</p>
</div>
<div style="padding: 10px; border: 1px solid #ffaa00; border-radius: 5px; background-color: #221100; margin: 10px 0;">
<p style="color: #ffaa00; margin: 0; text-align: center;">
<strong>💡 Manage your keys in the "Key Pairs Management" section above</strong>
</p>
</div>
<!-- Key status indicators -->
<div id="key-status" style="margin-top: 10px;">
<div id="public-key-status" style="display: none;">✅ Public key loaded</div>
<div id="private-key-status" style="display: none;">✅ Private key loaded</div>
</div>
</div>
<!-- Operation Toggle --> <!-- Operation Toggle -->
<div class="toggle-container"> <div class="toggle-container">
<span class="toggle-label">Encrypt</span> <span class="toggle-label">Encrypt</span>
@@ -90,9 +492,56 @@
</div> </div>
<!-- File Input Section --> <!-- File Input Section -->
<div id="file-section" class="form-group" style="display: none;"> <div id="file-section" class="form-group">
<input type="file" id="file-input" /> <!-- Single File Input (default) -->
<button type="button" id="remove-file-btn">Remove File</button> <div id="single-file-mode">
<input type="file" id="file-input" />
<button type="button" id="remove-file-btn">Remove File</button>
</div>
<!-- Bulk File Mode (hidden by default) -->
<div id="bulk-file-mode" style="display: none;">
<!-- Drag & Drop Zone -->
<div id="bulk-drop-zone" class="drop-zone">
<div class="drop-zone-content">
<div class="drop-zone-icon">📁</div>
<p>Drag & drop files here or <button type="button" id="bulk-file-select">select files</button></p>
<p style="font-size: 0.9em; color: #888;">Supports multiple files</p>
</div>
<input type="file" id="bulk-file-input" multiple style="display: none;" />
</div>
<!-- File Preview Section -->
<div id="bulk-file-preview" style="display: none; margin-top: 15px;">
<h3>Selected Files</h3>
<div id="bulk-file-list" class="file-list"></div>
<div class="button-group" style="margin-top: 15px;">
<button type="button" id="bulk-clear-btn" class="danger-button">Clear All</button>
</div>
</div>
<!-- Progress Section -->
<div id="bulk-progress-section" style="display: none; margin-top: 15px;">
<h3>Processing Progress</h3>
<div id="bulk-overall-progress" class="progress-container">
<div class="progress-bar">
<div id="bulk-overall-bar" class="progress-fill"></div>
</div>
<span id="bulk-overall-text">0 / 0 files processed</span>
</div>
<div id="bulk-file-progress-list" class="file-progress-list"></div>
</div>
<!-- Results Section -->
<div id="bulk-results-section" style="display: none; margin-top: 15px;">
<h3>Results</h3>
<div id="bulk-results-list" class="results-list"></div>
<div class="button-group" style="margin-top: 15px;">
<button type="button" id="bulk-download-all">Download All Results</button>
<button type="button" id="bulk-reset">Start Over</button>
</div>
</div>
</div>
</div> </div>
<!-- Action Buttons --> <!-- Action Buttons -->
@@ -112,8 +561,15 @@
<!-- File Sharing Section --> <!-- File Sharing Section -->
<section id="sharing-section" class="card form-group"> <section id="sharing-section" class="card form-group">
<h2 style="margin-bottom: unset;">PacShare</h2> <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 15px;">
<p style="margin-top: unset;">Securely share encrypted files.</p> <div>
<h2 style="margin: 0;">PacShare</h2>
<p style="margin: 5px 0 0 0;">Securely share encrypted files.</p>
</div>
<button type="button" id="pacshare-settings-btn" class="settings-button" title="PacShare Settings">
⚙️
</button>
</div>
<!-- Flash Messages --> <!-- Flash Messages -->
{% with messages = get_flashed_messages() %} {% with messages = get_flashed_messages() %}
@@ -138,58 +594,283 @@
<!-- File Upload Form --> <!-- File Upload Form -->
<form method="POST" enctype="multipart/form-data" class="form-group" id="upload-form">
<!-- Algorithm Selection for PacShare -->
<div class="form-group">
<label for="share-algorithm">Encryption Algorithm:</label>
<select id="share-algorithm" name="algorithm">
<!-- Options populated dynamically by JavaScript -->
</select>
</div>
<!-- Enhanced File Upload Section -->
<div class="form-group">
<!-- Drag & Drop Zone for PacShare -->
<div id="pacshare-drop-zone" class="drop-zone">
<div class="drop-zone-content">
<div class="drop-zone-icon">📤</div>
<p>Drag & drop files here or <button type="button" id="pacshare-file-select">select files</button></p>
<p style="font-size: 0.9em; color: #888;">Single file or multiple files supported</p>
</div>
<input type="file" name="file" id="upload-file" multiple style="display: none;" />
</div>
<!-- Selected Files Display -->
<div id="pacshare-file-list" style="display: none; margin-top: 15px;">
<h4 style="color: #00ff99; margin-bottom: 10px;">Selected Files</h4>
<div id="pacshare-files-container" class="file-list"></div>
<div class="button-group" style="margin-top: 10px;">
<button type="button" id="pacshare-clear-files" class="danger-button">Clear All</button>
</div>
</div>
</div>
<input type="password" name="enc_password" placeholder="Encryption/Decryption Password" required />
<input type="password" name="pickup_password" placeholder="Pickup Password" required />
<div class="button-group">
<button type="submit" id="pacshare-upload-btn">Upload and Generate Links</button>
</div>
</form>
<!-- Results Section -->
<div id="pacshare-results" style="display: none; margin-top: 20px;">
<h3 style="color: #00ff99;">Upload Results</h3>
<div id="pacshare-results-list" class="results-list"></div>
</div>
<!-- Progress Section -->
<div id="pacshare-progress" style="display: none; margin-top: 20px;">
<h3>Upload Progress</h3>
<div id="pacshare-overall-progress" class="progress-container">
<div class="progress-bar">
<div id="pacshare-overall-bar" class="progress-fill"></div>
</div>
<span id="pacshare-overall-text">0 / 0 files uploaded</span>
</div>
<div id="pacshare-file-progress" class="file-progress-list"></div>
</div>
<!-- Share Link Container (initially hidden) --> <!-- Share Link Container (initially hidden) -->
<div class="share-link-container" id="share-link-container" style="display: none;"> <div class="share-link-container" id="share-link-container" style="display: none;">
<a id="share-link" href="#" target="_blank"></a> <a id="share-link" href="#" target="_blank"></a>
<button type="button" id="copy-share-btn">Copy Link</button> <button type="button" id="copy-share-btn">Copy Link</button>
<div id="shared-link-feedback" class="copy-feedback">Link copied to clipboard!</div> <div id="shared-link-feedback" class="copy-feedback">Link copied to clipboard!</div>
</div> </div>
<form method="POST" enctype="multipart/form-data" class="form-group" id="upload-form">
<input type="file" name="file" id="upload-file" required /> <!-- 2FA Setup Container (initially hidden) -->
<input type="password" name="enc_password" placeholder="Encryption/Decryption Password" required /> <div id="tfa-setup-container" style="display: none; margin-top: 20px; padding: 15px; border: 2px solid #ffaa00; border-radius: 8px; background-color: #332200;">
<input type="password" name="pickup_password" placeholder="Pickup Password" required /> <h3 style="color: #ffaa00; margin-top: 0;">🔒 Important: Set Up 2FA Now!</h3>
<div class="button-group"> <p style="color: #ccc;">You enabled 2FA for this file. <strong>Scan this QR code NOW</strong> with your authenticator app:</p>
<button type="submit">Upload and Generate Link</button> <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> </div>
</form>
<!-- 2FA String Container -->
<div style="margin-top: 15px; padding: 10px; border: 1px solid #00ff99; border-radius: 5px; background-color: #001100;">
<p style="color: #00ff99; margin: 5px 0; font-size: 0.9em;"><strong>Or manually enter this string:</strong></p>
<div class="share-link-container" style="margin: 0;">
<input type="text" id="tfa-string" readonly style="flex: 1; background: #111; color: #00ff99; border: 1px solid #333; padding: 8px; font-family: monospace; font-size: 0.8em;" />
<button type="button" id="copy-tfa-string-btn">Copy String</button>
<div id="tfa-string-feedback" class="copy-feedback">2FA string copied to clipboard!</div>
</div>
</div>
<p style="color: #ff6b6b; font-weight: bold; margin-top: 15px;">⚠️ SAVE THIS QR CODE OR STRING NOW! It will not be shown again for security reasons.</p>
<p style="color: #ccc; font-size: 0.9em;">Recommended apps: Google Authenticator, Authy, Microsoft Authenticator</p>
<button type="button" onclick="pacShareEnhanced && pacShareEnhanced.closeTwoFactorSetup ? pacShareEnhanced.closeTwoFactorSetup() : (document.getElementById('tfa-setup-container').style.display = 'none')">I've Saved the 2FA Information</button>
</div>
<p style="color: #9c0000;">BOTH PASSWORDS ARE REQUIRED FOR PICKUP</p> <p style="color: #9c0000;">BOTH PASSWORDS ARE REQUIRED FOR PICKUP</p>
<script> <script>
document.getElementById('upload-form').addEventListener('submit', async (e) => { // Centralized Key Pairs Management
e.preventDefault(); let globalKeys = {
const formData = new FormData(e.target); 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 { try {
const response = await fetch('/', { const response = await fetch('/api/generate-keypair', {
method: 'POST', method: 'POST',
body: formData headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ algorithm: algorithm })
}); });
const data = await response.json(); const data = await response.json();
if (response.ok) {
if (data.error) { globalKeys.publicKey = data.public_key;
alert(data.error); globalKeys.privateKey = data.private_key;
return; globalKeys.algorithm = algorithm;
}
// Download keys
if (data.success && data.pickup_url) { downloadKeyPair(data.public_key, data.private_key, algorithm);
const shareLink = document.getElementById('share-link');
const shareLinkContainer = document.getElementById('share-link-container');
shareLink.href = data.pickup_url; updateKeyStatusIndicators();
shareLink.textContent = data.pickup_url; showKeypairFeedback('Keys generated and downloaded!');
shareLinkContainer.style.display = 'flex'; } else {
alert('Error: ' + data.error);
// Clear form fields
document.getElementById('upload-file').value = '';
document.getElementsByName('enc_password')[0].value = '';
document.getElementsByName('pickup_password')[0].value = '';
// Scroll to the share link
shareLinkContainer.scrollIntoView({ behavior: 'smooth' });
} }
} catch (error) { } catch (error) {
alert('Error uploading file: ' + error.message); alert('Failed to generate key pair: ' + error.message);
} }
}); });
// Load public key
document.getElementById('load-public-main-btn').addEventListener('click', () => {
document.getElementById('public-key-main-input').click();
});
document.getElementById('public-key-main-input').addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
globalKeys.publicKey = event.target.result;
updateKeyStatusIndicators();
showKeypairFeedback('Public key loaded!');
};
reader.readAsText(file);
}
});
// Load private key
document.getElementById('load-private-main-btn').addEventListener('click', () => {
document.getElementById('private-key-main-input').click();
});
document.getElementById('private-key-main-input').addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
globalKeys.privateKey = event.target.result;
updateKeyStatusIndicators();
showKeypairFeedback('Private key loaded!');
};
reader.readAsText(file);
}
});
// Clear keys
document.getElementById('clear-keys-btn').addEventListener('click', () => {
if (confirm('Are you sure you want to clear all loaded keys?')) {
globalKeys.publicKey = null;
globalKeys.privateKey = null;
updateKeyStatusIndicators();
showKeypairFeedback('All keys cleared!');
}
});
// Download current keys
document.getElementById('download-keys-btn').addEventListener('click', () => {
if (globalKeys.publicKey || globalKeys.privateKey) {
downloadKeyPair(globalKeys.publicKey, globalKeys.privateKey, globalKeys.algorithm);
showKeypairFeedback('Keys downloaded!');
}
});
function downloadKeyPair(publicKey, privateKey, algorithm) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
if (publicKey) {
const pubBlob = new Blob([publicKey], { type: 'text/plain' });
const pubUrl = URL.createObjectURL(pubBlob);
const pubLink = document.createElement('a');
pubLink.href = pubUrl;
pubLink.download = `${algorithm}_public_key_${timestamp}.pub`;
pubLink.click();
URL.revokeObjectURL(pubUrl);
}
if (privateKey) {
const privBlob = new Blob([privateKey], { type: 'text/plain' });
const privUrl = URL.createObjectURL(privBlob);
const privLink = document.createElement('a');
privLink.href = privUrl;
privLink.download = `${algorithm}_private_key_${timestamp}.key`;
privLink.click();
URL.revokeObjectURL(privUrl);
}
}
function showKeypairFeedback(message) {
const feedback = document.getElementById('keypair-feedback');
feedback.textContent = message;
feedback.style.display = 'block';
feedback.classList.add('show');
setTimeout(() => {
feedback.classList.remove('show');
setTimeout(() => {
feedback.style.display = 'none';
}, 300);
}, 2000);
}
// Make global keys available to other scripts
window.getGlobalKeys = () => globalKeys;
window.setGlobalKeys = (keys) => {
globalKeys = { ...globalKeys, ...keys };
updateKeyStatusIndicators();
};
// Initialize key status
updateKeyStatusIndicators();
</script> </script>
<!-- File Limits Information --> <!-- File Limits Information -->
+29
View File
@@ -45,8 +45,24 @@
<!-- File Info --> <!-- File Info -->
<div class="form-group"> <div class="form-group">
<p style="color: #00ff99; margin-bottom: 15px;">File ID: <code>{{ file_id }}</code></p> <p style="color: #00ff99; margin-bottom: 15px;">File ID: <code>{{ file_id }}</code></p>
{% if require_2fa %}
<p style="color: #ffaa00; margin-bottom: 15px;">🔒 This file requires 2FA (TOTP) authentication.</p>
{% endif %}
</div> </div>
{% if require_2fa %}
<div class="form-group" style="border: 2px solid #ffaa00; padding: 15px; margin-bottom: 20px; border-radius: 5px;">
<h3 style="color: #ffaa00; margin-top: 0;">⚠️ 2FA Required</h3>
<p style="color: #ccc;">
<strong>You should have already set up 2FA when uploading this file.</strong><br>
Enter the 6-digit code from your authenticator app below.
</p>
<p style="color: #ff6b6b; font-size: 0.9em;">
If you didn't set up 2FA during upload, you won't be able to access this file.
</p>
</div>
{% endif %}
<!-- Pickup Form --> <!-- Pickup Form -->
<form method="POST" class="form-group"> <form method="POST" class="form-group">
<div class="form-group"> <div class="form-group">
@@ -65,6 +81,19 @@
autocomplete="off" /> autocomplete="off" />
</div> </div>
{% if require_2fa %}
<div class="form-group">
<input type="text"
name="totp_code"
placeholder="6-Digit Authenticator Code"
required
maxlength="6"
pattern="[0-9]{6}"
autocomplete="off"
style="text-align: center; font-size: 1.2em; letter-spacing: 0.2em;" />
</div>
{% endif %}
<div class="button-group"> <div class="button-group">
<button type="submit">Decrypt and Download</button> <button type="submit">Decrypt and Download</button>
</div> </div>