<?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: Keshav Chauhan</title>
    <description>The latest articles on DEV Community by Keshav Chauhan (@sezronix).</description>
    <link>https://dev.to/sezronix</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%2F3949265%2F00170790-4259-4b3f-a5c8-32cb484d88eb.jpeg</url>
      <title>DEV Community: Keshav Chauhan</title>
      <link>https://dev.to/sezronix</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sezronix"/>
    <language>en</language>
    <item>
      <title>I'm 16. I built an AES-256-GCM encrypted journal app in Flutter. Here's exactly how the encryption works.</title>
      <dc:creator>Keshav Chauhan</dc:creator>
      <pubDate>Tue, 09 Jun 2026 02:00:00 +0000</pubDate>
      <link>https://dev.to/sezronix/i-built-a-private-encrypted-journal-app-at-16-heres-the-full-technical-breakdown-d4i</link>
      <guid>https://dev.to/sezronix/i-built-a-private-encrypted-journal-app-at-16-heres-the-full-technical-breakdown-d4i</guid>
      <description>&lt;p&gt;I'm Keshav, a solo developer from India building under my studio SezRonix.&lt;/p&gt;

&lt;p&gt;I started keeping a digital journal and noticed something uncomfortable — I was editing myself. Writing around difficult thoughts instead of through them. Eventually I understood why: my entries were sitting on a server I didn't control, in a form someone could technically read.&lt;/p&gt;

&lt;p&gt;So I built RozVibe. A private encrypted journaling app for Android.&lt;/p&gt;

&lt;p&gt;Since dev.to has been kind to my previous posts on &lt;a href="https://dev.to/roninyt_/why-searching-encrypted-data-is-harder-than-most-developers-think-20ij"&gt;searching encrypted data&lt;/a&gt; and &lt;a href="https://dev.to/roninyt_/the-hardest-part-of-building-an-encrypted-journaling-app-wasnt-encryption-3amj"&gt;the hardest parts of building this&lt;/a&gt;, I want to do a complete technical breakdown here — not marketing, just the actual implementation with the real trade-offs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The core design goal
&lt;/h2&gt;

&lt;p&gt;Every sensitive piece of a journal entry — the text content, mood, timestamps — is encrypted on the user's device before it leaves the app. What Firestore receives is a single opaque base64 string. The server never sees plaintext. Ever.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the encryption key works
&lt;/h2&gt;

&lt;p&gt;This is the most important part to understand correctly, so I'll be precise.&lt;/p&gt;

&lt;h3&gt;
  
  
  The key never persists anywhere
&lt;/h3&gt;

&lt;p&gt;The 32-byte AES key lives exclusively in RAM inside the &lt;code&gt;EncryptionService&lt;/code&gt; class as &lt;code&gt;_key&lt;/code&gt; of type &lt;code&gt;encrypt_pkg.Key&lt;/code&gt;. It is never written to Firestore, never written to disk, never saved to SharedPreferences. It exists only while the user is actively logged in.&lt;/p&gt;

&lt;p&gt;On logout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;clear&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="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// immediately wiped from memory&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gone. No trace of it on the device after logout.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key derivation — PBKDF2 with HMAC-SHA-256
&lt;/h3&gt;

&lt;p&gt;Since the key is never stored, it must be reconstructed on every login. This is done using PBKDF2 — a deterministic key derivation function. Same inputs always produce the same output.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;pbkdf2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PBKDF2KeyDerivator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HMac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SHA256Digest&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;pbkdf2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Pbkdf2Parameters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;keyBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pbkdf2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;utf8&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;${userId}&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="si"&gt;${pin ?? "default_secure_vault"}&lt;/span&gt;&lt;span class="s"&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;&lt;strong&gt;100,000 iterations&lt;/strong&gt; of HMAC-SHA-256. This is computationally expensive by design — it makes brute-force attacks against the PIN significantly slower.&lt;/p&gt;

&lt;p&gt;The 44-byte output is split:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bytes 0–31&lt;/strong&gt; → the 32-byte AES-256 key&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bytes 32–43&lt;/strong&gt; → legacy fallback IV (kept for backward compatibility)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The three inputs required to derive the key
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;The user's account ID (&lt;code&gt;userId&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The user's PIN or password&lt;/li&gt;
&lt;li&gt;A 16-byte cryptographically random salt&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Lose any one of these three and you cannot derive the key.&lt;/p&gt;

&lt;h3&gt;
  
  
  The salt — the only persisted cryptographic material
&lt;/h3&gt;

&lt;p&gt;When a user first signs up, a 16-byte random salt is generated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;encrypt_pkg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;IV&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromSecureRandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This salt is stored in two places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Locally&lt;/strong&gt;: Android's &lt;code&gt;EncryptedSharedPreferences&lt;/code&gt; (backed by the hardware Keystore)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firestore&lt;/strong&gt;: &lt;code&gt;users/$userId/crypto_salt&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Firestore copy is what enables multi-device sync — more on that below.&lt;/p&gt;




