diff options
-rw-r--r-- | README.md | 38 | ||||
-rw-r--r-- | get.html | 131 | ||||
-rw-r--r-- | index.html | 143 | ||||
-rw-r--r-- | paste-cgi.py | 142 | ||||
-rw-r--r-- | script.js | 230 |
5 files changed, 302 insertions, 382 deletions
@@ -1,7 +1,7 @@ # Paste-cgi ## Description -This is a dirty pastebin done in CGI. It is completely client-side encrypted and server only stored encrypted paste. +This is a dirty pastebin done in CGI. It is completely client-side encrypted and server only stores encrypted paste(Title is not encrypted). ## Images  @@ -10,24 +10,44 @@ This is a dirty pastebin done in CGI. It is completely client-side encrypted and ## Nginx Config ``` server { + server_name paste.example.com; + root /example/root/dir; - server_name SERVERURL; - root ROOTFOLDER; + # Global basic Auth + auth_basic "Restricted Content"; + auth_basic_user_file /etc/nginx/.htpasswd; - location / { - auth_basic "Restricted Content"; # Basic Auth - auth_basic_user_file /etc/nginx/.htpasswd; # Basic Auth + # Server static files + location = / { + try_files /index.html =404; + } + + location = /get { + try_files /get.html =404; + } + + location ~* \.(css|js|svg)$ { + try_files $uri =404; + access_log off; + expires 30d; + } + + # Handle cgi routes + location ~ ^/(paste|submit)$ { include fastcgi_params; - fastcgi_param SCRIPT_FILENAME PATH TO SCRIPT; + fastcgi_param SCRIPT_FILENAME /example/path/to/cgi; fastcgi_param PATH_INFO $uri; fastcgi_param QUERY_STRING $args; fastcgi_param HTTP_HOST $server_name; fastcgi_param CONTENT_LENGTH $content_length; fastcgi_param CONTENT_TYPE $content_type; - fastcgi_param ALLOWED_DIR PATH WHERE THE ALLOWED DIRECTORY IS TO CHANGE FILES; + fastcgi_param ALLOWED_DIR /example/allowed/dir; fastcgi_pass unix:/run/fcgiwrap.socket; } - + # Deny everything else + location / { + return 403; + } listen [::]:443 ssl; # managed by Certbot listen 443 ssl; # managed by Certbot @@ -3,15 +3,16 @@ <head> <meta charset="UTF-8"> - <meta name="description" content="AES-256 Encryption Example"> - <title>Secure Paste</title> + <title>paste-cgi</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> + <a href="/"> + <h1>paste-cgi</h1> + </a> <form id="cryptoForm"> <h3 id="title">Title</h3> @@ -30,129 +31,7 @@ <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> + <script src="/script.js"></script> </body> </html>
\ No newline at end of file @@ -3,15 +3,16 @@ <head> <meta charset="UTF-8"> - <meta name="description" content="AES-256 Encryption Example"> - <title>Secure Paste</title> + <title>paste-cgi</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> + <a href="/"> + <h1>paste-cgi</h1> + </a> <form id="cryptoForm"> <label for="title">Title</label> @@ -54,141 +55,7 @@ <button type="button" onclick="copyPaste()">Copy</button> </div> </main> - - <script> - const pasteTypeConstant = "pasteType"; - const passwordFieldConstant = "passwordField"; - const passwordTypeConstant = "password"; - const plainTypeConstant = "plain"; - const titleConstant = "title"; - const expirationConstant = "expiration"; - const plaintextConstant = "plaintext"; - const pasteUrlConstant = "pasteUrl" - const pasteUrlSectionConstant = "pasteUrlSection" - - function copyPaste(){ - var copyText = document.getElementById(pasteUrlConstant); - - copyText.select(); - copyText.setSelectionRange(0, 99999); - - navigator.clipboard.writeText(copyText.value); - } - - function togglePasteType() { - const passwordType = document.getElementById(pasteTypeConstant).value; - const passwordField = document.getElementById(passwordFieldConstant); - if (passwordType === passwordTypeConstant) { - passwordField.style.display = 'block'; - } else { - passwordField.style.display = 'none'; - } - } - - 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 encryptData(data, password) { - let salt = window.crypto.getRandomValues(new Uint8Array(16)); - let iv = window.crypto.getRandomValues(new Uint8Array(12)); - let key = await deriveKey(password, salt); - let encodedData = new TextEncoder().encode(data); - - let encryptedContent = await window.crypto.subtle.encrypt( - { - name: "AES-GCM", - iv: iv, - tagLength: 128, - }, - key, - encodedData, - ); - - let ciphertext = encryptedContent.slice( - 0, - encryptedContent.byteLength - 16, - ); - let authTag = encryptedContent.slice(encryptedContent.byteLength - 16); - - return { - ciphertext: new Uint8Array(ciphertext), - iv: iv, - authTag: new Uint8Array(authTag), - salt: salt, - }; - } - - async function handlePaste() { - let type = document.getElementById(pasteTypeConstant).value; - let title = document.getElementById(titleConstant).value; - let expiration = document.getElementById(expirationConstant).value; - let plaintext = document.getElementById(plaintextConstant).value; - let password = document.getElementById(passwordTypeConstant).value; - - let pasted_text = "" - if (type == plainTypeConstant) { - if (!plaintext || !title) return alert("Enter title and paste."); - - pasted_text = plaintext - } - else { - if (!plaintext || !password || !title) return alert("Enter title, paste and password."); - - let encrypted = await encryptData(plaintext, password); - pasted_text = btoa(JSON.stringify(encrypted)) - } - - let currentPath = window.location.origin; - - try { - let response = await fetch(currentPath + "/submit", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - type: type, - title: title, - expiration: expiration, - pasted_text: pasted_text - }), - }); - - if (response.ok) { - let jsonResponse = await response.json(); - document.getElementById(pasteUrlConstant).value = currentPath + "/get?id=" + jsonResponse.id; - document.getElementById(pasteUrlSectionConstant).style.display = 'block'; - } else { - console.error("Failed to submit data. Status:", response.status); - } - } catch (error) { - console.error("Error making the POST request:", error); - } - } - </script> + <script src="/script.js"></script> </body> </html>
\ No newline at end of file diff --git a/paste-cgi.py b/paste-cgi.py index 88bd802..dd1d9e1 100644 --- a/paste-cgi.py +++ b/paste-cgi.py @@ -46,13 +46,33 @@ def check_working_dir(allowed_dir): sys.exit(0) +def status_405(): + print("Status: 405 Method Not Allowed") + print("Content-Type: text/plain") + print("") + print("405 Method Not Allowed") + sys.exit(0) + + +def status_415(): + print("Status: 415 Unsupported Media Type") + print("Content-Type: text/plain") + print("") + print("415 Unsupported Media Type.") + sys.exit(0) + + +def status_404(): + print("Status: 404 Not Found") + print("Content-Type: text/plain") + print("") + print("File not found") + sys.exit(0) + + def check_method(method): if method not in ["GET", "POST"]: - print("Status: 405 Method Not Allowed") - print("Content-Type: text/plain") - print("") - print("405 Method Not Allowed") - sys.exit(0) + status_405() def get_content_lenght(): @@ -82,59 +102,7 @@ def validate_payload(payload): ): if SubmitConstants.PASTED_TEXT in payload: return - print("Status: 415 Unsupported Media Type") - print("Content-Type: text/plain") - print("") - print("415 Unsupported Media Type: Expected 'application/json'.") - sys.exit(0) - - -def return_index_html(): - try: - with open("index.html", "r") as file: - index_html = file.read() - - print("Content-Type: text/html") - print("") - print(index_html) - - except Exception: - print("Status: 404 Not Found") - print("Content-Type: text/html") - print("") - print("<html><body><h1>404 Not Found</h1></body></html>") - - -def return_pico_css(): - try: - with open("pico.min.css", "r") as file: - pico_css = file.read() - - print("Content-Type: text/css") - print("") - print(pico_css) - - except Exception: - print("Status: 404 Not Found") - print("Content-Type: text/html") - print("") - print("<html><body><h1>404 Not Found</h1></body></html>") - - -def return_favicon_svg(): - try: - with open("favicon.svg", "r") as file: - favicon_svg = file.read() - - print("Content-Type: image/svg+xml") - print("") - print(favicon_svg) - - except Exception: - print("Status: 404 Not Found") - print("Content-Type: text/html") - print("") - print("<html><body><h1>404 Not Found</h1></body></html>") + status_415() def submit(post_data): @@ -203,17 +171,11 @@ def handle_data(data): 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) + status_415() id = query_string.split("=") if len(id) != 2: - print("Content-Type: text/plain") - print("") - print(query_string) - sys.exit(0) + status_415() id = id[1] @@ -222,18 +184,10 @@ def return_paste(query_string): os.chdir(directory) if os.getcwd() != DATABASE_DIRECTORY: - print("Content-Type: text/plain") - print("") - print("NICE TRY") - sys.exit(0) + status_415() 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) + status_404() with open(full_path, "r") as file: data = json.load(file) @@ -243,10 +197,7 @@ def return_paste(query_string): if deleted: os.remove(full_path) - print("Status: 404 Not Found") - print("Content-Type: text/plain") - print("") - print("File not found") + status_404() print("Content-Type: application/json") print("") @@ -257,26 +208,7 @@ def return_paste(query_string): sys.exit(0) except Exception: - print("Status: 404 Not Found") - print("Content-Type: text/plain") - print("") - 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>") + status_404() allowed_dir = os.environ.get("ALLOWED_DIR", None) @@ -293,15 +225,7 @@ content_length = get_content_lenght() if method == "GET": - if script_name == "/": - return_index_html() - elif script_name == "/pico.min.css": - return_pico_css() - elif script_name == "/favicon.svg": - return_favicon_svg() - elif script_name == "/get": - return_get() - elif script_name == "/paste": + if script_name == "/paste": return_paste(query_string) elif method == "POST": post_data = sys.stdin.read(content_length) diff --git a/script.js b/script.js new file mode 100644 index 0000000..995156f --- /dev/null +++ b/script.js @@ -0,0 +1,230 @@ +const pasteTypeConstant = "pasteType"; +const passwordFieldConstant = "passwordField"; +const passwordTypeConstant = "password"; +const plainTypeConstant = "plain"; +const titleConstant = "title"; +const expirationConstant = "expiration"; +const plaintextConstant = "plaintext"; +const pasteUrlConstant = "pasteUrl"; +const pasteUrlSectionConstant = "pasteUrlSection"; +const decryptedOutputConstant = "decryptedOutput"; +const decryptButtonConstant = "decryptBtn"; + +let globalData; + +function copyPaste() { + const copyText = document.getElementById( + document.getElementById(pasteUrlConstant) + ? pasteUrlConstant + : decryptedOutputConstant + ); + copyText.select(); + copyText.setSelectionRange(0, 99999); + navigator.clipboard.writeText(copyText.value); +} + +function togglePasteType() { + const passwordType = document.getElementById(pasteTypeConstant)?.value || globalData?.type; + const passwordField = document.getElementById(passwordFieldConstant); + const decryptButton = document.getElementById(decryptButtonConstant); + + if (document.getElementById(titleConstant) && globalData?.title) { + document.getElementById(titleConstant).textContent = "Title:" + globalData.title; + } + + if (passwordType === passwordTypeConstant) { + passwordField.style.display = "block"; + if (decryptButton) decryptButton.style.display = "block"; + } else { + passwordField.style.display = "none"; + if (decryptButton) decryptButton.style.display = "none"; + if (document.getElementById(decryptedOutputConstant)) { + document.getElementById(decryptedOutputConstant).textContent = globalData?.pasted_text || ""; + } + document.getElementById(pasteUrlSectionConstant).style.display = "block"; + } +} + +async function deriveKey(password, salt) { + const encodedPassword = new TextEncoder().encode(password); + const baseKey = await window.crypto.subtle.importKey( + "raw", + encodedPassword, + { name: "PBKDF2" }, + false, + ["deriveKey"] + ); + + const 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 encryptData(data, password) { + const salt = window.crypto.getRandomValues(new Uint8Array(16)); + const iv = window.crypto.getRandomValues(new Uint8Array(12)); + const key = await deriveKey(password, salt); + const encodedData = new TextEncoder().encode(data); + + const encryptedContent = await window.crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128, + }, + key, + encodedData + ); + + const ciphertext = encryptedContent.slice(0, encryptedContent.byteLength - 16); + const authTag = encryptedContent.slice(encryptedContent.byteLength - 16); + + return { + ciphertext: new Uint8Array(ciphertext), + iv: iv, + authTag: new Uint8Array(authTag), + salt: salt, + }; +} + +async function decryptData(encryptedData, password) { + const { ciphertext, iv, authTag, salt } = encryptedData; + const key = await deriveKey(password, salt); + + const dataWithAuthTag = new Uint8Array(ciphertext.length + authTag.length); + dataWithAuthTag.set(ciphertext, 0); + dataWithAuthTag.set(authTag, ciphertext.length); + + const decryptedContent = await window.crypto.subtle.decrypt( + { name: "AES-GCM", iv: iv, tagLength: 128 }, + key, + dataWithAuthTag + ); + + return new TextDecoder().decode(decryptedContent); +} + +async function handlePaste() { + const type = document.getElementById(pasteTypeConstant).value; + const title = document.getElementById(titleConstant).value; + const expiration = document.getElementById(expirationConstant).value; + const plaintext = document.getElementById(plaintextConstant).value; + const password = document.getElementById(passwordTypeConstant).value; + + let pasted_text = ""; + + if (type === plainTypeConstant) { + if (!plaintext || !title) return alert("Enter title and paste."); + pasted_text = plaintext; + } else { + if (!plaintext || !password || !title) return alert("Enter title, paste and password."); + const encrypted = await encryptData(plaintext, password); + pasted_text = btoa(JSON.stringify(encrypted)); + } + + const currentPath = window.location.origin; + + try { + const response = await fetch(currentPath + "/submit", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + type: type, + title: title, + expiration: expiration, + pasted_text: pasted_text, + }), + }); + + if (response.ok) { + const jsonResponse = await response.json(); + document.getElementById(pasteUrlConstant).value = currentPath + "/get?id=" + jsonResponse.id; + document.getElementById(pasteUrlSectionConstant).style.display = "block"; + } else { + console.error("Failed to submit data. Status:", response.status); + } + } catch (error) { + console.error("Error making the POST request:", error); + } +} + +async function handleDecrypt() { + const base64 = globalData.pasted_text; + const password = document.getElementById(passwordTypeConstant).value; + if (!base64 || !password) return alert("Enter encrypted text and password."); + + try { + const parsed = JSON.parse(atob(base64)); + const 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)), + }; + + const decrypted = await decryptData(encryptedData, password); + document.getElementById(decryptedOutputConstant).value = decrypted; + document.getElementById(pasteUrlSectionConstant).style.display = "block"; + } catch (err) { + document.getElementById(decryptedOutputConstant).value = "❌ Decryption failed."; + } +} + +async function loadPasteFromUrl() { + const urlParams = new URLSearchParams(window.location.search); + const id = urlParams.get("id"); + + if (!id) return; + + const fetchUrl = `${window.location.origin}/paste?id=${id}`; + + try { + const response = await fetch(fetchUrl); + if (!response.ok) throw new Error("Failed to fetch data"); + + globalData = await response.json(); + togglePasteType(); + } catch (error) { + alert("No File Found"); + console.error("Error fetching data:", error); + } +} + +window.onload = async function () { + let path = window.location.pathname; + if (path != "/get") + return; + 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); + } +};
\ No newline at end of file |