DEV Community

FileShot
FileShot

Posted on

How URL Fragments Solve the Key Distribution Problem in Zero-Knowledge File Sharing

One of the core challenges in building a zero-knowledge file sharing service is key distribution: how do you give the recipient the decryption key without the server ever seeing it?

The answer has been embedded in the HTTP spec since 1994. It's called the URL fragment.

The Problem

In most "encrypted" file sharing services, the workflow looks like this:

  1. User uploads file
  2. Service encrypts it server-side (using their own keys)
  3. Service sends recipient a download link
  4. Service decrypts the file when the recipient requests it

This is encryption at rest. The service can read every file whenever they want. The encryption protects against disk theft, not against the service itself or its government.

True zero-knowledge means the server is architecturally prevented from decrypting files — not by policy, but by the fact that it never has the key.

The URL Fragment Solution

The HTTP/1.1 RFC 2396 specifies:

"A fragment identifier is separated from the rest of a URI by a hash (#) character and contains additional resource information."

And critically: HTTP clients (browsers) do not include the fragment in requests to servers. The fragment lives entirely in the browser.

This is why anchor links work without a round-trip: when you navigate to page.html#section-3, the browser fetches page.html and then scrolls to #section-3 locally, without telling the server which anchor you navigated to.

Applying This to Key Transport

When a file is shared with zero-knowledge encryption, the share URL looks like:

https://fileshot.io/d/a1b2c3d4e5#AES_KEY_GOES_HERE
Enter fullscreen mode Exit fullscreen mode

When the recipient visits this URL:

  1. Browser sends GET /d/a1b2c3d4e5 to the server — the fragment is stripped
  2. Server has no idea what AES_KEY_GOES_HERE is
  3. Server returns the encrypted ciphertext
  4. Browser extracts the key from location.hash
  5. Browser decrypts the file locally using the Web Crypto API

You can verify this behavior in your browser's DevTools. Navigate to any URL with a fragment and look at the Network tab — the request URL sent to the server will never include the #... portion.

The Implementation with Web Crypto API

Here's the core of how this works in browser JavaScript:

// Encrypt
async function encryptFile(file) {
  const key = await crypto.subtle.generateKey(
    { name: 'AES-GCM', length: 256 },
    true, // extractable
    ['encrypt', 'decrypt']
  );

  const iv = crypto.getRandomValues(new Uint8Array(12));
  const ciphertext = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    key,
    await file.arrayBuffer()
  );

  // Export key as URL-safe base64
  const rawKey = await crypto.subtle.exportKey('raw', key);
  const keyB64 = btoa(String.fromCharCode(...new Uint8Array(rawKey)))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');

  return { ciphertext, iv, keyFragment: keyB64 };
}

// Decrypt
async function decryptFile(ciphertext, iv, keyFragment) {
  // Re-pad base64
  const padded = keyFragment.replace(/-/g, '+').replace(/_/g, '/');
  const rawKey = Uint8Array.from(atob(padded), c => c.charCodeAt(0));

  const key = await crypto.subtle.importKey(
    'raw',
    rawKey,
    { name: 'AES-GCM', length: 256 },
    false,
    ['decrypt']
  );

  const plaintext = await crypto.subtle.decrypt(
    { name: 'AES-GCM', iv },
    key,
    ciphertext
  );

  return plaintext;
}
Enter fullscreen mode Exit fullscreen mode

The key extracted from location.hash.substring(1) is passed directly to decryptFile. The server never sees it.

The AES-GCM Tag Provides Authentication Too

A detail worth noting: AES-GCM (Galois/Counter Mode) is an authenticated encryption scheme. The 128-bit authentication tag appended to the ciphertext means:

  • If the ciphertext is tampered with (even one bit), decryption fails with an integrity error
  • The recipient knows the file hasn't been modified in transit
  • No separate HMAC is needed

This is why crypto.subtle.decrypt can throw — it's verifying the tag, not just decrypting.

What Can Go Wrong

Shared URLs over insecure channels

If you paste the share URL (including fragment) into an HTTP URL shortener that logs the full URL, or send it over an unencrypted channel that logs messages, the key is exposed. The fragment is only safe because HTTP requests don't include it — any logging of the full URL string exposes the key.

JavaScript injection on the share page

If an attacker can inject JavaScript into the decryption page, they can read location.hash before decryption. This is why zero-knowledge services need careful CSP headers and subresource integrity.

Key derivation vs. raw keys

Passing raw AES keys in URL fragments is fine for ephemeral file sharing. For longer-lived keys, HKDF derivation from a passphrase gives more flexibility and lets you revoke access without sharing the raw key material.

Try It

The implementation described here is the basis of FileShot.io — a zero-knowledge file sharing service with an MIT-licensed backend you can self-host:

git clone https://github.com/FileShot/FileShotZKE.git
cd FileShotZKE/backend && npm install && node server.js
Enter fullscreen mode Exit fullscreen mode

Open DevTools Network tab while using it and watch the upload/download requests — you'll never see a key.


The URL fragment has been part of the web for 30 years. It remains one of the most underused privacy primitives in browser engineering.

Top comments (0)