&lt;h2&gt;
  
  
  Per-entry encryption
&lt;/h2&gt;

&lt;p&gt;Every single encryption event generates a fresh 12-byte IV:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&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;encrypt_pkg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;IV&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromSecureRandom&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;Never reused. Never predictable.&lt;/p&gt;

&lt;p&gt;The cipher is AES-256-GCM. GCM mode gives both confidentiality and integrity — any tampering with the stored ciphertext is detectable on decryption. The final stored format is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Base64( IV[12 bytes] + Ciphertext + GCM Auth Tag )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What Firestore actually receives
&lt;/h2&gt;

&lt;p&gt;This is where the design becomes concrete. Inside &lt;code&gt;DiaryEntry.toEncryptedMap()&lt;/code&gt;, all sensitive fields are bundled into a JSON object and encrypted before the document is written:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// sensitive fields bundled and encrypted&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;encryptedData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encryptionService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;encryptData&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="s"&gt;'content'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// journal text&lt;/span&gt;
  &lt;span class="s"&gt;'mood'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;// emotional state&lt;/span&gt;
  &lt;span class="s"&gt;'createdAt'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;createdAt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toIso8601String&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="s"&gt;'updatedAt'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;updatedAt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toIso8601String&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// what gets written to Firestore&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s"&gt;'id'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="c1"&gt;// non-identifying UUID&lt;/span&gt;
  &lt;span class="s"&gt;'userId'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// for security rules&lt;/span&gt;
  &lt;span class="s"&gt;'date_index'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dateIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// for sorting queries&lt;/span&gt;
  &lt;span class="s"&gt;'isFavorite'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;isFavorite&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// for filtering&lt;/span&gt;
  &lt;span class="s"&gt;'data'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;encryptedData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// opaque base64 — everything else&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only the minimum unencrypted metadata needed for Firestore queries exists in plaintext. Everything a user actually wrote is inside that single &lt;code&gt;data&lt;/code&gt; field — unreadable without the key.&lt;/p&gt;

&lt;p&gt;A Firestore breach exposes four non-sensitive fields and one encrypted blob. Nothing readable.&lt;/p&gt;




&lt;h2&gt;
  
  
  How multi-device sync works
&lt;/h2&gt;

&lt;p&gt;This is the question I get most often: if the key never persists, how do entries appear on a different device when you log in?&lt;/p&gt;

&lt;p&gt;The answer is &lt;strong&gt;deterministic key reconstruction&lt;/strong&gt;, not key retrieval.&lt;/p&gt;

&lt;p&gt;Here's the exact flow when you log into a new device:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;AuthService&lt;/code&gt; calls &lt;code&gt;initializeEncryptionForUser&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The app fetches &lt;code&gt;users/$userId/crypto_salt&lt;/code&gt; from Firestore&lt;/li&gt;
&lt;li&gt;PBKDF2 runs with: &lt;code&gt;userId&lt;/code&gt; + &lt;code&gt;PIN&lt;/code&gt; + &lt;code&gt;fetched_salt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Because PBKDF2 is deterministic, the same inputs produce the exact same 32-byte key on the new device's CPU&lt;/li&gt;
&lt;li&gt;That key decrypts all the &lt;code&gt;data&lt;/code&gt; fields from Firestore&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key is never transmitted. It is never stored on the new device until login. It is mathematically reconstructed from credentials the user already knows plus a salt that was synced to Firestore. The moment the user logs out, it's wiped from RAM again.&lt;/p&gt;




&lt;h2&gt;
  
  
  Client-side search
&lt;/h2&gt;

&lt;p&gt;Firestore cannot search encrypted content — you can't run a &lt;code&gt;where&lt;/code&gt; query against a ciphertext field. So search runs entirely client-side.&lt;/p&gt;

&lt;p&gt;The actual implementation in &lt;code&gt;DiaryService.searchEntries&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Stream&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DiaryEntry&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;searchEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;query&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="n"&gt;getEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;entries&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="n"&gt;entries&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;entry&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="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;content&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&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;Flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetch all entries from Firestore (encrypted blobs)&lt;/li&gt;
&lt;li&gt;Decrypt each entry client-side using &lt;code&gt;fromEncryptedMap&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Filter the decrypted list in memory by case-insensitive substring match&lt;/li&gt;
&lt;li&gt;Return matching entries&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The search query itself never leaves the device. No server ever sees what the user is searching for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known limitation&lt;/strong&gt;: this approach decrypts the entire entry list in memory on every search.&lt;/p&gt;

&lt;p&gt;At current scale, that's perfectly acceptable. But as a journal grows into thousands of entries, this becomes increasingly expensive. Every search requires decrypting the vault, iterating through every entry, and performing string matching in memory.&lt;/p&gt;

