diff options
-rw-r--r-- | get.html | 158 | ||||
-rw-r--r-- | paste-cgi.py | 167 |
2 files changed, 303 insertions, 22 deletions
diff --git a/get.html b/get.html new file mode 100644 index 0000000..9799eba --- /dev/null +++ b/get.html @@ -0,0 +1,158 @@ +<!DOCTYPE html> +<html lang="en" data-theme="dark"> + +<head> + <meta charset="UTF-8"> + <meta name="description" content="AES-256 Encryption Example"> + <title>Secure Paste</title> + <link rel="icon" href="./favicon.svg" type="image/svg+xml"> + <link rel="stylesheet" href="./pico.min.css"> +</head> + +<body> + <main class="container"> + <h1>paste-cgi</h1> + + <form id="cryptoForm"> + <h3 id="title">Title</h3> + + <div id="passwordField" style="display: none;"> + <label for="password">Password</label> + <input type="password" id="password" placeholder="Enter your password" /> + </div> + + <button type="button" id="decryptBtn" onclick="handleDecrypt()" style="display: none;">Decrypt</button> + </form> + + <div id="pasteUrlSection" style="display: none;"> + <h3>Paste</h3> + <textarea id="decryptedOutput" rows="5" readonly></textarea> + <button type="button" onclick="copyPaste()">Copy</button> + </div> + </main> + + <script> + let globalData; + const pasteTypeConstant = "pasteType"; + const passwordFieldConstant = "passwordField"; + const passwordTypeConstant = "password"; + const decryptButtonConstant = "decryptBtn"; + const plainTypeConstant = "plain"; + const titleConstant = "title"; + const expirationConstant = "expiration"; + const plaintextConstant = "plaintext"; + const decryptedOutputConstant = "decryptedOutput" + const pasteUrlSectionConstant = "pasteUrlSection" + + window.onload = async function () { + const urlParams = new URLSearchParams(window.location.search); + const id = urlParams.get('id'); + + const fetchUrl = `${window.location.origin}/paste?id=${id}`; + + try { + const response = await fetch(fetchUrl); + + if (!response.ok) { + throw new Error('Failed to fetch data'); + } + + const data = await response.json(); + + globalData = data; + togglePasteType() + } catch (error) { + alert("NO File Found") + console.error('Error fetching data:', error); + } + }; + + function copyPaste() { + var copyText = document.getElementById(decryptedOutputConstant); + + copyText.select(); + copyText.setSelectionRange(0, 99999); + + navigator.clipboard.writeText(copyText.value); + } + function togglePasteType() { + const decryptButton = document.getElementById(decryptButtonConstant); + const passwordField = document.getElementById(passwordFieldConstant); + document.getElementById(titleConstant).textContent = globalData.title; + if (globalData.type === passwordTypeConstant) { + passwordField.style.display = 'block'; + decryptButton.style.display = 'block'; + } else { + decryptButton.style.display = 'none'; + passwordField.style.display = 'none'; + document.getElementById(decryptedOutputConstant).textContent = globalData.pasted_text + document.getElementById(pasteUrlSectionConstant).style.display = 'block'; + } + } + async function deriveKey(password, salt) { + let encodedPassword = new TextEncoder().encode(password); + let baseKey = await window.crypto.subtle.importKey( + "raw", + encodedPassword, + { name: "PBKDF2" }, + false, + ["deriveKey"], + ); + + let derivedKey = await window.crypto.subtle.deriveKey( + { + name: "PBKDF2", + salt: salt, + iterations: 600000, + hash: "SHA-256", + }, + baseKey, + { name: "AES-GCM", length: 256 }, + true, + ["encrypt", "decrypt"], + ); + + return derivedKey; + } + + async function decryptData(encryptedData, password) { + let { ciphertext, iv, authTag, salt } = encryptedData; + let key = await deriveKey(password, salt); + + let dataWithAuthTag = new Uint8Array(ciphertext.length + authTag.length); + dataWithAuthTag.set(ciphertext, 0); + dataWithAuthTag.set(authTag, ciphertext.length); + + let decryptedContent = await window.crypto.subtle.decrypt( + { name: "AES-GCM", iv: iv, tagLength: 128 }, + key, + dataWithAuthTag, + ); + + return new TextDecoder().decode(decryptedContent); + } + async function handleDecrypt() { + let base64 = globalData.pasted_text; + let password = document.getElementById(passwordTypeConstant).value; + if (!base64 || !password) return alert("Enter encrypted text and password."); + try { + + let parsed = JSON.parse(atob(base64)); + let encryptedData = { + salt: new Uint8Array(Object.values(parsed.salt)), + iv: new Uint8Array(Object.values(parsed.iv)), + authTag: new Uint8Array(Object.values(parsed.authTag)), + ciphertext: new Uint8Array(Object.values(parsed.ciphertext)), + }; + + let decrypted = await decryptData(encryptedData, password); + document.getElementById("decryptedOutput").value = decrypted; + document.getElementById(pasteUrlSectionConstant).style.display = 'block'; + } catch (err) { + document.getElementById("decryptedOutput").value = "❌ Decryption failed."; + } + } + </script> +</body> + +</html>
\ No newline at end of file diff --git a/paste-cgi.py b/paste-cgi.py index cf8a922..88bd802 100644 --- a/paste-cgi.py +++ b/paste-cgi.py @@ -4,6 +4,7 @@ import sys import random import string import json +from datetime import datetime, timezone, timedelta CWD = os.getcwd() @@ -28,6 +29,13 @@ class SubmitConstants: TITLE: str = "title" EXPIRATION: str = "expiration" PASTED_TEXT: str = "pasted_text" + DATE_CREATED: str = "date_created" + + +def convert(obj): + if isinstance(obj, datetime): + return obj.isoformat() + raise TypeError("Type not serializable") def check_working_dir(allowed_dir): @@ -130,34 +138,145 @@ def return_favicon_svg(): def submit(post_data): + if not os.path.exists(DATABASE_DIRECTORY): + os.makedirs(DATABASE_DIRECTORY) + full_path = "" + while True: + random_id = generate_random_string() + full_path = os.path.join(DATABASE_DIRECTORY, f"{random_id}.json") + if not os.path.exists(full_path): + break + + data = { + SubmitConstants.TYPE: post_data[SubmitConstants.TYPE], + SubmitConstants.TITLE: post_data[SubmitConstants.TITLE], + SubmitConstants.EXPIRATION: post_data[SubmitConstants.EXPIRATION], + SubmitConstants.PASTED_TEXT: post_data[SubmitConstants.PASTED_TEXT], + SubmitConstants.DATE_CREATED: datetime.now(timezone.utc), + } + + with open(full_path, "w") as json_file: + json.dump(data, json_file, indent=4, default=convert) + + print("Content-Type: application/json") + print("") + print(json.dumps({"id": random_id})) + + +def handle_data(data): + expiry = data[SubmitConstants.EXPIRATION] + date = datetime.fromisoformat(data[SubmitConstants.DATE_CREATED]) + date_now = datetime.now(timezone.utc) + + deleted = False + delete_after_read = False + + if expiry == "never": + pass + elif expiry == "burn_after_read": + deleted = False + delete_after_read = True + elif expiry == "10_minutes": + if date + timedelta(minutes=10) <= date_now: + deleted = True + elif expiry == "1_hour": + if date + timedelta(hours=1) <= date_now: + deleted = True + elif expiry == "1_week": + if date + timedelta(weeks=1) <= date_now: + deleted = True + elif expiry == "2_weeks": + if date + timedelta(weeks=2) <= date_now: + deleted = True + elif expiry == "1_month": + if date + timedelta(days=30) <= date_now: + deleted = True + elif expiry == "6_months": + if date + timedelta(days=180) <= date_now: + deleted = True + elif expiry == "1_year": + if date + timedelta(days=365) <= date_now: + deleted = True + + return delete_after_read, deleted + + +def return_paste(query_string): + if query_string is None or not query_string.startswith("id="): + print("Content-Type: text/plain") + print("") + print(query_string) + sys.exit(0) + + id = query_string.split("=") + if len(id) != 2: + print("Content-Type: text/plain") + print("") + print(query_string) + sys.exit(0) + + id = id[1] + + full_path = os.path.join(DATABASE_DIRECTORY, f"{id}.json") + directory = os.path.dirname(full_path) + os.chdir(directory) + + if os.getcwd() != DATABASE_DIRECTORY: + print("Content-Type: text/plain") + print("") + print("NICE TRY") + sys.exit(0) + + if not os.path.exists(full_path): + print("Status: 404 Not Found") + print("Content-Type: text/plain") + print("") + print("File not found") + print(full_path) + sys.exit(0) + + with open(full_path, "r") as file: + data = json.load(file) + try: - if not os.path.exists(DATABASE_DIRECTORY): - os.makedirs(DATABASE_DIRECTORY) - full_path = "" - while True: - random_id = generate_random_string() - full_path = os.path.join(DATABASE_DIRECTORY, f"{random_id}.json") - if not os.path.exists(full_path): - break - - data = { - SubmitConstants.TYPE: post_data[SubmitConstants.TYPE], - SubmitConstants.TITLE: post_data[SubmitConstants.TITLE], - SubmitConstants.EXPIRATION: post_data[SubmitConstants.EXPIRATION], - SubmitConstants.PASTED_TEXT: post_data[SubmitConstants.PASTED_TEXT], - } - - with open(full_path, "w") as json_file: - json.dump(data, json_file, indent=4) + delete_after_read, deleted = handle_data(data) + + if deleted: + os.remove(full_path) + print("Status: 404 Not Found") + print("Content-Type: text/plain") + print("") + print("File not found") print("Content-Type: application/json") print("") - print(json.dumps({"id": random_id})) - except Exception as e: + print(json.dumps(data, default=convert)) + + if delete_after_read: + os.remove(full_path) + + sys.exit(0) + except Exception: + print("Status: 404 Not Found") print("Content-Type: text/plain") print("") - print(str(e)) - sys.exit(0) + print("File not found") + + +def return_get(): + try: + with open("get.html", "r") as file: + get_html = file.read() + + print("Content-Type: text/html") + print("") + print(get_html) + + except Exception: + print("Status: 404 Not Found") + print("Content-Type: text/html") + print("") + print("<html><body><h1>404 Not Found</h1></body></html>") allowed_dir = os.environ.get("ALLOWED_DIR", None) @@ -180,6 +299,10 @@ if method == "GET": return_pico_css() elif script_name == "/favicon.svg": return_favicon_svg() + elif script_name == "/get": + return_get() + elif script_name == "/paste": + return_paste(query_string) elif method == "POST": post_data = sys.stdin.read(content_length) post_data = json.loads(post_data) |