<?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.us-east-2.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>Looking for Android Developers to Help Test My App (Happy to Test Yours Too!)</title>
      <dc:creator>Keshav Chauhan</dc:creator>
      <pubDate>Fri, 26 Jun 2026 10:25:34 +0000</pubDate>
      <link>https://dev.to/sezronix/looking-for-android-developers-to-help-test-my-app-happy-to-test-yours-too-4n55</link>
      <guid>https://dev.to/sezronix/looking-for-android-developers-to-help-test-my-app-happy-to-test-yours-too-4n55</guid>
      <description>&lt;p&gt;🚀 Looking for Android developers willing to help with Google Play Closed Testing&lt;/p&gt;

&lt;p&gt;Hi everyone! 👋&lt;/p&gt;

&lt;p&gt;I'm a 16-year-old indie developer from India, and I've been building &lt;strong&gt;RozVibe&lt;/strong&gt;, a privacy-first encrypted journaling app for Android.&lt;/p&gt;

&lt;p&gt;Some of its features include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔒 Client-side AES-256-GCM encryption&lt;/li&gt;
&lt;li&gt;📖 Secure private journaling&lt;/li&gt;
&lt;li&gt;😊 Mood tracking&lt;/li&gt;
&lt;li&gt;🔍 Privacy-preserving local search&lt;/li&gt;
&lt;li&gt;☁️ Secure cloud sync&lt;/li&gt;
&lt;li&gt;🌿 Calm, distraction-free writing experience&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm currently at the stage where Google Play requires a &lt;strong&gt;Closed Testing&lt;/strong&gt; period before I can publish the app publicly.&lt;/p&gt;

&lt;p&gt;I'm looking for fellow Android developers or testers who would be willing to join the closed test, use the app for a bit, and share any bugs or feedback they find.&lt;/p&gt;

&lt;p&gt;Unfortunately, I'm not in a position to offer payment, but I'd be more than happy to return the favor by joining your closed testing program and testing your app as well. I'm always glad to support other indie developers.&lt;/p&gt;

&lt;p&gt;If you're interested in helping (or would like me to test your app in return), please leave a comment. I'd really appreciate it.&lt;/p&gt;

&lt;p&gt;Building RozVibe has been an incredible learning journey—from implementing client-side encryption and secure synchronization to designing privacy-preserving search—and I'd love to get it into the hands of real users before launch.&lt;/p&gt;

&lt;p&gt;Thank you for reading, and thank you in advance to anyone willing to help. ❤️&lt;/p&gt;

&lt;h1&gt;
  
  
  Android #GooglePlay #ClosedTesting #Flutter #IndieDev #BuildInPublic #MobileDevelopment #AppTesting #Privacy #OpenToCollaborate
&lt;/h1&gt;

</description>
      <category>android</category>
      <category>flutter</category>
      <category>googleplay</category>
      <category>indiedev</category>
    </item>
    <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>
