Most journaling apps store your private thoughts as plaintext on remote servers. Developers can read them. Database breaches expose them. AI systems can profile them. The only thing standing between your innermost reflections and the outside world is a privacy policy — a legal document, not a technical safeguard.

RozVibe was built to change that. As a privacy-first encrypted journal , RozVibe encrypts every journal entry on your device before it ever touches a network. The server never sees your words. This isn't a marketing claim — it's a verifiable architectural decision, and this article explains exactly how it works.

What follows is a transparent, technically detailed walkthrough of RozVibe's encryption system. We'll cover the exact algorithms, byte-level structures, key management lifecycle, and the trade-offs we made along the way. If you're a security researcher, a fellow developer, or a privacy-conscious user who wants to know what happens under the hood, this article is for you.


The Encryption Lifecycle: From Plaintext to Ciphertext

Every time you save a journal entry in RozVibe, a precise cryptographic sequence executes entirely on your Android device. No part of this process involves a server. Here is the complete lifecycle, end to end:

1

Key derivation. Your credentials are combined with a cryptographic salt and processed through PBKDF2-HMAC-SHA256 with 100,000 iterations, producing 76 bytes of key material.

2

IV generation. A fresh 12-byte random Initialization Vector is generated using a cryptographically secure random number generator ( IV.fromSecureRandom(12) ).

3

AES-256-GCM encryption. The plaintext journal content is encrypted using the derived 256-bit key and the fresh IV. GCM mode produces both the ciphertext and a 16-byte authentication tag that guarantees integrity.

4

Payload assembly. The IV, ciphertext, and GCM authentication tag are concatenated into a single binary blob: [IV (12 bytes) + Ciphertext + Auth Tag] .

5

Base64 encoding. The assembled blob is Base64-encoded into a text string and stored as the data field in Cloud Firestore.

6

Cloud sync. Firestore receives only the encrypted Base64 string alongside minimal routing metadata. It never receives, processes, or indexes the plaintext content of your entries.

Decryption is the exact reverse: the app reads the Base64 string from Firestore, decodes it, extracts the 12-byte IV prefix, and decrypts the remaining ciphertext using the in-memory AES-256 key. The GCM authentication tag is verified automatically — if the ciphertext was tampered with, decryption fails entirely.


PBKDF2 Key Derivation: Turning Credentials into Cryptographic Keys

The foundation of RozVibe's encryption is PBKDF2-HMAC-SHA256 (Password-Based Key Derivation Function 2). This is the process that transforms your credentials into the actual cryptographic material used for encryption.

How the Derivation Input Is Constructed

RozVibe constructs the key derivation input using a specific format:

Key Derivation Input
input = "${userId}_${pin ?? "default_secure_vault"}" // If the user has set a PIN: "abc123uid_7294" // If the user has not set a PIN: "abc123uid_default_secure_vault"

This input is then fed into PBKDF2 along with the user's unique cryptographic salt. The function runs 100,000 iterations of HMAC-SHA256, deliberately making the derivation process slow. This is intentional — the computational cost means that an attacker attempting to brute-force the key offline would need to perform 100,000 HMAC operations for every single guess.

The 76-Byte Output Structure

PBKDF2 derives a total of 76 bytes of key material. These bytes are split into three distinct components, each serving a different cryptographic purpose:

Byte Range Length Purpose
Bytes 0–31 32 bytes AES-256 encryption key
Bytes 32–43 12 bytes Legacy fallback IV (backward compatibility)
Bytes 44–75 32 bytes HMAC-SHA256 search key (for blind indexing)

The first 32 bytes are the AES-256 encryption key — the actual key used to encrypt and decrypt your journal entries. The middle 12 bytes serve as a legacy fallback IV for backward compatibility with entries encrypted before per-entry IV generation was implemented. The final 32 bytes provide a dedicated HMAC-SHA256 key used exclusively for the blind index search system .

Why 76 bytes?

By deriving all three keys from a single PBKDF2 invocation, RozVibe avoids running the expensive 100,000-iteration process multiple times. This is a deliberate performance optimization: one derivation produces the encryption key, the legacy IV, and the search key in a single pass.


AES-256-GCM: The Encryption Algorithm

