Repository navigation
feat(encryption): add ChatCipher for end-to-end-encrypted DMs - #1616
Merged
Merged
Conversation
Implements scheme X25519_XCHACHA20POLY1305 as specified on EncryptedContent in flipcash2-protobuf-api messaging/v1/model.proto: Ed25519 to X25519 conversion, the HKDF-SHA256 chat key, message encrypt/decrypt, and blob encrypt/decrypt with the blob aad. The module is KMP over multiplatform-crypto-libsodium-bindings 0.9.5, which publishes every target shared-core builds, and reuses the ed25519 KeyPair and the hmac module for HKDF. It is exported from :kmp:shared-core so iOS runs the same code. Every public function is @throws(ChatCipherException) so a failed decrypt reaches Swift as an NSError rather than terminating the app. Tests read chat_cipher.json, synced from the orchestrator's test-vectors. There is no host test: libsodium's Android binding and the Ed25519 JNI do not load on a JVM host, so the suite runs as a device test and on the iOS simulator, which shared-core-tests.yml now includes.
A message goes out as EncryptedContent only in a DM (CONTACT_DM or TIP_DM) whose use_e2ee flag is set and whose peer is not @Flipcash. The @Flipcash account (FLIPCASH_USER_ID) still sends its onboarding DMs in plaintext. use_e2ee is transitional: after launch the rule drops it, and this is the only place that reads it. peerUserId is the raw 16-byte UserId. An id of any other length is not @Flipcash; it is not an error. FLIPCASH_USER_ID returns a copy so callers cannot change the constant. The policy section of chat_cipher.json covers the rule, and the vector suite checks it on the iOS simulator and on device.
…e out ChatCipher is now an interface, so chat code on either platform can fake it; Swift sees a ChatCipher protocol. DefaultChatCipher is the libsodium implementation and is still a stateless object, reached from Swift as DefaultChatCipher.shared. The crypto is unchanged. shouldEncrypt and FLIPCASH_USER_ID move to ChatEncryptionPolicy. They are a product rule about which chats encrypt, not part of the X25519_XCHACHA20POLY1305 scheme.
This was referenced Sep 29, 2026
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.
Adds
:libs:encryption:chat-cipher, the crypto for end-to-end-encrypted DMs.ChatCipheris the interface for schemeX25519_XCHACHA20POLY1305as specified onEncryptedContentin flipcash2-protobuf-api, andDefaultChatCipheris its libsodium implementation. The interface is there so chat code on either platform can use a fake; Swift sees it as aChatCipherprotocol. It's exported from:kmp:shared-core, so iOS runs the same Kotlin rather than a Swift copy. Nothing in the app calls it yet.chatKey(ownKeyPair, peerPublicKey, chatId)converts both Ed25519 keys to X25519 and runs HKDF over the shared secret. The result is the same from either member's side.encrypt/decrypthandle message content, andencryptBlob/decryptBlobhandle uploads. Each binds the chat, sender and recipient (plus the blob id for blobs) into the AAD. Blobs use their own label, so blob bytes can't be decrypted as a message.The send rule is separate, in
ChatEncryptionPolicy.shouldEncrypt(isDirectMessage, useE2ee, peerUserId): a DM withuse_e2eeset whose peer isn't @Flipcash (FLIPCASH_USER_ID).use_e2eeis transitional and leaves the rule after launch.Primitives come from
multiplatform-crypto-libsodium-bindings0.9.5, which publishes Android and all of shared-core's Apple targets. HKDF is built on:libs:encryption:hmac. Every public function is@Throws(ChatCipherException::class). Without that, a tampered message would terminate the iOS app instead of rendering as unsupported.The tests read
chat_cipher.json, generated from PyNaCl andcryptographyin code-payments/flipcash-client-orchestrator (test-vectors/gen_chat_cipher.py, code-payments/flipcash-client-orchestrator#29). There is no host test, because libsodium's Android binding and the Ed25519 JNI don't load on a JVM host. The suite runs asconnectedAndroidDeviceTestand inshared-core-tests.ymlon the iOS simulator. On Android that means it runs only when someone runs it on a device, like the other vector suites under the commented-outTODO(cross-platform-vectors)lane.