DEV Community

Cover image for Building an End-to-End Encrypted Journal: My First Steps Toward Privacy-First AI
Afonso Oliveira
Afonso Oliveira

Posted on

Building an End-to-End Encrypted Journal: My First Steps Toward Privacy-First AI

I have been working on a startup that launched an app focused on better mental health. The goal is not to provide therapy, but to support self-development and reflection.

That got me thinking.

AI can be genuinely useful for this kind of product. It can help surface patterns, organize thoughts, or enrich journaling with features that make reflection easier. But there is a catch: those features usually require your data to exist in plaintext on a server somewhere.

And when we are talking about journal entries and private conversations, that data can get very sensitive, very quickly. If people feel restrained in what they can write, they will not reflect as honestly. And if they do not reflect honestly, the whole thing becomes much less useful.

That is why I wanted to start exploring what AI features we can still build when content is encrypted end-to-end. Some possibilities live on-device, with models running locally. Others may involve more advanced server-side techniques, such as encrypted semantic search, where similarity can be computed without exposing the plaintext content.

Interesting idea. But what does that actually mean in practice? And how hard is it to get started?

In this post, I want to lay the groundwork. Before getting into the AI side, I want to explain what end-to-end encryption really means for an app like this. Once the constraints are clear, it becomes much easier to see both what we lose and what we can still get away with.

Let’s start: Imagine you write a journal entry about a difficult moment with Lily. You like Lily, she is your friend, but journals are exactly the place where we process thoughts we are not ready to share yet. So the question becomes: where is that entry stored, and who gets to read it?

The app we are building

The app itself is simple.

It is a place where you can write personal journal entries. You might also want to share some of those entries with other people, like a therapist, a coach, or a close friend.

That is it.

As for the stack, I wanted to explore Flutter for mobile development, so the client is in Flutter. The server is a FastAPI instance connected to a SQLite database.

Quick primer: client, server, database, encryption

For readers who are not deep into software, here is the short version.

An application usually has code running on your device, which we call the client, and code running on a remote machine, which we call the server. This split is useful because some computations are too heavy for your device, and because a server makes it easier to keep data synced across multiple devices.

Servers typically connect to databases, which are specialized systems for storing and retrieving data efficiently.

So when you send a journal entry, the server may process it and store it in the database. Later, it may fetch that content again to power features like search, tagging, or AI-based analysis.

Encryption means that data is stored or transmitted in a scrambled form instead of plain text. To turn it back into the original content, you need a key. Depending on the system design, that key might be held by the server, by the database layer, or only by your device.

If someone gets access to the encrypted data without the key, they just see noise.

Achieving E2EE step by step

To explain the inner workings of end-to-end encryption, let us build up to it progressively.

Achieving E2EE step by step

Step 1: Communication encryption, also known as encryption in transit

Suppose you write a journal entry about Lily and hit save.

The entry is sent to the server as plaintext, but the connection uses HTTPS. That means the data is encrypted while it travels between your device and the server, so outsiders on the network cannot read it.

This is standard and necessary. Without it, you would be in trouble almost immediately.

Still, once the data reaches the server, it is available there in plaintext. The server can store it, process it, and do whatever features the app is built to do.

If the engineering team follows good practices and the company behaves ethically, that may be fine. But no engineering team is perfect, breaches happen, and trust is still required.

Even if the company, led by CEO Caroline, says all the right things in the privacy policy and genuinely means them, someone with the right internal access could still read your entry. And if Lily happens to work there, things might get awkward.

Step 2: Server-side encryption, also known as encryption at rest

Now let us improve things a bit.

You send the data much like before. The server receives it, encrypts it using a key that the server controls, and stores the encrypted version in the database.

This is useful. If Lily has database access but not access to the encryption keys, she still cannot read your entries. Likewise, if someone steals raw storage from a data center, they get encrypted blobs instead of useful text.

This is the kind of setup commonly used in systems like AWS S3, Google Drive, and Dropbox.

So yes, it is better. But the company can still access your data, because the server holds the keys. That means you still have to trust the company not to inspect, analyze, or monetize the plaintext in ways you do not want.

And then there is your hacker cousin Mario. If Mario gets access only to the database, he learns nothing. But if he compromises the server, then we are basically back to step 1, because the server must have the keys needed to decrypt and process the data.

So this protects against some threats, but not against the service provider itself.

Step 3: Client-side symmetric encryption

Now we are talking.

Up until now, we have been improving things, but the company could still access your data if it really wanted to. At this step, that changes. Instead of sending your journal entry as plaintext to the server, your device encrypts it first. The server only ever receives ciphertext and only ever stores ciphertext. The key needed to decrypt the entry never has to leave your device.

That is end-to-end encryption.

A common approach is to derive the encryption key from your account password using a key derivation function (KDF). The details are not too important here; the key idea is that your password can deterministically generate the cryptographic key needed to decrypt your entries.

This has a convenient side effect. If you log in from another device, you can derive the same key again using your password and read your existing entries there too.

There is, however, an important tradeoff. If you forget the password and there is no recovery mechanism that preserves E2EE, your old encrypted entries are effectively gone. Painful, yes, but it also means the service provider cannot recover or read them either.

