A service for sharing text through one-time links. Paste a secret, set an expiry, optionally add a password, send the link.
Stack: Astro, TypeScript, PostgreSQL.
What reaches the server
The browser encrypts with AES-256-GCM. The key stays in the URL fragment.
The server wraps the payload again with AES-256-GCM using a key kept outside the database, then stores the ciphertext and a hash of the share token.
Opening the link does nothing. Reveal is a POST. The server removes its layer and the browser decrypts.
Two readers, one secret
Read-then-delete is racy. Delivery is decided by which request actually deletes the row:
DELETE FROM secrets
WHERE token_hash = $1
AND expires_at > now()
AND reveal_proof_hash IS NOT DISTINCT FROM $2::text
RETURNING ciphertext, control_hash, expires_at
$2 is the verified authorization-proof hash, or NULL.
Only the request whose delete returns a row can deliver the payload.
The storage layer is unwrapped before the delete. Wrong key or corrupt ciphertext fails without consuming the secret.
After commit there is no retry. If delivery fails, the secret is gone. The control link still reports read.
Password without sending it
The browser derives a key from the password and fragment key via PBKDF2-HMAC-SHA256 (600k iterations) and HKDF.
That key encrypts a random authorization proof. The server stores the encrypted verifier and a hash of the proof.
Before reveal, the browser fetches the verifier and tries to decrypt it locally. Wrong password fails there. Correct password unlocks the proof, which is sent and checked before deletion.
This stops someone with only the share token from consuming a password-protected secret.
Top comments (0)