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.

Note:

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 locally

When 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

  1. Set up your server: a key endpoint, key changes when members leave, and a channel type for encrypted channels.
  2. Encrypt messages: encrypt message text before it's sent.
  3. Show decrypted messages: decrypt messages once and show them in the message list and channel list.
  4. Limitations and go-live checklist: what this approach doesn't cover, and what to check before you release.