<?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: tacet-ink</title>
    <description>The latest articles on DEV Community by tacet-ink (@tacetink).</description>
    <link>https://dev.to/tacetink</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4246214%2F78752e07-0685-4210-9e62-509742351474.png</url>
      <title>DEV Community: tacet-ink</title>
      <link>https://dev.to/tacetink</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tacetink"/>
    <language>en</language>
    <item>
      <title>How to Send a Password Without Leaving It in Your Chat History</title>
      <dc:creator>tacet-ink</dc:creator>
      <pubDate>Sun, 15 Nov 2026 17:42:45 +0000</pubDate>
      <link>https://dev.to/tacet_ink/how-to-send-a-password-without-leaving-it-in-your-chat-history-5cd8</link>
      <guid>https://dev.to/tacet_ink/how-to-send-a-password-without-leaving-it-in-your-chat-history-5cd8</guid>
      <description>&lt;p&gt;You need to give a coworker the Wi-Fi password, or a family member the login for a shared account. The fastest way is to paste it into a chat.&lt;/p&gt;

&lt;p&gt;The trouble is that chat messages are built to last. They sync to every device you're signed in on, they get backed up, they show up in search, and they're still there years later when someone borrows your laptop or a backup leaks. A password pasted into a chat lives as long as the chat does.&lt;/p&gt;

&lt;p&gt;This post walks through a simple fix, the &lt;strong&gt;one-time secret link&lt;/strong&gt;: how it works, the browser crypto that makes it possible, and the limits worth knowing before you rely on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: a link that works once
&lt;/h2&gt;

&lt;p&gt;A one-time secret link (also called burn-after-reading) turns a piece of text into a link that the recipient can open exactly once. After that it's destroyed, and if nobody opens it before it expires, it's destroyed too.&lt;/p&gt;

&lt;p&gt;What's left in the chat history is a dead link. Anyone scrolling back later finds a link that no longer opens anything.&lt;/p&gt;

&lt;p&gt;Plenty of tools offer this. The interesting part is &lt;em&gt;how&lt;/em&gt; you build one so that the service in the middle can't read the secret either.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works under the hood
&lt;/h2&gt;

&lt;p&gt;A good design has four pieces.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Encrypt in the browser, before anything is uploaded
&lt;/h3&gt;

&lt;p&gt;The sender's browser generates a random key and encrypts the text locally, using AES-GCM via the Web Crypto API. Only the encrypted bytes (the ciphertext) go to the server.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Put the key in the URL fragment
&lt;/h3&gt;

&lt;p&gt;The key goes into the link, after the &lt;code&gt;#&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/s/abc123#k=Q3VyaW91cyBhcmVuJ3QgeW91
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The part after &lt;code&gt;#&lt;/code&gt; is the &lt;strong&gt;fragment&lt;/strong&gt;. Browsers don't send the fragment to the server when they request the page, and they leave it out of the &lt;code&gt;Referer&lt;/code&gt; header too. So the server stores ciphertext it has no key for, and the key travels only inside the link you send.&lt;/p&gt;

&lt;p&gt;The fragment does stay in the browser's address bar and history, and in the chat as part of the link. That's exactly the copy that goes dead once the secret is burned.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Burn on read, and expire
&lt;/h3&gt;

&lt;p&gt;When the recipient opens the link, the server hands over the ciphertext once and deletes it. If the link is never opened, a timer deletes it anyway.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Don't burn on page load
&lt;/h3&gt;

&lt;p&gt;Chat apps often fetch a link in the background to build a preview card. If merely loading the page reveals and burns the secret, a preview bot can use up the one view before your recipient ever sees it. The fix is to load the page with zero requests to the secret, and fetch the ciphertext only when a person presses an "open" button.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal sketch in JavaScript
&lt;/h2&gt;

