Encryption¶
Ghostbit uses true end-to-end encryption: all encryption and decryption happens in the client (browser or CLI). The server stores ciphertext only and can never read paste content.
Algorithm¶
| Component | Value |
|---|---|
| Cipher | AES-256-GCM |
| Key size | 256 bits |
| Nonce | 12 bytes (random, per paste) |
| Auth tag | 128 bits (included in ciphertext) |
| KDF (password) | PBKDF2-SHA256, 600 000 iterations |
| KDF salt | 16 bytes (random, per paste) |
Key management¶
A random 256-bit key is generated by crypto.subtle.generateKey() (browser) or os.urandom(32) (CLI).
The key is never sent to the server. It lives exclusively in the URL fragment:
The #fragment is never transmitted in HTTP requests — it stays in the browser.
The key is derived from the user's password using PBKDF2-SHA256 (600 000 iterations). A random 16-byte salt is generated per paste and stored server-side (it is not secret).
The URL fragment contains only the delete token:
The password is never sent to the server.
Fragment format¶
KEY_B64URL— base64url-encoded AES-256-GCM key (no padding)DELETE_TOKEN— raw delete token (shown once, used to enable the delete button)
The server stores a SHA-256 hash of the delete token, never the raw value.
Threat model¶
| Threat | Protected? |
|---|---|
| Server compromise | Yes — server only has ciphertext |
| Database leak | Yes — ciphertext without the key is useless |
| Network interception (HTTPS) | Yes — key is in fragment, never transmitted |
| Malicious server operator | Yes — server can't decrypt |
| URL shared with wrong person | No — the key is in the URL |
| Weak password | Depends on the user |
Secure Context
crypto.subtle requires a Secure Context (HTTPS or localhost).
Ghostbit automatically redirects 127.0.0.1 → localhost for local development.
CLI encryption¶
The CLI (gbit) mirrors the browser encryption using Python's cryptography library:
AESGCMfor AES-256-GCMPBKDF2HMACwith SHA-256 for password derivationos.urandomfor key and nonce generation
The ciphertext format is identical — pastes created by the CLI can be decrypted in the browser and vice versa.
Webhook signatures¶
When WEBHOOK_SECRET is configured, every webhook delivery is signed with HMAC-SHA256 and carries a timestamp header:
The signature is computed over the raw JSON request body. The timestamp header is always present (even without WEBHOOK_SECRET) and mirrors payload.timestamp — verifiers can use it for replay protection without having to parse the body first. A typical receiver rejects deliveries older than 5 minutes.
To verify on the receiving end:
Warning
Always use a constant-time comparison (hmac.compare_digest, timingSafeEqual) to prevent timing attacks.
Replay protection
Even with a valid signature, an attacker who once intercepted a delivery could replay it. Reject any request whose X-Ghostbit-Webhook-Timestamp differs from the current time by more than ~5 minutes: