Message Encryption
Stream Chat already encrypts messages in transit (TLS) and at rest. Some apps need more than that: message text that Stream itself can't read, protected by keys the app controls. This is often requested as "end-to-end encryption", for example to meet privacy or industry rules.
This section shows how to encrypt message text in your Flutter app with keys from your own server. Your app encrypts each message before it's sent and decrypts it after it arrives, so Stream only stores and delivers encrypted text.
Encryption is easy to get wrong in small ways that make it unsafe. Use the code in this section as a starting point, and have a security expert review your implementation before you release it.
Three kinds of end-to-end encryption
"End-to-end encryption" is used for three different requirements. Each has a very different cost.
| Requirement | Who can read messages | How |
|---|---|---|
| Messages are protected on the network and on disk | Channel members, your server, Stream | Built in. Stream uses TLS in transit and encrypts data stored on its servers. |
| Stream can't read messages; you control the keys | Channel members, your server | This section. Your server hands out a key per channel, and your app encrypts with it. |
| Nobody but the people in the chat can read messages | Channel members only | Device-to-device encryption, such as the Signal protocol or MLS. Not covered here. |
Device-to-device encryption brings hard trade-offs: a new device can't read earlier messages, shared inboxes and support teams get complicated, and your servers can't moderate or audit conversations. Most apps that ask for end-to-end encryption need the second row. See the end-to-end encryption glossary entry for background.
How it works
Your server holds a master secret and derives one key per channel from it. Your app asks your server for a channel's key, encrypts each message with it before sending, and decrypts each message after it arrives. Your server only hands out a key to members of that channel, so Stream's member list decides who can read a conversation.
sequenceDiagram
participant App as Flutter app
participant Server as Your server
participant Stream
App->>Server: Log in (your own auth)
Server-->>App: Stream token
App->>Stream: Query channels
Stream-->>App: Channels, each with a key_version
App->>Server: GET /channel-keys?cid=...
Server->>Stream: Is this user a member of the channel?
Stream-->>Server: Yes
Server-->>App: The channel's keys
App->>Stream: Send message with encrypted text
Stream-->>App: New message with encrypted text
Note over App: Decrypts locallyWhen someone is removed from or banned in a channel, your server increases the channel's key version. New messages then use a key that person can't get.
Who can read what
| Who | Can read message text |
|---|---|
| Current channel members | Yes |
| Removed members | No new messages. Stream and your server both stop serving them |
| Your server | Yes, because it can derive every key |
| Stream | No |
| Someone with access to Stream's data | No |
| Someone who unlocks a member's phone | Yes, the app shows decrypted text to whoever is using the phone |
What stays visible to Stream
Encryption hides what people write. To deliver messages and run your chat, Stream still needs to see:
- who is in each channel
- who sent each message, and when
- reactions, read state and pinned messages
Some other details stay visible because of choices this guide makes. You can hide them, at a cost:
| Visible to Stream | Why this guide shows it | How to hide it |
|---|---|---|
| Which users were mentioned | The placeholder text keeps each @-token, so mentioned users are still notified and see their unread mentions |
Leave the tokens out of the placeholder. Mentions then stop working: no mention notifications or unread mention counts |
| How long each message is | AES-GCM encrypted text is as long as the original text, plus a fixed 16 bytes | Pad the text to fixed sizes before encrypting, for example a multiple of 256 bytes. Messages then take more space |
| Channel names, user names, custom data | The guide only encrypts message text | Use opaque ids, or encrypt those fields the same way as message text |
These choices are specific to this guide, not limits of Stream Chat. If your own server must not be able to read messages either, use device-to-device encryption instead. Stream still needs the details in the first list to deliver messages, as any chat service does.
What stops working
Features that need to read message text can't work on encrypted text:
- message search
- link previews
- slash commands
- automatic translation
- Stream's AI moderation (your server can read messages, so it can moderate instead)
- reading messages in the Stream dashboard
- message text in push notifications, unless your app decrypts it on the device
This guide doesn't encrypt attachments, voice recordings, polls or shared locations, so it turns them off in encrypted channels, on your server and in the composer.
Other ways to manage keys
This section derives every key from a master secret, so your server doesn't need to store any keys. Other approaches work with the same Flutter code; they only change your server and how the app fetches keys.
| Approach | Your server stores keys | Can make one chat permanently unreadable | How the app reads older messages |
|---|---|---|---|
| Keys derived from a master secret (this guide) | No | No, the master secret recreates them | Asks your server for the older key |
| Random keys stored on your server | Yes, one per key version | Yes, delete that chat's keys | Asks your server for the older key |
| A key chain in the channel's custom data | No | No | Unwraps older keys from the newest one |
| A key management service such as Virgil Security | No, the service stores them | Depends on the service | Handled by the service's SDK |
With a key chain, each time your server changes a channel's key it also stores the previous key, encrypted with the new one, in the channel's custom data. An app with the newest key can then unlock every older key by itself. Channel custom data is limited to 5 KB, so a channel that changes keys often needs a limit on how many old keys it keeps.
In this section
- Set up your server: a key endpoint, key changes when members leave, and a channel type for encrypted channels.
- Encrypt messages: encrypt message text before it's sent.
- Show decrypted messages: decrypt messages once and show them in the message list and channel list.
- Limitations and go-live checklist: what this approach doesn't cover, and what to check before you release.