<?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: anon.li</title>
    <description>The latest articles on DEV Community by anon.li (@anonli).</description>
    <link>https://dev.to/anonli</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%2F3887160%2Fbe47ac9a-ecc5-4531-b6b1-5c13e0f9d537.png</url>
      <title>DEV Community: anon.li</title>
      <link>https://dev.to/anonli</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anonli"/>
    <language>en</language>
    <item>
      <title>Add end-to-end encrypted file uploads to your CLI tool: a hands-on walkthrough</title>
      <dc:creator>anon.li</dc:creator>
      <pubDate>Sat, 25 Apr 2026 22:00:00 +0000</pubDate>
      <link>https://dev.to/anonli/add-end-to-end-encrypted-file-uploads-to-your-cli-tool-a-hands-on-walkthrough-1mi3</link>
      <guid>https://dev.to/anonli/add-end-to-end-encrypted-file-uploads-to-your-cli-tool-a-hands-on-walkthrough-1mi3</guid>
      <description>&lt;p&gt;I have a CLI tool that generates internal reports and I needed a way to share them with a coworker who doesn't use our internal storage. I didn't want to email PDFs (audit trail), Slack files (search history forever), or spin up presigned S3 URLs (configurable, but I'd be the auth boundary).&lt;/p&gt;

&lt;p&gt;What I actually wanted: a one-liner in my CLI that hands the user back a URL, where the file is encrypted client-side, the server sees ciphertext only, and the link expires on its own.&lt;/p&gt;

&lt;p&gt;This post is the walkthrough of how I built that against &lt;a href="https://anon.li/docs/api/drop" rel="noopener noreferrer"&gt;anon.li's Drop API&lt;/a&gt;. It's about 150 lines of Node, no dependencies beyond &lt;code&gt;node:crypto&lt;/code&gt;. By the end you'll have a script you can drop into any CLI tool to add E2EE file sharing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the thing
&lt;/h2&gt;

&lt;p&gt;Drop's upload flow has four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create a drop&lt;/strong&gt; — POST to &lt;code&gt;/api/v1/drop&lt;/code&gt; with metadata (IV, file count, expiry). Get back a &lt;code&gt;drop_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a file&lt;/strong&gt; — POST to &lt;code&gt;/api/v1/drop/:id/file&lt;/code&gt; with the encrypted file's metadata. Get back a &lt;code&gt;fileId&lt;/code&gt; and presigned upload URLs (one per chunk).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PUT each chunk&lt;/strong&gt; to its presigned URL. Capture the ETag returned by the storage layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finish the upload&lt;/strong&gt; — PATCH &lt;code&gt;/api/v1/drop/:id?action=finish&lt;/code&gt; with the chunk ETags. The drop becomes ready.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then you build a share URL: &lt;code&gt;https://anon.li/d/&amp;lt;drop_id&amp;gt;#&amp;lt;base64-key&amp;gt;&lt;/code&gt;. The fragment carries the AES-256 key, which never reaches the server (browsers strip it from requests — see &lt;a href="https://anon.li/" rel="noopener noreferrer"&gt;my other post&lt;/a&gt; on why this matters if you're curious).&lt;/p&gt;

&lt;p&gt;We'll do everything client-side, in pure Node, with &lt;code&gt;crypto.subtle&lt;/code&gt; (Web Crypto, available in Node 16+) and &lt;code&gt;node:crypto&lt;/code&gt; for buffer juggling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: get an API key
&lt;/h2&gt;

&lt;p&gt;From the anon.li dashboard: &lt;strong&gt;API Keys → New&lt;/strong&gt;. Copy it once, it's only shown at creation. Format is &lt;code&gt;ak_&lt;/code&gt; + 32 hex chars. The server stores only the SHA-256 hash of it, which is why it can't show it again.&lt;/p&gt;

&lt;p&gt;For this script, expose it as an env var:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANON_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ak_yourkeyhere
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: scaffolding and key generation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:path&lt;/span&gt;&lt;span class="dl"&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;API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://anon.li/api/v1&lt;/span&gt;&lt;span class="dl"&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;KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ANON_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ANON_API_KEY not set&lt;/span&gt;&lt;span class="dl"&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;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &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="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Helper: base64url encoding (RFC 4648)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b64u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Buffer&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;buf&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Helper: derive a unique IV per chunk from a base IV&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deriveChunkIv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseIv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chunkIndex&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;iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;alloc&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="nx"&gt;baseIv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// first 8 bytes from base IV&lt;/span&gt;
  &lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeUInt32BE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunkIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// last 4 = chunk index, big-endian&lt;/span&gt;
  &lt;span class="k"&gt;return&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;deriveChunkIv&lt;/code&gt; helper is the workhorse of this whole script. Drop uses one base IV per file, and every chunk's actual IV is computed deterministically from &lt;code&gt;(baseIv, chunkIndex)&lt;/code&gt;. That means we only generate randomness once, and we never need to ship per-chunk IVs to the server — they're reproducible from the base IV alone.&lt;/p&gt;

&lt;p&gt;Filenames use the same scheme with a reserved chunk index of &lt;code&gt;0xFFFFFFFF&lt;/code&gt;, which is guaranteed not to collide with any real chunk (you'd need 4 billion chunks in one file before hitting 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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;uploadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expiry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxDownloads&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&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;fileBuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filePath&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;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 256-bit AES key, base IV for this drop, base IV for this file&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="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&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;dropIv&lt;/span&gt; &lt;span class="o"&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;randomBytes&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;fileIv&lt;/span&gt; &lt;span class="o"&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;randomBytes&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three random things, generated locally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;key&lt;/code&gt; — the AES-256 key. Goes in the URL fragment, never sent to the server.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dropIv&lt;/code&gt; — the base IV for drop-level metadata (title, message).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fileIv&lt;/code&gt; — the base IV for this file's chunks and filename.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For larger files you'd chunk by 50 MB (Drop's default) or whatever fits your memory budget. We'll keep it single-chunk for clarity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: encrypt the file and the filename
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// Encrypt the file payload (single chunk, index 0)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunkIv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deriveChunkIv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileIv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&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;cipher&lt;/span&gt; &lt;span class="o"&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;createCipheriv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aes-256-gcm&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;span class="nx"&gt;chunkIv&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;ciphertext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileBuf&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;final&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;authTag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAuthTag&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;encryptedData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authTag&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// 16-byte tag appended&lt;/span&gt;

  &lt;span class="c1"&gt;// Encrypt the filename with the reserved chunk index&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nameIv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deriveChunkIv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileIv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0xFFFFFFFF&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;nameCipher&lt;/span&gt; &lt;span class="o"&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;createCipheriv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aes-256-gcm&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;span class="nx"&gt;nameIv&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;encryptedName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;b64u&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;nameCipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;nameCipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;final&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;nameCipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAuthTag&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;p&gt;Two things worth noticing:&lt;/p&gt;

&lt;p&gt;The encrypted file is &lt;code&gt;ciphertext || authTag&lt;/code&gt; — Drop's wire format is "GCM ciphertext, then 16-byte tag." On decryption you split off the last 16 bytes and feed them to &lt;code&gt;setAuthTag()&lt;/code&gt; before &lt;code&gt;final()&lt;/code&gt;. This is consistent across chunks too: each chunk on the wire is &lt;code&gt;chunk_ciphertext || tag&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The filename is encrypted with the same key but a different IV (the &lt;code&gt;0xFFFFFFFF&lt;/code&gt; one). One key, many IVs, derived from the base — that's the whole pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: create the drop
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/drop`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&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;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&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="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&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="nf"&gt;b64u&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dropIv&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;fileCount&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="nx"&gt;expiry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;maxDownloads&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;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;createRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`create drop: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;createRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&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="nx"&gt;drop&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;createRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;dropId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;drop_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;expiry&lt;/code&gt; is days (max depends on plan: 3 free, 7 plus, 30 pro). &lt;code&gt;maxDownloads&lt;/code&gt; is optional. If you wanted to attach encrypted metadata (a description, JSON tags, anything), you'd encrypt it with the same key + &lt;code&gt;dropIv&lt;/code&gt; + &lt;code&gt;0xFFFFFFFF&lt;/code&gt; reserved index and pass it as &lt;code&gt;encryptedMessage&lt;/code&gt; here. The server stores the ciphertext and can't read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: register the file, get presigned upload URLs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fileRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/drop/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dropId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/file`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&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;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&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="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&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;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;encryptedData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;encryptedName&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="nf"&gt;b64u&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileIv&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/octet-stream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;chunkCount&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="na"&gt;chunkSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;encryptedData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;fileRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`add file: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fileRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fileRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fileId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"s3UploadId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"upload-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"uploadUrls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://r2-presigned-url..."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;uploadUrls&lt;/code&gt; is keyed by chunk number (1-indexed). For our one-chunk file we have one URL. For a multi-chunk file you'd get N URLs and PUT to each.&lt;/p&gt;

&lt;p&gt;The presigned URLs go directly to Cloudflare R2, bypassing anon.li's app servers entirely. That means the encrypted bytes don't transit through the API host at all — they go straight to object storage. Cleaner for them, faster for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: PUT the encrypted bytes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;putRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uploadUrls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;encryptedData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;putRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`upload chunk: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;putRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;etag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;putRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ETag&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no ETag returned from storage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Storage hands back an ETag for each chunk. We need it for the finalize step — it's how we prove that what we uploaded matches what the storage backend received.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: finalize
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;finishRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/drop/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dropId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?action=finish`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PATCH&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&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;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&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="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&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;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
        &lt;span class="na"&gt;fileId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fileId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;chunkIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;etag&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;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;finishRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`finish: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;finishRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This commits the multipart upload on the storage side and marks the drop as ready for download. Without this step the drop is in "uploading" limbo and the share URL won't work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: hand back the share URL
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shareUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://anon.li/d/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dropId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;b64u&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;shareUrl&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;p&gt;The fragment is the base64url-encoded raw key. The browser will keep it client-side. The server side of anon.li will only ever see &lt;code&gt;dropId&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Glue it together
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;uploadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expiry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;node share.js ./report.pdf
&lt;span class="gp"&gt;https://anon.li/d/abc123xyz#&lt;/span&gt;nT7l3K9Q...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Done. Send the link, recipient clicks, file decrypts in their browser. Three days later, the bytes are gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things I got wrong the first time
&lt;/h2&gt;

&lt;p&gt;A few stumbling blocks that bit me when I wrote this for real:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting the auth tag.&lt;/strong&gt; GCM's auth tag is 16 bytes appended after the ciphertext. If you use &lt;code&gt;cipher.update + cipher.final&lt;/code&gt; and forget &lt;code&gt;cipher.getAuthTag()&lt;/code&gt;, the receiving end can't verify integrity and decryption fails. Don't separate them — bundle as &lt;code&gt;ciphertext || tag&lt;/code&gt; always.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong IV byte order.&lt;/strong&gt; The chunk index is &lt;strong&gt;big-endian&lt;/strong&gt; in the last 4 bytes of the IV. I wrote &lt;code&gt;writeUInt32LE&lt;/code&gt; once and got &lt;code&gt;OperationError: decryption failed&lt;/code&gt; and spent fifteen minutes blaming the API. Read your &lt;code&gt;Buffer.write*&lt;/code&gt; docs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not reading the fragment correctly on the receive side.&lt;/strong&gt; If you build a Node receiver, &lt;code&gt;new URL(shareUrl).hash&lt;/code&gt; includes the leading &lt;code&gt;#&lt;/code&gt;. Strip it with &lt;code&gt;.slice(1)&lt;/code&gt; before base64-decoding. Browsers do the same with &lt;code&gt;location.hash&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using &lt;code&gt;chunkSize&lt;/code&gt; of the &lt;em&gt;plaintext&lt;/em&gt;.&lt;/strong&gt; The API wants the size on the wire, which is plaintext + 16-byte tag per chunk. For our single-chunk case that's &lt;code&gt;encryptedData.length&lt;/code&gt;. Get this wrong and either the upload fails or the receiver chunks the download incorrectly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd add next
&lt;/h2&gt;

&lt;p&gt;This script is the floor, not the ceiling. Real things you'd want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Streaming for big files.&lt;/strong&gt; Don't &lt;code&gt;readFile&lt;/code&gt; a 4 GB file. Use &lt;code&gt;createReadStream&lt;/code&gt;, encrypt chunk-by-chunk, PUT each chunk as it's ready. Drop's chunk format is designed for this — the IV derivation gives you an explicit &lt;code&gt;chunkIndex&lt;/code&gt; to count against.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password protection.&lt;/strong&gt; Drop supports wrapping the file key under an Argon2id-derived password key. The receiver enters the password, derives the wrapping key locally, unwraps the file key, and decrypts. You never send the password.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Owner-key recovery.&lt;/strong&gt; If you want the dashboard to be able to &lt;em&gt;see&lt;/em&gt; your own files later (just for management — it still can't decrypt the contents), you can wrap the file key under an account-level vault key and pass &lt;code&gt;ownerKey&lt;/code&gt; on creation. Optional, lets you delete files from the dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encrypted metadata.&lt;/strong&gt; Stick a JSON blob (description, tags, source app version) in &lt;code&gt;encryptedMessage&lt;/code&gt; so receivers see context after decryption.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of those is its own short post. The scaffold above gets you to the point where you can ship E2EE file sharing without a backend you control. For a CLI tool, that's an absurd amount of capability for ~150 lines.&lt;/p&gt;

&lt;p&gt;Source for the full script (and the corresponding download/decrypt counterpart) lives in the &lt;a href="https://anon.li/docs/api/drop" rel="noopener noreferrer"&gt;Drop API docs&lt;/a&gt;. Steal it.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How URL fragments make true zero-knowledge file sharing possible</title>
      <dc:creator>anon.li</dc:creator>
      <pubDate>Sat, 25 Apr 2026 21:25:39 +0000</pubDate>
      <link>https://dev.to/anonli/how-url-fragments-make-true-zero-knowledge-file-sharing-possible-9j0</link>
      <guid>https://dev.to/anonli/how-url-fragments-make-true-zero-knowledge-file-sharing-possible-9j0</guid>
      <description>&lt;p&gt;Here's a URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://anon.li/d/abc123#U2FsdGVkX1...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The thing after the &lt;code&gt;#&lt;/code&gt; is an AES-256 encryption key. The server hosting the file behind &lt;code&gt;abc123&lt;/code&gt; cannot see it, cannot log it, and cannot reproduce it from anything else it stores. If the server gets owned tomorrow, the attacker walks away with encrypted blobs and nothing to decrypt them with.&lt;/p&gt;

&lt;p&gt;This isn't marketing copy. It's a property of HTTP that has been there since 1996 and that almost nobody uses for what it's good at. Let's pull on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The HTTP fragment is special
&lt;/h2&gt;

&lt;p&gt;When your browser fetches &lt;code&gt;https://example.com/page?foo=bar#section&lt;/code&gt;, here's what's actually sent over the wire:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/page?foo=bar&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;#section&lt;/code&gt; part — the &lt;strong&gt;fragment identifier&lt;/strong&gt; - never appears in the request line, never appears in headers, never reaches the origin server. RFC 3986 defines it as client-side only: the browser uses it to scroll to anchors, the JavaScript runtime can read it via &lt;code&gt;location.hash&lt;/code&gt;, but it stops at the network boundary.&lt;/p&gt;

&lt;p&gt;This is non-negotiable browser behavior. It's not a feature you opt into. It's not a CDN setting. Every conformant HTTP client in existence treats it the same way, because if it didn't, every "back to top" anchor would generate server log noise.&lt;/p&gt;

&lt;p&gt;So: you have a side channel that travels with your URL but doesn't reach your server. What can you do with that?&lt;/p&gt;

&lt;h2&gt;
  
  
  Stick a key in it
&lt;/h2&gt;

&lt;p&gt;The trick that products like &lt;a href="https://anon.li/drop" rel="noopener noreferrer"&gt;anon.li's Drop&lt;/a&gt;, Send (RIP), and a handful of others use is roughly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate an AES-256 key in the browser.&lt;/li&gt;
&lt;li&gt;Encrypt the file in the browser.&lt;/li&gt;
&lt;li&gt;Upload only the ciphertext.&lt;/li&gt;
&lt;li&gt;Build a share URL where the path is the file's ID and the fragment is the base64 of the key.&lt;/li&gt;
&lt;li&gt;Hand that URL to the recipient.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When the recipient clicks the link, their browser fetches &lt;code&gt;/d/abc123&lt;/code&gt; (the server returns ciphertext + metadata), but the &lt;code&gt;#U2FsdGVkX1...&lt;/code&gt; part stays in their browser. Client-side JavaScript reads &lt;code&gt;location.hash&lt;/code&gt;, decrypts, and renders.&lt;/p&gt;

&lt;p&gt;The server never has the key. Not "promises not to look at the key." &lt;em&gt;Cryptographically cannot have&lt;/em&gt; the key. That's the difference between zero-knowledge and just "we encrypt at rest, trust us."&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;// Sender side&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="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="s1"&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="s1"&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="s1"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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="s1"&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keyB64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;btoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&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;rawKey&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
                 &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\+&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/=+$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ...encrypt and upload ciphertext...&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shareUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://anon.li/d/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dropId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;keyB64&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ↑ Anything after the # never touches the server.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Recipient side&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keyB64&lt;/span&gt; &lt;span class="o"&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="nf"&gt;slice&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&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="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyB64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/-/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/_/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;importKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;rawKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="kc"&gt;false&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="s1"&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;// fetch ciphertext, decrypt with key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The IV problem (and a clean solution)
&lt;/h2&gt;

&lt;p&gt;Once you decide to chunk large files — which you must, because nobody wants to load a 5 GB ArrayBuffer into RAM to encrypt it — you hit a practical question: how do you give every chunk a unique IV?&lt;/p&gt;

&lt;p&gt;AES-GCM is brutally unforgiving about IV reuse with the same key. Reuse one IV across two encryptions and you've leaked enough material to recover plaintext XORs and forge messages. Don't reuse IVs.&lt;/p&gt;

&lt;p&gt;The naive approach is "generate 12 random bytes per chunk." That works, but now you have to store every IV server-side, increasing metadata size and adding bookkeeping. Worse, you have to be sure your RNG is good for every single chunk.&lt;/p&gt;

&lt;p&gt;The pattern Drop uses is cleaner: derive chunk IVs deterministically from one base IV plus the chunk index.&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;// 12-byte IV: first 8 bytes from base IV, last 4 = chunk index (big-endian u32)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deriveChunkIv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseIv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chunkIndex&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;iv&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;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="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseIv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&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;DataView&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="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chunkIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// BE&lt;/span&gt;
  &lt;span class="k"&gt;return&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Properties this gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One random thing per file.&lt;/strong&gt; You only need to generate 12 random bytes once. Every chunk's IV is derived deterministically from it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uniqueness within a file.&lt;/strong&gt; As long as every chunk has a different &lt;code&gt;chunkIndex&lt;/code&gt;, every chunk gets a different IV. With &lt;code&gt;Uint32&lt;/code&gt; you've got 4 billion possible chunks per file before you collide — far more than you'll ever upload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uniqueness across files.&lt;/strong&gt; Different files get different base IVs, so the 8-byte prefix is independent. The combined 12-byte IV space is effectively the same as random per-chunk IVs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No per-chunk metadata needed.&lt;/strong&gt; The server only stores the base IV. The recipient can reconstruct the chunk IV from &lt;code&gt;(base IV, chunk index)&lt;/code&gt; alone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a nice extra trick: reserve a "magic" chunk index for filename encryption. Drop uses &lt;code&gt;0xFFFFFFFF&lt;/code&gt;, the max u32. Since real chunks count up from 0 and you'll never hit 4 billion of them, this index is guaranteed never to collide with a data chunk's IV — so you can encrypt the filename with the same key, derive its IV the same way, and you're done. No separate key, no separate KDF, no IV bookkeeping.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filenameIv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deriveChunkIv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseIv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0xFFFFFFFF&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// guaranteed never to overlap with chunk 0, 1, 2, ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This kind of design — where you find a way to &lt;em&gt;not need&lt;/em&gt; a thing rather than carefully managing it — is the secret to keeping crypto code reviewable. Every piece of state you eliminate is a piece of state you can't get wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authenticate, don't just encrypt
&lt;/h2&gt;

&lt;p&gt;AES-GCM is an authenticated cipher: every encryption produces a 16-byte tag that has to verify on decryption. Tamper with the ciphertext, change one bit, and the tag mismatches and decryption errors out.&lt;/p&gt;

&lt;p&gt;This matters more than people realize for chunked uploads. Without authentication, an attacker who controls the storage layer (or a CDN, or a proxy) could splice or corrupt chunks and the recipient would silently get garbage — or, worse, plausible-looking but altered data. With per-chunk auth tags, &lt;em&gt;each chunk&lt;/em&gt; has integrity. Tampering anywhere in the file fails decryption immediately.&lt;/p&gt;

&lt;p&gt;You get this for free with &lt;code&gt;crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plaintext)&lt;/code&gt;. The output already includes the auth tag appended. Don't roll your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an attacker who breaks the server actually gets
&lt;/h2&gt;

&lt;p&gt;Let's audit. Here's what a Drop-style server stores:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;It stores&lt;/th&gt;
&lt;th&gt;It does not store&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ciphertext (chunks of AES-GCM output)&lt;/td&gt;
&lt;td&gt;Plaintext bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encrypted filenames&lt;/td&gt;
&lt;td&gt;Original filenames&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File size, MIME type&lt;/td&gt;
&lt;td&gt;Encryption key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Base IV&lt;/td&gt;
&lt;td&gt;Password (if used)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expiry, download counter, owner ID&lt;/td&gt;
&lt;td&gt;Anything decryptable from above&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If the server is compromised, the attacker walks away with bytes that decrypt to nothing without a key they don't have. The keys are in URL fragments, which only ever exist in (a) the sender's browser at upload time, (b) the URLs the sender chose to share, and (c) the recipient's browser at download time. Steal the database, you get encrypted noise. That's the design.&lt;/p&gt;

&lt;p&gt;What can a malicious server still do? Two real things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Refuse service&lt;/strong&gt; — delete files, lie about expiry, return errors. This is unavoidable; you trust the server for availability, not confidentiality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serve malicious JavaScript&lt;/strong&gt; — if you trust the server to ship the decrypt code, a compromised server can ship a backdoored decrypt routine that exfiltrates the key after &lt;code&gt;location.hash&lt;/code&gt; is read. This is the genuine weakness of &lt;em&gt;any&lt;/em&gt; browser-based E2EE. Mitigations include CSP, &lt;a href="https://codeberg.org/anonli" rel="noopener noreferrer"&gt;open-source clients&lt;/a&gt; you can audit, browser extensions that lock the JS bundle, and reproducible builds. It's a real concern; be honest about it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why this design isn't more common
&lt;/h2&gt;

&lt;p&gt;A few reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It requires real client-side code. Web Crypto isn't hard, but it's harder than &lt;code&gt;multer.single('file')&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Big files become awkward without chunking + streams.&lt;/li&gt;
&lt;li&gt;Previews are tricky: you have to decrypt to render thumbnails. Drop handles this by treating preview requests as full downloads against the encrypted bytes — and counting them against the download limit, since they expose the same material.&lt;/li&gt;
&lt;li&gt;Search is impossible. The server can't index something it can't read. This is a feature, but it's a constraint.&lt;/li&gt;
&lt;li&gt;Most products don't actually want zero-knowledge. They want plausible-deniability marketing. A zero-knowledge architecture closes off a lot of "we'll add a feature later" doors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For consumer file sharing, those tradeoffs are usually worth it. For internal tools where the server has business reasons to read files (virus scanning, OCR, indexing), they're usually not. Pick the right tool for the threat model you actually have.&lt;/p&gt;

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

&lt;p&gt;If you want to see this pattern in action with code you can read end-to-end, &lt;a href="https://anon.li/docs/api/drop" rel="noopener noreferrer"&gt;anon.li's Drop API docs&lt;/a&gt; include the full Node.js encrypt/decrypt flow, and the &lt;a href="https://codeberg.org/anonli/anon.li" rel="noopener noreferrer"&gt;client implementation&lt;/a&gt; is open source. Read the encryption module, then read the share-link generator, then look at what actually goes over the wire in Network tab. Watching the fragment &lt;em&gt;not appear&lt;/em&gt; in any request log is the moment the whole thing clicks.&lt;/p&gt;

&lt;p&gt;The next time you see &lt;code&gt;#&lt;/code&gt; in a URL, look closer. It might be doing more work than you think.&lt;/p&gt;

</description>
      <category>security</category>
      <category>cryptography</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Build a disposable email alias CLI in 50 lines of Bash</title>
      <dc:creator>anon.li</dc:creator>
      <pubDate>Sat, 25 Apr 2026 21:21:58 +0000</pubDate>
      <link>https://dev.to/anonli/build-a-disposable-email-alias-cli-in-50-lines-of-bash-jln</link>
      <guid>https://dev.to/anonli/build-a-disposable-email-alias-cli-in-50-lines-of-bash-jln</guid>
      <description>&lt;p&gt;I have a rule I keep breaking: never give a website my real email if I'm not sure I want a relationship with them. Five minutes later I'm on some forum, signup form open, and I'm typing &lt;code&gt;me@gmail.com&lt;/code&gt; again because firing up a password manager, generating an alias, copying it, pasting it - it's just enough friction that I default to the lazy thing.&lt;/p&gt;

&lt;p&gt;So this weekend I built &lt;code&gt;alias&lt;/code&gt; - a single shell command that spits out a fresh forwarding address I can paste anywhere. Took about an hour. The whole thing is curl, jq, and one POST request.&lt;/p&gt;

&lt;p&gt;I'm using &lt;a href="https://anon.li/alias" rel="noopener noreferrer"&gt;anon.li&lt;/a&gt; as the backend because (a) it has a clean REST API, (b) it's open source, and (c) the free tier covers personal use. But the structure here works against any forwarding service that exposes an API - Addy, SimpleLogin, your own self-hosted thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The endpoint
&lt;/h2&gt;

&lt;p&gt;The Alias API has a Bitwarden-compatible "just generate me one" endpoint. From the docs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://anon.li/api/v1/alias?generate=true
Authorization: Bearer ak_...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can pass an optional &lt;code&gt;domain&lt;/code&gt; in the body if you've added a custom one. With no body, it returns a random alias on &lt;code&gt;anon.li&lt;/code&gt;. The response is the canonical alias object - &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, timestamps, the works.&lt;/p&gt;

&lt;p&gt;That's literally the entire API surface for what I want. One call, one address.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: get a key
&lt;/h2&gt;

&lt;p&gt;Sign up, go to &lt;strong&gt;Dashboard → API Keys&lt;/strong&gt;, generate one. It'll look like &lt;code&gt;ak_&lt;/code&gt; followed by 32 hex characters. Copy it once, because anon.li only shows the SHA-256 hash on their side after that - if you lose it, you rotate.&lt;/p&gt;

&lt;p&gt;I stash mine in &lt;code&gt;~/.config/anon/key&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.config/anon
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ak_yourkeyhere"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.config/anon/key
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 ~/.config/anon/key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're on a multi-user machine or just paranoid, swap this for &lt;code&gt;pass&lt;/code&gt;, &lt;code&gt;1Password CLI&lt;/code&gt;, &lt;code&gt;gpg --decrypt&lt;/code&gt;, whatever. Don't put it in &lt;code&gt;.bashrc&lt;/code&gt; in plaintext.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: the function
&lt;/h2&gt;

&lt;p&gt;Drop this into &lt;code&gt;~/.bashrc&lt;/code&gt; (or &lt;code&gt;~/.zshrc&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;alias&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;key
  &lt;span class="nv"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ~/.config/anon/key 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"no api key at ~/.config/anon/key"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="k"&gt;return &lt;/span&gt;1
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;anon&lt;/span&gt;&lt;span class="p"&gt;.li&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;response
  &lt;span class="nv"&gt;response&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"https://anon.li/api/v1/alias?generate=true"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;domain&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$domain&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="nb"&gt;local &lt;/span&gt;email
  &lt;span class="nv"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.data.email // empty'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"alias creation failed:"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="k"&gt;return &lt;/span&gt;1
  &lt;span class="k"&gt;fi

  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; pbcopy &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pbcopy &lt;span class="se"&gt;\&lt;/span&gt;
                  &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; wl-copy &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; wl-copy &lt;span class="se"&gt;\&lt;/span&gt;
                  &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; xclip &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; xclip &lt;span class="nt"&gt;-selection&lt;/span&gt; clipboard&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="s2"&gt; (copied)"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload your shell. Now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;alias
&lt;/span&gt;x7k9m2@anon.li &lt;span class="o"&gt;(&lt;/span&gt;copied&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Forty-something lines of Bash, one network call, address on my clipboard. I haven't typed &lt;code&gt;gmail.com&lt;/code&gt; into a signup form in six weeks.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Heads up: &lt;code&gt;alias&lt;/code&gt; is a Bash builtin (the thing you use for &lt;code&gt;alias ll='ls -la'&lt;/code&gt;). Defining a function with the same name shadows it inside your own shell, which is fine for personal use, but if you script anything that depends on &lt;code&gt;alias&lt;/code&gt; the builtin, name yours &lt;code&gt;aka&lt;/code&gt; or &lt;code&gt;alli&lt;/code&gt; or whatever. I personally don't care.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why not just use the dashboard?
&lt;/h2&gt;

&lt;p&gt;I do, for ones I want to label and track. But for a throwaway - "I just want to read this gated article" - I don't need to think about what to call it. I want it on my clipboard before I've finished alt-tabbing back to the browser. The CLI wins on that exact use case.&lt;/p&gt;

&lt;p&gt;The dashboard is also where I go later to disable the alias when the site inevitably starts spamming me. Toggling &lt;code&gt;active: false&lt;/code&gt; is one PATCH request, but I rarely bother scripting that part - I'm already in the dashboard reading the spam.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: domain switcher
&lt;/h2&gt;

&lt;p&gt;If you've added a custom domain (say, &lt;code&gt;mail.example.dev&lt;/code&gt;), you can pass it as an argument:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;alias &lt;/span&gt;mail.example.dev
random-suffix@mail.example.dev &lt;span class="o"&gt;(&lt;/span&gt;copied&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I use this for my "real but disposable" identity - the one I give to recruiters and conference signups. Random-looking but on a domain I own, so it doesn't trip the "is this a burner?" filters that some sites run against known alias domains.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus 2: skip the CLI, use Bitwarden
&lt;/h2&gt;

&lt;p&gt;If you live in Bitwarden anyway, anon.li implements the Addy.io-compatible flow. In &lt;strong&gt;Settings → Options → Username Generator&lt;/strong&gt;, pick &lt;strong&gt;Forwarded Email Alias → addy.io&lt;/strong&gt;, paste your API key, and set the URL to &lt;code&gt;https://anon.li/api/v1&lt;/code&gt;. Now the username field on every "create new login" sheet has a generate button that talks to the same endpoint. That's the fully no-friction version.&lt;/p&gt;

&lt;p&gt;The CLI still wins for terminal flows - quick test signups, registering API sandboxes, anywhere I'm not in the password manager UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's actually happening behind the curtain
&lt;/h2&gt;

&lt;p&gt;The Alias backend isn't a hosted mailbox. There's no inbox. anon.li's mail server takes the inbound message, optionally encrypts the forwarded copy with your recipient's PGP key, signs it with DKIM, rewrites the envelope sender (SRS) so SPF still aligns, and ships it to your real address. The only state stored per alias is aggregate counters: received, blocked, last-seen-at.&lt;/p&gt;

&lt;p&gt;That trust model is what makes me comfortable using disposable aliases for anything moderately sensitive - receipts, account confirmations, the dentist. Even if anon.li got fully owned tomorrow, there's no historical archive of my mail to leak. Just the routing config.&lt;/p&gt;

&lt;p&gt;It's not a replacement for E2EE messaging. SMTP is what it is. But it raises the floor by a meaningful amount, and the API makes it easy enough that I actually &lt;em&gt;use&lt;/em&gt; it instead of giving up and pasting my real address.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole script
&lt;/h2&gt;

&lt;p&gt;For copy-paste convenience:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/.bashrc&lt;/span&gt;
&lt;span class="nb"&gt;alias&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;key
  &lt;span class="nv"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ~/.config/anon/key 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"no api key at ~/.config/anon/key"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="k"&gt;return &lt;/span&gt;1
  &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;anon&lt;/span&gt;&lt;span class="p"&gt;.li&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;response
  &lt;span class="nv"&gt;response&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"https://anon.li/api/v1/alias?generate=true"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;domain&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$domain&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;email
  &lt;span class="nv"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.data.email // empty'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"alias creation failed:"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="k"&gt;return &lt;/span&gt;1
  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; pbcopy &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pbcopy &lt;span class="se"&gt;\&lt;/span&gt;
                  &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; wl-copy &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; wl-copy &lt;span class="se"&gt;\&lt;/span&gt;
                  &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; xclip &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; xclip &lt;span class="nt"&gt;-selection&lt;/span&gt; clipboard&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="s2"&gt; (copied)"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Friction is the enemy of good security habits. The smaller you can make the gap between "I want to do the right thing" and "the right thing is done," the more often you'll do it. A single shell function won me back the habit. Try it.&lt;/p&gt;

</description>
      <category>bash</category>
      <category>cli</category>
      <category>privacy</category>
      <category>productivity</category>
    </item>
    <item>
      <title>What do you use for file sharing? Why?</title>
      <dc:creator>anon.li</dc:creator>
      <pubDate>Sat, 25 Apr 2026 21:07:30 +0000</pubDate>
      <link>https://dev.to/anonli/what-do-you-use-for-file-sharing-why-3583</link>
      <guid>https://dev.to/anonli/what-do-you-use-for-file-sharing-why-3583</guid>
      <description>&lt;p&gt;Hi!&lt;br&gt;
Developer of &lt;a href="https://anon.li/drop" rel="noopener noreferrer"&gt;anon.li Drop&lt;/a&gt; here. We offer end-to-end encrypted file sharing of files up to 250GB in size through our website, API, CLI and even MCP for AI.&lt;br&gt;
What do you use for file sharing? What matters to you the most when it comes to file sharing? Is it privacy, modern UI, pricing...?&lt;br&gt;
Thank you for your answers!&lt;/p&gt;

</description>
      <category>discuss</category>
    </item>
    <item>
      <title>SimpleLogin vs anon.li - a developer's honest comparison</title>
      <dc:creator>anon.li</dc:creator>
      <pubDate>Sun, 19 Apr 2026 09:54:08 +0000</pubDate>
      <link>https://dev.to/anonli/simplelogin-vs-anonli-a-developers-honest-comparison-5e15</link>
      <guid>https://dev.to/anonli/simplelogin-vs-anonli-a-developers-honest-comparison-5e15</guid>
      <description>&lt;p&gt;If you care about your inbox - and your privacy - email aliasing is one of the best habits you can build. The idea is simple: instead of handing out your real address, you hand out a disposable alias that forwards mail to you. One service gets compromised? Disable the alias, never touch your real inbox.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SimpleLogin&lt;/strong&gt; is the established name in this space, now owned by Proton. &lt;strong&gt;anon.li&lt;/strong&gt; is a new privacy-focused alternative that launched in April 2026, built with a Liechtenstein jurisdiction philosophy and designed from the ground up for developers and privacy enthusiasts who want more than just forwarding.&lt;/p&gt;

&lt;p&gt;Let's go feature by feature.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick overview
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;SimpleLogin&lt;/th&gt;
&lt;th&gt;anon.li&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Open source&lt;/td&gt;
&lt;td&gt;✅ AGPL v3&lt;/td&gt;
&lt;td&gt;✅ AGPL v3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email forwarding&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Send from alias&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom domains&lt;/td&gt;
&lt;td&gt;✅ Premium&lt;/td&gt;
&lt;td&gt;✅ Premium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PGP forwarding&lt;/td&gt;
&lt;td&gt;✅ Premium&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;Free&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser extensions&lt;/td&gt;
&lt;td&gt;✅ Chrome, Firefox, Safari, Edge&lt;/td&gt;
&lt;td&gt;✅ Chrome, Firefox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile apps&lt;/td&gt;
&lt;td&gt;✅ iOS + Android&lt;/td&gt;
&lt;td&gt;❌ Web-first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REST API&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLI&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP server&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E2EE file sharing&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ (Drops)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Independent (no Big Tech parent)&lt;/td&gt;
&lt;td&gt;❌ (Proton)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Email aliasing - the core
&lt;/h2&gt;

&lt;p&gt;Both services nail the fundamentals: you create an alias, emails forward to your real inbox, and you can disable or delete aliases at any time. Neither service stores your email content - messages are forwarded and immediately discarded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free tier:&lt;/strong&gt; SimpleLogin requires a subscription to enable PGP encryption. anon.li offers it for free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replying from aliases:&lt;/strong&gt; Both support replying from an alias. Your real address is never exposed - not even in outbound mail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom domains:&lt;/strong&gt; Both SimpleLogin &amp;amp; anon.li support custom domains.&lt;/p&gt;




&lt;h2&gt;
  
  
  The developer surface
&lt;/h2&gt;

&lt;p&gt;This is where the comparison gets interesting. SimpleLogin has a solid REST API, and that's it. anon.li ships with a full developer ecosystem out of the gate.&lt;/p&gt;

&lt;h3&gt;
  
  
  REST API
&lt;/h3&gt;

&lt;p&gt;Both services expose a REST API for programmatic alias management. With anon.li you can create, list, toggle, and delete aliases, manage recipients, and manage encrypted file drops - all from your own scripts and applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  CLI
&lt;/h3&gt;

&lt;p&gt;SimpleLogin has no official CLI. anon.li ships one. If you live in the terminal - and many developers do - this is a significant quality-of-life difference.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Manage aliases from the terminal&lt;/span&gt;
anonli &lt;span class="nb"&gt;alias &lt;/span&gt;create &lt;span class="nt"&gt;--note&lt;/span&gt; &lt;span class="s2"&gt;"newsletter signup"&lt;/span&gt;
anonli &lt;span class="nb"&gt;alias &lt;/span&gt;list
anonli &lt;span class="nb"&gt;alias &lt;/span&gt;toggle abc123

&lt;span class="c"&gt;# Manage encrypted file drops&lt;/span&gt;
anonli drop list
anonli drop toggle abc123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI supports all API operations, including encrypted file drop management - useful for quickly sharing a secret, a config file, or a private key with a colleague without spinning up a separate file sharing service.&lt;/p&gt;

&lt;h3&gt;
  
  
  MCP server - the wildcard
&lt;/h3&gt;

&lt;p&gt;This is something SimpleLogin doesn't offer at all. anon.li ships a native &lt;strong&gt;Model Context Protocol (MCP) server&lt;/strong&gt;, which means AI assistants like Claude can directly manage your aliases.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;With the anon.li MCP server connected, you can ask your AI assistant to list your aliases, create a new one for a specific purpose, toggle an alias on or off, list your encrypted drops, or manage recipients - all without leaving your chat interface.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This isn't a gimmick. As AI assistants become part of everyday workflows, having your privacy tooling directly accessible from the assistant that's helping you draft emails, manage sign-ups, and organize subscriptions is genuinely useful. anon.li is ahead of the curve here.&lt;/p&gt;




&lt;h2&gt;
  
  
  Encrypted file sharing - Drops
&lt;/h2&gt;

&lt;p&gt;This is a feature category SimpleLogin doesn't touch at all. anon.li includes &lt;strong&gt;end-to-end encrypted file sharing&lt;/strong&gt;, called &lt;a href="https://anon.li/drop" rel="noopener noreferrer"&gt;anon.li Drop&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Files are encrypted client-side with the user's vault key before upload. Not even anon.li can read the contents or filenames. You share a drop link; the recipient downloads and decrypts. Drops support expiry dates, download count limits, and can be toggled off remotely and up to 250GB in size.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Encryption model&lt;/td&gt;
&lt;td&gt;Client-side E2EE. Files encrypted before they leave your device. Server stores ciphertext only.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access controls&lt;/td&gt;
&lt;td&gt;Set download limits, expiry dates. Disable a drop remotely at any time.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API + CLI access&lt;/td&gt;
&lt;td&gt;List, manage, and toggle drops via API, CLI, and MCP server - not just the web UI.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a developer who occasionally needs to share a &lt;code&gt;.env&lt;/code&gt; file, a private certificate, or a sensitive document - and wants to do it without trusting a third-party service - Drops is a genuinely useful feature that SimpleLogin simply doesn't compete on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Privacy posture and jurisdiction
&lt;/h2&gt;

&lt;p&gt;SimpleLogin is operated by Proton AG and subject to Swiss law - which has strong privacy protections, but Proton is now a large company with investor obligations, a broad product portfolio, and a corporate structure that has grown significantly since SimpleLogin was an independent project.&lt;/p&gt;

&lt;p&gt;anon.li is independently operated with a Liechtenstein jurisdiction philosophy. It's a smaller, more focused service - which cuts both ways: fewer resources, but also no corporate parent that could change direction, get acquired, or be pressured by a larger ecosystem.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ SimpleLogin was acquired by Proton in 2022. While Proton has a strong privacy reputation, the service is no longer community-independent. If you prefer your privacy tools to be genuinely independent, anon.li is the stronger philosophical fit.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Both services are &lt;strong&gt;AGPL v3&lt;/strong&gt; open source. Neither stores email content. Both use TLS in transit. SimpleLogin has optional PGP forwarding at the premium tier; anon.li has a zero-knowledge Drops system today and PGP on the roadmap.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ecosystem and integrations
&lt;/h2&gt;

&lt;p&gt;SimpleLogin's biggest ecosystem advantage is &lt;strong&gt;Proton Pass integration&lt;/strong&gt;. If you're already in the Proton ecosystem (ProtonMail, Proton VPN, Proton Pass), SimpleLogin slots in seamlessly - alias suggestions inside the password manager, one unified subscription, Proton's infrastructure behind you.&lt;/p&gt;

&lt;p&gt;anon.li's ecosystem advantage is developer depth. The combination of REST API + CLI + browser extension + MCP server means it integrates with your workflow however you prefer to work - from the terminal, from the browser, from an AI assistant, or via scripts in your own applications.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who should use which
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Choose SimpleLogin if...
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You're in the Proton ecosystem&lt;/strong&gt; - ProtonMail + Proton Pass + SimpleLogin is the most seamless bundle for privacy-focused non-developers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need PGP forwarding today&lt;/strong&gt; - SimpleLogin's PGP feature is mature and well-documented. anon.li has it on the roadmap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want iOS/Android apps&lt;/strong&gt; - SimpleLogin has polished native mobile apps. anon.li is web-first for now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want battle-tested reliability&lt;/strong&gt; - five years of production use, millions of aliases, Proton's infrastructure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Choose anon.li if...
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You're a developer&lt;/strong&gt; - API + CLI + MCP server means anon.li fits into your workflow in ways SimpleLogin can't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want E2EE file sharing&lt;/strong&gt; - Drops gives you a genuinely private way to share sensitive files. No equivalent exists in SimpleLogin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You prefer independence&lt;/strong&gt; - no Proton parent, no corporate ecosystem to navigate. One focused product, one team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You use AI assistants in your workflow&lt;/strong&gt; - the MCP server integration is unique. Manage aliases directly from Claude, Cursor, or any MCP-compatible client.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;SimpleLogin remains the most polished and widely trusted email aliasing service available. If you're already inside the Proton ecosystem, there's little reason to leave.&lt;/p&gt;

&lt;p&gt;But anon.li is a compelling new choice for developers and power users. The MCP server is genuinely novel. The CLI is overdue in this category. The encrypted Drops feature adds a dimension that no other aliasing service offers. And being independent - not part of a larger corporate stack - is increasingly a feature, not just a differentiator.&lt;/p&gt;

&lt;p&gt;Both are AGPL v3 open source. Both take your privacy seriously. The choice comes down to ecosystem fit and how deep you want your tooling to go.&lt;/p&gt;




&lt;p&gt;*Try anon.li at &lt;a href="https://anon.li" rel="noopener noreferrer"&gt;anon.li&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>anonymous</category>
      <category>security</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