&lt;p&gt;This isn't unique to RozVibe.&lt;/p&gt;

&lt;p&gt;Many encrypted note-taking and journaling systems face the same fundamental problem:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the server can't read your data, it can't build a traditional search index.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That means developers usually end up choosing one of two compromises:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Server-side indexing (fast search, weaker privacy)&lt;/li&gt;
&lt;li&gt;Full client-side scanning (strong privacy, slower search)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I wanted a third option.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Blind Index Architecture
&lt;/h3&gt;

&lt;p&gt;The next RozVibe update replaces full-vault scanning with a local blind index.&lt;/p&gt;

&lt;p&gt;When a journal entry is saved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SearchTokenizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;extractWords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HMAC_SHA256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;searchKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;blindIndex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;insert&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="n"&gt;entryId&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;Instead of storing plaintext words, the app stores HMAC-SHA256 hashes of those words inside a local SQLite database.&lt;/p&gt;

&lt;p&gt;A simplified record looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;token_hash  →  entry_id

8f2ab4...   →  entry_123
91dfe7...   →  entry_456
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plaintext word itself never enters SQLite.&lt;/p&gt;

&lt;p&gt;Only the cryptographic hash.&lt;/p&gt;

&lt;p&gt;An index is applied to the &lt;code&gt;token_hash&lt;/code&gt; column, allowing SQLite to perform extremely fast lookups.&lt;/p&gt;

&lt;p&gt;When a user searches for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;anxiety
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the search flow becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"anxiety"
      ↓
HMAC-SHA256(searchKey)
      ↓
Lookup token_hash in SQLite
      ↓
Return matching entry IDs
      ↓
Decrypt only those entries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of decrypting every journal entry, the application decrypts only the entries that actually match.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recovering The Index On A New Device
&lt;/h3&gt;

&lt;p&gt;One challenge with local indexing is device migration.&lt;/p&gt;

&lt;p&gt;The SQLite database isn't synchronized through Firestore.&lt;/p&gt;

&lt;p&gt;So what happens when a user logs into a new phone?&lt;/p&gt;

&lt;p&gt;RozVibe performs a one-time backfill process.&lt;/p&gt;

&lt;p&gt;The application:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Downloads encrypted entries from Firestore&lt;/li&gt;
&lt;li&gt;Reconstructs the encryption and search keys&lt;/li&gt;
&lt;li&gt;Decrypts each entry locally&lt;/li&gt;
&lt;li&gt;Generates blind-index hashes&lt;/li&gt;
&lt;li&gt;Rebuilds the SQLite database&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A simplified version looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;encryptedEntries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokenize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;content&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;index&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&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;Once complete, future searches become local database lookups.&lt;/p&gt;

&lt;p&gt;The search query never leaves the device.&lt;/p&gt;

&lt;p&gt;The journal content never leaves the device.&lt;/p&gt;

&lt;p&gt;And the server still has no searchable view of user data.&lt;/p&gt;

&lt;p&gt;That trade-off is important to me.&lt;/p&gt;

&lt;p&gt;Fast search is easy when the server can read everything.&lt;/p&gt;

&lt;p&gt;Building fast search while keeping the server blind is a much more interesting engineering problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  The trade-off I made consciously
&lt;/h2&gt;

&lt;p&gt;Storing the salt in Firestore means someone with both Firebase admin access AND the user's PIN could derive their key.&lt;/p&gt;

&lt;p&gt;I made this trade-off deliberately. The alternative — requiring users to manually export and store their own cryptographic key — is theoretically purer but practically means users lose their journals when they change phones. The UX failure rate on manual key backup is extremely high for a consumer app.&lt;/p&gt;

&lt;p&gt;I chose recoverability over theoretical zero-knowledge purity. I think it was the right call for this use case. I'm open to being wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the app does beyond encryption
&lt;/h2&gt;

