Compare commits
5 Commits
4be72c9a5a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67a0c83965 | ||
|
|
276fceee68 | ||
|
|
4dc05294ff | ||
|
|
bcc6ea2951 | ||
|
|
aaabe83379 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -168,3 +168,6 @@ cython_debug/
|
|||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
|
|
||||||
|
tmp/
|
||||||
|
tests/resources/*.gpg
|
||||||
|
|||||||
321
app.py
Normal file
321
app.py
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
# gpg_api.py
|
||||||
|
|
||||||
|
from flask import Flask, Request, render_template, request, jsonify, send_file
|
||||||
|
import gnupg
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
import io
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config
|
||||||
|
|
||||||
|
UPLOAD_DIR = os.environ.get("GPG_UPLOAD_DIR", "tmp/gpg_files")
|
||||||
|
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
||||||
|
os.chmod(UPLOAD_DIR, 0o700) # Ensure proper permissions
|
||||||
|
|
||||||
|
GPG_KEYRING_DIR = os.environ.get("GPG_KEYRING_DIR", "tmp/gpg_keyring")
|
||||||
|
os.makedirs(GPG_KEYRING_DIR, exist_ok=True)
|
||||||
|
os.chmod(GPG_KEYRING_DIR, 0o700) # Ensure proper permissions
|
||||||
|
|
||||||
|
# Setup logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Initialize GPG instance
|
||||||
|
gpg_instance = gnupg.GPG(
|
||||||
|
gnupghome=GPG_KEYRING_DIR,
|
||||||
|
)
|
||||||
|
gpg_instance.encoding = "utf-8"
|
||||||
|
|
||||||
|
|
||||||
|
def valid_email(email):
|
||||||
|
"""Validate email format using regex pattern"""
|
||||||
|
pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
||||||
|
return re.match(pattern, email) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_file(filepath):
|
||||||
|
"""Safely cleanup a file with error handling"""
|
||||||
|
try:
|
||||||
|
if os.path.exists(filepath):
|
||||||
|
os.remove(filepath)
|
||||||
|
logger.info(f"Cleaned up file: {filepath}")
|
||||||
|
except OSError as e:
|
||||||
|
logger.warning(f"Failed to clean up {filepath}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_gpg_context(armor=False, passphrase=None):
|
||||||
|
"""Create a GPG context with common settings"""
|
||||||
|
try:
|
||||||
|
# For python-gnupg, we return the global instance
|
||||||
|
# Armor and passphrase are handled per operation
|
||||||
|
return gpg_instance
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to create GPG context: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def find_key_by_email(email, secret=False):
|
||||||
|
"""Find a GPG key by email address"""
|
||||||
|
try:
|
||||||
|
keys = gpg_instance.list_keys(secret=secret)
|
||||||
|
for key in keys:
|
||||||
|
for uid in key["uids"]:
|
||||||
|
if email.lower() in uid.lower():
|
||||||
|
return key
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to find key for {email}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# @app.route("/api/setup/gnupg", methods=["GET"])
|
||||||
|
# def setup_gnupg():
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
"""Render the main application page"""
|
||||||
|
return render_template("index.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/generate-key", methods=["POST"])
|
||||||
|
def generate_key():
|
||||||
|
data = request.json
|
||||||
|
|
||||||
|
# Input validation
|
||||||
|
if not data or not all(key in data for key in ["name", "email", "passphrase"]):
|
||||||
|
return jsonify({"error": "Missing required fields"}), 400
|
||||||
|
|
||||||
|
name = data["name"].strip()
|
||||||
|
email = data["email"].strip()
|
||||||
|
comment = data.get("comment", "").strip()
|
||||||
|
passphrase = data["passphrase"]
|
||||||
|
|
||||||
|
# Validate inputs
|
||||||
|
if not valid_email(email):
|
||||||
|
return jsonify({"error": "Invalid email format"}), 400
|
||||||
|
|
||||||
|
if len(name) < 2 or len(name) > 100:
|
||||||
|
return jsonify({"error": "Name must be between 2 and 100 characters"}), 400
|
||||||
|
|
||||||
|
if len(passphrase) < 8:
|
||||||
|
return jsonify({"error": "Passphrase must be at least 8 characters"}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Generate key using python-gnupg
|
||||||
|
input_data = gpg_instance.gen_key_input(
|
||||||
|
name_real=name,
|
||||||
|
name_email=email,
|
||||||
|
name_comment=comment if comment else None,
|
||||||
|
key_type="RSA",
|
||||||
|
key_length=4096,
|
||||||
|
expire_date=0, # No expiration
|
||||||
|
passphrase=passphrase,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = gpg_instance.gen_key(input_data)
|
||||||
|
|
||||||
|
if result.status == "ok":
|
||||||
|
logger.info(f"GPG key generated successfully for {email}")
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"status": "Key generated successfully",
|
||||||
|
"fingerprint": str(result.fingerprint),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.error(f"Key generation failed for {email}: {result.stderr}")
|
||||||
|
return (
|
||||||
|
jsonify({"error": "Key generation failed", "details": result.stderr}),
|
||||||
|
500,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Key generation failed for {email}: {e}")
|
||||||
|
return jsonify({"error": "Key generation failed", "details": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/download/public-key", methods=["POST"])
|
||||||
|
def export_key():
|
||||||
|
email = request.json.get("email")
|
||||||
|
|
||||||
|
if not email:
|
||||||
|
return jsonify({"error": "Email parameter is required"}), 400
|
||||||
|
|
||||||
|
if not valid_email(email):
|
||||||
|
return jsonify({"error": "Invalid email format"}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Find the key by email
|
||||||
|
key = find_key_by_email(email, secret=False)
|
||||||
|
if not key:
|
||||||
|
return jsonify({"error": "No public key found for this email"}), 404
|
||||||
|
|
||||||
|
# Export the key to a string
|
||||||
|
exported_key = gpg_instance.export_keys(key["fingerprint"], armor=True)
|
||||||
|
|
||||||
|
if not exported_key:
|
||||||
|
return jsonify({"error": "Failed to export key"}), 500
|
||||||
|
|
||||||
|
# Create an in-memory stream
|
||||||
|
file_stream = io.BytesIO(exported_key.encode("utf-8"))
|
||||||
|
filename = f"{secure_filename(email)}_public.asc"
|
||||||
|
|
||||||
|
logger.info(f"Public key exported for {email}")
|
||||||
|
return send_file(
|
||||||
|
file_stream,
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=filename,
|
||||||
|
mimetype="application/pgp-keys",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Export failed for {email}: {e}")
|
||||||
|
return jsonify({"error": "Export failed"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/upload-decrypt", methods=["POST"])
|
||||||
|
def upload_and_decrypt():
|
||||||
|
if "file" not in request.files:
|
||||||
|
return jsonify({"error": "Missing file"}), 400
|
||||||
|
file = request.files["file"]
|
||||||
|
passphrase = request.form.get("passphrase")
|
||||||
|
|
||||||
|
if not file.filename:
|
||||||
|
return jsonify({"error": "No file selected"}), 400
|
||||||
|
|
||||||
|
if not passphrase:
|
||||||
|
return jsonify({"error": "Passphrase is required"}), 400
|
||||||
|
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
if not filename.endswith(".gpg"):
|
||||||
|
return jsonify({"error": "File must have .gpg extension"}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Read the encrypted file directly into memory
|
||||||
|
encrypted_data = file.read()
|
||||||
|
|
||||||
|
# Decrypt the data using python-gnupg
|
||||||
|
result = gpg_instance.decrypt(encrypted_data, passphrase=passphrase)
|
||||||
|
|
||||||
|
if not result.ok:
|
||||||
|
logger.error(f"Decryption failed for {filename}: {result.stderr}")
|
||||||
|
return (
|
||||||
|
jsonify({"error": "Decryption failed", "details": result.stderr}),
|
||||||
|
500,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create an in-memory stream for the decrypted data
|
||||||
|
base_name = filename[:-4] if filename.endswith(".gpg") else filename
|
||||||
|
output_filename = f"decrypted_{base_name}.zip"
|
||||||
|
|
||||||
|
decrypted_stream = io.BytesIO(result.data)
|
||||||
|
|
||||||
|
logger.info(f"File {filename} decrypted successfully")
|
||||||
|
return send_file(
|
||||||
|
decrypted_stream,
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=output_filename,
|
||||||
|
mimetype="application/zip",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Decryption failed for {filename}: {e}")
|
||||||
|
return jsonify({"error": "Decryption failed", "details": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/encrypt-file", methods=["POST"])
|
||||||
|
def encrypt_file():
|
||||||
|
if "file" not in request.files:
|
||||||
|
return jsonify({"error": "Missing file"}), 400
|
||||||
|
|
||||||
|
file = request.files["file"]
|
||||||
|
email = request.form.get("email")
|
||||||
|
|
||||||
|
if not file.filename:
|
||||||
|
return jsonify({"error": "No file selected"}), 400
|
||||||
|
|
||||||
|
if not email:
|
||||||
|
return jsonify({"error": "Email parameter is required"}), 400
|
||||||
|
|
||||||
|
if not valid_email(email):
|
||||||
|
return jsonify({"error": "Invalid email format"}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Find the public key by email
|
||||||
|
key = find_key_by_email(email, secret=False)
|
||||||
|
if not key:
|
||||||
|
return jsonify({"error": "No public key found for this email"}), 404
|
||||||
|
|
||||||
|
# Read the file data
|
||||||
|
file_data = file.read()
|
||||||
|
|
||||||
|
# Encrypt the file using the public key
|
||||||
|
result = gpg_instance.encrypt(
|
||||||
|
file_data,
|
||||||
|
recipients=[key["fingerprint"]],
|
||||||
|
armor=False,
|
||||||
|
always_trust=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result.ok:
|
||||||
|
logger.error(f"Encryption failed for {file.filename}: {result.stderr}")
|
||||||
|
return (
|
||||||
|
jsonify({"error": "Encryption failed", "details": result.stderr}),
|
||||||
|
500,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create filename for encrypted file
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
encrypted_filename = f"{filename}.gpg"
|
||||||
|
|
||||||
|
# Create an in-memory stream for the encrypted data
|
||||||
|
encrypted_stream = io.BytesIO(result.data)
|
||||||
|
|
||||||
|
logger.info(f"File {filename} encrypted successfully for {email}")
|
||||||
|
return send_file(
|
||||||
|
encrypted_stream,
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=encrypted_filename,
|
||||||
|
mimetype="application/pgp-encrypted",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Encryption failed for {file.filename}: {e}")
|
||||||
|
return jsonify({"error": "Encryption failed", "details": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
# @app.route("/api/list-keys", methods=["GET"])
|
||||||
|
# def list_keys():
|
||||||
|
# """List all available GPG keys"""
|
||||||
|
# try:
|
||||||
|
# keys = []
|
||||||
|
# public_keys = gpg_instance.list_keys()
|
||||||
|
# private_keys = gpg_instance.list_keys(secret=True)
|
||||||
|
|
||||||
|
# for key in public_keys:
|
||||||
|
# key_info = {
|
||||||
|
# "fingerprint": key.get("fingerprint", ""),
|
||||||
|
# "keyid": key.get("keyid", ""),
|
||||||
|
# "type": key.get("type", ""),
|
||||||
|
# "length": key.get("length", ""),
|
||||||
|
# "date": key.get("date", ""),
|
||||||
|
# "expires": key.get("expires", ""),
|
||||||
|
# "uids": key.get("uids", []),
|
||||||
|
# "trust": key.get("trust", ""),
|
||||||
|
# "secret": any(
|
||||||
|
# priv_key["fingerprint"] == key.get("fingerprint", "")
|
||||||
|
# for priv_key in private_keys
|
||||||
|
# ),
|
||||||
|
# }
|
||||||
|
# keys.append(key_info)
|
||||||
|
|
||||||
|
# logger.info(f"Listed {len(keys)} keys")
|
||||||
|
# return jsonify({"keys": keys, "count": len(keys)})
|
||||||
|
# except Exception as e:
|
||||||
|
# logger.error(f"Failed to list keys: {e}")
|
||||||
|
# return jsonify({"error": "Failed to list keys", "details": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host="0.0.0.0", port=8080, debug=True)
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
# gpg_api.py
|
|
||||||
|
|
||||||
from flask import Flask, request, jsonify, send_file
|
|
||||||
import subprocess
|
|
||||||
import tempfile
|
|
||||||
import os
|
|
||||||
from werkzeug.utils import secure_filename
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
UPLOAD_DIR = "/tmp/gpg_files"
|
|
||||||
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
|
||||||
|
|
||||||
@app.route("/api/setup/gnupg", methods=["GET"])
|
|
||||||
def setup_gnupg():
|
|
||||||
try:
|
|
||||||
subprocess.run(["gpg", "--version"], check=True)
|
|
||||||
return jsonify({"status": "GnuPG is already installed"})
|
|
||||||
except Exception:
|
|
||||||
subprocess.run(["apt", "update"], check=True)
|
|
||||||
subprocess.run(["apt", "install", "-y", "gnupg"], check=True)
|
|
||||||
return jsonify({"status": "GnuPG installed"})
|
|
||||||
|
|
||||||
@app.route("/api/generate-key", methods=["POST"])
|
|
||||||
def generate_key():
|
|
||||||
data = request.json
|
|
||||||
name = data['name']
|
|
||||||
email = data['email']
|
|
||||||
comment = data.get('comment', '')
|
|
||||||
passphrase = data['passphrase']
|
|
||||||
|
|
||||||
key_input = f"""
|
|
||||||
%echo Generating GPG Key
|
|
||||||
Key-Type: RSA
|
|
||||||
Key-Length: 4096
|
|
||||||
Name-Real: {name}
|
|
||||||
Name-Email: {email}
|
|
||||||
Name-Comment: {comment}
|
|
||||||
Expire-Date: 0
|
|
||||||
Passphrase: {passphrase}
|
|
||||||
%commit
|
|
||||||
%echo Done
|
|
||||||
"""
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f:
|
|
||||||
f.write(key_input)
|
|
||||||
keyfile_path = f.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
subprocess.run(["gpg", "--batch", "--generate-key", keyfile_path], check=True)
|
|
||||||
return jsonify({"status": "Key generated successfully"})
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
return jsonify({"error": "Key generation failed", "details": str(e)}), 500
|
|
||||||
finally:
|
|
||||||
os.remove(keyfile_path)
|
|
||||||
|
|
||||||
@app.route("/api/download/public-key", methods=["GET"])
|
|
||||||
def export_key():
|
|
||||||
email = request.args.get('email')
|
|
||||||
filename = os.path.join(UPLOAD_DIR, f"{secure_filename(email)}_public.asc")
|
|
||||||
try:
|
|
||||||
subprocess.run(["gpg", "--armor", "--output", filename, "--export", email], check=True)
|
|
||||||
return send_file(filename, as_attachment=True)
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": "Export failed", "details": str(e)}), 500
|
|
||||||
|
|
||||||
@app.route("/api/upload-decrypt", methods=["POST"])
|
|
||||||
def upload_and_decrypt():
|
|
||||||
if 'file' not in request.files:
|
|
||||||
return jsonify({"error": "Missing file"}), 400
|
|
||||||
file = request.files['file']
|
|
||||||
passphrase = request.form.get('passphrase')
|
|
||||||
|
|
||||||
filename = secure_filename(file.filename)
|
|
||||||
gpg_path = os.path.join(UPLOAD_DIR, filename)
|
|
||||||
output_path = os.path.join(UPLOAD_DIR, f"decrypted_{filename[:-4]}.zip")
|
|
||||||
file.save(gpg_path)
|
|
||||||
|
|
||||||
try:
|
|
||||||
subprocess.run([
|
|
||||||
"gpg", "--batch", "--yes",
|
|
||||||
"--passphrase", passphrase,
|
|
||||||
"--output", output_path,
|
|
||||||
"--decrypt", gpg_path
|
|
||||||
], check=True)
|
|
||||||
return send_file(output_path, as_attachment=True)
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
return jsonify({"error": "Decryption failed", "details": str(e)}), 500
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
app.run(host='0.0.0.0', port=8080, debug=True)
|
|
||||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Flask==3.1.1
|
||||||
|
Werkzeug==3.1.3
|
||||||
|
python-gnupg==0.4.5
|
||||||
700
static/script.js
Normal file
700
static/script.js
Normal file
@@ -0,0 +1,700 @@
|
|||||||
|
// Application State
|
||||||
|
let currentTab = "generate"
|
||||||
|
let selectedFiles = []
|
||||||
|
let isProcessing = false
|
||||||
|
|
||||||
|
// DOM Elements
|
||||||
|
const elements = {
|
||||||
|
// Tabs
|
||||||
|
tabGenerate: document.getElementById("tab-generate"),
|
||||||
|
tabDecrypt: document.getElementById("tab-decrypt"),
|
||||||
|
generateSection: document.getElementById("generate-section"),
|
||||||
|
decryptSection: document.getElementById("decrypt-section"),
|
||||||
|
|
||||||
|
// Key Generation Form
|
||||||
|
keyGenerationForm: document.getElementById("key-generation-form"),
|
||||||
|
nameInput: document.getElementById("name"),
|
||||||
|
emailInput: document.getElementById("email"),
|
||||||
|
passphraseInput: document.getElementById("passphrase"),
|
||||||
|
confirmPassphraseInput: document.getElementById("confirm-passphrase"),
|
||||||
|
commentInput: document.getElementById("comment"),
|
||||||
|
generateBtn: document.getElementById("generate-btn"),
|
||||||
|
generationStatus: document.getElementById("generation-status"),
|
||||||
|
loadingStatus: document.getElementById("loading-status"),
|
||||||
|
successStatus: document.getElementById("success-status"),
|
||||||
|
errorStatus: document.getElementById("error-status"),
|
||||||
|
errorMessage: document.getElementById("error-message"),
|
||||||
|
|
||||||
|
// File Decryption
|
||||||
|
dropZone: document.getElementById("drop-zone"),
|
||||||
|
fileInput: document.getElementById("file-input"),
|
||||||
|
selectFilesBtn: document.getElementById("select-files-btn"),
|
||||||
|
fileList: document.getElementById("file-list"),
|
||||||
|
filesContainer: document.getElementById("files-container"),
|
||||||
|
decryptionForm: document.getElementById("decryption-form"),
|
||||||
|
privateKeyInput: document.getElementById("private-key"),
|
||||||
|
keyPassphraseInput: document.getElementById("key-passphrase"),
|
||||||
|
decryptBtn: document.getElementById("decrypt-btn"),
|
||||||
|
decryptionStatus: document.getElementById("decryption-status"),
|
||||||
|
progressBar: document.getElementById("progress-bar"),
|
||||||
|
decryptionMessage: document.getElementById("decryption-message"),
|
||||||
|
decryptionResults: document.getElementById("decryption-results"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Application
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
initializeEventListeners()
|
||||||
|
setupFormValidation()
|
||||||
|
setupFileHandling()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Event Listeners
|
||||||
|
function initializeEventListeners() {
|
||||||
|
// Tab Navigation
|
||||||
|
elements.tabGenerate.addEventListener("click", () => switchTab("generate"))
|
||||||
|
elements.tabDecrypt.addEventListener("click", () => switchTab("decrypt"))
|
||||||
|
|
||||||
|
// Key Generation Form
|
||||||
|
elements.keyGenerationForm.addEventListener("submit", handleKeyGeneration)
|
||||||
|
|
||||||
|
// File Upload
|
||||||
|
elements.selectFilesBtn.addEventListener("click", () =>
|
||||||
|
elements.fileInput.click()
|
||||||
|
)
|
||||||
|
elements.fileInput.addEventListener("change", handleFileSelection)
|
||||||
|
|
||||||
|
// Drag and Drop
|
||||||
|
elements.dropZone.addEventListener("dragover", handleDragOver)
|
||||||
|
elements.dropZone.addEventListener("dragleave", handleDragLeave)
|
||||||
|
elements.dropZone.addEventListener("drop", handleFileDrop)
|
||||||
|
|
||||||
|
// Decryption Form
|
||||||
|
elements.decryptionForm.addEventListener("submit", handleFileDecryption)
|
||||||
|
|
||||||
|
// Keyboard accessibility for drop zone
|
||||||
|
elements.dropZone.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault()
|
||||||
|
elements.fileInput.click()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab Management
|
||||||
|
function switchTab(tabName) {
|
||||||
|
currentTab = tabName
|
||||||
|
|
||||||
|
// Update tab buttons
|
||||||
|
document.querySelectorAll(".tab-button").forEach((btn) => {
|
||||||
|
btn.classList.remove("active")
|
||||||
|
btn.classList.add("text-white/70")
|
||||||
|
})
|
||||||
|
|
||||||
|
const activeTab = document.getElementById(`tab-${tabName}`)
|
||||||
|
activeTab.classList.add("active")
|
||||||
|
activeTab.classList.remove("text-white/70")
|
||||||
|
activeTab.classList.add("text-white")
|
||||||
|
|
||||||
|
// Update tab content
|
||||||
|
elements.generateSection.classList.toggle("hidden", tabName !== "generate")
|
||||||
|
elements.decryptSection.classList.toggle("hidden", tabName !== "decrypt")
|
||||||
|
|
||||||
|
// Reset states when switching tabs
|
||||||
|
resetFormStates()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form Validation
|
||||||
|
function setupFormValidation() {
|
||||||
|
// Real-time validation
|
||||||
|
elements.nameInput.addEventListener("input", () => validateField("name"))
|
||||||
|
elements.emailInput.addEventListener("input", () => validateField("email"))
|
||||||
|
elements.passphraseInput.addEventListener("input", () =>
|
||||||
|
validateField("passphrase")
|
||||||
|
)
|
||||||
|
elements.confirmPassphraseInput.addEventListener("input", () =>
|
||||||
|
validateField("confirm-passphrase")
|
||||||
|
)
|
||||||
|
elements.privateKeyInput.addEventListener("input", () =>
|
||||||
|
validateField("private-key")
|
||||||
|
)
|
||||||
|
elements.keyPassphraseInput.addEventListener("input", () =>
|
||||||
|
validateField("key-passphrase")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateField(fieldName) {
|
||||||
|
const field = document.getElementById(fieldName)
|
||||||
|
const errorElement = document.getElementById(`${fieldName}-error`)
|
||||||
|
let isValid = true
|
||||||
|
let errorMessage = ""
|
||||||
|
|
||||||
|
switch (fieldName) {
|
||||||
|
case "name":
|
||||||
|
if (!field.value.trim()) {
|
||||||
|
isValid = false
|
||||||
|
errorMessage = "Full name is required"
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case "email":
|
||||||
|
if (!field.value.trim()) {
|
||||||
|
isValid = false
|
||||||
|
errorMessage = "Email address is required"
|
||||||
|
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(field.value)) {
|
||||||
|
isValid = false
|
||||||
|
errorMessage = "Please enter a valid email address"
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case "passphrase":
|
||||||
|
if (!field.value) {
|
||||||
|
isValid = false
|
||||||
|
errorMessage = "Passphrase is required"
|
||||||
|
} else if (field.value.length < 8) {
|
||||||
|
isValid = false
|
||||||
|
errorMessage = "Passphrase must be at least 8 characters long"
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case "confirm-passphrase":
|
||||||
|
if (field.value !== elements.passphraseInput.value) {
|
||||||
|
isValid = false
|
||||||
|
errorMessage = "Passphrases do not match"
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case "private-key":
|
||||||
|
if (!field.value.trim()) {
|
||||||
|
isValid = false
|
||||||
|
errorMessage = "Private key is required"
|
||||||
|
} else if (
|
||||||
|
!field.value.includes("-----BEGIN PGP PRIVATE KEY BLOCK-----")
|
||||||
|
) {
|
||||||
|
isValid = false
|
||||||
|
errorMessage = "Please enter a valid PGP private key"
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case "key-passphrase":
|
||||||
|
if (!field.value) {
|
||||||
|
isValid = false
|
||||||
|
errorMessage = "Key passphrase is required"
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update field styling and error message
|
||||||
|
if (isValid) {
|
||||||
|
field.classList.remove("border-red-500")
|
||||||
|
field.classList.add("border-gray-300")
|
||||||
|
errorElement.classList.add("hidden")
|
||||||
|
errorElement.textContent = ""
|
||||||
|
} else {
|
||||||
|
field.classList.add("border-red-500")
|
||||||
|
field.classList.remove("border-gray-300")
|
||||||
|
errorElement.classList.remove("hidden")
|
||||||
|
errorElement.textContent = errorMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateForm(formType) {
|
||||||
|
let isValid = true
|
||||||
|
|
||||||
|
if (formType === "generation") {
|
||||||
|
isValid = validateField("name") && isValid
|
||||||
|
isValid = validateField("email") && isValid
|
||||||
|
isValid = validateField("passphrase") && isValid
|
||||||
|
isValid = validateField("confirm-passphrase") && isValid
|
||||||
|
} else if (formType === "decryption") {
|
||||||
|
isValid = validateField("private-key") && isValid
|
||||||
|
isValid = validateField("key-passphrase") && isValid
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key Generation
|
||||||
|
async function handleKeyGeneration(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (isProcessing || !validateForm("generation")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isProcessing = true
|
||||||
|
showGenerationStatus("loading")
|
||||||
|
|
||||||
|
const formData = {
|
||||||
|
name: elements.nameInput.value.trim(),
|
||||||
|
email: elements.emailInput.value.trim(),
|
||||||
|
passphrase: elements.passphraseInput.value,
|
||||||
|
comment: elements.commentInput.value.trim() || undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Simulate API call (replace with actual API endpoint)
|
||||||
|
const responseKeyGeneration = await keyGenerationAPI(formData)
|
||||||
|
|
||||||
|
if (!responseKeyGeneration.success) {
|
||||||
|
console.error("Key generation failed:", responseKeyGeneration.error)
|
||||||
|
throw new Error("Key generation failed")
|
||||||
|
}
|
||||||
|
// Auto-download public key
|
||||||
|
const responsePublicKey = await publicKeyDownloadAPI(formData.email)
|
||||||
|
if (!responsePublicKey.ok) {
|
||||||
|
const errorData = await response.json()
|
||||||
|
console.error("Failed to download public key:", errorData)
|
||||||
|
throw new Error("Failed to download public key")
|
||||||
|
}
|
||||||
|
downloadFile(
|
||||||
|
await responsePublicKey.blob(),
|
||||||
|
responsePublicKey.filename,
|
||||||
|
responsePublicKey.mimetype
|
||||||
|
)
|
||||||
|
|
||||||
|
showGenerationStatus("success")
|
||||||
|
|
||||||
|
// Reset form after successful generation
|
||||||
|
setTimeout(() => {
|
||||||
|
elements.keyGenerationForm.reset()
|
||||||
|
showGenerationStatus("hidden")
|
||||||
|
}, 3000)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Key generation error:", error)
|
||||||
|
showGenerationStatus("error", error.message)
|
||||||
|
} finally {
|
||||||
|
isProcessing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showGenerationStatus(type, message = "") {
|
||||||
|
elements.generationStatus.classList.remove("hidden")
|
||||||
|
elements.loadingStatus.classList.add("hidden")
|
||||||
|
elements.successStatus.classList.add("hidden")
|
||||||
|
elements.errorStatus.classList.add("hidden")
|
||||||
|
|
||||||
|
if (type === "loading") {
|
||||||
|
elements.loadingStatus.classList.remove("hidden")
|
||||||
|
elements.generateBtn.disabled = true
|
||||||
|
elements.generateBtn.innerHTML =
|
||||||
|
'<i class="fas fa-spinner loading-spinner mr-2"></i>Generating...'
|
||||||
|
} else if (type === "success") {
|
||||||
|
elements.successStatus.classList.remove("hidden")
|
||||||
|
elements.generateBtn.disabled = false
|
||||||
|
elements.generateBtn.innerHTML =
|
||||||
|
'<i class="fas fa-key mr-2"></i>Generate Key Pair'
|
||||||
|
} else if (type === "error") {
|
||||||
|
elements.errorStatus.classList.remove("hidden")
|
||||||
|
elements.errorMessage.textContent = message
|
||||||
|
elements.generateBtn.disabled = false
|
||||||
|
elements.generateBtn.innerHTML =
|
||||||
|
'<i class="fas fa-key mr-2"></i>Generate Key Pair'
|
||||||
|
} else {
|
||||||
|
elements.generationStatus.classList.add("hidden")
|
||||||
|
elements.generateBtn.disabled = false
|
||||||
|
elements.generateBtn.innerHTML =
|
||||||
|
'<i class="fas fa-key mr-2"></i>Generate Key Pair'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// File Handling
|
||||||
|
function setupFileHandling() {
|
||||||
|
// Prevent default drag behaviors
|
||||||
|
;["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
|
||||||
|
document.addEventListener(eventName, preventDefaults, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
function preventDefaults(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragOver(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
elements.dropZone.classList.add("drag-over")
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragLeave(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
elements.dropZone.classList.remove("drag-over")
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileDrop(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
elements.dropZone.classList.remove("drag-over")
|
||||||
|
|
||||||
|
const files = Array.from(e.dataTransfer.files)
|
||||||
|
processFiles(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileSelection(e) {
|
||||||
|
const files = Array.from(e.target.files)
|
||||||
|
processFiles(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
function processFiles(files) {
|
||||||
|
const validFiles = []
|
||||||
|
const allowedTypes = [".asc", ".gpg", ".pgp", ".txt"]
|
||||||
|
const maxSize = 10 * 1024 * 1024 // 10MB
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
const extension = "." + file.name.split(".").pop().toLowerCase()
|
||||||
|
|
||||||
|
if (!allowedTypes.includes(extension)) {
|
||||||
|
showNotification(
|
||||||
|
`File "${file.name}" has an unsupported format.`,
|
||||||
|
"error"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
showNotification(`File "${file.name}" is too large (max 10MB).`, "error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
validFiles.push(file)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (validFiles.length > 0) {
|
||||||
|
selectedFiles = validFiles
|
||||||
|
displaySelectedFiles()
|
||||||
|
elements.decryptionForm.classList.remove("hidden")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function displaySelectedFiles() {
|
||||||
|
elements.fileList.classList.remove("hidden")
|
||||||
|
elements.filesContainer.innerHTML = ""
|
||||||
|
|
||||||
|
selectedFiles.forEach((file, index) => {
|
||||||
|
const fileElement = document.createElement("div")
|
||||||
|
fileElement.className =
|
||||||
|
"flex items-center justify-between p-3 bg-gray-50 rounded-lg"
|
||||||
|
fileElement.innerHTML = `
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<i class="fas fa-file-alt text-gray-500"></i>
|
||||||
|
<div>
|
||||||
|
<div class="font-medium text-gray-900">${file.name}</div>
|
||||||
|
<div class="text-sm text-gray-500">${formatFileSize(
|
||||||
|
file.size
|
||||||
|
)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-red-500 hover:text-red-700 transition-colors"
|
||||||
|
onclick="removeFile(${index})"
|
||||||
|
aria-label="Remove file"
|
||||||
|
>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
elements.filesContainer.appendChild(fileElement)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFile(index) {
|
||||||
|
selectedFiles.splice(index, 1)
|
||||||
|
|
||||||
|
if (selectedFiles.length === 0) {
|
||||||
|
elements.fileList.classList.add("hidden")
|
||||||
|
elements.decryptionForm.classList.add("hidden")
|
||||||
|
} else {
|
||||||
|
displaySelectedFiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function 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]
|
||||||
|
}
|
||||||
|
|
||||||
|
// File Decryption
|
||||||
|
async function handleFileDecryption(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (
|
||||||
|
isProcessing ||
|
||||||
|
!validateForm("decryption") ||
|
||||||
|
selectedFiles.length === 0
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isProcessing = true
|
||||||
|
showDecryptionStatus(true)
|
||||||
|
|
||||||
|
const privateKey = elements.privateKeyInput.value.trim()
|
||||||
|
const passphrase = elements.keyPassphraseInput.value
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = []
|
||||||
|
|
||||||
|
for (let i = 0; i < selectedFiles.length; i++) {
|
||||||
|
const file = selectedFiles[i]
|
||||||
|
const progress = ((i + 1) / selectedFiles.length) * 100
|
||||||
|
|
||||||
|
updateProgress(progress, `Decrypting ${file.name}...`)
|
||||||
|
|
||||||
|
// Read file content
|
||||||
|
const fileContent = await readFileAsText(file)
|
||||||
|
|
||||||
|
// Simulate API call for decryption
|
||||||
|
const result = await simulateDecryptionAPI({
|
||||||
|
encryptedContent: fileContent,
|
||||||
|
privateKey: privateKey,
|
||||||
|
passphrase: passphrase,
|
||||||
|
filename: file.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
results.push({
|
||||||
|
filename: file.name,
|
||||||
|
decryptedContent: result.decryptedContent,
|
||||||
|
success: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Auto-download decrypted file
|
||||||
|
const decryptedFilename = file.name.replace(
|
||||||
|
/\.(asc|gpg|pgp)$/,
|
||||||
|
"_decrypted.txt"
|
||||||
|
)
|
||||||
|
downloadFile(result.decryptedContent, decryptedFilename, "text/plain")
|
||||||
|
} else {
|
||||||
|
results.push({
|
||||||
|
filename: file.name,
|
||||||
|
error: result.error,
|
||||||
|
success: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
displayDecryptionResults(results)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Decryption error:", error)
|
||||||
|
showNotification("Decryption failed: " + error.message, "error")
|
||||||
|
} finally {
|
||||||
|
isProcessing = false
|
||||||
|
setTimeout(() => {
|
||||||
|
showDecryptionStatus(false)
|
||||||
|
elements.decryptionForm.reset()
|
||||||
|
selectedFiles = []
|
||||||
|
elements.fileList.classList.add("hidden")
|
||||||
|
elements.decryptionForm.classList.add("hidden")
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDecryptionStatus(show) {
|
||||||
|
elements.decryptionStatus.classList.toggle("hidden", !show)
|
||||||
|
elements.decryptBtn.disabled = show
|
||||||
|
|
||||||
|
if (show) {
|
||||||
|
elements.decryptBtn.innerHTML =
|
||||||
|
'<i class="fas fa-spinner loading-spinner mr-2"></i>Decrypting...'
|
||||||
|
updateProgress(0, "Preparing decryption...")
|
||||||
|
} else {
|
||||||
|
elements.decryptBtn.innerHTML =
|
||||||
|
'<i class="fas fa-unlock mr-2"></i>Decrypt Files'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgress(percentage, message) {
|
||||||
|
elements.progressBar.style.width = percentage + "%"
|
||||||
|
elements.decryptionMessage.textContent = message
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayDecryptionResults(results) {
|
||||||
|
elements.decryptionResults.innerHTML = ""
|
||||||
|
|
||||||
|
results.forEach((result) => {
|
||||||
|
const resultElement = document.createElement("div")
|
||||||
|
resultElement.className = `p-3 rounded-lg ${
|
||||||
|
result.success
|
||||||
|
? "bg-green-50 border border-green-200"
|
||||||
|
: "bg-red-50 border border-red-200"
|
||||||
|
}`
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
resultElement.innerHTML = `
|
||||||
|
<div class="flex items-center gap-2 text-green-700">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
<span class="font-medium">${result.filename}</span>
|
||||||
|
<span class="text-sm">- Decrypted successfully</span>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
} else {
|
||||||
|
resultElement.innerHTML = `
|
||||||
|
<div class="flex items-center gap-2 text-red-700">
|
||||||
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
|
<span class="font-medium">${result.filename}</span>
|
||||||
|
<span class="text-sm">- ${result.error}</span>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.decryptionResults.appendChild(resultElement)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility Functions
|
||||||
|
async function downloadFile(content, filename, mimeType) {
|
||||||
|
// Create download link
|
||||||
|
const url = URL.createObjectURL(content)
|
||||||
|
const a = document.createElement("a")
|
||||||
|
a.href = url
|
||||||
|
a.download = filename
|
||||||
|
|
||||||
|
// Trigger download
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFileAsText(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => resolve(e.target.result)
|
||||||
|
reader.onerror = () => reject(new Error("Failed to read file"))
|
||||||
|
reader.readAsText(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNotification(message, type = "info") {
|
||||||
|
// Create notification element
|
||||||
|
const notification = document.createElement("div")
|
||||||
|
notification.className = `fixed top-4 right-4 p-4 rounded-lg shadow-lg z-50 ${
|
||||||
|
type === "error"
|
||||||
|
? "bg-red-500 text-white"
|
||||||
|
: type === "success"
|
||||||
|
? "bg-green-500 text-white"
|
||||||
|
: "bg-blue-500 text-white"
|
||||||
|
}`
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<i class="fas ${
|
||||||
|
type === "error"
|
||||||
|
? "fa-exclamation-circle"
|
||||||
|
: type === "success"
|
||||||
|
? "fa-check-circle"
|
||||||
|
: "fa-info-circle"
|
||||||
|
}"></i>
|
||||||
|
<span>${message}</span>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
document.body.appendChild(notification)
|
||||||
|
|
||||||
|
// Remove notification after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.remove()
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFormStates() {
|
||||||
|
// Reset generation form
|
||||||
|
elements.keyGenerationForm.reset()
|
||||||
|
showGenerationStatus("hidden")
|
||||||
|
|
||||||
|
// Reset decryption form
|
||||||
|
elements.decryptionForm.reset()
|
||||||
|
selectedFiles = []
|
||||||
|
elements.fileList.classList.add("hidden")
|
||||||
|
elements.decryptionForm.classList.add("hidden")
|
||||||
|
showDecryptionStatus(false)
|
||||||
|
|
||||||
|
// Clear validation errors
|
||||||
|
document.querySelectorAll(".text-red-500").forEach((error) => {
|
||||||
|
error.classList.add("hidden")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reset field styling
|
||||||
|
document.querySelectorAll(".border-red-500").forEach((field) => {
|
||||||
|
field.classList.remove("border-red-500")
|
||||||
|
field.classList.add("border-gray-300")
|
||||||
|
})
|
||||||
|
|
||||||
|
isProcessing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Simulation Functions (Replace with actual API calls)
|
||||||
|
async function keyGenerationAPI(data) {
|
||||||
|
const response = await fetch("/api/generate-key", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
|
||||||
|
return await response.json().then((result) => {
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
success: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function publicKeyDownloadAPI(email) {
|
||||||
|
const response = await fetch("/api/download/public-key", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: email }),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to download public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: response.ok,
|
||||||
|
blob: () => response.blob(),
|
||||||
|
mimetype: response.headers.get("content-type"),
|
||||||
|
filename:
|
||||||
|
response.headers
|
||||||
|
.get("content-disposition")
|
||||||
|
?.split("filename=")[1]
|
||||||
|
?.replace(/"/g, "") || "public.asc",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function simulateDecryptionAPI(data) {
|
||||||
|
// Simulate API delay
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
|
// Simulate API response
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
decryptedContent: `Decrypted content from ${data.filename}
|
||||||
|
|
||||||
|
This is a simulated decryption result.
|
||||||
|
Original file: ${data.filename}
|
||||||
|
Decrypted at: ${new Date().toISOString()}
|
||||||
|
|
||||||
|
Your encrypted content would appear here in a real implementation.`,
|
||||||
|
message: "File decrypted successfully",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncomment for actual API call:
|
||||||
|
/*
|
||||||
|
const response = await fetch('/api/decrypt-file', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
*/
|
||||||
|
}
|
||||||
275
templates/index.html
Normal file
275
templates/index.html
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>SecureKey Manager - OpenPGP Key Management</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
animation: {
|
||||||
|
'fade-in': 'fadeIn 0.6s ease-out',
|
||||||
|
'slide-up': 'slideUp 0.5s ease-out',
|
||||||
|
'pulse-success': 'pulse 1s ease-in-out infinite',
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
fadeIn: {
|
||||||
|
'0%': { opacity: '0', transform: 'translateY(20px)' },
|
||||||
|
'100%': { opacity: '1', transform: 'translateY(0)' }
|
||||||
|
},
|
||||||
|
slideUp: {
|
||||||
|
'0%': { opacity: '0', transform: 'translateY(30px)' },
|
||||||
|
'100%': { opacity: '1', transform: 'translateY(0)' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
/* @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.gradient-bg {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-morphism {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-zone.drag-over {
|
||||||
|
background: rgba(99, 102, 241, 0.1);
|
||||||
|
border-color: #6366f1;
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button.active {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="min-h-screen gradient-bg">
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="text-center mb-8 animate-fade-in">
|
||||||
|
<div class="flex items-center justify-center gap-3 mb-4">
|
||||||
|
<i class="fas fa-shield-alt text-3xl text-white"></i>
|
||||||
|
<h1 class="text-4xl font-bold text-white">SecureKey Manager</h1>
|
||||||
|
</div>
|
||||||
|
<p class="text-white/80 max-w-2xl mx-auto text-lg">
|
||||||
|
Generate OpenPGP key pairs and decrypt files securely.
|
||||||
|
All cryptographic operations are performed via secure API endpoints.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Navigation Tabs -->
|
||||||
|
<nav class="flex justify-center mb-8 animate-slide-up">
|
||||||
|
<div class="glass-morphism rounded-full p-1 shadow-lg">
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<button id="tab-generate"
|
||||||
|
class="tab-button px-6 py-3 rounded-full font-medium transition-all duration-300 flex items-center gap-2 text-white active"
|
||||||
|
data-tab="generate" aria-label="Generate Keys Tab">
|
||||||
|
<i class="fas fa-key"></i>
|
||||||
|
Generate Keys
|
||||||
|
</button>
|
||||||
|
<button id="tab-decrypt"
|
||||||
|
class="tab-button px-6 py-3 rounded-full font-medium transition-all duration-300 flex items-center gap-2 text-white/70 hover:text-white"
|
||||||
|
data-tab="decrypt" aria-label="Decrypt Files Tab">
|
||||||
|
<i class="fas fa-file-text"></i>
|
||||||
|
Decrypt Files
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Content Container -->
|
||||||
|
<main class="max-w-4xl mx-auto">
|
||||||
|
<!-- Key Generation Tab -->
|
||||||
|
<section id="generate-section" class="tab-content">
|
||||||
|
<div
|
||||||
|
class="bg-white/95 backdrop-blur-sm rounded-2xl shadow-xl p-8 border border-white/20 hover:shadow-2xl hover:-translate-y-1 transition-all duration-300 animate-fade-in">
|
||||||
|
<div class="text-center mb-6">
|
||||||
|
<i class="fas fa-key text-3xl text-indigo-600 mb-3"></i>
|
||||||
|
<h2 class="text-2xl font-bold text-gray-800 mb-2">Generate OpenPGP Key Pair</h2>
|
||||||
|
<p class="text-gray-600">Create a new key pair for secure communication</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="key-generation-form" class="space-y-6">
|
||||||
|
<!-- Full Name -->
|
||||||
|
<div>
|
||||||
|
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
<i class="fas fa-user mr-2"></i>Full Name *
|
||||||
|
</label>
|
||||||
|
<input type="text" id="name" name="name" required
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
|
||||||
|
placeholder="Enter your full name" aria-describedby="name-error" autofocus value="juliocesar">
|
||||||
|
<div id="name-error" class="text-red-500 text-sm mt-1 hidden" role="alert"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email Address -->
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
<i class="fas fa-envelope mr-2"></i>Email Address *
|
||||||
|
</label>
|
||||||
|
<input type="email" id="email" name="email" required
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
|
||||||
|
placeholder="Enter your email address" aria-describedby="email-error" value="juliocesar@example.com">
|
||||||
|
<div id="email-error" class="text-red-500 text-sm mt-1 hidden" role="alert"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Passphrase -->
|
||||||
|
<div>
|
||||||
|
<label for="passphrase" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
<i class="fas fa-lock mr-2"></i>Passphrase *
|
||||||
|
</label>
|
||||||
|
<input type="password" id="passphrase" name="passphrase" required minlength="8"
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
|
||||||
|
placeholder="Enter a strong passphrase (min 8 characters)" aria-describedby="passphrase-error"
|
||||||
|
value="strongpassphrase">
|
||||||
|
<div id="passphrase-error" class="text-red-500 text-sm mt-1 hidden" role="alert"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirm Passphrase -->
|
||||||
|
<div>
|
||||||
|
<label for="confirm-passphrase" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
<i class="fas fa-lock mr-2"></i>Confirm Passphrase *
|
||||||
|
</label>
|
||||||
|
<input type="password" id="confirm-passphrase" name="confirm-passphrase" required
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
|
||||||
|
placeholder="Confirm your passphrase" aria-describedby="confirm-passphrase-error"
|
||||||
|
value="strongpassphrase">
|
||||||
|
<div id="confirm-passphrase-error" class="text-red-500 text-sm mt-1 hidden" role="alert"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Optional Comment -->
|
||||||
|
<div>
|
||||||
|
<label for="comment" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
<i class="fas fa-comment mr-2"></i>Comment (Optional)
|
||||||
|
</label>
|
||||||
|
<textarea id="comment" name="comment" rows="3"
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors resize-none"
|
||||||
|
placeholder="Add an optional comment to your key"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Generate Button -->
|
||||||
|
<div class="text-center">
|
||||||
|
<button type="submit" id="generate-btn"
|
||||||
|
class="bg-indigo-600 text-white px-8 py-3 rounded-lg font-medium hover:bg-indigo-700 focus:ring-4 focus:ring-indigo-200 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
aria-describedby="generate-status">
|
||||||
|
<i class="fas fa-key mr-2"></i>Generate Key Pair
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Generation Status -->
|
||||||
|
<div id="generation-status" class="mt-6 text-center hidden">
|
||||||
|
<div id="loading-status" class="flex items-center justify-center gap-3 text-indigo-600">
|
||||||
|
<i class="fas fa-spinner animate-spin"></i>
|
||||||
|
<span>Generating your key pair...</span>
|
||||||
|
</div>
|
||||||
|
<div id="success-status" class="text-green-600 hidden animate-pulse-success">
|
||||||
|
<i class="fas fa-check-circle mr-2"></i>
|
||||||
|
<span>Key pair generated successfully! Public key downloaded.</span>
|
||||||
|
</div>
|
||||||
|
<div id="error-status" class="text-red-600 hidden">
|
||||||
|
<i class="fas fa-exclamation-circle mr-2"></i>
|
||||||
|
<span id="error-message"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- File Decryption Tab -->
|
||||||
|
<section id="decrypt-section" class="tab-content hidden">
|
||||||
|
<div
|
||||||
|
class="bg-white/95 backdrop-blur-sm rounded-2xl shadow-xl p-8 border border-white/20 hover:shadow-2xl hover:-translate-y-1 transition-all duration-300 animate-fade-in">
|
||||||
|
<div class="text-center mb-6">
|
||||||
|
<i class="fas fa-file-shield text-3xl text-indigo-600 mb-3"></i>
|
||||||
|
<h2 class="text-2xl font-bold text-gray-800 mb-2">Decrypt Files</h2>
|
||||||
|
<p class="text-gray-600">Upload encrypted files to decrypt them securely</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- File Upload Zone -->
|
||||||
|
<div id="drop-zone"
|
||||||
|
class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center cursor-pointer hover:border-indigo-400 hover:bg-indigo-50/50 transition-all duration-300"
|
||||||
|
role="button" tabindex="0" aria-label="File upload area">
|
||||||
|
<i class="fas fa-cloud-upload-alt text-4xl text-gray-400 mb-4"></i>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-700 mb-2">Drag & Drop Files Here</h3>
|
||||||
|
<p class="text-gray-500 mb-4">or click to select files</p>
|
||||||
|
<button type="button" id="select-files-btn"
|
||||||
|
class="bg-indigo-600 text-white px-6 py-2 rounded-lg font-medium hover:bg-indigo-700 transition-colors">
|
||||||
|
Select Files
|
||||||
|
</button>
|
||||||
|
<input type="file" id="file-input" multiple accept=".asc,.gpg,.pgp,.txt" class="hidden"
|
||||||
|
aria-describedby="file-info">
|
||||||
|
<div id="file-info" class="text-sm text-gray-500 mt-4">
|
||||||
|
Supported formats: .asc, .gpg, .pgp, .txt | Max size: 10MB per file
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- File List -->
|
||||||
|
<div id="file-list" class="mt-6 space-y-3 hidden">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-700">Files to Decrypt</h3>
|
||||||
|
<div id="files-container" class="space-y-2"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Decryption Form -->
|
||||||
|
<form id="decryption-form" class="mt-6 space-y-4 hidden">
|
||||||
|
<div>
|
||||||
|
<label for="private-key" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
<i class="fas fa-key mr-2"></i>Private Key *
|
||||||
|
</label>
|
||||||
|
<textarea id="private-key" name="private-key" rows="6" required
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors resize-none font-mono text-sm"
|
||||||
|
placeholder="Paste your private key here..." aria-describedby="private-key-error"></textarea>
|
||||||
|
<div id="private-key-error" class="text-red-500 text-sm mt-1 hidden" role="alert"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="key-passphrase" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
<i class="fas fa-lock mr-2"></i>Key Passphrase *
|
||||||
|
</label>
|
||||||
|
<input type="password" id="key-passphrase" name="key-passphrase" required
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
|
||||||
|
placeholder="Enter your private key passphrase" aria-describedby="key-passphrase-error">
|
||||||
|
<div id="key-passphrase-error" class="text-red-500 text-sm mt-1 hidden" role="alert"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<button type="submit" id="decrypt-btn"
|
||||||
|
class="bg-indigo-600 text-white px-8 py-3 rounded-lg font-medium hover:bg-indigo-700 focus:ring-4 focus:ring-indigo-200 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
|
<i class="fas fa-unlock mr-2"></i>Decrypt Files
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Decryption Status -->
|
||||||
|
<div id="decryption-status" class="mt-6 hidden">
|
||||||
|
<div id="decryption-progress" class="bg-gray-200 rounded-full h-2 mb-4">
|
||||||
|
<div id="progress-bar" class="bg-indigo-600 h-2 rounded-full transition-all duration-300"
|
||||||
|
style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<div id="decryption-message" class="text-center text-gray-600"></div>
|
||||||
|
<div id="decryption-results" class="mt-4 space-y-2"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
1
tests/resources/testing_file.txt
Normal file
1
tests/resources/testing_file.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Test text
|
||||||
144
tests/test_api.py
Normal file
144
tests/test_api.py
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for the GPG API using python-gpg library
|
||||||
|
"""
|
||||||
|
|
||||||
|
from app import UPLOAD_DIR, GPG_KEYRING_DIR, app
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import gnupg
|
||||||
|
|
||||||
|
|
||||||
|
class TestGPGAPI(unittest.TestCase):
|
||||||
|
"""Test class for GPG API endpoints"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
"""Set up test fixtures before each test method."""
|
||||||
|
cls.test_name = "Test User"
|
||||||
|
cls.test_email = "test@example.com"
|
||||||
|
cls.test_passphrase = "testpassphrase123"
|
||||||
|
cls.app = app.test_client() # Create a test client for the Flask app
|
||||||
|
cls.app.testing = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
"""Clean up after all tests in this class."""
|
||||||
|
print("\nTearing down test class...")
|
||||||
|
try:
|
||||||
|
if os.path.exists(UPLOAD_DIR):
|
||||||
|
shutil.rmtree(UPLOAD_DIR) # Remove directory and all contents
|
||||||
|
if os.path.exists(GPG_KEYRING_DIR):
|
||||||
|
shutil.rmtree(GPG_KEYRING_DIR) # Remove directory and all contents
|
||||||
|
except OSError as e:
|
||||||
|
print(f"Warning: Failed to clean up directories: {e}")
|
||||||
|
print("Test class teardown complete.")
|
||||||
|
|
||||||
|
def test_1_key_generation(self):
|
||||||
|
"""Test key generation"""
|
||||||
|
print("\nTesting key generation...")
|
||||||
|
|
||||||
|
key_data = {
|
||||||
|
"name": "Test User",
|
||||||
|
"email": self.test_email,
|
||||||
|
"comment": "Test Key",
|
||||||
|
"passphrase": self.test_passphrase,
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.app.post("/api/generate-key", json=key_data)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_2_export_key(self):
|
||||||
|
"""Test key export"""
|
||||||
|
print("\nTesting key export...")
|
||||||
|
response = self.app.post(
|
||||||
|
"/api/download/public-key", json={"email": self.test_email}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_3_encrypt_file(self):
|
||||||
|
"""Test file encryption using the API"""
|
||||||
|
print("\nTesting file encryption...")
|
||||||
|
|
||||||
|
# Test file path
|
||||||
|
test_file_path = Path(__file__).parent / "resources" / "testing_file.txt"
|
||||||
|
if not test_file_path.exists():
|
||||||
|
print(f"Warning: Test file not found at {test_file_path}")
|
||||||
|
self.skipTest("Test file not found")
|
||||||
|
|
||||||
|
# Encrypt file using the API
|
||||||
|
with open(test_file_path, "rb") as f:
|
||||||
|
data = {"email": self.test_email, "file": (f, "testing_file.txt")}
|
||||||
|
response = self.app.post("/api/encrypt-file", data=data)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
print(f"Encryption Status: {response.status_code}")
|
||||||
|
print(f"Response: {response.get_json()}")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Save the encrypted file for the decrypt test
|
||||||
|
encrypted_file_path = (
|
||||||
|
Path(__file__).parent / "resources" / "testing_encrypted.gpg"
|
||||||
|
)
|
||||||
|
with open(encrypted_file_path, "wb") as f:
|
||||||
|
f.write(response.data)
|
||||||
|
|
||||||
|
return encrypted_file_path
|
||||||
|
|
||||||
|
def test_4_decrypt_file(self):
|
||||||
|
"""Test file decryption"""
|
||||||
|
print("\nTesting file decryption...")
|
||||||
|
|
||||||
|
# First encrypt a file using the API
|
||||||
|
encrypted_file_path = self.test_3_encrypt_file()
|
||||||
|
|
||||||
|
# Now decrypt it using the decrypt endpoint
|
||||||
|
with open(encrypted_file_path, "rb") as f:
|
||||||
|
data = {
|
||||||
|
"passphrase": self.test_passphrase,
|
||||||
|
"file": (f, "api_encrypted.gpg"),
|
||||||
|
}
|
||||||
|
response = self.app.post("/api/upload-decrypt", data=data)
|
||||||
|
|
||||||
|
print(f"Decryption Status: {response.status_code}")
|
||||||
|
if response.status_code != 200:
|
||||||
|
print(f"Response: {response.get_json()}")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
|
# tests = [
|
||||||
|
# test_setup,
|
||||||
|
# test_key_generation,
|
||||||
|
# test_list_keys,
|
||||||
|
# test_export_key,
|
||||||
|
# test_text_encryption,
|
||||||
|
# ]
|
||||||
|
#
|
||||||
|
# passed = 0
|
||||||
|
# for test in tests:
|
||||||
|
# try:
|
||||||
|
# if test():
|
||||||
|
# passed += 1
|
||||||
|
# print("✓ PASSED")
|
||||||
|
# else:
|
||||||
|
# print("✗ FAILED")
|
||||||
|
# except Exception as e:
|
||||||
|
# print(f"✗ ERROR: {e}")
|
||||||
|
# print("-" * 50)
|
||||||
|
#
|
||||||
|
# print(f"\nTest Results: {passed}/{len(tests)} tests passed")
|
||||||
|
#
|
||||||
|
# if passed == len(tests):
|
||||||
|
# print("🎉 All tests passed!")
|
||||||
|
# else:
|
||||||
|
# print("❌ Some tests failed")
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# if __name__ == "__main__":
|
||||||
|
# sys.exit(main())
|
||||||
Reference in New Issue
Block a user