The Problem We Needed to Solve

When you create a one-time link on file.digipull.cloud, your browser needs to do two things with a single random key:

  1. Encrypt your secret — so nobody can read it, not even our server
  2. 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.

The problem: one key, two jobs
🔑One random keyGenerated in your browser
Same key for both?Encrypt AND identify?
⚠️Security riskServer could decrypt

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 file.digipull.cloud, our input is a 20-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 key
  • auth — produces the authentication token sent to the server
How HKDF derives two keys from one
🔑Master keyRandom 16-char string
🧬HKDF ExtractSalt: onetimelink:v2
🔀HKDF ExpandTwo separate outputs
The two derived keys
🔒Encryption keyinfo: "encrypt" → AES-GCM-256
🏷️Auth tokeninfo: "auth" → server lookup ID

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.

PropertyRaw SHA-256HKDF
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 file.digipull.cloud Uses HKDF

Here is the complete flow when you create a one-time link:

Creating a link (sender)

  1. Your browser generates a random 16-character key using the Web Crypto API
  2. HKDF derives two keys from it:
    • Encryption key (info: encrypt) — AES-GCM-256
    • Auth token (info: auth) — 256-bit hex string. The sender uploads only SHA-256 of this value; the recipient presents the value itself when reading
  3. Your browser encrypts the secret using the encryption key
  4. The encrypted blob + a SHA-256 hash of the auth token are sent to the server — never the token itself
  5. The master key goes into the URL fragment (#) — never sent to the server
Creating a link — what goes where
🔑Master keyStays in URL # fragment
🔒Encrypted blobSent to server (unreadable)
🏷️Auth tokenOnly its SHA-256 is uploaded

Opening a link (recipient)

  1. The recipient clicks the link
  2. The browser reads the master key from the URL fragment (never sent to server)
  3. HKDF derives the same two keys: encryption key + auth token
  4. The browser sends the auth token to the server, which hashes it and compares against the stored value, then returns the encrypted blob
  5. The server returns the blob and permanently deletes it — immediately, on this first read, which is the default. A link created to allow several reads is decremented instead and deleted when the last one is spent
  6. The browser decrypts the blob using the encryption key
  7. The secret is displayed to the recipient

At no point does the server have access to the master key or the encryption key. At rest it holds only a hash of the auth token and the encrypted blob — neither of which can read or destroy a secret. The auth token itself reaches the server only in the instant a recipient consumes the link. By default that read is the only one, and the record is deleted immediately after; a link created with a higher view allowance is decremented and deleted once it runs out.

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://file.digipull.cloud/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 20-character random string is generated using crypto.getRandomValues(). The character set is A-Za-z0-9-_ (64 URL-safe 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 keycrypto.subtle.deriveKey() with HKDF params (hash: SHA-256, salt: "onetimelink:v2", info: "encrypt"), producing an AES-GCM key with 256-bit length
  • Auth tokencrypto.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 SHA-256 of 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, alongside that hash. It never receives the auth token at save time, so the stored record cannot be used to retrieve or destroy the secret — reading requires the preimage, which only someone holding the link can derive.

The generated URL has the format: https://file.digipull.cloud/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 — immediately, on the first read, unless the link was created with a larger view allowance (up to 10), in which case the counter is decremented, the original TTL preserved, and the blob deleted on the final read.

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