privateburn/index.html

307 lines
11 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PrivateBin Clone</title>
<style>
:root {
--bg: #1e1e1e;
--surface: #2d2d2d;
--primary: #10a37f;
--text: #ececec;
}
body {
background: var(--bg);
color: var(--text);
font-family: sans-serif;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
}
.card {
width: 90%;
max-width: 600px;
background: var(--surface);
padding: 2rem;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
textarea {
width: 100%;
height: 250px;
background: #111;
border: 1px solid #444;
color: #fff;
padding: 1rem;
border-radius: 8px;
font-size: 16px;
resize: none;
box-sizing: border-box;
outline: none;
}
textarea:focus {
border-color: var(--primary);
}
.btn {
width: 100%;
background: var(--primary);
color: white;
border: none;
padding: 15px;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
margin-top: 1rem;
}
.btn:active {
transform: scale(0.98);
}
.url-box {
background: #111;
padding: 15px;
border-radius: 8px;
border: 1px dashed var(--primary);
margin-top: 1rem;
cursor: pointer;
word-break: break-all;
text-align: center;
}
.hidden {
display: none;
}
.status {
font-size: 12px;
color: #888;
margin-top: 8px;
text-align: center;
}
</style>
</head>
<body>
<div class="card" id="main">
<div id="input-view">
<h2 id="send-status" style="margin-bottom: 0"></h2>
<textarea
id="text"
placeholder="Type or paste your secret here..."
></textarea>
<button class="btn" onclick="burnIt()">Create Burn Link</button>
<div class="status">
Maximum size: 10MB. Content is encrypted in-browser.
</div>
</div>
<div id="result-view" class="hidden">
<h2 style="margin-top: 0">Link Generated!</h2>
<p style="color: #aaa">
Copy the link below. It will be deleted forever after the
first person opens it.
</p>
<div
class="url-box"
id="url-display"
onclick="copyToClipboard()"
></div>
<button
class="btn"
style="background: #444"
onclick="location.reload()"
>
Create Another
</button>
</div>
<div id="read-view" class="hidden">
<h2 id="read-status" style="margin-bottom: 0">Decrypting...</h2>
<div id="content-container" class="hidden">
<div
id="content"
style="
white-space: pre-wrap;
background: #111;
padding: 1rem;
border-radius: 8px;
margin-top: 1rem;
border: 1px solid #333;
max-height: 400px;
overflow-y: auto;
"
></div>
<div style="display: flex; gap: 10px; margin-top: 1rem">
<button
class="btn"
onclick="copyDecryptedText()"
style="flex: 1"
>
📋 Copy Secret
</button>
<button
class="btn secondary"
onclick="location.href = '/'"
style="flex: 1; background: #444"
>
Dismiss
</button>
</div>
</div>
</div>
</div>
<script>
// Make sure this matches MAX_SIZE_BYTES in server.js
const MAX_SIZE_BYTES = 10 * 1024 * 1024; // 10MB
// Check for existing link in URL on load
window.onload = () => {
if (window.location.hash.includes("id=")) initDecryption();
};
async function burnIt() {
const text = document.getElementById("text").value;
if (!text) return;
// 1. Generate Key
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"],
);
const iv = crypto.getRandomValues(new Uint8Array(12));
// 2. Encrypt
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
new TextEncoder().encode(text),
);
// 3. Upload Blob (IV + Ciphertext)
const blob = new Blob([iv, encrypted]);
if (blob.size > MAX_SIZE_BYTES) {
setErrorMessage("send-status", "message too large");
return;
}
let res;
try {
res = await fetch("/api/secret", {
method: "POST",
body: blob,
});
if (!res.ok) throw new Error(res.statusText);
// We need this catch since res.ok only works with some errors
} catch (e) {
let msg = e.message.replace("NetworkError", "networking problem");
setErrorMessage("send-status", msg);
return;
}
const { id } = await res.json();
// 4. Create Hash-Link (Key stays in hash, never sent to server)
const jwk = await crypto.subtle.exportKey("jwk", key);
const keyStr = btoa(JSON.stringify(jwk));
const fullUrl = `${window.location.origin}/#id=${id}&key=${keyStr}`;
document.getElementById("input-view").classList.add("hidden");
document
.getElementById("result-view")
.classList.remove("hidden");
document.getElementById("url-display").innerText = fullUrl;
navigator.clipboard.writeText(fullUrl);
}
async function initDecryption() {
document.getElementById("input-view").classList.add("hidden");
document.getElementById("read-view").classList.remove("hidden");
const statusEl = document.getElementById("read-status");
const contentContainer =
document.getElementById("content-container");
try {
const params = new URLSearchParams(
window.location.hash.substring(1),
);
const id = params.get("id");
const keyStr = params.get("key");
if (!id || !keyStr)
throw new Error("Invalid or malformed link.");
const res = await fetch(`/api/secret/${id}`);
if (!res.ok)
throw new Error(
"This secret was already burned or never existed.",
);
const buf = await res.arrayBuffer();
const iv = buf.slice(0, 12);
const data = buf.slice(12);
const jwk = JSON.parse(atob(keyStr));
const key = await window.crypto.subtle.importKey(
"jwk",
jwk,
"AES-GCM",
true,
["decrypt"],
);
const decrypted = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
key,
data,
);
const text = new TextDecoder().decode(decrypted);
// Update Header with Warning
statusEl.innerHTML = `
<span style="color: var(--primary)">🔥 Secret Decrypted & Burned</span>
<div style="font-size: 0.9rem; color: #ffab00; margin-top: 12px; font-weight: normal; line-height: 1.4; border-left: 3px solid #ffab00; padding-left: 10px;">
<strong>Attention:</strong> This is the only time and place you can view this secret.
It has been deleted from the server. Copy it now before closing this tab.
</div>
`;
document.getElementById("content").innerText = text;
contentContainer.classList.remove("hidden");
// Wipe hash from URL immediately for safety
window.history.replaceState(null, null, " ");
} catch (e) {
setErrorMessage("read-status", e.message);
const content = document.getElementById("content");
contentContainer.classList.remove("hidden");
}
}
function setErrorMessage(id, msg) {
const statusEl = document.getElementById(id);
statusEl.innerText = `❌ Error: ${msg}`;
statusEl.style.color = "#ff4444";
}
function copyDecryptedText() {
const text = document.getElementById("content").innerText;
navigator.clipboard.writeText(text);
const copyBtn = event.target;
const originalText = copyBtn.innerText;
copyBtn.innerText = "✅ Copied!";
setTimeout(() => (copyBtn.innerText = originalText), 2000);
}
function copyToClipboard() {
const text = document.getElementById("url-display").innerText;
navigator.clipboard.writeText(text);
alert("Copied!");
}
</script>
</body>
</html>