RozVibe uses AES-256-GCM (Advanced Encryption Standard with 256-bit keys in Galois/Counter Mode) via the encrypt Dart package. AES-256-GCM is an authenticated encryption algorithm, which means it provides two guarantees simultaneously:

  • Confidentiality: The ciphertext is computationally indistinguishable from random data without the correct key. An attacker with access to Firestore sees only meaningless bytes.
  • Integrity: The 16-byte GCM authentication tag detects any modification to the ciphertext. If even a single bit is altered — whether by corruption, a bug, or deliberate tampering — decryption fails. The app will reject the entry rather than present corrupted data.

Why GCM Over CBC?

AES-CBC (Cipher Block Chaining) is a common alternative, but it provides only confidentiality — not integrity. CBC-encrypted data can be silently modified through bit-flipping attacks without the application detecting the tampering. GCM eliminates this entire class of vulnerability by building authentication into the encryption process itself.

IV Handling: A Fresh Random IV Per Encryption

The security of GCM depends critically on never reusing an IV with the same key. RozVibe generates a fresh 12-byte random IV for every single encryption event using IV.fromSecureRandom(12) , which sources randomness from the platform's cryptographically secure random number generator.

This means that if you edit the same journal entry ten times, each save produces a completely different ciphertext — even though the content may be identical. An observer monitoring Firestore cannot determine whether two encrypted blobs contain similar or identical content.

The Ciphertext Format

The final encrypted payload stored in Firestore follows this binary structure:

Ciphertext Format
// Binary layout (before Base64 encoding): [ IV (12 bytes) | Ciphertext (variable) | GCM Auth Tag (16 bytes) ] // Stored in Firestore as: "data" : "dG9wX3NlY3JldF9kYXRh..." // Base64-encoded blob

The IV is prepended to the ciphertext so that the decryption process can extract it without needing to store it separately. This is a standard and well-established pattern in symmetric encryption.


Key Management: Keys That Never Touch Disk

Key management is where most encrypted journal apps either succeed or quietly fail. Even the strongest encryption is worthless if the keys are stored insecurely, leaked through logs, or persisted to disk where they can be extracted. RozVibe takes an uncompromising approach: encryption keys exist only in memory (RAM) and are never written to persistent storage .

The Key Lifecycle

  1. Login: When you authenticate, the EncryptionService derives the 76-byte key material from your credentials and salt via PBKDF2. The AES-256 key (bytes 0–31) and HMAC search key (bytes 44–75) are stored as in-memory variables.
  2. Active use: While you use the app, these keys remain in RAM. Every encrypt and decrypt operation references them directly from memory.
  3. Logout: When you log out, the clear() method is called, explicitly setting _key = null and _searchKey = null . The key material is wiped from memory. No trace remains.

Keys never appear in SharedPreferences, in local databases, in Firestore documents, or in application logs. They cannot be extracted from a Firestore database dump, a device backup, or an APK decompilation — because they simply are not there.

Important distinction

The cryptographic salt is stored persistently (in FlutterSecureStorage and Firestore). The encryption keys are not. The salt is an input to key derivation — it is not the key itself. Possessing the salt without the user's credentials does not enable decryption.


Salt Management: Generation, Storage, and Cloud Sync

The cryptographic salt is a 16-byte random value generated once per user account using IV.fromSecureRandom(16) . It serves a critical purpose: ensuring that two users with identical credentials produce entirely different encryption keys.

Where the Salt Lives

The salt is stored in two locations:

  • Locally: In FlutterSecureStorage , which is backed by Android's EncryptedSharedPreferences . This provides hardware-backed encryption of the salt at rest on the device.
  • In the cloud: In the user's Firestore document, enabling multi-device key reconstruction.

This dual storage is a deliberate design choice. Local storage ensures fast key derivation without a network round-trip. Cloud storage ensures that when a user logs into a new device, the app can fetch the salt and reconstruct the same encryption keys — without the user needing to manually transfer any cryptographic material.

We discuss the security implications of this decision in the trade-offs section below.


What Firestore Actually Stores

Because client-side encryption happens before data reaches the network, Firestore functions purely as a blind storage and synchronization relay. Here is the exact structure of a journal entry document in Firestore:

