<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Derick J. David</title>
    <description>The latest articles on DEV Community by Derick J. David (@derick_jdavid_2e9c83287).</description>
    <link>https://dev.to/derick_jdavid_2e9c83287</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3712322%2F8ad5ff7a-f5bf-4b3c-85bc-a09768ed7bb1.jpg</url>
      <title>DEV Community: Derick J. David</title>
      <link>https://dev.to/derick_jdavid_2e9c83287</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/derick_jdavid_2e9c83287"/>
    <language>en</language>
    <item>
      <title>How I built a Zero-Knowledge Secret Sharer using Next.js and the Web Crypto API</title>
      <dc:creator>Derick J. David</dc:creator>
      <pubDate>Thu, 15 Jan 2026 09:20:49 +0000</pubDate>
      <link>https://dev.to/derick_jdavid_2e9c83287/how-i-built-a-zero-knowledge-secret-sharer-using-nextjs-and-the-web-crypto-api-4mn</link>
      <guid>https://dev.to/derick_jdavid_2e9c83287/how-i-built-a-zero-knowledge-secret-sharer-using-nextjs-and-the-web-crypto-api-4mn</guid>
      <description>&lt;p&gt;Most "secure" sharing tools require you to trust the server. You paste your password, the server encrypts it, and stores it. But if the server logs the request, or if the database leaks, your secret is gone.&lt;/p&gt;

&lt;p&gt;I wanted a tool where &lt;strong&gt;I&lt;/strong&gt; (the developer) literally could not read the data even if I wanted to.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Nix&lt;/strong&gt; (&lt;a href="https://nix.jaid.dev" rel="noopener noreferrer"&gt;https://nix.jaid.dev&lt;/a&gt;), an open-source, zero-knowledge secret sharing app. Here is the technical breakdown of how it works, using &lt;strong&gt;AES-GCM&lt;/strong&gt; and the &lt;strong&gt;URL Hash Fragment&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;The core constraint was &lt;strong&gt;Zero Knowledge&lt;/strong&gt;. The server must never receive the decryption key.&lt;/p&gt;

&lt;p&gt;To achieve this, we use the browser's URL fragment (&lt;code&gt;#&lt;/code&gt;).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Alice&lt;/strong&gt; generates a random key in her browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alice&lt;/strong&gt; encrypts the data client-side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alice&lt;/strong&gt; sends &lt;em&gt;only&lt;/em&gt; the ciphertext (wrapped in a JSON envelope) to the server (Supabase).&lt;/li&gt;
&lt;li&gt;The browser constructs a link: &lt;code&gt;https://nix.jaid.dev/view/[ID]#[KEY]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bob&lt;/strong&gt; clicks the link. His browser requests the ID.&lt;/li&gt;
&lt;li&gt;His browser extracts the &lt;code&gt;#[KEY]&lt;/code&gt; from the URL (which was never sent to the server) and decrypts the data locally.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; Next.js 16 (App Router)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; Supabase (Postgres)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crypto:&lt;/strong&gt; Native Web Crypto API (&lt;code&gt;window.crypto.subtle&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Styling:&lt;/strong&gt; Tailwind CSS&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Hard Part: Web Crypto API
&lt;/h2&gt;

&lt;p&gt;The Web Crypto API is powerful but verbose. Here is how I handled the encryption flow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generating the Key
&lt;/h3&gt;

&lt;p&gt;We need a cryptographic-strength random key. I used the SubtleCrypto API to ensure it's generated securely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Generate a secure AES-GCM key&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AES-GCM&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;encrypt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;decrypt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Export to raw bytes (and then to Base64) for the URL&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;exported&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exportKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;raw&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Encryption (AES-GCM)
&lt;/h3&gt;

&lt;p&gt;I chose &lt;strong&gt;AES-GCM&lt;/strong&gt; because it provides both confidentiality and integrity.&lt;/p&gt;

&lt;p&gt;One "gotcha" with AES-GCM is the &lt;strong&gt;Initialization Vector (IV)&lt;/strong&gt;. You must generate a unique IV for every single encryption operation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encodedContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;encoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 96-bit IV for GCM&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; 

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encryptedContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AES-GCM&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;encodedContent&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Return serialized JSON with IV and Ciphertext&lt;/span&gt;
  &lt;span class="c1"&gt;// We convert the typed arrays to regular arrays for easy stringification&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encryptedContent&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The URL Hash Hack
&lt;/h2&gt;

&lt;p&gt;This is the "magic" trick.&lt;/p&gt;

&lt;p&gt;When a browser visits &lt;code&gt;example.com/page#secret123&lt;/code&gt;, the server &lt;strong&gt;only&lt;/strong&gt; sees &lt;code&gt;GET /page&lt;/code&gt;. It ignores everything after the hash.&lt;/p&gt;

&lt;p&gt;This allows us to transport the decryption key from Alice to Bob via the link, without the server ever intercepting it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// On the Client (useEffect)&lt;/span&gt;
&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// "#5f3a..."&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keyString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Remove '#'&lt;/span&gt;
     &lt;span class="c1"&gt;// Trigger decryption...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Database &amp;amp; Expiration (Supabase)
&lt;/h2&gt;

&lt;p&gt;Since the server holds only encrypted blobs, I used Supabase with Row Level Security (RLS) to handle storage.&lt;/p&gt;

&lt;p&gt;To handle "Burn on Read" and expiration, the application logic enforces the rules before showing the secret:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fetch:&lt;/strong&gt; The client retrieves the encrypted record.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check Expiration:&lt;/strong&gt; The client compares the &lt;code&gt;expires_at&lt;/code&gt; timestamp with the current time. If it's passed, the secret is considered expired and deleted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Burn on Read:&lt;/strong&gt; If the &lt;strong&gt;metadata&lt;/strong&gt; marks the secret as "Burn on Read", the client immediately issues a delete request to Supabase as soon as the data is successfully retrieved.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hydration Errors:&lt;/strong&gt; Next.js Server Components don't have access to &lt;code&gt;window&lt;/code&gt;. You have to be very careful to only invoke &lt;code&gt;window.crypto&lt;/code&gt; in &lt;code&gt;useEffect&lt;/code&gt; or event handlers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encoding Hell:&lt;/strong&gt; Moving between &lt;code&gt;ArrayBuffer&lt;/code&gt;, &lt;code&gt;Uint8Array&lt;/code&gt;, and strings is painful. The &lt;code&gt;TextEncoder&lt;/code&gt; and &lt;code&gt;TextDecoder&lt;/code&gt; APIs are your friends.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust:&lt;/strong&gt; Building a security tool requires transparency. I made the repo open source immediately because I wouldn't use a closed-source tool for this myself.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try it out
&lt;/h2&gt;

&lt;p&gt;I’m looking for feedback on the encryption implementation and the UX.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://nix.jaid.dev" rel="noopener noreferrer"&gt;nix.jaid.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://www.google.com/search?q=https://github.com/jaid/nix" rel="noopener noreferrer"&gt;github.com/jaid/nix&lt;/a&gt; (Stars appreciated!)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>nextjs</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
