🤫 Shh

Encrypted self-destructing secrets

Create a Secret

Encrypted in your browser — the server never sees it.

0 / 1024

If set, the recipient must enter this to decrypt. It never leaves your browser.

🔥

Self-Destructing

Links expire after viewing or set time

🔐

Zero-Knowledge E2EE

Server never sees plaintext — encrypted in your browser

Fast & Simple

No registration required

📚 API Documentation

Base URL

/api

Health Check 🌐 PUBLIC

GET /api/health

Returns system status, feature flags, and configuration.

curl https://your-site.pages.dev/api/health

Create Secret 🌐 PUBLIC

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.

Check Secret Metadata 🌐 PUBLIC

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 .

View & Decrypt Secret 🌐 PUBLIC

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.

File Attachments checking...

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.

Admin Stats 🔐 ADMIN ONLY

GET /api/admin/stats

curl -H "X-Admin-Token: your_token" https://your-site.pages.dev/api/admin/stats

Admin Purge All 🔐 ADMIN ONLY

POST /api/admin/purge

curl -X POST -H "X-Admin-Token: your_token" https://your-site.pages.dev/api/admin/purge

Admin Delete Single Secret 🔐 ADMIN ONLY

DELETE /api/admin/secret/{id}

curl -X DELETE -H "X-Admin-Token: your_token" https://your-site.pages.dev/api/admin/secret/{id}

Rate Limits

Create: 10/IP/hour · View: 50/IP/hour · Admin: 5/hour

Configurable via RATE_LIMIT_CREATE, RATE_LIMIT_VIEW, RATE_LIMIT_ADMIN env vars.

🔐 Security Notes

  • All secrets encrypted client-side with AES-256-GCM before leaving your browser
  • Encryption key travels in the URL fragment (#key) — never sent to the server
  • Optional passphrase: key is PBKDF2-HMAC-SHA-512 derived (210,000 iterations) and wrapped via AES-KW, entirely client-side
  • Server stores only ciphertext — zero-knowledge architecture
  • Secrets destroyed on last view (or at TTL expiry)
  • Admin token comparison is timing-safe (no length-leak side-channel)

💻 Client-Side Decryption (JavaScript)

How 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);

Frequently asked questions

Can Shh's server read my 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.

What happens after the secret is viewed?

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.

Can I protect a secret with a password?

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.

Can I attach files, not just text?

Yes, up to 5 files and 15 MB total per secret, encrypted client-side the same way as the text.

Is Shh free and open source?

Yes, the source is publicly available on GitHub and it's free to use or self-host.