Firestore Document Structure
{ "data" : "dG9wX3NlY3JldF9kYXRh..." , // Base64([IV + Ciphertext + Auth Tag]) "userId" : "abc123uid" , // Firebase Auth UID "date_index" : "2026-06-05" , // Date for calendar navigation "isFavorite" : false // Favorite flag for filtering }

Notice what is not in this document: no title, no body text, no mood label, no tags, no rich-text content. All of that is inside the encrypted data blob. The only metadata Firestore can see is the user ID (required for access control), the date index (required for calendar navigation), and the favorite flag (required for filtering).

An attacker who gains full read access to the Firestore database would see thousands of Base64 strings. They could determine that a user made an entry on a specific date. They could not determine what the entry says, what mood was recorded, or what topics were discussed.

Firestore is a courier that delivers sealed envelopes. It knows when a letter was sent and who it belongs to. It cannot read what is inside.

Multi-Device Sync Through Deterministic Key Reconstruction

A common challenge with client-side encryption is multi-device access. If encryption keys are generated on one device, how does a second device decrypt the same data? Some apps solve this by transmitting the key (which undermines the security model). Others require users to manually transfer a recovery key (which most users will lose).

RozVibe uses a third approach: deterministic key reconstruction .

Because PBKDF2 is a deterministic function — the same inputs always produce the same outputs — the encryption key can be independently reconstructed on any device that has the correct inputs. Those inputs are:

  1. The user's Firebase Auth user ID
  2. The user's PIN (or the default fallback string)
  3. The user's cryptographic salt

When you log into a new device, the app authenticates you via Firebase Auth (which provides your user ID), fetches your salt from Firestore, and prompts for your PIN if one is set. With all three inputs available, PBKDF2 derives the exact same 76 bytes of key material. The second device now holds the same AES-256 key in memory, and can decrypt every entry in the Firestore collection.

The key itself is never transmitted. Only the salt crosses the network — and the salt alone is insufficient for decryption.

What About the Blind Index on New Devices?

The blind index search database (stored in local SQLite) is device-specific. When you log into a new device, the search index doesn't exist yet. RozVibe handles this through a lazy backfill system : as entries are decrypted for display, the app progressively rebuilds the blind index in the background. Over time, the search index on the new device catches up to the full entry set without requiring a bulk re-download or decryption pass.


Searching encrypted data is one of the hardest problems in applied cryptography. The naive approach — decrypt everything into RAM, then search — works for small datasets but doesn't scale, and it exposes the entire plaintext corpus in memory simultaneously.

RozVibe implements a blind index search architecture using HMAC-SHA256 token hashes stored in a local SQLite database ( rozvibe_search.db ).

How It Works

  1. Tokenization: When a journal entry is saved, the plaintext content is split into individual word tokens on-device.
  2. Hashing: Each token is hashed using HMAC-SHA256 with the dedicated search key (bytes 44–75 from the PBKDF2 output). This produces a deterministic but irreversible hash for each word.
  3. Storage: These token hashes are stored in a local SQLite database alongside the entry ID they belong to. The database never contains plaintext words.
  4. Search: When you search for a word, the app hashes your search query with the same HMAC key and looks for matching hashes in SQLite. Matching entry IDs are returned, and only those entries are decrypted for display.

The key property of this system is that the SQLite database contains only HMAC hashes — fixed-length, irreversible outputs. An attacker who extracts rozvibe_search.db from the device cannot reverse the hashes back to the original words. They would see entries like a7f3b2c1e8d9... and have no way to determine whether it represents "happy," "anxious," or any other word.

Why HMAC instead of a plain hash?

A plain SHA-256 hash of common words would be vulnerable to rainbow table attacks — an attacker could precompute hashes of every dictionary word and match them against the database. HMAC-SHA256 requires the secret search key to compute, making precomputation impossible without first compromising the key.


Legacy Decryption Fallback: Backward Compatibility

Software evolves, and cryptographic implementations evolve with it. Early versions of RozVibe used a fixed IV derived from PBKDF2 (bytes 32–43) rather than generating a fresh random IV per encryption event. While functional, this approach meant that entries encrypted with the same key always used the same IV — a pattern that weakens GCM's security guarantees over time.

The current implementation generates a fresh 12-byte random IV for every encryption event, prepending it to the ciphertext. But entries encrypted under the old scheme still exist in users' Firestore collections and need to remain decryptable.

RozVibe handles this through a legacy decryption fallback :

  1. The decryption routine first attempts to extract the IV from the first 12 bytes of the decoded payload (the current format).
  2. If decryption fails (because the entry was encrypted under the old scheme without a prepended IV), the system falls back to using the legacy IV from bytes 32–43 of the PBKDF2 output.
  3. This fallback is transparent to the user. Old entries decrypt correctly without any manual migration.

New entries are always encrypted with the current, more secure per-entry IV scheme. The legacy path exists solely for reading old data, and no new entries are created using the fixed IV.


Threat Model: What This Protects Against — and What It Doesn't

No security system protects against everything, and any product that claims otherwise is being dishonest. RozVibe's security architecture has a clearly defined threat model. Here is exactly what it covers and where it defers to device-level security.

Protected Against Not Protected Against
Database breaches exposing journal content Malware on the user's device (keyloggers, screen capture)
Unauthorized access to Firestore data Compromised or rooted operating system
Developer/admin access to plaintext entries Physical access to an unlocked device while the app is active
Network interception during sync (TLS + encrypted payload) PIN or credential disclosure through social engineering
Passive data profiling by cloud infrastructure OS-level memory forensics on an active device

What the Model Handles Well

RozVibe's client-side encryption neutralizes the most common real-world threats to personal data: server-side breaches, insider access, and cloud infrastructure surveillance. A compromised Firestore instance yields only AES-256-GCM encrypted blobs. Without the user's credentials and the PBKDF2 derivation parameters, decryption is computationally infeasible — there is no known practical attack against AES-256.

This also means that RozVibe's developers cannot read your entries even if they wanted to. The architecture makes it impossible, not just against policy.

What the Model Cannot Handle

RozVibe relies on the integrity of the underlying operating system. If an attacker has installed a keylogger on your device, they can capture your PIN as you type it. If the OS itself is compromised (a rooted device running malicious code), the attacker can read memory contents while the app is active. If someone picks up your phone while RozVibe is open and unlocked, the decrypted entries are visible on screen.

These are not failures of RozVibe's encryption — they are fundamental limitations that apply to every application running on a general-purpose operating system. No app can protect data from an adversary who controls the hardware or the OS beneath it. We state this explicitly because we believe transparency about limitations is as important as transparency about capabilities.


Trade-Offs: The Decisions Behind the Design

Cryptographic architecture is not about achieving theoretical perfection. It's about making deliberate, transparent choices between security, usability, and recoverability. Here are the key trade-offs in RozVibe's design.

Trade-Off 1: Salt Stored in Firestore

What we chose: The cryptographic salt is stored in Firestore alongside the user's encrypted data.

The alternative: A strict offline key backup system where users would need to securely store a 128-bit recovery key themselves.

Why we chose this: We prioritized recoverability and multi-device usability. Storing the salt in Firestore enables seamless key reconstruction on new devices — log in with your credentials, and you're done. The alternative would mean that a lost recovery key equals permanently lost data. For a journaling app where entries may span years of personal history, the risk of irrevocable data loss outweighs the minimal risk of salt exposure. The salt is not the key — knowing the salt without the user's credentials does not enable decryption.

Trade-Off 2: Metadata Visibility

What we chose: Certain metadata fields ( date_index , isFavorite ) are stored as plaintext in Firestore.

The alternative: Encrypting all metadata alongside the journal content.

Why we chose this: Calendar navigation and favorite filtering are core features that require Firestore to query on these fields. Encrypting them would require decrypting the entire entry collection client-side before any filtering could occur — an unacceptable performance and battery cost on mobile devices. The exposed metadata reveals only that an entry exists on a given date and whether it was favorited. It reveals nothing about the entry's content, mood, or topics.

Trade-Off 3: Default Fallback vs. Mandatory PIN

What we chose: If a user does not set a PIN, the key derivation input uses a default fallback string ( "default_secure_vault" ).

The alternative: Requiring every user to set a PIN before any journaling can occur.

Why we chose this: A mandatory PIN creates friction that discourages adoption. Users who are new to journaling may abandon the app before writing their first entry. The default fallback still produces valid, unique encryption keys (because the user ID and salt contribute entropy), while offering a lower barrier to entry. Users who want stronger key derivation entropy can set a PIN at any time through the app's settings.


The Technology Stack Behind the Encryption

RozVibe is built with Flutter and Dart (SDK >=3.3.0 <4.0.0). The encryption implementation relies on the following components:

  • encrypt Dart package: Provides the AES-256-GCM implementation used for all journal encryption and decryption operations.
  • PBKDF2-HMAC-SHA256: Key derivation with 100,000 iterations, producing the 76-byte key material structure.
  • FlutterSecureStorage: Backed by Android's EncryptedSharedPreferences , used for persistent local storage of the cryptographic salt.
  • Firebase Authentication: Handles identity management via Email/Password and Google Sign-In, providing the user ID used in key derivation.
  • Cloud Firestore: Functions as the blind, encrypted storage and synchronization relay. Supports offline caching through Firebase's built-in offline persistence.
  • Riverpod: State management framework that orchestrates the relationship between the EncryptionService , AuthService , and UI layers.
  • SQLite ( rozvibe_search.db ): Local database storing HMAC-SHA256 token hashes for the blind index search system.

Every cryptographic operation — key derivation, encryption, decryption, HMAC computation — executes natively on the user's Android device. No server-side computation is involved in any cryptographic process.


Frequently Asked Questions

RozVibe uses AES-256-GCM (Advanced Encryption Standard with 256-bit keys in Galois/Counter Mode) for encrypting journal content. Key derivation uses PBKDF2-HMAC-SHA256 with 100,000 iterations to transform your credentials into the 256-bit encryption key. AES-256-GCM is an authenticated encryption algorithm that provides both confidentiality and integrity — it prevents both unauthorized reading and undetected tampering of your entries.

No. Encryption keys exist only in your device's RAM while you use the app. They are derived on-the-fly from your credentials and cryptographic salt via PBKDF2, and they are explicitly wiped from memory when you log out. Keys are never written to disk, sent to any server, or stored in Firestore. The cryptographic salt is stored in Firestore to enable multi-device sync, but the salt alone cannot derive the encryption key without your credentials.

No. Because encryption happens entirely on your device before data is synced, Firestore only stores encrypted Base64 blobs. Without your credentials and the key derivation inputs, the ciphertext is computationally infeasible to decrypt. This is an architectural guarantee, not a policy promise — the developers literally cannot access the plaintext even with full database access.

RozVibe uses a blind index search architecture. Words from your entries are hashed using HMAC-SHA256 with a dedicated search key, and these irreversible token hashes are stored in a local SQLite database. When you search, your query is hashed the same way and matched against the index. The database never contains plaintext words — only fixed-length hashes that cannot be reversed back to the original content.

Your cryptographic salt is synced to Firestore. When you log in on a new device, the app fetches this salt and re-derives your encryption keys locally using the same PBKDF2 process with your credentials. Because PBKDF2 is deterministic, the same inputs always produce the same key. Your keys are never transmitted — only the salt is shared, and the salt alone is insufficient for decryption. The local search index is rebuilt progressively through a lazy backfill system.

We deliberately avoid both terms. "End-to-end encryption" typically describes multi-party communication (like messaging), which doesn't apply to a personal journal. "Military-grade" is a marketing buzzword with no technical meaning. RozVibe uses client-side encryption — all cryptographic operations happen on your device before data leaves it. The server never sees plaintext. We prefer precise terminology because vague claims erode trust.

This is a deliberate trade-off for usability and multi-device support. Storing the salt in Firestore allows seamless key reconstruction on new devices — you just log in and your keys are re-derived locally. The alternative — requiring users to back up a 128-bit recovery key on paper or a USB drive — risks permanent, irrevocable data loss if the key is misplaced. For a journaling app where entries may span years of personal history, this risk is unacceptable. The salt alone is not sufficient to derive the encryption key without your credentials.

RozVibe cannot protect against threats at the device level: malware (keyloggers, screen capture), a compromised or rooted operating system, or physical access to an unlocked device while the app is active. The security model assumes your device's OS is trustworthy. These are fundamental limitations that apply to every application running on a general-purpose operating system — no app can protect data from an adversary who controls the hardware or OS beneath it. RozVibe protects against server-side threats: database breaches, unauthorized cloud access, developer access, and infrastructure surveillance.


KC

Keshav Chauhan

Founder of SezRonix · Creator of RozVibe

Keshav Chauhan is the founder of SezRonix and creator of RozVibe, a privacy-first encrypted journaling platform. He writes about digital privacy, encryption, journaling, Flutter development, and building thoughtful software.