&lt;p&gt;The encryption is infrastructure. The actual product:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mood tracking&lt;/strong&gt;: 5 states — Radiant, Calm, Neutral, Low, Stormy — selected per entry&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Insights dashboard&lt;/strong&gt;: 30-day mood trend chart, frequency histogram, distribution pie chart — all computed on-device using fl_chart&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sanctuary Lock&lt;/strong&gt;: 4-digit PIN screen gating app access on every open&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rich text editor&lt;/strong&gt;: flutter_quill for formatted journaling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writing streak tracker&lt;/strong&gt;: consecutive day counter&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Journaling prompts&lt;/strong&gt;: guided starting points for blank-page anxiety&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline support&lt;/strong&gt;: Firebase's offline persistence cache — works without internet&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Full tech stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;Flutter (Dart, SDK ≥3.3.0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State management&lt;/td&gt;
&lt;td&gt;Riverpod + riverpod_generator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;Firebase Authentication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;Cloud Firestore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encryption&lt;/td&gt;
&lt;td&gt;pointycastle (PBKDF2), encrypt (AES-256-GCM)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secure storage&lt;/td&gt;
&lt;td&gt;flutter_secure_storage (encryptedSharedPreferences:true)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Charts&lt;/td&gt;
&lt;td&gt;fl_chart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rich text&lt;/td&gt;
&lt;td&gt;flutter_quill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Calendar&lt;/td&gt;
&lt;td&gt;table_calendar&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Explore password-based salt derivation&lt;/strong&gt;&lt;br&gt;
Rather than storing the salt in Firestore at all, derive it deterministically from the password itself using a separate KDF. Eliminates the Firestore salt dependency entirely at the cost of making salt rotation impossible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Open source the encryption layer&lt;/strong&gt;&lt;br&gt;
The &lt;code&gt;EncryptionService&lt;/code&gt; specifically would benefit from public audit. It hasn't happened yet. It should.&lt;/p&gt;




&lt;h2&gt;
  
  
  Download
&lt;/h2&gt;

&lt;p&gt;If you're curious about how these ideas translate into a real product, RozVibe is available to try.&lt;/p&gt;

&lt;p&gt;Free on Android via Uptodown — no Play Store required:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://rozvibe.en.uptodown.com/android" rel="noopener noreferrer"&gt;https://rozvibe.en.uptodown.com/android&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Website: &lt;strong&gt;&lt;a href="https://rozvibe.me" rel="noopener noreferrer"&gt;https://rozvibe.me&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;I read every comment personally. If you see something wrong in the implementation — especially in the key derivation or GCM usage — I genuinely want to know. This is my first app at this scale and the security community here is more qualified than I am to spot problems.&lt;/p&gt;

&lt;p&gt;— Keshav&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>flutter</category>
      <category>privacy</category>
      <category>security</category>
    </item>
    <item>
      <title>Why Searching Encrypted Data Is Harder Than Most Developers Think</title>
      <dc:creator>Keshav Chauhan</dc:creator>
      <pubDate>Tue, 02 Jun 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/sezronix/why-searching-encrypted-data-is-harder-than-most-developers-think-20ij</link>
      <guid>https://dev.to/sezronix/why-searching-encrypted-data-is-harder-than-most-developers-think-20ij</guid>
      <description>&lt;p&gt;Most developers take search for granted.&lt;/p&gt;

&lt;p&gt;Add a search bar.&lt;/p&gt;

&lt;p&gt;Query the database.&lt;/p&gt;

&lt;p&gt;Return matching results.&lt;/p&gt;

&lt;p&gt;Simple.&lt;/p&gt;

&lt;p&gt;At least that's what I thought before building RozVibe, a privacy-first encrypted journaling app.&lt;/p&gt;

&lt;p&gt;Then encryption entered the picture.&lt;/p&gt;

&lt;p&gt;And suddenly one of the most basic features in software became surprisingly difficult.&lt;/p&gt;

&lt;p&gt;The Search Problem Nobody Notices&lt;/p&gt;

&lt;p&gt;When you search inside a typical application, the backend already knows your data.&lt;/p&gt;

&lt;p&gt;That makes searching straightforward.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;Blog platforms search article content&lt;br&gt;
Note-taking apps search stored notes&lt;br&gt;
CRMs search customer information&lt;br&gt;
Journaling apps search journal entries&lt;/p&gt;

&lt;p&gt;The server can index everything because the server can read everything.&lt;/p&gt;

&lt;p&gt;But what happens when the server cannot read the data?&lt;/p&gt;

&lt;p&gt;That's where things get interesting.&lt;/p&gt;

&lt;p&gt;Encryption Changes The Rules&lt;/p&gt;

&lt;p&gt;At RozVibe, journal entries are encrypted on the user's device before they're synced.&lt;/p&gt;

&lt;p&gt;The server only receives encrypted ciphertext.&lt;/p&gt;

&lt;p&gt;Not titles.&lt;/p&gt;

&lt;p&gt;Not moods.&lt;/p&gt;

&lt;p&gt;Not reflections.&lt;/p&gt;

&lt;p&gt;Not memories.&lt;/p&gt;

&lt;p&gt;Just encrypted data.&lt;/p&gt;

&lt;p&gt;That's great for privacy.&lt;/p&gt;

&lt;p&gt;It's terrible for traditional search.&lt;/p&gt;

&lt;p&gt;Because databases cannot search what they cannot understand.&lt;/p&gt;

&lt;p&gt;Imagine storing this:&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;
  "content": "Today was a great day."&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;A traditional database can easily find the word "great".&lt;/p&gt;

&lt;p&gt;Now imagine storing:&lt;/p&gt;

&lt;p&gt;Q7x6Mz8Pj4T2vNf...&lt;/p&gt;

