DEV Community

Cover image for One-time secret sharing
Emiliano Saurin
Emiliano Saurin

Posted on Originally published at dev.to

One-time secret sharing

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
Enter fullscreen mode Exit fullscreen mode

$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.

Try it · https://github.com/njoylab/secrets

Top comments (0)