Skip to content

feat(chat): encrypt DM text end to end - #913

Merged
bmc08gt merged 4 commits into
mainfrom
feat/e2ee-dm-crypto
Sep 29, 2026
Merged

bmc08gt merged 4 commits into
mainfrom
feat/e2ee-dm-crypto

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Phase 2 of E2EE DMs, on top of the UI from #909. Text and replies in a DM are now encrypted with shared-core's ChatCipher (code-payments/code-android-app#1616).

Changes

  • Bumps SharedCoreKit to 0.10.0, the first release with ChatCipher.
  • E2eePolicy asks shared-core's ChatEncryptionPolicy, which carries the @Flipcash user id, so the app's own constant and its test seam are gone. The @Flipcash chat never encrypts.
  • EncryptedChatClient wraps FlipClient. Send, retry and edit seal Text and Reply(Text) into EncryptedContent when the policy says so. Every message read back (fetch, delta, stream, and the server's echo of a send) is decrypted before it reaches the store.
  • The peer's key comes from Resolver.Resolve(user_id) and is kept per user. Resolving your own user id returns your owner key, which is what makes this the right source.
  • Decrypted text is stored next to the ciphertext (schema 43). A message that won't open keeps its cause, which picks the hint under "This message can't be displayed": an unknown scheme or type asks to update Flipcash, an auth failure asks {first name} to send it again, or asks you to try again when it's your own message.
  • A message whose key couldn't be fetched stays out of the transcript and the chat-list preview until a later load decrypts it.
  • ENCRYPTION_NOT_ALLOWED leaves a failed bubble you can retry, and drops the cached chat so the retry reads use_e2ee again.
  • An Encrypted marker sits before the first loaded encrypted message, placed from the transcript rather than use_e2ee. Tapping it opens the DM learn-more sheet.
  • The notification service extension decrypts the push body, fetching the message by id when the push carries only that, and keeps the server's body on any failure. The notification's quick reply encrypts.

Media is out of scope.

E2eePolicy now asks shared-core's ChatEncryptionPolicy, which carries the
@Flipcash user id, so the app's own nil constant and its injection seam
go. Both DM types count as direct messages.

The conversation doesn't say which member is self, so each member is
checked as the peer; self is never @Flipcash, so only the @Flipcash chat
fails. A member without a user id has no key to encrypt to and keeps
the chat in plaintext.

Needs a SharedCoreKit build with ChatCipher (code-android-app#1616);
until it's released, build with FLIPCASH_SHARED_CORE_LOCAL.
Text and Reply(Text) in a DM are sealed into EncryptedContent when
ChatEncryptionPolicy says so. EncryptedChatClient wraps FlipClient so
send, retry and edit all encrypt, and every message read back from the
server (fetch, delta, stream) is decrypted before it reaches the store.
The server's echo of a sent message is decrypted the same way.

The peer's key comes from Resolver.Resolve(user_id) and is kept per user.
Decrypted text is stored next to the ciphertext (schema 43). A message
that fails to open keeps its cause, which picks the unavailable hint:
an unknown scheme or type asks to update Flipcash, an auth failure asks
the sender to send it again, or asks you to try again for your own
message. A message whose key couldn't be fetched is hidden from the
transcript and the chat-list preview until a later load decrypts it.

ENCRYPTION_NOT_ALLOWED leaves a retryable failed bubble and drops the
cached chat, so the retry reads use_e2ee again.

The transcript gets an Encrypted marker before the first loaded
encrypted message, placed from the messages rather than use_e2ee;
tapping it opens the DM learn-more sheet.

The notification service extension decrypts the push body, fetching the
message by id when the push carries only that, and keeps the server's
body on any failure. The quick reply from the notification encrypts.
@bmc08gt bmc08gt self-assigned this Sep 29, 2026
The NSE target has no tests, so the rule that swaps in a decrypted body
moves to NotificationPayload.decryptedBody in FlipcashCore. Tests pin
it: plaintext only for an encrypted message that decrypted to text, and
nil (keep the server's body) when the message is missing, was never
encrypted, or failed to decrypt. chatMessageID gets tests for embedded,
id-only, and absent message references.
@bmc08gt
bmc08gt marked this pull request as ready for review September 29, 2026 20:38
@bmc08gt
bmc08gt merged commit 7ad3f54 into main Sep 29, 2026
1 check failed
@bmc08gt
bmc08gt deleted the feat/e2ee-dm-crypto branch September 29, 2026 21:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant