commit 2749b734e4343269752590a41c32cc62870ccc1d
parent aaf8f3e43a7a692988f323f245cf28338a722949
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Fri, 29 Sep 2023 01:56:59 +0200

fix(e2ee): content type spoofing
- refactor event bus
- add encrypted message indicator

Diffstat:
Mcore/src/main/assets/lang/en_US.json | 4++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt | 6+++---
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Experimental.kt | 1+
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/event/EventBus.kt | 3++-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/event/EventDispatcher.kt | 34++++++++++++++--------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/event/events/AbstractHookEvent.kt | 22++++++++++++++++++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/EndToEndEncryption.kt | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/SendOverride.kt | 2+-
8 files changed, 100 insertions(+), 43 deletions(-)

diff --git a/core/src/main/assets/lang/en_US.json b/core/src/main/assets/lang/en_US.json @@ -524,6 +524,10 @@ "name": "End-To-End Encryption", "description": "Encrypts your messages with AES using a shared secret key\nMake sure to save your key somewhere safe!" }, + "encrypted_message_indicator": { + "name": "Encrypted Message Indicator", + "description": "Adds a \uD83D\uDD12 emoji next to encrypted messages" + }, "add_friend_source_spoof": { "name": "Add Friend Source Spoof", "description": "Spoofs the source of a Friend Request" diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt @@ -137,9 +137,9 @@ class SnapEnhance { if (appContext.config.experimental.nativeHooks.globalState != true) return@apply initOnce(appContext.androidContext.classLoader) nativeUnaryCallCallback = { request -> - appContext.event.post(UnaryCallEvent(request.uri, request.buffer))?.also { - request.buffer = it.buffer - request.canceled = it.canceled + appContext.event.post(UnaryCallEvent(request.uri, request.buffer)) { + request.buffer = buffer + request.canceled = canceled } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Experimental.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Experimental.kt @@ -13,6 +13,7 @@ class Experimental : ConfigContainer() { val unlimitedMultiSnap = boolean("unlimited_multi_snap") { addNotices(FeatureNotice.BAN_RISK)} val noFriendScoreDelay = boolean("no_friend_score_delay") val useE2EEncryption = boolean("e2e_encryption") + val encryptedMessageIndicator = boolean("encrypted_message_indicator") { addNotices(FeatureNotice.UNSTABLE) } val hiddenSnapchatPlusFeatures = boolean("hidden_snapchat_plus_features") { addNotices(FeatureNotice.BAN_RISK, FeatureNotice.UNSTABLE) } val addFriendSourceSpoof = unique("add_friend_source_spoof", "added_by_username", diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventBus.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventBus.kt @@ -48,7 +48,7 @@ class EventBus( subscribers[event]!!.remove(listener) } - fun <T : Event> post(event: T): T? { + fun <T : Event> post(event: T, afterBlock: T.() -> Unit = {}): T? { if (!subscribers.containsKey(event::class)) { return null } @@ -63,6 +63,7 @@ class EventBus( context.log.error("Error while handling event ${event::class.simpleName} by ${listener::class.simpleName}", t) } } + afterBlock(event) return event } 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 @@ -62,10 +62,8 @@ class EventDispatcher( destinations = MessageDestinations(param.arg(0)), messageContent = MessageContent(param.arg(1)), callback = param.arg(2) - ).apply { adapter = param })?.also { - if (it.canceled) { - param.setResult(null) - } + ).apply { adapter = param }) { + postHookEvent() } } @@ -79,10 +77,8 @@ class EventDispatcher( conversationId = conversationId, messageId = messageId ) - )?.also { - if (it.canceled) { - param.setResult(null) - } + ) { + postHookEvent() } } @@ -98,10 +94,8 @@ class EventDispatcher( intent = intent, action = action ) - )?.also { - if (it.canceled) { - param.setResult(null) - } + ) { + postHookEvent() } } @@ -120,13 +114,13 @@ class EventDispatcher( ).apply { adapter = param } - )?.also { event -> + ) { with(param) { - setArg(0, event.view) - setArg(1, event.index) - setArg(2, event.layoutParams) + setArg(0, view) + setArg(1, index) + setArg(2, layoutParams) } - if (event.canceled) param.setResult(null) + postHookEvent() } } @@ -141,9 +135,9 @@ class EventDispatcher( ).apply { adapter = param } - )?.also { event -> - event.request.setObjectField("mUrl", event.url) - if (event.canceled) param.setResult(null) + ) { + request.setObjectField("mUrl", url) + postHookEvent() } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/event/events/AbstractHookEvent.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/event/events/AbstractHookEvent.kt @@ -5,4 +5,26 @@ import me.rhunk.snapenhance.hook.HookAdapter abstract class AbstractHookEvent : Event() { lateinit var adapter: HookAdapter + private val invokeLaterCallbacks = mutableListOf<() -> Unit>() + + fun addInvokeLater(callback: () -> Unit) { + invokeLaterCallbacks.add(callback) + } + + private fun invokeLater() { + invokeLaterCallbacks.forEach { it() } + } + + fun postHookEvent(block: AbstractHookEvent.() -> Unit = {}) { + block().apply { + invokeLater() + if (canceled) adapter.setResult(null) + } + } + + fun invokeOriginal() { + canceled = true + invokeLater() + adapter.invokeOriginal() + } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/EndToEndEncryption.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/EndToEndEncryption.kt @@ -6,6 +6,8 @@ import android.view.ViewGroup import android.view.ViewGroup.LayoutParams import android.view.ViewGroup.MarginLayoutParams import android.widget.Button +import android.widget.LinearLayout +import android.widget.RelativeLayout import android.widget.TextView import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent @@ -46,6 +48,7 @@ class EndToEndEncryption : MessagingRuleFeature( private val pkRequests = mutableMapOf<Long, ByteArray>() private val secretResponses = mutableMapOf<Long, ByteArray>() + private val encryptedMessages = mutableListOf<Long>() private fun getE2EParticipants(conversationId: String): List<String> { return context.database.getConversationParticipants(conversationId)?.filter { friendId -> e2eeInterface.friendKeyExists(friendId) } ?: emptyList() @@ -166,7 +169,7 @@ class EndToEndEncryption : MessagingRuleFeature( }.show() } - @SuppressLint("SetTextI18n") + @SuppressLint("SetTextI18n", "DiscouragedApi") override fun onActivityCreate() { if (!isEnabled) return // add button to input bar @@ -182,9 +185,13 @@ class EndToEndEncryption : MessagingRuleFeature( } } + val encryptedMessageIndicator by context.config.experimental.encryptedMessageIndicator + val chatMessageContentContainerId = context.resources.getIdentifier("chat_message_content_container", "id", context.androidContext.packageName) + // hook view binder to add special buttons val receivePublicKeyTag = Random.nextLong().toString(16) val receiveSecretTag = Random.nextLong().toString(16) + val encryptedMessageTag = Random.nextLong().toString(16) context.event.subscribe(BindViewEvent::class) { event -> event.chatMessage { conversationId, messageId -> @@ -198,6 +205,32 @@ class EndToEndEncryption : MessagingRuleFeature( viewGroup.removeView(it) } + if (encryptedMessageIndicator) { + viewGroup.findViewWithTag<ViewGroup>(encryptedMessageTag)?.also { + val chatMessageContentContainer = viewGroup.findViewById<View>(chatMessageContentContainerId) as? LinearLayout ?: return@chatMessage + it.removeView(chatMessageContentContainer) + viewGroup.removeView(it) + viewGroup.addView(chatMessageContentContainer, 0) + } + + if (encryptedMessages.contains(messageId.toLong())) { + val chatMessageContentContainer = viewGroup.findViewById<View>(chatMessageContentContainerId) as? LinearLayout ?: return@chatMessage + viewGroup.removeView(chatMessageContentContainer) + + viewGroup.addView(RelativeLayout(viewGroup.context).apply { + tag = encryptedMessageTag + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + addView(chatMessageContentContainer) + addView(TextView(viewGroup.context).apply { + text = "\uD83D\uDD12" + textAlignment = View.TEXT_ALIGNMENT_TEXT_END + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + setPadding(20, 0, 20, 0) + }) + }, 0) + } + } + secretResponses[messageId.toLong()]?.also { secret -> viewGroup.addView(Button(context.mainActivity!!).apply { text = "Accept secret" @@ -271,17 +304,14 @@ class EndToEndEncryption : MessagingRuleFeature( if (conversationParticipants.isEmpty()) return@eachBuffer val participantId = conversationParticipants.firstOrNull { participantIdHash.contentEquals(hashParticipantId(it, iv)) } ?: return@eachBuffer messageContent.content = e2eeInterface.decryptMessage(participantId, ciphertext, iv) + encryptedMessages.add(messageId) return@eachBuffer } if (!participantIdHash.contentEquals(hashParticipantId(context.database.myUserId, iv))) return@eachBuffer messageContent.content = e2eeInterface.decryptMessage(senderId, ciphertext, iv) - } - - // fix content type - messageContent.contentType?.also { - messageContent.contentType = fixContentType(it, reader) + encryptedMessages.add(messageId) } }.onFailure { context.log.error("Failed to decrypt message id: $messageId", it) @@ -329,22 +359,19 @@ class EndToEndEncryption : MessagingRuleFeature( context.event.subscribe(SendMessageWithContentEvent::class) { param -> val messageContent = param.messageContent val destinations = param.destinations - if (destinations.conversations.size != 1 || destinations.stories.isNotEmpty()) return@subscribe - - if (!getState(destinations.conversations.first().toString())) return@subscribe + if (destinations.conversations.none { getState(it.toString()) }) return@subscribe - if (messageContent.contentType == ContentType.SNAP) { - messageContent.contentType = ContentType.EXTERNAL_MEDIA - } + param.addInvokeLater { + if (messageContent.contentType == ContentType.SNAP) { + messageContent.contentType = ContentType.EXTERNAL_MEDIA + } - if (messageContent.contentType == ContentType.CHAT) { - messageContent.contentType = ContentType.SHARE + if (messageContent.contentType == ContentType.CHAT) { + messageContent.contentType = ContentType.SHARE + } } } - } - override fun init() { - if (!isEnabled) return context.event.subscribe(UnaryCallEvent::class) { event -> if (event.uri != "/messagingcoreservice.MessagingCoreService/CreateContentMessage") return@subscribe val protoReader = ProtoReader(event.buffer) @@ -363,7 +390,6 @@ class EndToEndEncryption : MessagingRuleFeature( if (participantsIds.isEmpty()) { context.longToast("You don't have any friends in this conversation to encrypt messages with!") - event.canceled = true return@subscribe } val messageReader = protoReader.followPath(4) ?: return@subscribe @@ -414,6 +440,10 @@ class EndToEndEncryption : MessagingRuleFeature( } }.toByteArray() } + } + + override fun init() { + if (!isEnabled) return context.classCache.message.hookConstructor(HookStage.AFTER) { param -> val message = Message(param.thisObject()) @@ -424,6 +454,11 @@ class EndToEndEncryption : MessagingRuleFeature( senderId = message.senderId.toString(), messageContent = message.messageContent ) + + message.messageContent.contentType?.also { + message.messageContent.contentType = fixContentType(it, ProtoReader(message.messageContent.content)) + } + message.messageContent.instanceNonNull() .getObjectField("mQuotedMessage") ?.getObjectField("mContent") diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/SendOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/SendOverride.kt @@ -106,7 +106,7 @@ class SendOverride : Feature("Send Override", loadParams = FeatureLoadParams.INI } } - event.adapter.invokeOriginal() + event.invokeOriginal() } .setNegativeButton(context.translation["button.cancel"], null) .show()