commit eb7fbaff6fc700938e50133f154db3bd26247a6d
parent da19408c757975c31b0927d39d26746aa62c44eb
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Mon,  6 May 2024 22:38:21 +0200

fix(core): e2ee

Diffstat:
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt | 156+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/MessageContent.kt | 2++
2 files changed, 79 insertions(+), 79 deletions(-)

diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt @@ -40,6 +40,7 @@ import me.rhunk.snapenhance.core.ui.addForegroundDrawable import me.rhunk.snapenhance.core.ui.removeForegroundDrawable import me.rhunk.snapenhance.core.util.EvictingMap import me.rhunk.snapenhance.core.util.hook.HookStage +import me.rhunk.snapenhance.core.util.hook.Hooker import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.ktx.getObjectFieldOrNull @@ -406,18 +407,74 @@ class EndToEndEncryption : MessagingRuleFeature( } } + private fun ProtoWriter.writeEncryptedMessage( + participantsIds: List<String>, + messageContent: ByteArray, + ) { + from(2) { + from(1) { + addVarInt(1, ENCRYPTED_MESSAGE_ID) + participantsIds.forEach { participantId -> + val encryptedMessage = e2eeInterface.encryptMessage(participantId, + messageContent + ) ?: run { + throw Exception("Failed to encrypt message for participant $participantId") + } + context.log.debug("encrypted message size = ${encryptedMessage.ciphertext.size}") + from(2) { + // participantId is hashed with iv to prevent leaking it when sending to multiple conversations + addBuffer(1, hashParticipantId(participantId, encryptedMessage.iv)) + addBuffer(2, encryptedMessage.iv) + addBuffer(3, encryptedMessage.ciphertext) + } + } + if (ContentType.fromMessageContainer(ProtoReader(messageContent)) == ContentType.SNAP) { + addVarInt(5, 1) + } + } + } + } + + private fun MessageDestinations.getEndToEndConversations(): List<String> { + return conversations!!.filter { getState(it.toString()) && getE2EParticipants(it.toString()).isNotEmpty() }.map { it.toString() } + } + override fun asyncInit() { if (!isEnabled) return val forceMessageEncryption by context.config.experimental.e2eEncryption.forceMessageEncryption + context.mappings.useMapper(CallbackMapper::class) { + callbacks.getClass("UploadDelegate")?.hook("uploadMedia", HookStage.BEFORE) { param -> + val messageDestinations = MessageDestinations(param.arg(1)) + val uploadCallback = param.arg<Any>(2) + val e2eeConversations = messageDestinations.getEndToEndConversations() + if (e2eeConversations.isEmpty()) return@hook + + if (messageDestinations.conversations!!.size != e2eeConversations.size || messageDestinations.stories?.isNotEmpty() == true) { + context.log.debug("skipping encryption") + return@hook + } + + Hooker.hookObjectMethod(uploadCallback::class.java, uploadCallback, "onUploadFinished", HookStage.BEFORE) { methodParam -> + val messageContent = MessageContent(methodParam.arg(1)) + runCatching { + messageContent.content = ProtoWriter().apply { + writeEncryptedMessage(e2eeConversations.map { getE2EParticipants(it) }.flatten().distinct(), messageContent.content!!) + }.toByteArray() + }.onFailure { + context.log.error("Failed to encrypt message", it) + context.longToast(translation["encryption_failed_toast"]) + } + } + } + } + // trick to disable fidelius encryption context.event.subscribe(SendMessageWithContentEvent::class) { event -> val messageContent = event.messageContent val destinations = event.destinations - val e2eeConversations = destinations.conversations!!.filter { getState(it.toString()) && getE2EParticipants(it.toString()).isNotEmpty() } - - if (e2eeConversations.isEmpty()) return@subscribe + val e2eeConversations = destinations.getEndToEndConversations().takeIf { it.isNotEmpty() } ?: return@subscribe if (e2eeConversations.size != destinations.conversations!!.size || destinations.stories?.isNotEmpty() == true) { if (!forceMessageEncryption) return@subscribe @@ -433,12 +490,19 @@ class EndToEndEncryption : MessagingRuleFeature( } event.addInvokeLater { - if (messageContent.contentType == ContentType.SNAP) { - messageContent.contentType = ContentType.EXTERNAL_MEDIA + if (event.messageContent.localMediaReferences?.isEmpty() == true) { + runCatching { + event.messageContent.content = ProtoWriter().apply { + writeEncryptedMessage(e2eeConversations.map { getE2EParticipants(it) }.flatten().distinct(), messageContent.content!!) + }.toByteArray() + }.onFailure { + context.log.error("Failed to encrypt message", it) + context.longToast(translation["encryption_failed_toast"]) + } } - if (messageContent.contentType == ContentType.CHAT) { - messageContent.contentType = ContentType.SHARE + if (event.messageContent.contentType == ContentType.SNAP) { + event.messageContent.contentType = ContentType.EXTERNAL_MEDIA } } } @@ -446,83 +510,17 @@ class EndToEndEncryption : MessagingRuleFeature( context.event.subscribe(NativeUnaryCallEvent::class) { event -> if (event.uri != "/messagingcoreservice.MessagingCoreService/CreateContentMessage") return@subscribe val protoReader = ProtoReader(event.buffer) - var hasStory = false - - val conversationIds = mutableListOf<SnapUUID>() - protoReader.eachBuffer(3) { - if (contains(2)) { - hasStory = true - return@eachBuffer - } - conversationIds.add(SnapUUID(getByteArray(1, 1, 1) ?: return@eachBuffer)) - } - - if (hasStory) { - context.log.debug("Skipping encryption for story message") - return@subscribe - } - - if (conversationIds.any { !getState(it.toString()) || getE2EParticipants(it.toString()).isEmpty() }) { - context.log.debug("Skipping encryption for conversation ids: ${conversationIds.joinToString(", ")}") - return@subscribe - } - - val participantsIds = conversationIds.map { getE2EParticipants(it.toString()) }.flatten().distinct() - - if (participantsIds.isEmpty()) { - context.shortToast(translation["no_participants_to_encrypt_toast"]) - return@subscribe - } val messageReader = protoReader.followPath(4) ?: return@subscribe - if (messageReader.getVarInt(4, 2, 1, 1) != null) { - return@subscribe - } - - event.buffer = ProtoEditor(event.buffer).apply { - val contentType = fixContentType( - ContentType.fromId(messageReader.getVarInt(2)?.toInt() ?: -1), - messageReader.followPath(4) ?: return@apply - ) ?: return@apply - val messageContent = messageReader.getByteArray(4) ?: return@apply - - runCatching { + if (messageReader.getVarInt(4, 2, 1, 5) == 1L) { + event.buffer = ProtoEditor(event.buffer).apply { edit(4) { - //set message content type remove(2) - addVarInt(2, contentType.id) - - //set encrypted content - remove(4) - add(4) { - from(2) { - from(1) { - addVarInt(1, ENCRYPTED_MESSAGE_ID) - participantsIds.forEach { participantId -> - val encryptedMessage = e2eeInterface.encryptMessage(participantId, - messageContent - ) ?: run { - context.log.error("Failed to encrypt message for $participantId") - return@forEach - } - context.log.debug("encrypted message size = ${encryptedMessage.ciphertext.size} for $participantId") - from(2) { - // participantId is hashed with iv to prevent leaking it when sending to multiple conversations - addBuffer(1, hashParticipantId(participantId, encryptedMessage.iv)) - addBuffer(2, encryptedMessage.iv) - addBuffer(3, encryptedMessage.ciphertext) - } - } - } - } - } + addVarInt(2, ContentType.SNAP.id) + context.log.verbose("fixed snap content type") } - }.onFailure { - event.canceled = true - context.log.error("Failed to encrypt message", it) - context.longToast(translation["encryption_failed_toast"]) - } - }.toByteArray() + }.toByteArray() + } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/MessageContent.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/MessageContent.kt @@ -12,4 +12,6 @@ class MessageContent(obj: Any?) : AbstractWrapper(obj) { var quotedMessage by field("mQuotedMessage") { QuotedMessage(it) } @get:JSGetter @set:JSSetter var contentType by enum("mContentType", ContentType.UNKNOWN) + @get:JSGetter @set:JSSetter + var localMediaReferences by field<ArrayList<*>>("mLocalMediaReferences") } \ No newline at end of file