aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--get.html158
-rw-r--r--paste-cgi.py167
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)