The Problem We Needed to Solve
When you create a one-time link on onetimelink.me, your browser needs to do two things with a single random key:
- Encrypt your secret — so nobody can read it, not even our server
- Identify the secret on the server — so the recipient can look it up and retrieve it
The challenge: if we use the same key for both, the server could potentially use the lookup identifier to decrypt the secret. We need two separate keys — one for encryption, one for authentication — derived from a single master key. That is exactly what HKDF does.
What Is HKDF?
HKDF stands for HMAC-based Key Derivation Function. It is defined in RFC 5869 and is one of the most widely used key derivation standards in modern cryptography.
In simple terms: HKDF takes one piece of secret material (your key) and produces multiple independent keys from it. Each derived key is cryptographically independent — knowing one tells you nothing about the others.
Think of it like a tree. You plant one seed (your master key), and it grows into separate branches (derived keys). Each branch is strong on its own, and cutting one branch does not affect the others.
HKDF Has Two Steps
HKDF works in two phases:
Step 1: Extract
The extract step takes your input key material (which might not be uniformly random) and a salt value, and produces a fixed-length pseudorandom key (PRK). This "concentrates" the randomness into a clean, uniformly distributed key.
At onetimelink.me, our input is a 16-character random string generated using the Web Crypto API, and our salt is onetimelink:v2.
Step 2: Expand
The expand step takes the PRK from step 1 and an "info" string, and produces as many derived keys as you need. The info string acts as a label — different labels produce completely different keys.
We use two info strings:
encrypt— produces the AES-GCM-256 encryption keyauth— produces the authentication token sent to the server
Why Not Just Use SHA-256?
Our previous approach was simple: take the master key, hash it with SHA-256, and use the hash as both the server identifier and part of the encryption process. This worked, but it had a subtle weakness.
| Property | Raw SHA-256 | HKDF |
|---|---|---|
| Key separation | ✗ One hash for everything | ✓ Independent keys per purpose |
| Formal security proof | ✗ Ad-hoc construction | ✓ Proven in RFC 5869 |
| Domain separation | ✗ No labeling mechanism | ✓ Info parameter separates contexts |
| Salt support | ~ Manual concatenation | ✓ Built-in salt parameter |
| Industry standard | ✗ Custom scheme | ✓ Used by TLS 1.3, Signal, WireGuard |
The core issue with raw SHA-256 is that there is no formal separation between the encryption key and the auth token. They are mathematically related in a way that, while not practically exploitable today, does not follow cryptographic best practices. HKDF guarantees that the two derived keys are cryptographically independent — knowing the auth token gives you zero information about the encryption key.
Who else uses HKDF? TLS 1.3 (every HTTPS connection), the Signal Protocol (WhatsApp, Signal), WireGuard VPN, and the Noise Framework all use HKDF for key derivation. We are in good company.
How onetimelink.me Uses HKDF
Here is the complete flow when you create a one-time link:
Creating a link (sender)
- Your browser generates a random 16-character key using the Web Crypto API
- HKDF derives two keys from it:
- Encryption key (info:
encrypt) — AES-GCM-256 - Auth token (info:
auth) — 256-bit hex string
- Encryption key (info:
- Your browser encrypts the secret using the encryption key
- The encrypted blob + auth token are sent to the server
- The master key goes into the URL fragment (
#) — never sent to the server
Opening a link (recipient)
- The recipient clicks the link
- The browser reads the master key from the URL fragment (never sent to server)
- HKDF derives the same two keys: encryption key + auth token
- The browser sends the auth token to the server to fetch the encrypted blob
- The server returns the blob and permanently deletes it
- The browser decrypts the blob using the encryption key
- The secret is displayed to the recipient
At no point does the server have access to the master key or the encryption key. It only ever sees the auth token (which cannot be reversed to obtain the encryption key) and the encrypted blob (which is meaningless without the encryption key).
Why the URL Fragment Matters
The master key lives in the URL fragment — the part after the # symbol. This is critical because URL fragments are never sent to the server. They are a client-side-only feature defined in the HTTP specification.
When your browser requests https://onetimelink.me/v#abc123, it sends a request for /v — the #abc123 part stays entirely in the browser. This is not a custom security feature — it is how every browser has worked since the beginning of the web.
Important distinction: Query parameters (?key=abc123) ARE sent to the server. URL fragments (#abc123) are NOT. This is why the key must be in the fragment, not in a query parameter. Many other secret-sharing services get this wrong.
When Should You Use HKDF?
HKDF is the right tool whenever you need to:
- Derive multiple keys from one secret — the most common use case
- Separate concerns — encryption, authentication, signing should use different keys
- Convert non-uniform randomness — HKDF's extract step normalizes entropy
- Version your key scheme — changing the salt or info string produces completely new keys without changing the master key
HKDF is NOT the right tool for:
- Password hashing — use Argon2, bcrypt, or scrypt instead (HKDF is not designed to be slow)
- Generating keys from weak passwords — HKDF assumes the input already has sufficient entropy
The Full Cryptographic Flow
For developers who want to implement something similar or audit our approach, here is the complete flow step by step.
1. Key material generation
A 16-character random string is generated using crypto.getRandomValues() with rejection sampling to avoid modulo bias. The character set is 0-9A-Za-z (62 characters). If the user set an optional passphrase, it is prepended to the random string to form the full secret key: fullSecretKey = userPassphrase + randomKey.
2. HKDF key derivation
The full secret key is imported as raw key material into the Web Crypto API with algorithm HKDF. Two independent outputs are derived:
- Encryption key —
crypto.subtle.deriveKey()with HKDF params(hash: SHA-256, salt: "onetimelink:v2", info: "encrypt"), producing an AES-GCM key with 256-bit length - Auth token —
crypto.subtle.deriveBits()with HKDF params(hash: SHA-256, salt: "onetimelink:v2", info: "auth"), producing 256 bits, hex-encoded to a 64-character string
3. Encryption
A random 12-byte initialization vector (IV) is generated via crypto.getRandomValues(). The secret message is encrypted with AES-GCM using the derived encryption key and this IV. The output ciphertext includes the GCM authentication tag (built into the Web Crypto API).
The final encrypted payload is formatted as base64url(iv).base64url(ciphertext) — the IV and ciphertext concatenated with a dot separator, both URL-safe base64 encoded.
4. Storage and URL structure
The encrypted payload and the hex-encoded auth token are sent to the server via POST /api/saveSecret. The server stores the encrypted blob keyed by a server-generated ID and indexed by the auth token.
The generated URL has the format: https://onetimelink.me/v/#randomKeyServerId. The random key lives in the URL fragment after # — it is never sent to the server by the browser. The server ID is appended so the recipient's browser knows which blob to request.
5. Decryption (recipient side)
The recipient's browser extracts the random key from the URL fragment, re-derives the auth token using the same HKDF process, and sends it to the server to fetch the encrypted blob. Then it re-derives the encryption key, splits the payload at the dot to recover the IV and ciphertext, and decrypts with AES-GCM. The server permanently deletes the blob after returning it.
The entire implementation is open source — about 200 lines of JavaScript with zero dependencies beyond the Web Crypto API. You can read the full encryption code on GitHub and verify every claim in this article yourself.
See HKDF in action
Create an encrypted one-time link. Your secret is protected by HKDF-derived AES-GCM-256 encryption, entirely in your browser.
Create a secure link