Repository navigation
feat(chat): encrypt and decrypt end-to-end-encrypted DMs - #1619
Merged
Merged
Conversation
The E2EE learn-more sheet lists its items with the same icon-and-line row the contacts rationale uses. Moved as-is, with a spacing parameter for the sheet's 12dp gap; existing callers keep grid.x2.
DM profiles get a footer saying messages are end-to-end encrypted when E2eePolicy says the chat is; Group Info always says group chats aren't. Learn More opens a sheet listing what is and isn't encrypted, one for DMs (node 10416:1533) and one for groups (node 10557:1412). The footer is the scaffold's bottom bar rather than MenuList's footer, so it stays at the bottom while the rows scroll. ChatViewModel maps the chat's metadata through E2eePolicy, which stays the only reader of use_e2ee; the policy gains an @Inject constructor for that.
An encrypted message used to render as a tombstone saying it isn't
supported on this version. It now matches node 10416:1576: a dashed
bubble with "This message can't be displayed" and "Update Flipcash to
see it" underneath.
The line underneath is an UndecryptableHint, so decryption can add
"Ask {first name} to send it again" and "Try sending it again" for
messages that fail to open. Only UpdateApp is produced for now.
The DM profile with its footer on, Group Info, both learn-more sheets and the unreadable-message bubble with each hint, written to build/screenshots/ for comparison with nodes 10557:1304, 10557:1643, 10416:1533, 10557:1412 and 10416:1576. Same Robolectric mechanics as ChatIdentityScreenshotTest; the profiles are built from the screens' scaffold, header, rows and footer because the screens take their view models.
Three of the new strings used ’ while the group footer used ', which is what most of strings.xml uses.
…Policy The @Flipcash exemption now comes from ChatEncryptionPolicy, which both apps share, so the app's own null constant and the test for its unset state are gone. CONTACT_DM and TIP_DM map to isDirectMessage, matching the server's IsDmChatType.
ChatContentCrypto turns Text and Reply(Text) into EncryptedContent and back, using the viewer's account key pair and the peer's registered owner key from the Resolver. The derived chat key is cached per chat and account, so a transcript costs one key fetch. Opening sorts failures by what the UI should say: an unknown scheme or a plaintext type this client doesn't render (including media) is Unsupported, a failed authentication or a sender who isn't a member is Authentication, and a failed key fetch is KeyPending so the message can be retried instead of shown as broken. ChatCipher is injected through Hilt so tests can use a fake; DefaultChatCipher's libsodium binding doesn't load on a JVM host.
When E2eePolicy says a DM encrypts, sendMessage, retryMessage and editMessage seal Text and Reply(Text) before the request. The pending row and the stored reply keep the plaintext, so the viewer's own message never has to be decrypted back. An edit stays encrypted if the original was, even after the chat stops encrypting; a plaintext message edited in a chat that now encrypts goes out sealed. A send whose chat can't be read, whose peer key can't be fetched, or that the server refuses with EncryptionNotAllowed fails its pending row, so the transcript offers a retry rather than sending plaintext. The fake ChatCipher moves to :services:flipcash testFixtures so the chat module's tests can seal and open with it.
…text Incoming messages are opened in ChatMessageDataSource before they're written, so the transcript, quotes and chat-list preview all read plaintext from Room. The ciphertext is kept beside it in two new columns, ciphertext_json and encryption_state (migration 39 to 40), which lets a later copy of the same message reuse the stored plaintext instead of opening it again. A message that didn't open shows the "can't be displayed" bubble with a hint picked by cause: an unknown scheme or type asks for an update; a failed authentication asks the sender by first name to resend, or the viewer to send their own again. A failed peer key fetch is not a decrypt failure: the row is stored KEY_PENDING, hidden, and opened again on the chat's next write and at login. Rows stored encrypted before this version are marked KEY_PENDING by the migration.
An "Encrypted" row sits above the oldest stored message that arrived encrypted, and at the head of the transcript when every message did (nodes 10416:1404, 10416:1490). Its position comes from the stored rows, not from use_e2ee. Tapping it opens the DM encryption sheet. The paging separator pass takes one item per gap. When the marker's gap also needs a date separator or the unread divider, the marker carries it and draws it above itself. The list's scroll-to-unread lookups match a divider nested inside the marker.
The server can't read an end-to-end encrypted DM, so the body it pushes is a stand-in. applyChatStyle now asks the chat coordinator to open the pushed message and shows its text instead. A long message arrives as message_id only; that one is read from Room if stored, else fetched with GetMessage. Any failure (plaintext message, bad ciphertext, key or message fetch failure) returns null and the notification keeps the server's body. Group chats never encrypt, so they skip the lookup.
ChatMetadataDao.upsert wrote use_e2ee only on first insert; updateServerOwnedFields left it out. A DM stored before the server set the flag kept use_e2ee = 0 on every later sync, so sends in it stayed plaintext and the profile footer never showed. The column came in with #1573, so code/cash has the same gap.
It matches the date separator above it instead of the white the plan called for.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Encrypts DM text and replies with
ChatCipher(#1616) and opens them on the way in, so an end-to-end-encrypted DM renders, previews and notifies as plaintext. Stacked on #1618. Media is out of scope and still renders as unsupported.Policy.
E2eePolicynow delegates to the sharedChatEncryptionPolicy, so the @Flipcash exemption comes fromFLIPCASH_USER_IDrather than an app constant.CONTACT_DMandTIP_DMcount as direct messages, matching the server'sIsDmChatType.Keys.
ChatContentCryptoderives the chat key from the viewer's account key pair and the peer's registered owner key from the Resolver, and caches it per chat and account.ChatCipheris injected through Hilt; every test here usesFakeChatCipher, becauseDefaultChatCipher's libsodium binding doesn't load on a JVM host.Send.
sendMessage,retryMessageandeditMessagesealTextandReply(Text)when the policy says so. An edit stays encrypted if the original was. If the chat can't be read, the peer key can't be fetched, or the server returnsEncryptionNotAllowed, the pending row fails and offers a retry instead of sending plaintext.Receive.
ChatMessageDataSourceopens messages before writing them, so the transcript, quotes and chat-list preview read plaintext from Room. Migration 39 to 40 addsciphertext_jsonandencryption_statebeside the content. A message that doesn't open shows the Phase 1 bubble with a hint picked by cause:KEY_PENDING, hidden, reopened on the chat's next write and at loginMarker. An "Encrypted" row sits above the oldest stored encrypted message (nodes 10416:1404, 10416:1490), placed from the transcript rather than from
use_e2ee. Tapping it opens the DM encryption sheet. When its gap also needs a date separator or the unread divider, the marker draws that above itself.Push.
applyChatStyleasks the chat coordinator to open the pushed message. A long message arrives asmessage_idonly and is read from Room, or fetched withGetMessage. Any failure keeps the server's body.use_e2eerefresh.ChatMetadataDao.upsertwroteuse_e2eeonly on first insert, so a DM stored before the server set the flag never encrypted. That gap came in with #1573 and is also oncode/cash; it's fixed here in its own commit.