&lt;p&gt;That's what encrypted content looks like.&lt;/p&gt;

&lt;p&gt;The database has no idea what's inside.&lt;/p&gt;

&lt;p&gt;And that's exactly the point.&lt;/p&gt;

&lt;p&gt;The Obvious Solution Is Also The Wrong One&lt;/p&gt;

&lt;p&gt;When many developers first encounter this problem, the obvious answer is:&lt;/p&gt;

&lt;p&gt;"Why not decrypt everything on the server before searching?"&lt;/p&gt;

&lt;p&gt;Technically, that works.&lt;/p&gt;

&lt;p&gt;But it completely breaks the privacy model.&lt;/p&gt;

&lt;p&gt;The moment a server can decrypt user content, you've reintroduced trust requirements.&lt;/p&gt;

&lt;p&gt;Now users must trust:&lt;/p&gt;

&lt;p&gt;your infrastructure&lt;br&gt;
your employees&lt;br&gt;
your logging systems&lt;br&gt;
your future business decisions&lt;br&gt;
your security practices&lt;/p&gt;

&lt;p&gt;The architecture is no longer truly private.&lt;/p&gt;

&lt;p&gt;We wanted something different.&lt;/p&gt;

&lt;p&gt;How We Solved Search In RozVibe&lt;/p&gt;

&lt;p&gt;Instead of searching in the cloud, we moved search entirely to the device.&lt;/p&gt;

&lt;p&gt;The process looks roughly like this:&lt;/p&gt;

&lt;p&gt;Retrieve encrypted entries&lt;br&gt;
Decrypt locally in memory&lt;br&gt;
Perform search on-device&lt;br&gt;
Display results&lt;br&gt;
Discard temporary memory&lt;/p&gt;

&lt;p&gt;The backend never participates in search operations.&lt;/p&gt;

&lt;p&gt;The user's query never leaves the device.&lt;/p&gt;

&lt;p&gt;The journal content never leaves the device in readable form.&lt;/p&gt;

&lt;p&gt;Privacy remains intact.&lt;/p&gt;

&lt;p&gt;The Tradeoff Nobody Talks About&lt;/p&gt;

&lt;p&gt;Privacy-first engineering is largely a series of tradeoffs.&lt;/p&gt;

&lt;p&gt;Client-side search introduces advantages:&lt;/p&gt;

&lt;p&gt;✅ Better privacy&lt;/p&gt;

&lt;p&gt;✅ Zero-knowledge architecture&lt;/p&gt;

&lt;p&gt;✅ No searchable user profiles&lt;/p&gt;

&lt;p&gt;✅ No server-side indexing&lt;/p&gt;

&lt;p&gt;But it also introduces costs:&lt;/p&gt;

&lt;p&gt;❌ More memory usage&lt;/p&gt;

&lt;p&gt;❌ More CPU work on the device&lt;/p&gt;

&lt;p&gt;❌ Increased complexity&lt;/p&gt;

&lt;p&gt;❌ Slower searches for very large datasets&lt;/p&gt;

&lt;p&gt;Privacy isn't free.&lt;/p&gt;

&lt;p&gt;It simply changes where complexity lives.&lt;/p&gt;

&lt;p&gt;Building Features With A Blind Backend&lt;/p&gt;

&lt;p&gt;Search wasn't the only challenge.&lt;/p&gt;

&lt;p&gt;Once the backend becomes intentionally blind, many common SaaS features become harder.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;p&gt;Search&lt;/p&gt;

&lt;p&gt;The server can't index content.&lt;/p&gt;

&lt;p&gt;Recommendations&lt;/p&gt;

&lt;p&gt;The server can't analyze user behavior.&lt;/p&gt;

&lt;p&gt;AI Features&lt;/p&gt;

&lt;p&gt;The server can't inspect journal entries.&lt;/p&gt;

&lt;p&gt;Analytics&lt;/p&gt;

&lt;p&gt;The server can't understand emotional patterns.&lt;/p&gt;

&lt;p&gt;Moderation&lt;/p&gt;

&lt;p&gt;The server can't review stored content.&lt;/p&gt;

&lt;p&gt;Every feature must be reconsidered through a different architectural lens.&lt;/p&gt;

&lt;p&gt;What This Taught Me About Privacy&lt;/p&gt;

&lt;p&gt;Before building RozVibe, I thought privacy was mostly about encryption.&lt;/p&gt;

&lt;p&gt;Now I think privacy is more about restraint.&lt;/p&gt;

&lt;p&gt;Encryption is the easy part.&lt;/p&gt;

&lt;p&gt;The difficult part is willingly giving up access to data that could make product development easier.&lt;/p&gt;

&lt;p&gt;Many software systems are built around visibility.&lt;/p&gt;

&lt;p&gt;Privacy-first systems are built around intentional blindness.&lt;/p&gt;

&lt;p&gt;And that changes almost every engineering decision.&lt;/p&gt;

