Self-hosted, zero-knowledge, end-to-end encrypted pastebin built with PHP, MySQL, Web Crypto API, and a responsive single-page UI.
Share sensitive text securely. Encryption happens in the browser before upload, the server stores ciphertext only, and the decryption key stays in the URL fragment (#...) so it is never sent to the server.
π Live Demo: https://sphost.theazizi.ir
βοΈ Cloudflare Worker version: https://github.com/TheGreatAzizi/Secure-Pastebin-Cloudflare-Worker
- π Client-side AES-256-GCM encryption using the native Web Crypto API
- π‘οΈ Zero-knowledge architecture β server never receives plaintext, password, or key
- π§Ύ Optional subject field included inside the encrypted payload
- π Optional password protection with PBKDF2 (100,000 iterations, SHA-256)
- π₯ Burn after reading support
- β±οΈ Preset expiration plus custom expiration date & time
- π Markdown authoring + rendering
- compact formatting toolbar in the composer
- Markdown is rendered after decryption only
- π Two share-link formats
- full link:
/p/{id}#key - short link:
/#id:key
- full link:
- π Copy actions for full link, short link, and decrypted text
- π± Responsive UI with improved mobile layout
- π RTL-aware text handling for Persian / Arabic / Hebrew content
- π€ No external font CDN β uses a local Vazirmatn-based font stack
- βοΈ Documented HTTP API with
/api/docs
- The browser generates or derives the encryption key locally.
- The browser encrypts the payload locally using AES-256-GCM.
- The server receives only:
- paste ID
- IV
- ciphertext
- metadata such as expiration / burn-after-read / password flag
- The decryption key is kept in the URL fragment (
#...). URL fragments are not sent in normal HTTP requests. - The recipient opens the link, downloads the encrypted payload, and decrypts it locally.
If the full share URL is lost, the message is not recoverable. The server cannot reconstruct the decryption key.
- Frontend: HTML, CSS, vanilla JavaScript
- Crypto: Web Crypto API
- Backend/router: PHP
- Database: MySQL / MariaDB
- Storage model: encrypted payload + metadata only
Compared with the older README, the current app now includes compact share links, custom expiration timestamps, a Markdown toolbar and renderer, password strength feedback, an API docs page, updated responsive layout, and a local-font setup with no external font dependency. The older README you uploaded still documents the earlier API shape and older sharing format. ξfileciteξturn0file0ξ
https://your-domain.com/p/AbCdEf1234567890#BASE64URL_KEY
https://your-domain.com/p/AbCdEf1234567890#BASE64URL_SALT:pwd
https://your-domain.com/#AbCdEf1234567890:BASE64URL_KEY
https://your-domain.com/#AbCdEf1234567890:BASE64URL_SALT:pwd
The backend currently accepts these ID formats:
- legacy 32-character lowercase hex IDs
- compact 16-character URL-safe IDs (
[A-Za-z0-9_-]{16})
The UI currently generates the compact 16-character format by default.
The browser encrypts a JSON payload. Subject and content are both inside the encrypted blob.
Example logical structure before encryption:
{
"subject": "Optional subject",
"content": "Secret message with **Markdown** support"
}The server never sees this plaintext object.
Create a database, then import database.sql.
CREATE TABLE IF NOT EXISTS pastes (
id VARCHAR(32) PRIMARY KEY,
data TEXT NOT NULL,
created_at BIGINT NOT NULL,
expires_at BIGINT NOT NULL,
burn_after_read TINYINT(1) DEFAULT 0,
has_password TINYINT(1) DEFAULT 0,
views INT DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE INDEX idx_expires ON pastes(expires_at);Edit the database constants in index.php:
const DB_HOST = 'localhost';
const DB_USER = 'your_db_user';
const DB_PASS = 'your_db_password';
const DB_NAME = 'your_db_name';Upload the project files to your web root.
/public_html/
βββ .htaccess
βββ api-docs.php
βββ database.sql
βββ index.html
βββ index.php
βββ LICENSE
βββ README.md
βββ script.js
βββ style.css
Apache .htaccess used by the project:
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
RewriteRule ^ index.php [L]crypto.subtle requires a secure context in production. Use:
- HTTPS on your domain, or
localhostduring local development
| File | Purpose |
|---|---|
index.php |
router + API backend |
index.html |
single-page app UI |
script.js |
encryption, decryption, UI behavior, Markdown tools |
style.css |
responsive styling |
api-docs.php |
human-friendly API documentation page |
database.sql |
MySQL schema |
.htaccess |
Apache rewrite rules |
- optional subject
- Markdown toolbar
- multiline textarea
- preset expiration dropdown
- custom expiration datetime picker
- password-protection toggle
- burn-after-reading toggle
- password strength meter
- subject display
- rendered Markdown output
- copy decrypted text button
- burn-after-read warning when applicable
Markdown is stored as plain text inside the encrypted payload and is rendered only after decryption.
Supported authoring helpers include:
- Bold
- Italic
Strikethrough- headings
- quotes
- bullet lists
- numbered lists
- links
- inline code
- fenced code blocks
This keeps the stored payload simple while still giving the recipient a readable result view.
Once deployed, the built-in docs page is available at:
https://your-domain.com/api/docs
- API stores ciphertext only
- API does not perform encryption for you
- client must encrypt before sending data
- password or raw key must never be sent to the server
| Method | Route | Description |
|---|---|---|
GET |
/api/health |
service health check |
GET |
/api/options |
API capabilities, limits, routes |
GET |
/api/docs |
HTML API documentation |
POST |
/api/pastes |
create a paste |
POST |
/api/pastes/{id} |
create a paste with a specific ID |
GET |
/api/pastes/{id} |
fetch encrypted payload |
GET |
/api/pastes/{id}/meta |
fetch metadata only |
POST |
/api/create |
legacy create route |
GET |
/api/get/{id} |
legacy read route |
From the current backend:
- minimum expiration: 300 seconds
- maximum expiration: 31536000 seconds (365 days)
- max encrypted payload size: 4 MiB
You can create a paste using any of these shapes:
- nested byte arrays
{
"encryptedData": {
"iv": [12, 34, 56],
"data": [99, 88, 77]
}
}- top-level byte arrays
{
"iv": [12, 34, 56],
"data": [99, 88, 77]
}- nested base64url strings
{
"encryptedData": {
"ivBase64": "AAECAwQFBgcICQoL",
"dataBase64": "mYh3"
}
}- top-level base64url strings
{
"ivBase64": "AAECAwQFBgcICQoL",
"dataBase64": "mYh3"
}ivBase64url / dataBase64url are also accepted.
| Field | Required | Description |
|---|---|---|
id |
optional | custom paste ID |
encryptedData or equivalent |
yes | ciphertext payload |
expiresIn |
optional | expiry in seconds |
customExpiresAt |
optional | exact Unix timestamp in seconds |
burnAfterRead |
optional | delete after first successful read |
hasPassword |
optional | indicates password is required on the client |
If customExpiresAt is present, it overrides expiresIn.
curl -X POST https://your-domain.com/api/pastes \
-H "Content-Type: application/json" \
-d '{
"id": "AbCdEf1234567890",
"encryptedData": {
"iv": [12, 34, 56, 78, 90, 12, 34, 56, 78, 90, 12, 34],
"data": [189, 45, 78, 201, 156, 78, 33, 45]
},
"expiresIn": 86400,
"burnAfterRead": false,
"hasPassword": false
}'{
"success": true,
"apiVersion": "1.2",
"id": "AbCdEf1234567890",
"expiresAt": 1735689600,
"expiresIn": 86400,
"burnAfterRead": false,
"hasPassword": false,
"url": "https://your-domain.com/p/AbCdEf1234567890",
"retrieveUrl": "https://your-domain.com/api/pastes/AbCdEf1234567890",
"metaUrl": "https://your-domain.com/api/pastes/AbCdEf1234567890/meta",
"docsUrl": "https://your-domain.com/api/docs"
}curl https://your-domain.com/api/pastes/AbCdEf1234567890Example response:
{
"success": true,
"apiVersion": "1.2",
"id": "AbCdEf1234567890",
"encryptedData": {
"iv": [12, 34, 56, 78, 90, 12, 34, 56, 78, 90, 12, 34],
"data": [189, 45, 78, 201, 156, 78, 33, 45],
"ivBase64": "AAECAwQFBgcICQoL",
"dataBase64": "mYh3"
},
"data": {
"iv": [12, 34, 56, 78, 90, 12, 34, 56, 78, 90, 12, 34],
"data": [189, 45, 78, 201, 156, 78, 33, 45]
},
"burnAfterRead": false,
"hasPassword": false,
"created": 1735603200000,
"expiresAt": 1735689600,
"views": 1
}When burnAfterRead is enabled, the first successful fetch from GET /api/pastes/{id} returns the ciphertext and then removes that paste from storage.
curl https://your-domain.com/api/pastes/AbCdEf1234567890/metaExample response:
{
"success": true,
"apiVersion": "1.2",
"id": "AbCdEf1234567890",
"shareUrl": "https://your-domain.com/p/AbCdEf1234567890",
"retrieveUrl": "https://your-domain.com/api/pastes/AbCdEf1234567890",
"created": 1735603200000,
"expiresAt": 1735689600,
"remainingSeconds": 86400,
"burnAfterRead": false,
"hasPassword": false,
"views": 0
}curl https://your-domain.com/api/healthcurl https://your-domain.com/api/options/api/options returns API version, limits, presets, capabilities, endpoints, and notes.
| Browser | Status |
|---|---|
| Chrome / Edge | β |
| Firefox | β |
| Safari | β |
| Internet Explorer | β |
A secure context is required for the Web Crypto API.
- database leaks still expose ciphertext only
- server admins do not have plaintext or keys
- password material stays client-side
- URL fragment is not sent to the backend
- malware on sender or recipient device
- leaked full share URLs
- weak passwords chosen by users
- copied plaintext after decryption
- screenshots or shoulder surfing
- send password separately from the URL
- use burn-after-read for highly sensitive messages
- prefer long random passwords
- use private browsing on shared devices
- do not paste highly sensitive links into third-party chatbots or analytics tools
- improved composer layout for desktop and mobile
- cleaner stacked settings cards for expiration and security options
- compact Markdown toolbar
- icon-only social links in the footer
- consistent visual style shared by the app and API docs page
- encrypted file attachments
- QR code for secure links
- separate key-sharing mode
- OpenAPI / Swagger export
- admin-only cleanup / moderation tools
- theme switcher
Issues and pull requests are welcome.
If you open a PR, try to keep these guarantees intact:
- client-side encryption only
- zero-knowledge storage model
- no accidental leakage of key material to the server
- backward compatibility for existing shared links where possible
MIT β see LICENSE.
TheGreatAzizi
- GitHub: @TheGreatAzizi
- X: @the_azzi
- Telegram: @luluch_code
This project is provided as is without warranty of any kind. You are responsible for your own deployment security, backups, SSL/TLS configuration, database hardening, and safe sharing of URLs and passwords.