Encrypted self-destructing secrets
Encrypted in your browser — the server never sees it.
If set, the recipient must enter this to decrypt. It never leaves your browser.
✅ Your secret link is ready — copy it now
⏱️ Expires in · viewed
🔑 Remind the recipient: they'll need the passphrase you set.
Links expire after viewing or set time
Server never sees plaintext — encrypted in your browser
No registration required
/api
GET /api/health
Returns system status, feature flags, and configuration.
curl https://your-site.pages.dev/api/health
POST /api/secret
💻 Bash example — encrypt then create:
#!/bin/bash
# 1. Generate a random 256-bit key
KEY_B64=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\n')
# 2. Generate a random 12-byte IV
IV_HEX=$(openssl rand -hex 12)
# 3. Encrypt with AES-256-GCM
SECRET="My secret message"
ENCRYPTED=$(printf '%s' "$SECRET" \
| openssl enc -aes-256-gcm -nosalt -nopad \
-K $(echo "$KEY_B64" | tr '-_' '+/' | base64 -d | xxd -p -c 256) \
-iv "$IV_HEX" 2>/dev/null | base64 | tr '+/' '-_' | tr -d '=\n')
# 4. Combine IV + ciphertext (matches browser format)
IV_B64=$(echo "$IV_HEX" | xxd -r -p | base64 | tr '+/' '-_' | tr -d '=\n')
PAYLOAD="${IV_B64}${ENCRYPTED}"
# 5. POST to API
RESULT=$(curl -s -X POST https://your-site.pages.dev/api/secret \
-H "Content-Type: application/json" \
-d "{\"encryptedData\":\"$PAYLOAD\",\"ttl\":12,\"maxViews\":1}")
ID=$(echo "$RESULT" | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
echo "Share: https://your-site.pages.dev/view/$ID#$KEY_B64"
⚠️ Browser-based creation is strongly recommended — the Web Crypto API handles GCM authentication tags correctly. The bash example above is for automation/testing only.
GET /api/secret/{id}/metadata
Returns: exists, viewed, hasFiles, fileCount, viewsRemaining, maxViews, expiresAt, passphraseProtected
💻 Bash example:
curl -s https://your-site.pages.dev/api/secret/{id}/metadata | jq .
POST /api/secret/{id}/view
Returns encrypted ciphertext. Decrements view count; deletes on last view.
💻 Bash example — fetch and decrypt:
#!/bin/bash
# Full URL: https://your-site.pages.dev/view/{id}#{key}
URL="https://your-site.pages.dev/view/abc-123-def#xY9zK2mP..."
ID=$(echo "$URL" | sed 's|.*/view/||' | cut -d'#' -f1)
KEY_B64=$(echo "$URL" | cut -d'#' -f2)
# Fetch encrypted payload (⚠️ this consumes a view)
RESPONSE=$(curl -s -X POST "https://your-site.pages.dev/api/secret/$ID/view")
PAYLOAD=$(echo "$RESPONSE" | grep -o '"encryptedData":"[^"]*"' | cut -d'"' -f4)
# Split IV (first 16 base64url chars = 12 bytes) from ciphertext
IV_B64=$(echo "$PAYLOAD" | head -c 16)
CT_B64=$(echo "$PAYLOAD" | tail -c +17)
IV_HEX=$(echo "$IV_B64" | tr '-_' '+/' | base64 -d | xxd -p -c 256)
KEY_HEX=$(echo "$KEY_B64" | tr '-_' '+/' | base64 -d | xxd -p -c 256)
echo "$CT_B64" | tr '-_' '+/' | base64 -d \
| openssl enc -d -aes-256-gcm -nosalt -nopad -K "$KEY_HEX" -iv "$IV_HEX"
⚠️ Each POST to /view counts as one view and cannot be undone.
Loading feature status from server...
💻 Bash example — encrypt a file and attach to a secret:
#!/bin/bash
# 1. Generate key and IV
KEY_B64=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\n')
IV_HEX=$(openssl rand -hex 12)
KEY_HEX=$(echo "$KEY_B64" | tr '-_' '+/' | base64 -d | xxd -p -c 256)
IV_B64=$(echo "$IV_HEX" | xxd -r -p | base64 | tr '+/' '-_' | tr -d '=\n')
# 2. Encrypt the secret text
SECRET="My secret message"
TEXT_CT=$(printf '%s' "$SECRET" \
| openssl enc -aes-256-gcm -nosalt -nopad -K "$KEY_HEX" -iv "$IV_HEX" 2>/dev/null \
| base64 | tr '+/' '-_' | tr -d '=\n')
TEXT_PAYLOAD="${IV_B64}${TEXT_CT}"
# 3. Encrypt the file using the same key
FILE="/path/to/document.pdf"
FILE_IV_HEX=$(openssl rand -hex 12)
FILE_IV_B64=$(echo "$FILE_IV_HEX" | xxd -r -p | base64 | tr '+/' '-_' | tr -d '=\n')
FILE_CT=$(openssl enc -aes-256-gcm -nosalt -nopad -K "$KEY_HEX" -iv "$FILE_IV_HEX" \
-in "$FILE" 2>/dev/null | base64 | tr '+/' '-_' | tr -d '=\n')
FILE_PAYLOAD="${FILE_IV_B64}${FILE_CT}"
FILENAME=$(basename "$FILE")
FILESIZE=$(wc -c < "$FILE" | tr -d ' ')
MIMETYPE="application/pdf"
# 4. POST secret + encrypted file
RESULT=$(curl -s -X POST https://your-site.pages.dev/api/secret \
-H "Content-Type: application/json" \
-d "{
\"encryptedData\": \"$TEXT_PAYLOAD\",
\"ttl\": 12,
\"maxViews\": 1,
\"files\": [{
\"name\": \"$FILENAME\",
\"type\": \"$MIMETYPE\",
\"size\": $FILESIZE,
\"data\": \"$FILE_PAYLOAD\"
}]
}")
ID=$(echo "$RESULT" | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
echo "Share: https://your-site.pages.dev/view/$ID#$KEY_B64"
⚠️ Browser-based creation is recommended. The bash approach omits the AES-GCM authentication tag that OpenSSL produces — the recipient's browser will reject files encrypted this way. Use the web UI for real file transfers; this example shows the payload structure only.
GET /api/admin/stats
curl -H "X-Admin-Token: your_token" https://your-site.pages.dev/api/admin/stats
POST /api/admin/purge
curl -X POST -H "X-Admin-Token: your_token" https://your-site.pages.dev/api/admin/purge
DELETE /api/admin/secret/{id}
curl -X DELETE -H "X-Admin-Token: your_token" https://your-site.pages.dev/api/admin/secret/{id}
Create: 10/IP/hour · View: 50/IP/hour · Admin: 5/hour
Configurable via RATE_LIMIT_CREATE, RATE_LIMIT_VIEW, RATE_LIMIT_ADMIN env vars.
#key) — never sent to the serverHow to decrypt a secret programmatically in the browser or Node.js:
// Full URL: https://your-site.pages.dev/view/{id}#{key}
const url = window.location.href;
const secretId = url.split('/view/')[1].split('#')[0];
const keyB64url = url.split('#')[1];
// Import AES-256-GCM key from URL fragment
const keyBytes = Uint8Array.from(
atob(keyB64url.replace(/-/g, '+').replace(/_/g, '/')),
c => c.charCodeAt(0)
);
const key = await crypto.subtle.importKey(
'raw', keyBytes, { name: 'AES-GCM' }, false, ['decrypt']
);
// Fetch encrypted payload (⚠️ consumes a view)
const res = await fetch(`/api/secret/${secretId}/view`, { method: 'POST' });
const { encryptedData } = await res.json();
// Decode base64url → bytes, split IV (12 bytes) + ciphertext
const b64 = encryptedData.replace(/-/g, '+').replace(/_/g, '/');
const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
const iv = bytes.slice(0, 12);
const cipher = bytes.slice(12);
// Decrypt
const plain = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, cipher);
const secret = new TextDecoder().decode(plain);
console.log('Secret:', secret);
No. Encryption happens in your browser with AES-256-GCM before anything is sent. The encryption key lives only in the URL fragment after the #, which browsers never send to the server, so the server only ever stores ciphertext it cannot decrypt.
The secret is deleted once it reaches its allowed view count (1 to 10, default 1) or when its time-to-live expires, whichever comes first. Once deleted, the link can no longer reveal anything.
Yes, an optional passphrase adds a second factor. The passphrase never leaves the browser: it's used with PBKDF2 to derive a key that wraps the encryption key, and the recipient must enter the same passphrase to unwrap it.
Yes, up to 5 files and 15 MB total per secret, encrypted client-side the same way as the text.
Yes, the source is publicly available on GitHub and it's free to use or self-host.