commit 09139f0681d86eeddeb62ae4c2fefd837973d6ee Author: Marius Pana Date: Thu Jan 15 13:38:02 2026 +0200 engage diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8fce603 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +data/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..0487140 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# 🔥 SecureBurn +A ultra-minimalist, zero-dependency Node.js "PrivateBin" clone. It allows users to share encrypted secrets that self-destruct (burn) immediately after the first access. + +# 🛡️ Security Model: Zero-Knowledge +Encryption: AES-256-GCM encryption happens entirely in the sender's browser. + +Privacy: The decryption key is stored in the URL after the # symbol (the fragment identifier). + +Blind Storage: Browsers do not send the URL fragment to the server. Therefore, the server only ever sees and stores encrypted binary data. It has no way to read your secrets. + +Self-Destruction: The server deletes the encrypted file from the disk the moment it is streamed to a recipient. + +# 🚀 Getting Started +## Prerequisites + +Node.js (v16.0.0 or higher recommended) +No package manager (npm/yarn) is required. + +## Installation & Running + +Clone or copy the three files (server.js, index.html, auth.json) into a directory. + +Start the server: + +```Bash +node server.js +``` +Access the UI: Open http://localhost:3000 in your browser. + + +# 🛠️ Configuration +The application is designed to be plug-and-play. You can modify the constants at the top of server.js: + +PORT: The port the server listens on (default: 3000). + +DATA_DIR: Where encrypted blobs are stored (default: ./data). + +MAX_SIZE_BYTES: Maximum secret size (default: 10MB). + +# 📡 API Usage +You can create burn codes programmatically without using the web UI. + +Create a Secret + +Endpoint: POST /api/secret + +Body: Raw binary data (the encrypted payload). + +Example using curl: + +```Bash +curl -X POST --data-binary "@encrypted_file.bin" http://localhost:3000/api/secret +Response: + +JSON +{ "id": "550e8400-e29b-41d4-a716-446655440000" } +``` + +# 📋 Features +Zero Dependencies: Uses only native Node.js modules (http, fs, crypto, path). + +10MB Capacity: Handles large text blocks or small files. + +One-Click Copy: Generated links are automatically copied to the clipboard. + +Mobile Friendly: Clean, responsive "Chat-style" UI. + +Secure Erasure: Uses fs.unlink to ensure the file is removed from the filesystem after one read. + +# ⚠️ Important Notes +Persistence: Since it uses the file system, secrets will survive a server restart until they are burned. + +HTTPS: To use the Web Crypto API (window.crypto), this app must be served over HTTPS in production (except for localhost). + + +# Todo +- tested only on macos using safari (not sure if works on other platforms) +- add some method to delete secrets that have not been accessed within a certain time frame. +- add some method to expiry secrets (e.g. when creating, maybe define how long they should be valid for) diff --git a/index.html b/index.html new file mode 100644 index 0000000..1be1f7b --- /dev/null +++ b/index.html @@ -0,0 +1,283 @@ + + + + + + PrivateBin Clone + + + +
+
+ + +
+ Maximum size: 10MB. Content is encrypted in-browser. +
+
+ + + + +
+ + + + diff --git a/server.js b/server.js new file mode 100644 index 0000000..969e171 --- /dev/null +++ b/server.js @@ -0,0 +1,68 @@ +const http = require('node:http'); +const fs = require('node:fs'); +const path = require('node:path'); +const crypto = require('node:crypto'); + +const PORT = process.env.PORT || 3000; +const DATA_DIR = path.join(__dirname, 'data'); +const MAX_SIZE_BYTES = 10 * 1024 * 1024; // 10MB + +if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR); + +const sendJSON = (res, status, data) => { + res.writeHead(status, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(data)); +}; + +const server = http.createServer((req, res) => { + const { method, url } = req; + + // Serve Frontend + if (method === 'GET' && (url === '/' || url.startsWith('/view'))) { + res.writeHead(200, { 'Content-Type': 'text/html' }); + fs.createReadStream(path.join(__dirname, 'index.html')).pipe(res); + return; + } + + // CREATE Secret (Public POST) + if (method === 'POST' && url === '/api/secret') { + const contentLength = parseInt(req.headers['content-length'], 10); + if (contentLength > MAX_SIZE_BYTES) { + res.writeHead(413); return res.end('Too large'); + } + + const id = crypto.randomUUID(); // Unique and Random URL component + const filePath = path.join(DATA_DIR, id); + const writeStream = fs.createWriteStream(filePath); + + req.pipe(writeStream); + req.on('end', () => sendJSON(res, 201, { id })); + writeStream.on('error', () => { res.writeHead(500); res.end(); }); + return; + } + + // READ & BURN Secret + if (method === 'GET' && url.startsWith('/api/secret/')) { + const id = url.split('/').pop(); + if (!/^[a-z0-9-]+$/i.test(id)) { res.writeHead(400); return res.end(); } + + const filePath = path.join(DATA_DIR, id); + if (!fs.existsSync(filePath)) { + res.writeHead(404); return res.end('Burned or Not Found'); + } + + const readStream = fs.createReadStream(filePath); + res.writeHead(200, { 'Content-Type': 'application/octet-stream' }); + readStream.pipe(res); + + // Delete immediately after stream ends + readStream.on('end', () => { + fs.unlink(filePath, (err) => { if (err) console.error("Burn failed", err); }); + }); + return; + } + + res.writeHead(404); res.end(); +}); + +server.listen(PORT, () => console.log(`Public Burn Server: http://localhost:${PORT}`));