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:
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()