commit d5db0ea7f653b473fd5e1350775baab016a7b88d
parent dbafd6de210706a3fc8eb533f179db6d612c2def
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Tue, 23 Jul 2024 18:00:58 +0200

fix(e2ee): key exchange

Signed-off-by: rhunk <101876869+rhunk@users.noreply.github.com>

Diffstat:
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/event/EventDispatcher.kt | 29+++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/event/events/impl/MediaUploadEvent.kt | 22++++++++++++++++++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt | 57++++++++++++++++++++++++++-------------------------------
3 files changed, 77 insertions(+), 31 deletions(-)

diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventDispatcher.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventDispatcher.kt @@ -19,6 +19,7 @@ import me.rhunk.snapenhance.core.wrapper.impl.Message import me.rhunk.snapenhance.core.wrapper.impl.MessageContent import me.rhunk.snapenhance.core.wrapper.impl.MessageDestinations import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID +import me.rhunk.snapenhance.mapper.impl.CallbackMapper import me.rhunk.snapenhance.mapper.impl.ViewBinderMapper import java.nio.ByteBuffer @@ -274,6 +275,34 @@ class EventDispatcher( } } + context.mappings.useMapper(CallbackMapper::class) { + callbacks.getClass("UploadDelegate")?.hook("uploadMedia", HookStage.BEFORE) { param -> + val uploadCallback = param.arg<Any>(2) + + val event = context.event.post(MediaUploadEvent( + localMessageContent = MessageContent(param.arg(0)), + destinations = MessageDestinations(param.arg(1)), + callback = uploadCallback + ).apply { + adapter = param + }) + + if (event?.canceled == true) { + param.setResult(null) + return@hook + } + + event?.mediaUploadCallbacks?.takeIf { it.isNotEmpty() }?.let { callbacks -> + Hooker.ephemeralHookObjectMethod(uploadCallback::class.java, uploadCallback, "onUploadFinished", HookStage.BEFORE) { methodParam -> + val messageContent = MessageContent(methodParam.arg(1)) + callbacks.forEach { + it(MediaUploadEvent.MediaUploadResult(messageContent)) + } + } + } + } + } + hookViewBinder() } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/event/events/impl/MediaUploadEvent.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/event/events/impl/MediaUploadEvent.kt @@ -0,0 +1,21 @@ +package me.rhunk.snapenhance.core.event.events.impl + +import me.rhunk.snapenhance.core.event.events.AbstractHookEvent +import me.rhunk.snapenhance.core.wrapper.impl.MessageContent +import me.rhunk.snapenhance.core.wrapper.impl.MessageDestinations + +class MediaUploadEvent( + val localMessageContent: MessageContent, + val destinations: MessageDestinations, + val callback: Any, +): AbstractHookEvent() { + class MediaUploadResult( + val messageContent: MessageContent + ) + + val mediaUploadCallbacks = mutableListOf<(MediaUploadResult) -> Unit>() + + fun onMediaUploaded(callback: (MediaUploadResult) -> Unit) { + mediaUploadCallbacks.add(callback) + } +}+ \ No newline at end of file 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 @@ -7,11 +7,8 @@ import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.shapes.Shape import android.view.View import android.view.ViewGroup -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding +import android.widget.LinearLayout +import androidx.compose.foundation.layout.* import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.Text @@ -29,18 +26,15 @@ import me.rhunk.snapenhance.common.util.lazyBridge import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.common.util.protobuf.ProtoWriter -import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent -import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent -import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent -import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent +import me.rhunk.snapenhance.core.event.events.impl.* import me.rhunk.snapenhance.core.features.MessagingRuleFeature import me.rhunk.snapenhance.core.features.impl.ui.ConversationToolbox import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper import me.rhunk.snapenhance.core.ui.addForegroundDrawable +import me.rhunk.snapenhance.core.ui.findParent 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 @@ -250,7 +244,9 @@ class EndToEndEncryption : MessagingRuleFeature( context.event.subscribe(BindViewEvent::class) { event -> event.chatMessage { conversationId, messageId -> - val viewGroup = event.view.parent as? ViewGroup ?: return@subscribe + val viewGroup = event.view.findParent(maxIteration = 3) { + it is LinearLayout + } as? ViewGroup ?: event.view.parent as? ViewGroup ?: return@chatMessage viewGroup.findViewWithTag<View>(specialCard)?.also { viewGroup.removeView(it) @@ -312,28 +308,23 @@ class EndToEndEncryption : MessagingRuleFeature( defer { 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 + context.event.subscribe(MediaUploadEvent::class) { event -> + val e2eeConversations = event.destinations.getEndToEndConversations() + if (e2eeConversations.isEmpty()) return@subscribe - if (messageDestinations.conversations!!.size != e2eeConversations.size || messageDestinations.stories?.isNotEmpty() == true) { - context.log.debug("skipping encryption") - return@hook - } + if (event.destinations.conversations!!.size != e2eeConversations.size || event.destinations.stories?.isNotEmpty() == true) { + context.log.debug("skipping encryption") + return@subscribe + } - 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"]) - } + event.onMediaUploaded { result -> + runCatching { + result.messageContent.content = ProtoWriter().apply { + writeEncryptedMessage(e2eeConversations.map { getE2EParticipants(it) }.flatten().distinct(), result.messageContent.content!!) + }.toByteArray() + }.onFailure { + context.log.error("Failed to encrypt message", it) + context.longToast(translation["encryption_failed_toast"]) } } } @@ -359,6 +350,10 @@ class EndToEndEncryption : MessagingRuleFeature( } event.addInvokeLater { + // check if the content is already encrypted + if (ProtoReader(messageContent.content!!).getByteArray(2, 1, 2) != null) { + return@addInvokeLater + } if (event.messageContent.localMediaReferences?.isEmpty() == true) { runCatching { event.messageContent.content = ProtoWriter().apply {