DEV Community

Unknown
Unknown

Posted on

How to Build a Zero-Knowledge, Burn-After-Reading Vault with the Web Crypto API

Data privacy is one of the most critical challenges in modern web development. When building secure messaging apps or file-sharing tools, developers often rely on server-side encryption. The fatal flaw in this approach is that the server still holds the decryption keys. If the database is compromised, the plaintext data is exposed.

To solve this, I built ZeroKey, an open-source, end-to-end encrypted (E2EE), burn-after-reading payload delivery system.

In this tutorial, I will break down the zero-knowledge architecture behind ZeroKey and explain how you can implement client-side AES-256-GCM encryption using the native Web Crypto API, Vercel Serverless Functions, and Supabase Row Level Security (RLS).

The Architecture of a Zero-Knowledge Application

A true zero-knowledge architecture guarantees that the server routing the data mathematically cannot read the payload. To achieve this, ZeroKey relies on three core security pillars:

  1. Client-Side Cryptography: Data is encrypted in the browser before network transmission.
  2. URL Fragment Key Exchange: The decryption key is passed via the URL hash, which browsers never send to the server.
  3. Hard Data Destruction: A read-once, burn-after-reading protocol ensures zero data retention.

Let's break down the implementation.

1. Client-Side Encryption with AES-256-GCM

Instead of relying on third-party cryptographic libraries that increase bundle size and introduce supply chain risks, ZeroKey uses the browser's native window.crypto.subtle API.

When a user creates a payload, the application generates a secure 16-byte salt and a 12-byte Initialization Vector (IV). Using PBKDF2 with 100,000 SHA-256 iterations, we derive a robust 256-bit cryptographic key from an auto-generated or user-provided PIN.

Here is a simplified look at the encryption implementation:

async function encryptPayload(dataBuffer, key) {
    // Generate a cryptographically secure random IV
    const iv = window.crypto.getRandomValues(new Uint8Array(12));

    // Encrypt the payload using AES-256-GCM
    const encryptedContent = await window.crypto.subtle.encrypt(
        { name: "AES-GCM", iv: iv }, 
        key, 
        dataBuffer
    );

    return { 
        encryptedBase64: bufferToBase64(encryptedContent), 
        ivBase64: bufferToBase64(iv) 
    };
}
Enter fullscreen mode Exit fullscreen mode

This guarantees that the payload is converted into unreadable ciphertext before any API requests are made.

  1. Zero-Knowledge Routing via URL Fragments The biggest challenge in E2EE is key exchange. How do we give the recipient the decryption key without the server intercepting it? ZeroKey utilizes a fundamental mechanic of web browsers: the URL fragment. When a secure link is generated, it looks like this:
https://zerokey.vercel.app/view?id=[UUID]&iv=[Base64]&salt=[Base64]#[Decryption_Key]
Enter fullscreen mode Exit fullscreen mode

According to HTTP specifications, browsers do not transmit the fragment identifier (anything following the # symbol) to the server. The Vercel routing layer and the Supabase database only receive the ciphertext, the UUID, the salt, and the IV. The decryption key remains securely isolated in the client's local memory, allowing the recipient's device to execute the AES-GCM decryption locally.

  1. Securing the Database with Supabase RLS Storing encrypted data still presents a risk if an attacker can continuously query the database. To mitigate this, ZeroKey utilizes Supabase Row Level Security (RLS). The PostgreSQL database is entirely locked down from public access. The frontend cannot execute direct read or write operations. Instead, the application communicates exclusively with Vercel Serverless API routes. These Node.js functions utilize a secure Service Role key to bypass RLS, ensuring that database transactions are strictly controlled by backend logic.
  2. The Burn-After-Reading Protocol To ensure strict zero data retention, payloads must not persist after being consumed. When the recipient opens the link and successfully decrypts the payload locally, the frontend fires an asynchronous destruction signal to the /api/destroySecret serverless function. This triggers a hard DELETE command on both the PostgreSQL row and any associated media blobs in the Supabase Storage bucket. If the link is clicked a second time, a 404 response is returned. The data is permanently purged. Review the Code Building this project was an incredible dive into applied cryptography and secure full-stack architecture. By combining the Web Crypto API with modern serverless infrastructure, developers can build tools that prioritize user privacy by default. The entire ZeroKey project is open-source. I encourage security researchers, web developers, and cryptography enthusiasts to review the codebase, test the architecture, or contribute to the project.
    • GitHub Repository: github.com/kdippan/zerokey
    • Live Application: zerokey.vercel.app Have you implemented the Web Crypto API in your own projects? I would love to hear your thoughts on this architecture in the comments below.

Top comments (0)