&lt;p&gt;The Unexpected Benefit&lt;/p&gt;

&lt;p&gt;One of the most interesting outcomes wasn't technical.&lt;/p&gt;

&lt;p&gt;It was psychological.&lt;/p&gt;

&lt;p&gt;When users know their thoughts remain private, they write differently.&lt;/p&gt;

&lt;p&gt;More honestly.&lt;/p&gt;

&lt;p&gt;More openly.&lt;/p&gt;

&lt;p&gt;More completely.&lt;/p&gt;

&lt;p&gt;And for a journaling app, that matters far more than a slightly faster search query.&lt;/p&gt;

&lt;p&gt;Final Thoughts&lt;/p&gt;

&lt;p&gt;Search feels simple because most applications can read their own data.&lt;/p&gt;

&lt;p&gt;Once you adopt a privacy-first architecture, that assumption disappears.&lt;/p&gt;

&lt;p&gt;Suddenly every feature becomes a design decision.&lt;/p&gt;

&lt;p&gt;Not just a technical one.&lt;/p&gt;

&lt;p&gt;Building RozVibe taught me that privacy isn't something you add later.&lt;/p&gt;

&lt;p&gt;It fundamentally shapes the architecture from day one.&lt;/p&gt;

&lt;p&gt;And surprisingly, one of the hardest parts wasn't encryption.&lt;/p&gt;

&lt;p&gt;It was search.&lt;/p&gt;

&lt;p&gt;About RozVibe&lt;/p&gt;

&lt;p&gt;RozVibe is a privacy-first encrypted journaling app designed to help people reflect, track moods, and write freely without surveillance.&lt;/p&gt;

&lt;p&gt;Download: &lt;a href="https://rozvibe.uptodown.com/" rel="noopener noreferrer"&gt;[DOWNLOAD LINK]&lt;/a&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>programming</category>
      <category>security</category>
      <category>privacy</category>
    </item>
    <item>
      <title>The Hardest Part of Building an Encrypted Journaling App Wasn’t Encryption</title>
      <dc:creator>Keshav Chauhan</dc:creator>
      <pubDate>Tue, 26 May 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/sezronix/the-hardest-part-of-building-an-encrypted-journaling-app-wasnt-encryption-3amj</link>
      <guid>https://dev.to/sezronix/the-hardest-part-of-building-an-encrypted-journaling-app-wasnt-encryption-3amj</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Lessons learned building client-side AES-256 encryption, secure sync, and emotionally safe UX in Flutter.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most apps treat privacy as a feature.&lt;/p&gt;

&lt;p&gt;We treated it as infrastructure.&lt;/p&gt;

&lt;p&gt;When we started building RozVibe - a privacy-first encrypted journaling app built with Flutter - we quickly realized something uncomfortable:&lt;/p&gt;

&lt;p&gt;A journaling app without real privacy creates emotional hesitation.&lt;/p&gt;

&lt;p&gt;People write differently when they think someone else might read their thoughts.&lt;/p&gt;

&lt;p&gt;And that changes everything.&lt;/p&gt;

&lt;p&gt;Because journaling is not just data storage.&lt;/p&gt;

&lt;p&gt;It’s cognitive decompression.&lt;/p&gt;

&lt;p&gt;It’s emotional honesty.&lt;/p&gt;

&lt;p&gt;And honesty requires trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Problem With Most “Private” Apps&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A surprising number of apps marketed as “private” still process user data server-side.&lt;/p&gt;

&lt;p&gt;Yes, they may use HTTPS.&lt;/p&gt;

&lt;p&gt;Yes, databases may be encrypted at rest.&lt;/p&gt;

&lt;p&gt;But in many systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the company can still access user content&lt;/li&gt;
&lt;li&gt;administrators theoretically retain visibility&lt;/li&gt;
&lt;li&gt;journal entries may be processed in plaintext&lt;/li&gt;
&lt;li&gt;personal reflections become behavioral analytics data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Technically, the data may be secured.&lt;/p&gt;

&lt;p&gt;Psychologically, it still doesn’t feel safe.&lt;/p&gt;

&lt;p&gt;That distinction became incredibly important while designing RozVibe.&lt;/p&gt;

&lt;p&gt;Because emotional safety is not only a UX problem.&lt;/p&gt;

&lt;p&gt;It’s an architectural problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Our Decision: Encrypt Before Data Leaves the Device&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;From the beginning, we adopted a strict engineering principle:&lt;/p&gt;

&lt;p&gt;User journal content should never be readable by our servers.&lt;/p&gt;

&lt;p&gt;That decision immediately shaped the entire system architecture.&lt;/p&gt;

&lt;p&gt;Instead of relying on traditional server-side encryption, we implemented client-side AES-256-GCM encryption directly on the device.&lt;/p&gt;

