Encrypting the local cache with a key that never touches disk
Freesbe opens instantly. Click a channel and the latest messages are already there, before the server has answered. That’s because the app keeps a local copy of what you’ve recently seen.
Every chat app that feels fast does this. Most of them also leave that copy on disk in plain text, readable by anything that can read your files: a backup tool, malware, or whoever finds the laptop you left in a taxi.
We wanted the speed without that. Freesbe’s local cache is encrypted, and the key that unlocks it never touches the disk.
What’s in the cache
For each workspace you use on a device, the app keeps:
- what it needs to draw the app before the server answers: your channels, the people in them;
- the newest 100 messages of up to 50 channels you’ve opened.
It’s capped at about 10 MB per workspace, at 30 days, and at 10 workspaces. When you open a channel, the app shows the cached messages at once, then asks the server for anything newer and for edits and deletes since, and fixes up the view. If you’ve been away for long enough that the cache is far behind, it’s simply dropped and reloaded.
How the encryption works
Every record in the cache is encrypted with AES-GCM before it’s written to the browser’s database. The key comes from the server:
- When the app opens a workspace, it asks the server for that workspace’s cache key.
- The server checks you’re still a member and still meet the workspace’s sign-in rules (single sign-on, a second step), then derives the key from a server secret, the workspace, you, and a counter called the epoch. Nothing is stored per key.
- The app imports the key as non-extractable. Its JavaScript can use it to encrypt and decrypt, but can’t read the key’s bytes, so it can’t accidentally write them anywhere.
- The key lives in memory only. Close the app and it’s gone.
Each record is also bound to its slot in the cache, so a record copied from one place to another fails to decrypt instead of showing up in the wrong channel.
Remote wipe, for free
That epoch counter is what makes this more than encryption at rest. When an admin deactivates someone, or removes them from a workspace, the server bumps their epoch. Their next key is different, so their old cache can’t be decrypted by anyone, including them. The app notices the key changed and throws the old cache away.
The cache is also wiped when you sign out, when the server says you’re signed out, when another account signs in on the same device, and when you lose access to a workspace.
The trade-off we chose
There’s one thing this design can’t do: read your messages offline. Without the server, there’s no key, so the cache stays locked. Opening the app on a plane shows nothing until you’re back online.
We decided that was the right default for a work chat app. The cache exists for speed, not for offline reading, and a cache that only opens for someone who is still allowed in is worth more to the companies using Freesbe than offline reading would be.
What we turned down
- A plain local cache. Fastest to build, and common, but your messages end up readable on every device you’ve ever used.
- A key derived from your password. It breaks the moment you sign in with Google or Microsoft, where we never see a password, and it can’t be revoked from the server.
- Storing the key on the device. It makes offline reading possible, but then disk encryption is all that protects the cache, and a deactivated person’s cache stays readable until they open the app again.
Where we chose differently
Phones are different. People open a chat app on the train, in a lift, wherever the signal drops, and a phone already has a hardware-backed secure store for exactly this kind of key. So our phone app makes the opposite trade: it keeps the key in the phone’s secure storage, so the app can open from its cache with no signal, and it still checks with the server each time you open a workspace and wipes the cache if the key changed or you lost access.
Same goal, different device, different answer. We think that’s the honest way to make a security decision: say what you’re protecting against, then pick the trade-off per platform.