Back to blog

One-Time Secrets 101: How Self-Destructing Links Actually Work

Published September 15, 2025
Updated September 15, 2025
6 min read

A practical, beginner-friendly explanation of one-time secrets: the tokens, storage, encryption, expiration, and the real-world pitfalls like link preview bots.

If you ever send a password in Slack, email, or just copy it in chat, you probably feel this small stress: “Now this secret is living forever in history.” One-time secrets try to fix this. You paste a secret, you get a link, and after first open (or after some time) it is gone.

Sounds like magic, but it is very normal engineering. In this post I explain how these links work, what security they really give, and where they still can fail. If you want to try a real one-time secret app while reading, you can use mine: bitburner.vberkoz.com.

What is a “One-Time Secret” (and what it is not)

A one-time secret service is a tool that lets you share a piece of sensitive text (password, API key, recovery code) using a URL that can be opened only once.

Important: it does not mean “nobody can steal it”. It means:

  • If someone opens the link first, you will not be able to open it after.
  • The secret is not stored forever in chat logs, tickets, or email threads.
  • The service tries to delete the secret after it was revealed (or after expiration time).

If your recipient device is compromised, or if the link is leaked, one-time secret will not save you. It’s a tool to reduce accidental exposure and reduce long-term blast radius.

The big reason why one-time secret feels safer is not only “one view”. It is also “less copies”.

When you paste a password into chat, it sits in history, backups, search, exports, screenshots.

When you paste it into one-time secret tool, chat contains only a random URL token, while the real secret is stored somewhere else for a short time.

So even if somebody reads chat history months later, they don’t see the password.

At minimum, a one-time secret service needs to create a random “lookup key” and store the secret under it.

Typical flow:

  1. User submits secret text (maybe also TTL like 10 minutes).
  2. Server generates a random token, like 32 bytes, then encodes as URL-safe Base64.
  3. Server stores a record: { token_hash, ciphertext, expires_at, status }
  4. Server returns a link: https://example.com/s/<token>

Why not store plain token? Because logs happen. If your database leaks, attacker can take tokens and fetch secrets. So many implementations store only a hash of token, similar to password hashing (but usually simple SHA-256 is enough, because token already is high entropy).

So database keeps sha256(token) and the URL contains the raw token. When someone opens the link, server hashes it and finds the record.

Expiration: TTL is a security feature, not only UX

Most services support “expires in 1 hour / 1 day”. This is not just convenience.

Expiration reduces how long the secret is available if the link leaks, and how long it can survive in backups.

Implementation details:

  • store expires_at timestamp,
  • on read, check time and reject if expired,
  • run a cleanup job (cron) to delete old rows,
  • for cache-based storage (Redis), use native TTL so deletion happens automatically.

Even if you do cleanup job, still best is: don’t keep it longer than needed.

“Self-destruct” is mainly an atomic delete-on-read

The most important part is preventing second read. If two requests arrive at same time and you do “read then delete”, you can accidentally reveal twice.

So you need an atomic operation: “claim this secret if it is not consumed yet, and return it once”. In SQL it’s usually a transaction + conditional update/delete. In Redis it can be GETDEL (or a small Lua script). Main idea: no separate “read then delete”.

Where encryption fits: server-side, client-side, or both

One-time secret can store your secret as plain text in DB and still be “one-time”. But it is not good practice.

Common options:

  1. Encrypt at rest (server-side encryption): server encrypts secret before storing, using a server key (KMS, env var, HSM).
  2. End-to-end / client-side encryption: browser encrypts secret locally, server stores only ciphertext, and decryption key lives in the URL fragment.

Option 2 is very interesting:

  • Link looks like https://example.com/s/<id>#<key>
  • The #<key> part (URL fragment) is not sent to server in HTTP request.
  • Server never sees the decryption key, only stores ciphertext.

So even if server database leaks, attacker gets ciphertext but not key. Nice.

But it also has tradeoffs: you must trust frontend crypto, and you still must protect the full URL (if attacker gets it, they have the key).

In practice, best systems do: client-side encryption + short TTL + atomic consume.

This is the part many people don’t expect. When you paste a URL into Slack, Teams, iMessage, email clients, they often fetch it to generate a preview. Also corporate security scanners may “click” links in emails automatically.

For one-time secret, that means:

  • The bot might open the link first.
  • Your human recipient then sees “already consumed”.

How services handle it:

  • show a landing page “Click to reveal” (so preview bot only sees landing, not the secret),
  • require a POST action (form submit) to reveal, not just GET,
  • add a captcha or simple confirmation step (tradeoff with UX),
  • detect known preview user agents (not perfect, but can help).

If you implement your own, do not reveal secret on first GET request without user intent. Always have a second step.

Logging and observability: don’t leak secrets by accident

Even if your storage is perfect, you can still leak secrets through logs.

Common mistakes: logging request bodies, logging full URLs, sending secrets to error trackers, or capturing URLs in analytics.

Minimum rules I use: never log secret payload, never log raw tokens, and set Cache-Control: no-store + Referrer-Policy: no-referrer on reveal pages.

Usability details that matter more than you think

One-time secrets are security tool, but UX decides if people use it.

Small features that help a lot:

  • “Copy link” button with clear “This link works only once”.
  • Option to set TTL (10 min, 1 hour, 1 day).
  • Burn-after-reading (default on).
  • Optional passphrase (extra layer if link leaks).
  • Clear error message when consumed (“It was opened already or expired”).

Also, consider that people will screenshot the secret. You can’t prevent it. Your goal is to reduce accidental exposure, not to control user life.

Final thoughts ✅

Self-destructing links are not a Hollywood hack trick. It is a simple pattern: store secret shortly, give a random pointer, and delete (atomically) on first read.

You get a big win: secrets are not sitting forever in chat history, and the damage window is smaller. But a one-time link is still a link—anyone who gets it first can read it first. Keep TTL short and rotate credentials when you can.

If you want a simple place to share a password without leaving it in chat history, use my one-time secrets app: bitburner.vberkoz.com.