- Rayhan Memon
- Posts
- #20 - How to Secure User Data in Your App
#20 - How to Secure User Data in Your App
Today marks 20 weeks in a row of consistently pumping these out. Small number (small subscriber count too tbh), but I’m proud of it none the less.
Anyways.
My friends and I are building a mobile app (tired of hearing me say that yet?), and our newest challenge is encrypting user data, on device, in transit, and in the cloud, to achieve as high a standard of data privacy as we can given certain constraints.
We’re taking this very seriously. So seriously in fact, that we aim to abide by Kerckhoffs's Principle, which states that a system should remain secure even if everything about it (except for secret keys) is publicly known.
We can’t rely on “Security by Obscurity”, where secrecy of the design is relied upon to maintain security. Secrets are hard to keep. Whatever vulnerabilities can be exploited will be exploited.
So let’s design a secure system. So secure in fact, that one could publish an article detailing how exactly how it works, and sleep easy at night.
Background
Ours is a video sharing app, where users belong to one or more groups and can cross-post videos to multiple groups if desired.
Row Level Security (RLS) policies in Postgres and storage bucket policies are already in place to ensure data can only be accessed by authenticated users that belong to the correct groups. Now we need to tackle encryption.
In our Postgres database, we have some sensitive columns across various tables:
User display name
Group name
Reply content
We also have a storage bucket where sensitive assets are stored:
User profile pics
User-created videos
High-level architecture with unessential columns omitted (sensitive data highlighted in orange)
Constraints
On the client data, sensitive data is not vulnerable to device theft, device backups, or malicious apps/malware.
In transit, sensitive data is not vulnerable to man-in-the-middle attacks.
In the cloud, sensitive data is invisible to and impenetrable to attackers perpetrating a data breach.
Only privileged staff should be able to, in theory, decrypt sensitive user data directly. No other engineer should be able to decrypt sensitive data if they wanted to.
Backend services should be able to access data when needed to power enhanced features and analytics.
Encryption and decryption should be low-latency and totally transparent to users.
The solution should be general enough to easily be applied to new forms of user metadata and assets.
Solution
The backbone of the entire solution is a master key. A symmetric key used to encrypt/decrypt other data and even other symmetric keys. It’s stored securely in the backend, accessible by the database service role (in order to perform encryption and decryption), and can only be viewed by a small number of privileged staff.
How this master key is used to secure data is dependant on the type of data we’re dealing with.
We currently have three categories of data:
Group-specific database columns
i.e.
replies.content
andgroups.name
User-specific, multi-group database columns
i.e.
users.display_name
User-specific, multi-group assets
i.e. user-created videos and user avatars.
We’ll tackle each of them individually.
1. Group-Specific Database Columns
Examples
replies.content
groups.name
(Any column whose plaintext should be visible only to members of a single group.)
Conceptual Approach
Each group has a Group Key (symmetric AES-256), encrypted with a Master Key.
We’ll store all encrypted group keys in a single table called
group_keys
.
CREATE TABLE groups_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
encrypted_key BYTEA NOT NULL,
iv BYTEA NOT NULL, -- if we choose to use an IV for AES
created_at TIMESTAMP DEFAULT now()
);
encrypted_key: Key is itself encrypted by the Master Key.
For our target tables (e.g.,
replies
orgroups
), replace sensitive columns with BYTEA columns for encrypted data.For
replies
that looks like:ALTER TABLE replies ADD COLUMN encrypted_content BYTEA DROP COLUMN content ;
Upload & Encryption
Because sensitive data is unencrypted during upload, we ensure we’re using HTTPS for all client-server communications to leverage Transport Layer Security (TLS)
When the record is created or updated, a database trigger
BEFORE INSERT
orBEFORE UPDATE
fires to do the following:Get the corresponding group key and decrypt it
Encrypt sensitive columns with the group key
Store the record
Decryption
Instead of reading data directly from the base tables, users can read it from a database view, which will decrypt data before sending it down.
In Flutter, we could also fetch the encrypted data and decrypt it client-side, since we may already have to do so for assets like videos. But a common approach is letting the backend do it so we minimize client complexity.
2. User-Specific, Multi-Group Database Columns
Examples
users.display_name
Potentially other user info that should be accessible to all groups that this user belongs to, but not to the entire user base, like contact info.
Conceptual Approaches
Similar to before, each user has their own symmetric key, their User Key, stored in a users_keys
table encrypted by the master key.
CREATE TABLE user_keys (
user_id UUID PRIMARY KEY,
encrypted_key BYTEA NOT NULL,
iv BYTEA NOT NULL
created_at TIMESTAMP DEFAULT now()
);
For target tables, we can replace sensitive columns with new columns, same as before. E.g:
ALTER TABLE users
ADD COLUMN encrypted_display_name BYTEA
Upload & Encryption
Similar to the “group-specific” encryption, when the record is created or updated, a database trigger BEFORE INSERT
or BEFORE UPDATE
fires to do the following:
Get the corresponding group key and decrypt it
Encrypt sensitive columns with the group key
Store the record
Decryption / Visibility
As before, we’ll let the backend handle the logic. If a group member is authorized to see a user’s display name, via RLS policies, the backend uses the user’s key (decrypted with the master key) to fetch it.
3. User-Specific, Multi-Group Assets
Examples
User Avatars
User-Created Videos (that the user posts and shares across multiple groups)
Conceptual Approach:
This is a little different of a pattern because we don’t want to rely on server-side encryption and decryption. This should happen on the client, for lower latency and cost.
And because this must happen on the client, we shouldn’t use user keys to encrypt user videos and avatars. user_2
shouldn’t have user_1
’s key on its device. So here’s the pattern for assets that should be accessible from multiple groups:
Generate a File-Specific Key for the asset.
Encrypt the file on the client before uploading to the storage bucket.
For each group that can access this asset, encrypt the ephemeral key using that group’s key. Store these encrypted ephemeral keys in a new table.
Unfortunately, to maintain the cascading deletes that are huge advantage of relational databases, we need to create a keys table per asset type.
For something like posts, where there’s only one per group, we can create tables like the following:
CREATE TABLE post_keys (
post_id UUID PRIMARY KEY,
encrypted_key BYTEA NOT NULL,
iv BYTEA NOT NULL,
);
For assets like user avatars, where the relationship is one-to-many, we need to create a record per group.
CREATE TABLE user_avatar_keys (
user_id UUID NOT NULL,
group_id UUID NOT NULL,
encrypted_key BYTEA NOT NULL,
PRIMARY KEY (user_id, group_id)
);
When a user joins an new group, we need a database trigger to generate new records for this ephemeral key for the newly created groups, so those members can also access this data.
Client-Side Encryption in Flutter
We can use two libraries:
flutter_secure_storage
to store the user’s group keys (or user keys) locally. This ensures that we use iOS keychain and Android Keystore respectively to store these encryption keys, so keys are not exposed in storage backups are accessible by phone malware.A crypto library like
pointycastle
orcryptography
for AES file encryption:
Uploading assets: (client side):
A random key is generated, the asset is encrypted, then both the key and encrypted asset are uploaded to the cloud.
Downloading & Decrypting
When a user in a group tries to view the asset:
The user fetches the asset as well as the record in the associated keys table that is specific to their group. A database view can be used to support this.
Client-side, the user decrypts the encrypted key with their group key (fetched from secure storage.
The now decrypted ephemeral key is used to decrypt the asset.
Quick reminder - If you appreciate my writing, please reply to this email or “add to address book”. These positive signals help my emails land in your inbox.
If you don't want these emails, you can unsubscribe below. If you were sent this email and want more, you can subscribe here.
See you next week — Rayhan