&lt;p&gt;Here's the core of both sides using the Web Crypto API. &lt;code&gt;upload&lt;/code&gt; and &lt;code&gt;fetchAndBurn&lt;/code&gt; stand in for your own server calls.&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;// helpers: bytes &amp;lt;-&amp;gt; URL-safe base64&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;toB64Url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bytes&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;\&lt;span class="o"&gt;+/&lt;/span&gt;&lt;span class="n"&gt;g&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="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="o"&gt;/&lt;/span&gt;\&lt;span class="c1"&gt;//g, "_").replace(/=+$/, "");&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;fromB64Url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nc"&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="n"&gt;s&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="o"&gt;/-/&lt;/span&gt;&lt;span class="n"&gt;g&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="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="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;g&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="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&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="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Sender: encrypt locally, upload only ciphertext, keep the key for the link&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret&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="n"&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="n"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&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="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"AES-GCM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&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="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="s2"&gt;"encrypt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"decrypt"&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="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="n"&gt;ciphertext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"AES-GCM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iv&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&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;TextEncoder&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="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;rawKey&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&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="s2"&gt;"raw"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;id&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;upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iv&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="n"&gt;ciphertext&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="c1"&gt;// server sees iv + ciphertext only&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sb"&gt;`https://example.com/s/${id}#k=${toB64Url(rawKey)}`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Recipient: runs only after the person clicks "open"&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;openLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// base64url has no &amp;amp; or =, so URLSearchParams can parse the fragment safely&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;k&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;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&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="p"&gt;)&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="s2"&gt;"k"&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="n"&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="n"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&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="s2"&gt;"raw"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;fromB64Url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"AES-GCM"&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="s2"&gt;"decrypt"&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ciphertext&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="nf"&gt;fetchAndBurn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// server returns once, then deletes&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;plain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"AES-GCM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iv&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&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="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AES-GCM is authenticated encryption, so if the ciphertext was altered on the way, &lt;code&gt;decrypt&lt;/code&gt; throws instead of returning garbage.&lt;/p&gt;

&lt;p&gt;This sketch fetches before it decrypts, so a link with a wrong or truncated key would still burn the secret. A stricter design checks the link locally before contacting the server, so a broken link never reaches it. The implementation below works that way.&lt;/p&gt;

&lt;h2&gt;
  
  
  One implementation: tacet's one-time secret
&lt;/h2&gt;

&lt;p&gt;tacet's one-time secret follows this pattern, and its explainer page documents the details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The key is generated in your browser and never leaves the device; it lives only in the link fragment (&lt;code&gt;#k=&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The content is encrypted with AES-GCM before upload. The server receives only a truncated hash as a locator, plus the ciphertext.&lt;/li&gt;
&lt;li&gt;It's burned on one open. If the expiry (1, 3 or 7 days) passes first, it's destroyed automatically, with no backup and no recovery.&lt;/li&gt;
&lt;li&gt;Loading the page makes zero requests; the server is only asked when you press open, so a preview bot can't burn it.&lt;/li&gt;
&lt;li&gt;If the locator and the key in the link don't match, no request is made, so a link that couldn't be opened anyway doesn't get burned.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The page also lists its own limits: there's no native app, only a web version; the crypto core is open source but the main program is not; and expiry comes in three fixed tiers. Details: &lt;a href="https://tacet.ink/one-time-secret/en/" rel="noopener noreferrer"&gt;tacet.ink/one-time-secret/en/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What one-time links protect, and what they don't
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;They help with:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The chat history problem.&lt;/strong&gt; After the secret is opened, or expires, the link in the chat is dead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The server in the middle.&lt;/strong&gt; The server only ever holds ciphertext, and the key never reaches it. A breach of the server's storage exposes encrypted bytes, not your password.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;They don't help with:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Someone who gets the link first.&lt;/strong&gt; Until it's opened, the link &lt;em&gt;is&lt;/em&gt; the secret. If someone else opens it first, they read it, but your recipient will then see "already destroyed". Treat that as a signal to change the password.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A compromised device&lt;/strong&gt; on either end, or a recipient who copies the secret somewhere unsafe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The code you're served.&lt;/strong&gt; Encryption in the browser only helps if the JavaScript doing it is honest. A malicious operator could in theory serve tampered code. Open-source crypto code and pages that let you check the behavior yourself reduce that risk, but they don't remove it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Getting it back.&lt;/strong&gt; Once it's opened or expired, it's gone. Don't use a one-time link for the only copy of something you need to keep.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A quick checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Use the shortest expiry that works for the recipient.&lt;/li&gt;
&lt;li&gt;Send the link in a different channel from the username or context it belongs to.&lt;/li&gt;
&lt;li&gt;If the recipient says the link was "already used", assume it leaked and rotate the password.&lt;/li&gt;
&lt;li&gt;For anything long-lived and shared by a team, a shared vault in a password manager is a better home. One-time links are for handing something over, not for storing it.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;This article was created with the help of AI and reviewed by a person before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>privacy</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