&lt;p&gt;Before any journal entry is synced:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Content is encrypted locally&lt;/li&gt;
&lt;li&gt;A unique nonce/IV is generated for every encryption operation&lt;/li&gt;
&lt;li&gt;Authentication tags are attached&lt;/li&gt;
&lt;li&gt;Only ciphertext is transmitted to the backend&lt;/li&gt;
&lt;li&gt;The server stores encrypted blobs only&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The backend never sees plaintext journal entries.&lt;/p&gt;

&lt;p&gt;Even if storage infrastructure were compromised, the stored data would remain unreadable without user-controlled encryption keys.&lt;/p&gt;

&lt;p&gt;That trust model mattered deeply to us.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why We Didn’t Use Server-Side Encryption&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Server-side encryption solves infrastructure security problems.&lt;/p&gt;

&lt;p&gt;But it does not fully solve trust problems.&lt;/p&gt;

&lt;p&gt;With server-side encryption:&lt;/p&gt;

&lt;p&gt;the backend still controls decryption&lt;br&gt;
plaintext may exist during processing&lt;br&gt;
administrators can theoretically access content&lt;br&gt;
users must trust infrastructure they cannot verify&lt;/p&gt;

&lt;p&gt;We wanted a different model.&lt;/p&gt;

&lt;p&gt;In RozVibe, encryption happens before data leaves the device.&lt;/p&gt;

&lt;p&gt;The server stores ciphertext - not journal entries.&lt;/p&gt;

&lt;p&gt;That architectural distinction fundamentally changes the relationship between the product and the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why AES-256-GCM?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;We evaluated multiple encryption approaches before choosing AES-256-GCM.&lt;/p&gt;

&lt;p&gt;For a mobile journaling application, we needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authenticated encryption&lt;/li&gt;
&lt;li&gt;strong security guarantees&lt;/li&gt;
&lt;li&gt;tamper detection&lt;/li&gt;
&lt;li&gt;low performance overhead&lt;/li&gt;
&lt;li&gt;reliable mobile compatibility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AES-GCM offered all of those advantages.&lt;/p&gt;

&lt;p&gt;Performance mattered more than we initially expected.&lt;/p&gt;

&lt;p&gt;People open journaling apps during emotionally important moments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;late-night reflection&lt;/li&gt;
&lt;li&gt;anxiety spikes&lt;/li&gt;
&lt;li&gt;emotional overwhelm&lt;/li&gt;
&lt;li&gt;quick memory capture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Encryption cannot introduce noticeable friction.&lt;/p&gt;

&lt;p&gt;Otherwise people stop writing.&lt;/p&gt;

&lt;p&gt;One of the most overlooked parts of privacy engineering is this:&lt;/p&gt;

&lt;p&gt;Security that feels heavy often becomes abandoned security.&lt;/p&gt;

&lt;p&gt;AES-GCM gave us both security and responsiveness.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Hardest Engineering Problem Wasn’t Encryption&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Surprisingly, implementing encryption itself was not the hardest challenge.&lt;/p&gt;

&lt;p&gt;Key management was.&lt;/p&gt;

&lt;p&gt;Because encryption strength becomes meaningless if key handling is weak.&lt;/p&gt;

&lt;p&gt;Mobile apps constantly deal with unstable environments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;devices restart&lt;/li&gt;
&lt;li&gt;sessions expire&lt;/li&gt;
&lt;li&gt;users reinstall apps&lt;/li&gt;
&lt;li&gt;cloud sync introduces edge cases&lt;/li&gt;
&lt;li&gt;operating systems aggressively manage memory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We explored multiple approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Secure Enclave / Keystore integration&lt;/li&gt;
&lt;li&gt;OS-protected secret storage&lt;/li&gt;
&lt;li&gt;session-derived keys&lt;/li&gt;
&lt;li&gt;encrypted persistence layers&lt;/li&gt;
&lt;li&gt;recovery edge cases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Balancing usability with strong security became one of the most difficult architectural tradeoffs in the entire project.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Privacy-First Architecture Changes Everything&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;One of the surprising realizations during development was how quickly privacy-first architecture complicates otherwise normal product decisions.&lt;/p&gt;

&lt;p&gt;Even simple features become harder when the backend is intentionally blind.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Search&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Traditional search systems index plaintext content server-side.&lt;/p&gt;

&lt;p&gt;Encrypted journaling systems cannot safely do that.&lt;/p&gt;

&lt;p&gt;That forces difficult tradeoffs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local indexing&lt;/li&gt;
&lt;li&gt;encrypted search models&lt;/li&gt;
&lt;li&gt;limited search capabilities&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Syncing&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Traditional sync systems assume the backend understands the data structure.&lt;/p&gt;

&lt;p&gt;Encrypted sync changes that completely.&lt;/p&gt;

