privateburn/index.html

343 lines
13 KiB
HTML
Raw Normal View History

2026-01-15 13:38:02 +02:00
<!doctype html>
<html lang="en">
<head>
2026-01-16 10:44:01 +00:00
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🔥</text></svg>"
>
2026-01-15 13:38:02 +02:00
<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;
2026-01-16 13:48:15 +00:00
--smalltext: #888;
2026-01-15 13:38:02 +02:00
}
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;
}
2026-01-16 13:48:15 +00:00
.expires {
font-size: 12px;
color: var(--smalltext);
margin-top: 8px;
text-align: center;
}
.expires > input {
background: #111;
border: 1px solid #444;
color: var(--text);
}
2026-01-15 13:38:02 +02:00
.status {
font-size: 12px;
2026-01-16 13:48:15 +00:00
color: var(--smalltext);
2026-01-15 13:38:02 +02:00
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>
2026-01-15 13:38:02 +02:00
<textarea
id="text"
placeholder="Type or paste your secret here..."
></textarea>
2026-01-16 13:48:15 +00:00
<div class="expires">
Expires in <input id="max-age" type="number" min="1" max="1000" value="48"> hours if unread.
</div>
2026-01-15 13:38:02 +02:00
<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="reload()"
2026-01-15 13:38:02 +02:00
>
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>
2026-01-16 13:48:15 +00:00
// Make sure these match MAX_SIZE_BYTES and MAX_AGE_HOURS in server.js
const MAX_SIZE_BYTES = 10 * 1024 * 1024; // 10MB
2026-01-16 13:48:15 +00:00
const MAX_AGE_HOURS = 1000;
2026-01-15 13:38:02 +02:00
// Check for existing link in URL on load
window.onload = () => {
if (window.location.hash.includes("id=")) initDecryption();
};
// This is a workaround for Firefox trying to be helpful. Most
// browsers clear content on reload, but Firefox does not.
async function reload() {
document.getElementById("text").value = "";
window.location.reload();
}
2026-01-15 13:38:02 +02:00
async function burnIt() {
2026-01-16 13:48:15 +00:00
const text = document.getElementById("text").value;
const maxAge = document.getElementById("max-age").value;
2026-01-15 13:38:02 +02:00
if (!text) return;
2026-01-16 13:48:15 +00:00
if (maxAge > MAX_AGE_HOURS) {
setErrorMessage(
"send-status",
`maximum age is higher than ${MAX_AGE_HOURS}`
);
return;
}
2026-01-15 13:38:02 +02:00
// 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),
);
const blob = new Blob([iv, encrypted]);
if (blob.size > MAX_SIZE_BYTES) {
setErrorMessage("send-status", "message too large");
return;
}
2026-01-16 13:48:15 +00:00
// 3. Upload Blob (IV + Ciphertext)
let res;
try {
2026-01-16 13:48:15 +00:00
res = await fetch(`/api/secret?max-age=${maxAge}`, {
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;
}
2026-01-15 13:38:02 +02:00
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(
2026-01-16 13:48:15 +00:00
"this secret was already burned or never existed",
2026-01-15 13:38:02 +02:00
);
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);
2026-01-15 13:38:02 +02:00
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";
}
2026-01-15 13:38:02 +02:00
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>