This design is used by tools like Bitwarden, 1Password, and Standard Notes. At this point, your entries stay private even if they mention Lily, Mario, Caroline, or anyone else in your life.

Great! But there is a catch. What if you want to share a specific entry with someone else?

We are definitely not sharing the same secret key for everything. If Lily received that key, one shared note could silently become access to all of them. So how do we share content without breaking the whole thing?

Sharing E2EE content step by step

Let us keep going, this time focusing on sharing.

Caroline, Lily, and Mario are all lovely people. It is just that sometimes they do things that set you off a bit, and journaling is where you sort those feelings out before turning them into something more coherent and less dramatic.

Most entries are not meant to be shared.

But sometimes you write something kind, insightful, or unexpectedly wholesome, and suddenly you do want to send it to them.

How do you do that while keeping the rest of the journal private?

Sharing E2EE content

Step 4: Enter asymmetric key pairs

I still remember the day I first ran into this in a cybersecurity class. It is such a simple and powerful idea, but for some reason my brain really did not want to accept it at first. I was too attached to the usual lock-and-key metaphor, and asymmetric cryptography just refused to fit neatly into it.

The core idea is this: instead of one secret key, you now have two linked keys, a public key and a private key. What one encrypts, only the other can decrypt.

You keep the private key to yourself, and you can share the public key with anyone. So if Lily has a public key, you can encrypt something for her using that public key, and only Lily will be able to decrypt it with her private key.

That is the trick.

This is also the general idea behind systems like Proton Mail.

So far, so good. But if you want to share one entry with three people, are you supposed to encrypt the whole entry three different times? That works, but it is wasteful. It duplicates storage, increases computation, and gets annoying fast if the entry is later edited.

We can do better.

Step 5: Symmetric and asymmetric together

Symmetric encryption is fast. Asymmetric encryption gives us selective sharing. Naturally, we use both. Here is the usual pattern.

When you create a journal entry, your device generates a fresh symmetric key just for that entry. Let us call it the content key. The entry itself is encrypted with that content key.

Then the content key is encrypted with your public key, so only you can recover it later using your private key.

A similar idea can be used for your private key too: it can be stored server-side in encrypted form, protected by the main key derived from your password.

Now we have a nice setup:

  • the entry is encrypted once with a symmetric key
  • the symmetric key can be separately wrapped for whoever should have access

That makes sharing much cleaner.

Step 6: Encrypting the content key for recipients

This is the final step, and thankfully it is much more natural once the earlier pieces are in place.

You write an entry about how Caroline, Lily, and Mario make your days better. Your device encrypts the entry with a new content key. Then it encrypts that content key with your public key so that you can later decrypt it yourself.

Now you decide to share the entry.

Your device fetches Caroline’s, Lily’s, and Mario’s public keys. It then encrypts the same content key three more times, once for each recipient, producing three encrypted copies of that key. Those encrypted copies, together with the recipients’ identifiers, are sent to the server.

Now each of them can retrieve their copy of the encrypted content key, decrypt it with their private key, and use it to read the shared entry.

One important caveat: this assumes the server gives you the real public keys for those people. In production systems, users usually need ways to verify each other’s keys, so the server cannot quietly swap them out.

And with that, we have a basic end-to-end encrypted sharing model.

How you can try this out

We live in the era of AI-generated code, so naturally I asked an agent to build this setup for me.

You can find the code in this repo link, with tags for different commits marking the implementation at each step. The repo also documents a few cryptographic and infrastructure simplifications that were made to keep the focus on the E2EE concepts. Those are listed in a Known Limitations section so it is clear where a real-world system would need extra work.

I still had to make some tweaks here and there after generation, but the project now works nicely as a playground.

At each step, you can create a new entry, inspect what ends up in the database, ask an LLM to try extracting the information using only server-side knowledge, and poke around as much as you like.

Later, I found a critical implementation mistake.

Given our goal, it is a pretty important one: if Lily has access to the server code, she can inject some dubious logic and end up reading your entries after all.

The overall design is fine and the issue is not one of the deliberate teaching simplifications documented in the repository. This one slipped through during implementation. After all this work… can you guess what it is?

A few years ago, spotting that kind of issue would have required real cybersecurity expertise. Now you can just clone the repo and ask your friendly neighborhood coding agent to take a suspiciously close look.

And if you enjoy the post, give the repo a star while you are at it.

If you find the leak, feel free to leave it in the comments. And if you spot anything else, even better. Systems like these always have more details hiding in the corners.

That’s it

With proper end-to-end encryption, even if Mario gets access to the database, Lily happens to work at the company, and Caroline runs the servers, none of them can read your journal entries unless you decide to share them.

That gives you more freedom to be honest with yourself. To write the messy version first. To reflect on how you feel, and where you want to go, without wondering who else might be peeking behind the curtain.

The sad thing is that, with all this privacy in place, we had to let go of some of the cool server-side AI features CEO Caroline was cooking up.

Now I want to explore what she could still offer in this setup.

I already have a few ideas. Let me know if you have some too.

Top comments (0)