&lt;p&gt;The server becomes intentionally unaware of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;journal content&lt;/li&gt;
&lt;li&gt;emotional metadata&lt;/li&gt;
&lt;li&gt;search context&lt;/li&gt;
&lt;li&gt;user meaning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That affected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;conflict resolution&lt;/li&gt;
&lt;li&gt;sync optimization&lt;/li&gt;
&lt;li&gt;storage debugging&lt;/li&gt;
&lt;li&gt;recovery flows&lt;/li&gt;
&lt;li&gt;consistency handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Privacy-first engineering forces you to rethink standard SaaS assumptions from the ground up.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Security UX Is Emotional UX&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;One lesson became increasingly clear while building RozVibe:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Security UX is emotional UX.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If privacy tools feel intimidating, users disengage.&lt;/p&gt;

&lt;p&gt;Many secure products accidentally create anxiety through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;aggressive warnings&lt;/li&gt;
&lt;li&gt;technical overload&lt;/li&gt;
&lt;li&gt;complicated onboarding&lt;/li&gt;
&lt;li&gt;“cybersecurity dashboard” aesthetics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We wanted the opposite.&lt;/p&gt;

&lt;p&gt;So we intentionally designed RozVibe with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;minimal visual noise&lt;/li&gt;
&lt;li&gt;calm writing spaces&lt;/li&gt;
&lt;li&gt;quiet onboarding&lt;/li&gt;
&lt;li&gt;simple privacy explanations&lt;/li&gt;
&lt;li&gt;reduced cognitive overload&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We didn’t want users to constantly think about encryption.&lt;/p&gt;

&lt;p&gt;We wanted them to feel psychologically safe enough to write honestly.&lt;/p&gt;

&lt;p&gt;That distinction matters more than many engineers realize.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Privacy Is Also a Psychological Design Problem&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Building RozVibe changed how we think about software itself.&lt;/p&gt;

&lt;p&gt;Modern apps are often optimized for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;engagement&lt;/li&gt;
&lt;li&gt;retention&lt;/li&gt;
&lt;li&gt;extraction&lt;/li&gt;
&lt;li&gt;behavioral profiling&lt;/li&gt;
&lt;li&gt;surveillance-driven personalization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Over time, users subconsciously learn this.&lt;/p&gt;

&lt;p&gt;And they become less honest online.&lt;/p&gt;

&lt;p&gt;Especially in personal spaces.&lt;/p&gt;

&lt;p&gt;People begin self-censoring.&lt;/p&gt;

&lt;p&gt;Even privately.&lt;/p&gt;

&lt;p&gt;That realization fundamentally changed how we approached product design.&lt;/p&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;p&gt;“How much data can we collect?”&lt;/p&gt;

&lt;p&gt;We started asking:&lt;/p&gt;

&lt;p&gt;“How little data do we actually need?”&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;p&gt;“How do we maximize engagement?”&lt;/p&gt;

&lt;p&gt;We ask:&lt;/p&gt;

&lt;p&gt;“How do we reduce emotional friction?”&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;p&gt;“How do we maximize retention?”&lt;/p&gt;

&lt;p&gt;We ask:&lt;/p&gt;

&lt;p&gt;“How do we create trust?”&lt;/p&gt;

&lt;p&gt;Those questions lead to very different software.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What Building RozVibe Taught Us&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before building RozVibe, we viewed encryption mostly as a technical system.&lt;/p&gt;

&lt;p&gt;Now we see it differently.&lt;/p&gt;

&lt;p&gt;For deeply personal software, encryption becomes emotional infrastructure.&lt;/p&gt;

&lt;p&gt;It gives people space to think honestly without feeling observed.&lt;/p&gt;

&lt;p&gt;And honestly, building privacy-first software changed the way we think about engineering entirely.&lt;/p&gt;

&lt;p&gt;Not just technically.&lt;/p&gt;

&lt;p&gt;Philosophically.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Final Thoughts&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The internet has trained people to expect surveillance by default.&lt;/p&gt;

&lt;p&gt;That expectation quietly changes human behavior.&lt;/p&gt;

&lt;p&gt;Especially in emotionally vulnerable spaces.&lt;/p&gt;

&lt;p&gt;Maybe privacy-first software is ultimately about restoring something much simpler:&lt;/p&gt;

&lt;p&gt;The ability to be honest with yourself.&lt;/p&gt;

&lt;p&gt;If you’re building privacy-first products, secure systems, or thoughtful software architecture, I’d genuinely love to hear how you think about trust, encryption, and emotional safety in modern apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;About RozVibe&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;RozVibe is a privacy-first encrypted journaling app focused on emotional safety, secure reflection, calm UX, and client-side encrypted storage.&lt;/p&gt;

&lt;p&gt;Download: &lt;a href="https://rozvibe.uptodown.com/" rel="noopener noreferrer"&gt;https://rozvibe.uptodown.com/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>security</category>
      <category>privacy</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
