DEV Community

Graysoft Dev
Graysoft Dev

Posted on

How I Built Zero-Knowledge File Sharing Using the Web Crypto API (No Server Ever Sees Your Data)

When I built FileShot, I had one hard requirement: the server must never be able to read a user's file — ever.

Not encrypted at rest by a key the server holds. Not TLS. I mean genuinely zero-knowledge: the encryption key is generated in the browser, used in the browser, and never transmitted anywhere.

Here's how I did it using the Web Crypto API.

Why Zero-Knowledge?

Most file sharing services say "your files are encrypted" — but they mean encrypted with their key. If their database leaks, or a subpoena hits, all your files are exposed. True zero-knowledge means even the developer can't read the files.

The Web Crypto API is built into every modern browser and gives us access to hardware-backed cryptography. No npm packages, no external dependencies.

The Encryption Flow

Step 1: Generate the Key

const key = await crypto.subtle.generateKey(
  { name: 'AES-GCM', length: 256 },
  true,
  ['encrypt', 'decrypt']
);
Enter fullscreen mode Exit fullscreen mode

AES-GCM is authenticated encryption — it provides both confidentiality and integrity. If anyone tampers with the ciphertext, decryption will fail.

Step 2: Encrypt the File

async function encryptFile(file, key) {
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const fileBuffer = await file.arrayBuffer();
  const ciphertext = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    key,
    fileBuffer
  );
  return { ciphertext, iv };
}
Enter fullscreen mode Exit fullscreen mode

The IV (initialization vector) must be unique per encryption — never reuse an IV with the same key. crypto.getRandomValues() is cryptographically secure.

Step 3: Share the Key — In the URL Fragment

Here's the clever part: the decryption key needs to reach the recipient, but can't go through the server. We embed the exported key in the URL fragment (the # part):

const exported = await crypto.subtle.exportKey('raw', key);
const keyHex = Array.from(new Uint8Array(exported))
  .map(b => b.toString(16).padStart(2, '0'))
  .join('');

const shareUrl = 'https://fileshot.io/d/' + fileId + '#' + keyHex;
Enter fullscreen mode Exit fullscreen mode

Why the fragment? URL fragments are never sent to the server in HTTP requests. The server sees /d/FILE_ID — it never sees the #KEYHEX part. This is a standard technique for client-side key transport.

Step 4: Decrypt on Download

When someone opens the share link, the decryption key is read from window.location.hash — entirely in-browser:

async function decryptFile(ciphertext, iv, keyHex) {
  const keyBytes = new Uint8Array(keyHex.match(/.{2}/g).map(b => parseInt(b, 16)));
  const key = await crypto.subtle.importKey(
    'raw', keyBytes,
    { 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

What the Server Actually Stores

The server stores only:

  • The encrypted ciphertext (AES-256-GCM)
  • The IV (not secret — required for decryption, safe to store)
  • File metadata (original filename, size, expiry, upload timestamp)

It never has:

  • The plaintext file content
  • The decryption key
  • Any way to derive the key

Even if you subpoena FileShot, there is nothing to hand over.

Browser Compatibility

The Web Crypto API is supported in all modern browsers: Chrome 37+, Firefox 34+, Safari 11+, Edge 12+. It is only available in secure contexts (HTTPS or localhost).

Try It Yourself

You can see this in action at FileShot.io — upload any file and the generated share link contains the decryption key in the URL fragment. Open DevTools Network tab and confirm the key never appears in any request.

The full encryption/decryption happens under 50ms even for multi-megabyte files, thanks to AES-GCM's hardware acceleration via AES-NI instructions on modern CPUs.


Zero-knowledge architecture is one of those things that sounds complicated but is surprisingly approachable with the Web Crypto API. If you're building any kind of file sharing, messaging, or notes app and care about user privacy, this pattern is worth adding to your toolkit.

Questions or feedback? Drop them in the comments.

Top comments (0)