🤫 Shh - Secure Secrets

Share secrets securely with self-destructing links

Create a Secret

0 / 1024 characters
🔥

Self-Destructing

Links expire after viewing or set time

🔐

Secure

No server-side logs of secret content

Fast & Simple

No registration required

Admin Portal
📚 API Documentation

Base URL

/api

Health Check

GET /api/health

Returns system status and configuration

{
  "status": "ok",
  "kvBound": true,
  "adminTokenSet": true,
  "features": {...},
  "rateLimits": {...}
}

Create Secret

POST /api/secret

Request Body:

{
  "encryptedData": "base64url_encrypted_content",
  "ttl": 12,  // hours (1-24)
  "files": []  // optional
}

Response:

{
  "id": "uuid-v4",
  "expiresIn": 12
}

⚠️ Data must be client-side encrypted with AES-256-GCM before sending

Check Secret Metadata

GET /api/secret/{id}/metadata

Response:

{
  "exists": true,
  "viewed": false,
  "hasFiles": false,
  "fileCount": 0
}

View Secret (One-Time)

POST /api/secret/{id}/view

Response:

{
  "encryptedData": "base64url_encrypted_content",
  "files": []
}

⚠️ Secret is deleted immediately after retrieval. Client must decrypt with key from URL fragment.

Admin Stats

GET /api/admin/stats

Headers: X-Admin-Token: your_token

Response:

{
  "totalSecrets": 5,
  "secrets": [{"id": "uuid"}, ...]
}

Rate Limits

  • Create: 10 secrets per IP per hour
  • View: 50 views per IP per hour
  • Admin: 5 actions per hour

Configurable via environment variables

🔐 Security Notes

  • All secrets are encrypted client-side with AES-256-GCM
  • Server never sees plaintext (zero-knowledge architecture)
  • Encryption keys are in URL fragments (never sent to server)
  • One-time use: secrets are deleted immediately after viewing
  • Rate limiting prevents abuse
  • Admin tokens use timing-safe comparison
  • Anonymous users cannot list secret IDs - only admins can
  • Unauthenticated API access - only allows retrieving specific secrets with valid ID

💻 Client-Side Decryption Example

How to decrypt a secret programmatically:

// Extract ID and key from URL
const url = "https://shh.example.com/view/{id}#{key}";
const [path, fragment] = url.split('#');
const secretId = path.split('/').pop();
const encryptionKey = fragment;

// Fetch encrypted secret
const res = await fetch(`/api/secret/${secretId}/view`, {
  method: 'POST'
});
const { encryptedData } = await res.json();

// Import key from base64url
const keyBytes = Uint8Array.from(
  atob(encryptionKey.replace(/-/g,'+').replace(/_/g,'/')),
  c => c.charCodeAt(0)
);
const key = await crypto.subtle.importKey(
  'raw', keyBytes, {name: 'AES-GCM'}, false, ['decrypt']
);

// Decrypt
const combined = Uint8Array.from(
  atob(encryptedData.replace(/-/g,'+').replace(/_/g,'/')),
  c => c.charCodeAt(0)
);
const iv = combined.slice(0, 12);
const ciphertext = combined.slice(12);

const plaintext = await crypto.subtle.decrypt(
  {name: 'AES-GCM', iv}, key, ciphertext
);
const secret = new TextDecoder().decode(plaintext);
console.log('Secret:', secret);