<?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: Achyuth S</title>
    <description>The latest articles on DEV Community by Achyuth S (@oneakira).</description>
    <link>https://dev.to/oneakira</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%2F4131475%2F1aade8c1-550a-4f1c-ab2d-8c8e1fffdb2e.jpg</url>
      <title>DEV Community: Achyuth S</title>
      <link>https://dev.to/oneakira</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/oneakira"/>
    <language>en</language>
    <item>
      <title>Follow-up: I added key rotation to Kagelin's encryption. Here's how it works and what it still misses.</title>
      <dc:creator>Achyuth S</dc:creator>
      <pubDate>Fri, 09 Oct 2026 05:30:30 +0000</pubDate>
      <link>https://dev.to/oneakira/follow-up-i-added-key-rotation-to-kagelins-encryption-heres-how-it-works-and-what-it-still-4iki</link>
      <guid>https://dev.to/oneakira/follow-up-i-added-key-rotation-to-kagelins-encryption-heres-how-it-works-and-what-it-still-4iki</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/oneakira/i-shipped-client-side-encryption-for-kagelin-heres-the-honest-threat-model-including-everywhere-1lph"&gt;last post&lt;/a&gt; I listed what Kagelin's client-side encryption doesn't cover. One gap was key rotation: if your key leaked, it stayed valid for the life of your account. v1.47.0 fixes that.&lt;/p&gt;

&lt;p&gt;A little overkill for a todo app? Yes but I'm having fun building things I've never built!&lt;/p&gt;

&lt;p&gt;Why changing your passphrase wasn't enough&lt;/p&gt;

&lt;p&gt;Your passphrase only wraps the content key. Changing it re-wraps the same key. If someone already has the key, a new passphrase doesn't stop them.&lt;/p&gt;

&lt;p&gt;What rotation does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It makes a new content key. Old keys are kept only until all your data is re-encrypted, and then they're deleted.&lt;/li&gt;
&lt;li&gt;It needs a new passphrase and gives you a new recovery code, because either one could be what leaked.&lt;/li&gt;
&lt;li&gt;It signs out your other sessions.&lt;/li&gt;
&lt;li&gt;The key swap is one server-side transaction. It commits fully or not at all, so you can't end up with a key that nothing can unwrap.&lt;/li&gt;
&lt;li&gt;After the swap, the server rejects any write encrypted with an old key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I didn't use a two-tier setup with a fixed master key that wraps a series of content keys. Anyone who held the leaked master key could still unwrap every new key.&lt;/p&gt;

&lt;p&gt;Rotation only runs when you start it. It is never automatic.&lt;/p&gt;

&lt;p&gt;Ciphertext is now tied to its row&lt;/p&gt;

&lt;p&gt;Before this release, someone with write access to the database could move encrypted values between your rows, and the app couldn't tell. Now every value is sealed with its user, table, column and row id as associated data. A value moved anywhere else fails to decrypt.&lt;/p&gt;

&lt;p&gt;To make this work, every encrypted table now generates its ids on the client, before encryption.&lt;/p&gt;

&lt;p&gt;A value that can't be read no longer breaks the list&lt;/p&gt;

&lt;p&gt;Before, if one value failed to decrypt, the whole query failed and you saw nothing. Now that one field shows "Can't be read" and the rest of the row still works. The app also won't write a row like that back to the server.&lt;/p&gt;

&lt;p&gt;Devices that still have the old key&lt;/p&gt;

&lt;p&gt;A device that missed the rotation tries to save with the old key, and the server refuses. The app then checks for a newer key. If there is one, the device locks and asks for your new passphrase. Changes on that device that weren't synced are discarded, and the unlock screen tells you so.&lt;/p&gt;

&lt;p&gt;What it still doesn't cover&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replaying an older value. Someone could put an older encrypted version of a field back into the same row, and it would still decrypt. Catching that needs a version counter, which isn't built yet.&lt;/li&gt;
&lt;li&gt;Backups. Nightly backups keep the old ciphertext and key wrappers for up to 30 days after a rotation. Protection is not instant.&lt;/li&gt;
&lt;li&gt;Restoring the key row from an old backup. This locks out every device that has already unlocked with the newer key.&lt;/li&gt;
&lt;li&gt;The same gaps as last time. A compromised browser tab (XSS) or a compromised build pipeline is still out of scope.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it:&lt;/p&gt;

&lt;p&gt;&lt;a href="//app.kagelin.app"&gt;app.kagelin.app&lt;/a&gt; · &lt;a href="https://github.com/Achyuth072/kagelin" rel="noopener noreferrer"&gt;Achyuth072/kagelin (AGPL-3.0)&lt;/a&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>security</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I shipped client-side encryption for Kagelin. Here's the honest threat model, including everywhere it still fails.</title>
      <dc:creator>Achyuth S</dc:creator>
      <pubDate>Mon, 21 Sep 2026 06:09:18 +0000</pubDate>
      <link>https://dev.to/oneakira/i-shipped-client-side-encryption-for-kagelin-heres-the-honest-threat-model-including-everywhere-1lph</link>
      <guid>https://dev.to/oneakira/i-shipped-client-side-encryption-for-kagelin-heres-the-honest-threat-model-including-everywhere-1lph</guid>
      <description>&lt;p&gt;A brief intro on &lt;strong&gt;Kagelin&lt;/strong&gt;, its an AIO productivity tool that doesn't distract you. Task, habit, calendars are basis of this tool and much more planned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kagelin&lt;/strong&gt; now encrypts your data client-side. The server never sees the key so a DB leak, a compromised server, or a legal request only gets ciphertext.&lt;/p&gt;

&lt;p&gt;What's encrypted: task text, habit names, project/label names, calendar event details, push notification content.&lt;/p&gt;

&lt;p&gt;What's not: dates, priorities, completion status, due dates. Those stay readable server-side so reminders and sorting still work. Same for basic account info like email, item counts.&lt;/p&gt;

&lt;p&gt;What it doesn't protect against: a compromised browser tab (XSS), or a compromised build pipeline. Key rotation also isn't built yet.&lt;/p&gt;

&lt;p&gt;Short version: Kagelin can't read what you wrote. It can see that your account exists and roughly how much is in it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://app.kagelin.app/" rel="noopener noreferrer"&gt;app.kagelin.app&lt;/a&gt; · &lt;a href="https://github.com/Achyuth072/kagelin" rel="noopener noreferrer"&gt;github.com/Achyuth072/kagelin&lt;/a&gt; (AGPL-3.0)&lt;/p&gt;

&lt;p&gt;If you've shipped E2EE before, how'd you handle key rotation?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>security</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
