Limitations and Go-Live Checklist

This approach hides message text from Stream with keys your server controls. Before you release, check what it doesn't cover, and go through the checklist below.

Limitations

  • Text only. Attachments and voice recordings aren't encrypted, so the guide turns them off. To encrypt them, encrypt files before upload, for example with a custom AttachmentFileUploader, and build your own widgets that download and decrypt them. Server thumbnails and image resizing then stop working.
  • Features that read message text don't work. Search, link previews, slash commands, translation and Stream's AI moderation only see the placeholder. The channel type turns off search, link previews, slash commands and automod, together with polls and shared locations, which would store readable data. The composer also turns off link previews. Keep automatic translation off for these channels.
  • Your server can read every message. It holds the master secret, so it can derive every key. If even your own servers must not read messages, you need device-to-device encryption, such as the Signal protocol or MLS.
  • The phone must be trusted. Anyone who can unlock a user's phone can read their chats, because the app shows decrypted text.
  • Every logged-in device gets the keys. Keys come from your server for each login, so a new phone can read the full history. There are no keys tied to one device.
  • New members can read older messages. The key endpoint hands out every key version up to the current one. Set up your server shows how to prevent this.
  • Messages have a size limit. Custom data is limited to 5 KB per message, and encrypted text takes about a third more space than the original. Limit message length in the composer.
  • Push notifications show the placeholder. To show the real text, your app has to decrypt it on the device when the notification arrives.
  • Your own failed messages show the placeholder. Until a failed message is sent, it shows "Encrypted message" instead of the text you typed, because the SDK resends a failed message exactly as it's shown.
  • A soft delete keeps the encrypted text. Stream keeps the original content of a soft-deleted message, including its enc field, and your server can still read it. Use a hard delete when the content must go.
  • A leak can't be undone. Once readable text reaches Stream, for example from a composer that doesn't encrypt or from drafts left on, deleting the message doesn't remove it everywhere: moderation logs keep the text of deleted messages. Preventing leaks is the only remedy.

Checklist before going live

  • Every message gets a new random nonce, and no nonce is used twice with the same key.
  • Text is turned into bytes with utf8.encode, not codeUnits, and encrypted values are stored as base64.
  • The channel id and message id are part of the encryption, and every message includes a format version.
  • Every composer encrypts through the messageComposer builder, including thread pages, and so does anywhere else your code sends or edits messages.
  • Messages are decrypted once and cached, never in build.
  • Keys never appear in messages, logs, crash reports or analytics.
  • Server drafts are off.
  • Files, voice recordings, polls, shared locations and slash commands are turned off for encrypted channels, and replies and quotes are still on.
  • Your push notification template uses generic text.
  • With a network proxy, you checked that sending, editing, replying, quote replies, thread replies, reacting, pinning, flagging, drafts, attachments and retrying after going offline send no readable text.
  • The key endpoint uses your app's own login, and only hands out keys to active channel members: not banned from the channel or the app, and not only invited. Shadow-banned members still get keys, so the shadow ban goes unnoticed.
  • Channel key versions are stored in your database. The key_version in Stream is only a copy that tells apps to fetch keys again.
  • Your webhook handler ignores retried webhooks, and logs and retries key changes that fail.
  • Content that must go is hard-deleted, not soft-deleted.
  • Master secrets are in a secret manager, and keys change when someone is removed from or banned in a channel, including bans from the whole app.
  • Logging out removes ChatApp, then calls textCache.dispose() and channelKeys.close().
  • A security expert has reviewed your implementation.

Words used in this guide

Word Meaning
Encrypt Turn readable text into scrambled text that only someone with the right key can read.
Decrypt Turn scrambled text back into readable text, using the key.
Key A secret value used to encrypt and decrypt. Anyone who has the key can read the messages.
Placeholder The short text, "Encrypted message", that Stream stores and shows instead of the real message.
Custom data Extra fields you can add to a message, user or channel. The encrypted text is saved here.
Nonce A random value used once per message, so two identical messages never look the same.
Master secret The one secret on your server that every channel key is derived from.
Key version A number saved on each channel. Increasing it switches the channel to a new key.
Key id The master secret version and key version together, such as 2.5, saved with each message.