From 67a0c83965da147e54802e6aa45511e36ca1be95 Mon Sep 17 00:00:00 2001 From: Julio Cesar Date: Wed, 9 Jul 2025 16:54:14 +0200 Subject: [PATCH] Add main application page, key generation and download is working --- app.py | 29 +- static/script.js | 700 +++++++++++++++++++++++++++++++++++++++++++ templates/index.html | 275 +++++++++++++++++ 3 files changed, 982 insertions(+), 22 deletions(-) create mode 100644 static/script.js create mode 100644 templates/index.html diff --git a/app.py b/app.py index 0e4eb4b..4f08c01 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,6 @@ # gpg_api.py -from flask import Flask, Request, request, jsonify, send_file +from flask import Flask, Request, render_template, request, jsonify, send_file import gnupg import tempfile import os @@ -74,27 +74,12 @@ def find_key_by_email(email, secret=False): # @app.route("/api/setup/gnupg", methods=["GET"]) # def setup_gnupg(): -# """Check if GnuPG is available through python-gnupg""" -# try: -# # Try to get version info -# version_info = gpg_instance.version -# logger.info("GnuPG accessible through python-gnupg") -# return jsonify( -# { -# "status": "GnuPG is available through python-gnupg", -# "version": version_info, -# } -# ) -# except Exception as e: -# logger.warning(f"GnuPG not accessible: {e}") -# return ( -# jsonify( -# { -# "error": "GnuPG is not accessible. Please ensure it's properly installed." -# } -# ), -# 404, -# ) + + +@app.route("/") +def index(): + """Render the main application page""" + return render_template("index.html") @app.route("/api/generate-key", methods=["POST"]) diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..19c1a36 --- /dev/null +++ b/static/script.js @@ -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 = + 'Generating...' + } else if (type === "success") { + elements.successStatus.classList.remove("hidden") + elements.generateBtn.disabled = false + elements.generateBtn.innerHTML = + 'Generate Key Pair' + } else if (type === "error") { + elements.errorStatus.classList.remove("hidden") + elements.errorMessage.textContent = message + elements.generateBtn.disabled = false + elements.generateBtn.innerHTML = + 'Generate Key Pair' + } else { + elements.generationStatus.classList.add("hidden") + elements.generateBtn.disabled = false + elements.generateBtn.innerHTML = + '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 = ` +
+ +
+
${file.name}
+
${formatFileSize( + file.size + )}
+
+
+ + ` + 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 = + 'Decrypting...' + updateProgress(0, "Preparing decryption...") + } else { + elements.decryptBtn.innerHTML = + '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 = ` +
+ + ${result.filename} + - Decrypted successfully +
+ ` + } else { + resultElement.innerHTML = ` +
+ + ${result.filename} + - ${result.error} +
+ ` + } + + 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 = ` +
+ + ${message} +
+ ` + + 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(); + */ +} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..9815c5b --- /dev/null +++ b/templates/index.html @@ -0,0 +1,275 @@ + + + + + + + SecureKey Manager - OpenPGP Key Management + + + + + + + +
+ +
+
+ +

SecureKey Manager

+
+

+ Generate OpenPGP key pairs and decrypt files securely. + All cryptographic operations are performed via secure API endpoints. +

+
+ + + + + +
+ +
+
+
+ +

Generate OpenPGP Key Pair

+

Create a new key pair for secure communication

+
+ +
+ +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + +
+ + +
+ +
+
+ + + +
+
+ + + +
+
+ + + + + \ No newline at end of file