From 60ee3680a484a1de4fc305a6ce64834c6dde3ed4 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Tue, 13 Feb 2024 22:55:30 +0100 Subject: [PATCH] feat(experimental): session events - refactor SnapUUID --- common/src/main/assets/lang/en_US.json | 14 ++ .../common/config/impl/E2EEConfig.kt | 8 - .../common/config/impl/Experimental.kt | 11 + .../common/data/SessionEventsData.kt | 42 ++++ .../impl/experiments/EndToEndEncryption.kt | 4 +- .../impl/experiments/SessionEvents.kt | 232 ++++++++++++++++++ .../core/features/impl/messaging/Messaging.kt | 2 +- .../features/impl/messaging/Notifications.kt | 2 +- .../impl/tweaks/UnsaveableMessages.kt | 2 +- .../core/manager/impl/FeatureManager.kt | 1 + .../core/scripting/impl/CoreMessaging.kt | 4 +- .../core/wrapper/AbstractWrapper.kt | 2 +- .../core/wrapper/impl/ConversationManager.kt | 2 +- .../snapenhance/core/wrapper/impl/SnapUUID.kt | 60 +++-- native/jni/src/hooks/duplex_hook.h | 26 ++ native/jni/src/library.cpp | 2 + 16 files changed, 373 insertions(+), 41 deletions(-) delete mode 100644 common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/E2EEConfig.kt create mode 100644 common/src/main/kotlin/me/rhunk/snapenhance/common/data/SessionEventsData.kt create mode 100644 core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/SessionEvents.kt create mode 100644 native/jni/src/hooks/duplex_hook.h diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index 86df87586..90cd6aa62 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -618,6 +618,20 @@ } } }, + "session_events": { + "name": "Session Events", + "description": "Records session events", + "properties": { + "capture_duplex_events": { + "name": "Capture Duplex Events", + "description": "Capture presence and messaging events when a session is active" + }, + "allow_running_in_background": { + "name": "Allow Running in Background", + "description": "Allows session to run in the background" + } + } + }, "spoof": { "name": "Spoof", "description": "Spoof various information about you", diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/E2EEConfig.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/E2EEConfig.kt deleted file mode 100644 index 2feeb67ed..000000000 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/E2EEConfig.kt +++ /dev/null @@ -1,8 +0,0 @@ -package me.rhunk.snapenhance.common.config.impl - -import me.rhunk.snapenhance.common.config.ConfigContainer - -class E2EEConfig : ConfigContainer(hasGlobalState = true) { - val encryptedMessageIndicator = boolean("encrypted_message_indicator") - val forceMessageEncryption = boolean("force_message_encryption") -} \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt index e1f6e9b15..efda67eb6 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt @@ -4,11 +4,22 @@ import me.rhunk.snapenhance.common.config.ConfigContainer import me.rhunk.snapenhance.common.config.FeatureNotice class Experimental : ConfigContainer() { + class SessionEventsConfig : ConfigContainer(hasGlobalState = true) { + val captureDuplexEvents = boolean("capture_duplex_events", true) + val allowRunningInBackground = boolean("allow_running_in_background", true) + } + class NativeHooks : ConfigContainer(hasGlobalState = true) { val disableBitmoji = boolean("disable_bitmoji") } + class E2EEConfig : ConfigContainer(hasGlobalState = true) { + val encryptedMessageIndicator = boolean("encrypted_message_indicator") + val forceMessageEncryption = boolean("force_message_encryption") + } + val nativeHooks = container("native_hooks", NativeHooks()) { icon = "Memory"; requireRestart() } + val sessionEvents = container("session_events", SessionEventsConfig()) { requireRestart(); nativeHooks() } val spoof = container("spoof", Spoof()) { icon = "Fingerprint" ; addNotices(FeatureNotice.BAN_RISK); requireRestart() } val convertMessageLocally = boolean("convert_message_locally") { requireRestart() } val storyLogger = boolean("story_logger") { requireRestart(); addNotices(FeatureNotice.UNSTABLE); } diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/data/SessionEventsData.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/SessionEventsData.kt new file mode 100644 index 000000000..3a8c97c2c --- /dev/null +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/SessionEventsData.kt @@ -0,0 +1,42 @@ +package me.rhunk.snapenhance.common.data + + +data class FriendPresenceState( + val bitmojiPresent: Boolean, + val typing: Boolean, + val wasTyping: Boolean, + val speaking: Boolean, + val peeking: Boolean +) + +open class SessionEvent( + val type: SessionEventType, + val conversationId: String, + val authorUserId: String, +) + +class SessionMessageEvent( + type: SessionEventType, + conversationId: String, + authorUserId: String, + val serverMessageId: Long, + val messageData: ByteArray? = null, + val reactionId: Int? = null, +) : SessionEvent(type, conversationId, authorUserId) + + +enum class SessionEventType( + val key: String +) { + MESSAGE_READ_RECEIPTS("message_read_receipts"), + MESSAGE_DELETED("message_deleted"), + MESSAGE_SAVED("message_saved"), + MESSAGE_UNSAVED("message_unsaved"), + MESSAGE_REACTION_ADD("message_reaction_add"), + MESSAGE_REACTION_REMOVE("message_reaction_remove"), + SNAP_OPENED("snap_opened"), + SNAP_REPLAYED("snap_replayed"), + SNAP_REPLAYED_TWICE("snap_replayed_twice"), + SNAP_SCREENSHOT("snap_screenshot"), + SNAP_SCREEN_RECORD("snap_screen_record"), +} 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 index bf8200f9f..79f6b8ef4 100644 --- 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 @@ -90,7 +90,7 @@ class EndToEndEncryption : MessagingRuleFeature( private fun sendCustomMessage(conversationId: String, messageId: Int, message: ProtoWriter.() -> Unit) { context.messageSender.sendCustomChatMessage( - listOf(SnapUUID.fromString(conversationId)), + listOf(SnapUUID(conversationId)), ContentType.CHAT, message = { from(2) { @@ -444,7 +444,7 @@ class EndToEndEncryption : MessagingRuleFeature( hasStory = true return@eachBuffer } - conversationIds.add(SnapUUID.fromBytes(getByteArray(1, 1, 1) ?: return@eachBuffer)) + conversationIds.add(SnapUUID(getByteArray(1, 1, 1) ?: return@eachBuffer)) } if (hasStory) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/SessionEvents.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/SessionEvents.kt new file mode 100644 index 000000000..dcd9f4b5e --- /dev/null +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/SessionEvents.kt @@ -0,0 +1,232 @@ +package me.rhunk.snapenhance.core.features.impl.experiments + +import me.rhunk.snapenhance.common.data.SessionMessageEvent +import me.rhunk.snapenhance.common.data.SessionEvent +import me.rhunk.snapenhance.common.data.SessionEventType +import me.rhunk.snapenhance.common.data.FriendPresenceState +import me.rhunk.snapenhance.common.util.protobuf.ProtoReader +import me.rhunk.snapenhance.core.features.Feature +import me.rhunk.snapenhance.core.features.FeatureLoadParams +import me.rhunk.snapenhance.core.util.hook.HookStage +import me.rhunk.snapenhance.core.util.hook.hook +import me.rhunk.snapenhance.core.util.hook.hookConstructor +import me.rhunk.snapenhance.core.wrapper.impl.toSnapUUID +import me.rhunk.snapenhance.nativelib.NativeLib +import java.lang.reflect.Method +import java.nio.ByteBuffer + +class SessionEvents : Feature("Session Events", loadParams = FeatureLoadParams.INIT_SYNC) { + private val conversationPresenceState = mutableMapOf>() // conversationId -> (userId -> state) + + private fun handleVolatileEvent(protoReader: ProtoReader) { + context.log.verbose("volatile event\n$protoReader") + } + + private fun onConversationPresenceUpdate(conversationId: String, userId: String, oldState: FriendPresenceState?, currentState: FriendPresenceState?) { + context.log.verbose("presence state for $userId in conversation $conversationId\n$currentState") + } + + private fun onConversationMessagingEvent(event: SessionEvent) { + context.log.verbose("conversation messaging event\n${event.type} in ${event.conversationId} from ${event.authorUserId}") + } + + private fun handlePresenceEvent(protoReader: ProtoReader) { + val conversationId = protoReader.getString(6) ?: return + + val presenceMap = conversationPresenceState.getOrPut(conversationId) { mutableMapOf() }.toMutableMap() + val userIds = mutableSetOf() + + protoReader.eachBuffer(4) { + val participantUserId = getString(1)?.takeIf { it.contains(":") }?.substringBefore(":") ?: return@eachBuffer + userIds.add(participantUserId) + if (participantUserId == context.database.myUserId) return@eachBuffer + val stateMap = getVarInt(2, 1)?.toString(2)?.padStart(16, '0')?.reversed()?.map { it == '1' } ?: return@eachBuffer + + presenceMap[participantUserId] = FriendPresenceState( + bitmojiPresent = stateMap[0], + typing = stateMap[4], + wasTyping = stateMap[5], + speaking = stateMap[6] && stateMap[4], + peeking = stateMap[8] + ) + } + + presenceMap.keys.filterNot { it in userIds }.forEach { presenceMap[it] = null } + + presenceMap.forEach { (userId, state) -> + val oldState = conversationPresenceState[conversationId]?.get(userId) + if (oldState != state) { + onConversationPresenceUpdate(conversationId, userId, oldState, state) + } + } + + conversationPresenceState[conversationId] = presenceMap + } + + private fun handleMessagingEvent(protoReader: ProtoReader) { + // read receipts + protoReader.followPath(12) { + val conversationId = getByteArray(1, 1)?.toSnapUUID().toString() ?: return@followPath + + followPath(7) readReceipts@{ + val senderId = getByteArray(1, 1)?.toSnapUUID()?.toString() ?: return@readReceipts + val serverMessageId = getVarInt(2, 2) ?: return@readReceipts + + onConversationMessagingEvent( + SessionMessageEvent( + SessionEventType.MESSAGE_READ_RECEIPTS, + conversationId, + senderId, + serverMessageId, + ) + ) + } + } + + protoReader.followPath(6, 2) { + val conversationId = getByteArray(1, 1)?.toSnapUUID()?.toString() ?: return@followPath + val senderId = getByteArray(3, 1)?.toSnapUUID()?.toString() ?: return@followPath + val serverMessageId = getVarInt(2) ?: return@followPath + + if (contains(4)) { + onConversationMessagingEvent( + SessionMessageEvent( + SessionEventType.SNAP_OPENED, + conversationId, + senderId, + serverMessageId + ) + ) + } + + if (contains(13)) { + onConversationMessagingEvent( + SessionMessageEvent( + if (getVarInt(13, 1) == 2L) SessionEventType.SNAP_REPLAYED_TWICE else SessionEventType.SNAP_REPLAYED, + conversationId, + senderId, + serverMessageId + ) + ) + } + + if (contains(6) || contains(7)) { + onConversationMessagingEvent( + SessionMessageEvent( + if (contains(6)) SessionEventType.MESSAGE_SAVED else SessionEventType.MESSAGE_UNSAVED, + conversationId, + senderId, + serverMessageId + ) + ) + } + + if (contains(11) || contains(12)) { + onConversationMessagingEvent( + SessionMessageEvent( + if (contains(11)) SessionEventType.SNAP_SCREENSHOT else SessionEventType.SNAP_SCREEN_RECORD, + conversationId, + senderId, + serverMessageId, + ) + ) + } + + followPath(16) { + onConversationMessagingEvent( + SessionMessageEvent( + SessionEventType.MESSAGE_REACTION_ADD, conversationId, senderId, serverMessageId, reactionId = getVarInt(1, 1, 1)?.toInt() ?: -1 + ) + ) + } + + if (contains(17)) { + onConversationMessagingEvent( + SessionMessageEvent(SessionEventType.MESSAGE_REACTION_REMOVE, conversationId, senderId, serverMessageId) + ) + } + + followPath(8) { + onConversationMessagingEvent( + SessionMessageEvent(SessionEventType.MESSAGE_DELETED, conversationId, senderId, serverMessageId, messageData = getByteArray(1)) + ) + } + } + } + + override fun init() { + val sessionEventsConfig = context.config.experimental.sessionEvents + if (sessionEventsConfig.globalState != true) return + + if (sessionEventsConfig.allowRunningInBackground.get()) { + findClass("com.snapchat.client.duplex.DuplexClient\$CppProxy").apply { + // prevent disabling events when the app is inactive + hook("appStateChanged", HookStage.BEFORE) { param -> + if (param.arg(0).toString() == "INACTIVE") param.setResult(null) + } + // allow events when a notification is received + hookConstructor(HookStage.AFTER) { param -> + methods.first { it.name == "appStateChanged" }.let { method -> + method.invoke(param.thisObject(), method.parameterTypes[0].enumConstants.first { it.toString() == "ACTIVE" }) + } + } + } + } + + if (sessionEventsConfig.captureDuplexEvents.get()) { + val messageHandlerClass = findClass("com.snapchat.client.duplex.MessageHandler\$CppProxy").apply { + hook("onReceive", HookStage.BEFORE) { param -> + param.setResult(null) + + val byteBuffer = param.arg(0) + val content = byteBuffer.let { + val bytes = ByteArray(it.limit()) + it.get(bytes) + bytes + } + val reader = ProtoReader(content) + reader.getString(1, 1)?.let { + val eventData = reader.followPath(1, 2) ?: return@let + if (it == "volatile") { + handleVolatileEvent(eventData) + return@hook + } + + if (it == "presence") { + handlePresenceEvent(eventData) + return@hook + } + } + handleMessagingEvent(reader) + } + hook("nativeDestroy", HookStage.BEFORE) { it.setResult(null) } + } + + + findClass("com.snapchat.client.messaging.Session").hook("create", HookStage.BEFORE) { param -> + if (!NativeLib.initialized) { + context.log.warn("Can't register duplex message handler, native lib not initialized") + return@hook + } + + val method = param.method() as Method + val duplexClient = method.parameterTypes.indexOfFirst { it.name.endsWith("DuplexClient") }.let { + param.arg(it) + } + val dispatchQueue = method.parameterTypes.indexOfFirst { it.name.endsWith("DispatchQueue") }.let { + param.arg(it) + } + for (channel in arrayOf("pcs", "mcs")) { + duplexClient::class.java.methods.first { + it.name == "registerHandler" + }.invoke( + duplexClient, + channel, + messageHandlerClass.declaredConstructors.first().also { it.isAccessible = true }.newInstance(-1), + dispatchQueue + ) + } + } + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt index 35fae615c..e4f382a82 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt @@ -110,7 +110,7 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C if (it.startsWith("null")) return@hook } context.database.getConversationType(conversationId)?.takeIf { it == 1 }?.run { - lastFetchGroupConversationUUID = SnapUUID.fromString(conversationId) + lastFetchGroupConversationUUID = SnapUUID(conversationId) } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Notifications.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Notifications.kt index 40adbb6ae..58d23c340 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Notifications.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Notifications.kt @@ -193,7 +193,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN .toString() val myUser = context.database.myUserId.let { context.database.getFriendInfo(it) } ?: return@subscribe - context.messageSender.sendChatMessage(listOf(SnapUUID.fromString(conversationId)), input, onError = { + context.messageSender.sendChatMessage(listOf(SnapUUID(conversationId)), input, onError = { context.longToast("Failed to send message: $it") context.coroutineScope.launch(coroutineDispatcher) { appendNotificationText("Failed to send message: $it") diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/UnsaveableMessages.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/UnsaveableMessages.kt index 41bb55847..281c2acd1 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/UnsaveableMessages.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/UnsaveableMessages.kt @@ -27,7 +27,7 @@ class UnsaveableMessages : MessagingRuleFeature( if (contains(2)) { return@eachBuffer } - conversationIds.add(SnapUUID.fromBytes(getByteArray(1, 1, 1) ?: return@eachBuffer).toString()) + conversationIds.add(SnapUUID(getByteArray(1, 1, 1) ?: return@eachBuffer).toString()) } if (conversationIds.all { canUseRule(it) }) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt index d112a0473..5b785ee95 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt @@ -118,6 +118,7 @@ class FeatureManager( OperaViewerParamsOverride(), StealthModeIndicator(), DisablePermissionRequests(), + SessionEvents(), ) initializeFeatures() diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreMessaging.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreMessaging.kt index 8e186b6df..f738c389a 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreMessaging.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreMessaging.kt @@ -23,7 +23,7 @@ class CoreMessaging( fun isPresent() = conversationManager != null @JSFunction - fun newSnapUUID(uuid: String) = SnapUUID.fromString(uuid) + fun newSnapUUID(uuid: String) = SnapUUID(uuid) @JSFunction fun updateMessage( @@ -143,7 +143,7 @@ class CoreMessaging( message: String, result: (error: String?) -> Unit ) { - modContext.messageSender.sendChatMessage(listOf(SnapUUID.fromString(conversationId)), message, onSuccess = { result(null) }, onError = { result(it.toString()) }) + modContext.messageSender.sendChatMessage(listOf(SnapUUID(conversationId)), message, onSuccess = { result(null) }, onError = { result(it.toString()) }) } @JSFunction diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/AbstractWrapper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/AbstractWrapper.kt index 46f5ecc2f..a8e74a579 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/AbstractWrapper.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/AbstractWrapper.kt @@ -6,7 +6,7 @@ import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID import kotlin.reflect.KProperty abstract class AbstractWrapper( - protected var instance: Any? + protected open var instance: Any? ) { protected val uuidArrayListMapper: (Any?) -> ArrayList get() = { (it as ArrayList<*>).map { i -> SnapUUID(i) }.toCollection(ArrayList()) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/ConversationManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/ConversationManager.kt index 3f7e3b517..6449a454c 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/ConversationManager.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/ConversationManager.kt @@ -40,7 +40,7 @@ class ConversationManager( fun updateMessage(conversationId: String, messageId: Long, action: MessageUpdate, onResult: CallbackResult = {}) { updateMessageMethod.invoke( instanceNonNull(), - SnapUUID.fromString(conversationId).instanceNonNull(), + SnapUUID(conversationId).instanceNonNull(), messageId, context.classCache.messageUpdateEnum.enumConstants.first { it.toString() == action.toString() }, CallbackBuilder(getCallbackClass("Callback")) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/SnapUUID.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/SnapUUID.kt index 66a729ca5..a5f32a57c 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/SnapUUID.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/SnapUUID.kt @@ -6,41 +6,53 @@ import me.rhunk.snapenhance.core.wrapper.AbstractWrapper import java.nio.ByteBuffer import java.util.UUID -fun String.toSnapUUID() = SnapUUID.fromString(this) +fun String.toSnapUUID() = SnapUUID(this) +fun ByteArray.toSnapUUID() = SnapUUID(this) + +fun UUID.toBytes(): ByteArray = + ByteBuffer.allocate(16).let { + it.putLong(this.mostSignificantBits) + it.putLong(this.leastSignificantBits) + it.array() + } -class SnapUUID(obj: Any?) : AbstractWrapper(obj) { - private val uuidString by lazy { toUUID().toString() } +class SnapUUID( + private val obj: Any? +) : AbstractWrapper(obj) { + private val uuidBytes by lazy { + when { + obj is String -> { + UUID.fromString(obj).toBytes() + } + obj is ByteArray -> { + assert(obj.size == 16) + obj + } + obj is UUID -> obj.toBytes() + SnapEnhance.classCache.snapUUID.isInstance(obj) -> { + obj?.getObjectField("mId") as ByteArray + } + else -> ByteArray(16) + } + } - private val bytes: ByteArray get() = instanceNonNull().getObjectField("mId") as ByteArray + private val uuidString by lazy { ByteBuffer.wrap(uuidBytes).run { UUID(long, long) }.toString() } - private fun toUUID(): UUID { - val buffer = ByteBuffer.wrap(bytes) - return UUID(buffer.long, buffer.long) - } + override var instance: Any? + set(_) {} + get() = SnapEnhance.classCache.snapUUID.getConstructor(ByteArray::class.java).newInstance(uuidBytes) override fun toString(): String { return uuidString } - fun toBytes() = bytes + fun toBytes() = uuidBytes override fun equals(other: Any?): Boolean { - return other is SnapUUID && other.uuidString == uuidString + return other is SnapUUID && other.uuidBytes.contentEquals(this.uuidBytes) } - companion object { - fun fromString(uuid: String): SnapUUID { - return fromUUID(UUID.fromString(uuid)) - } - fun fromBytes(bytes: ByteArray): SnapUUID { - val constructor = SnapEnhance.classCache.snapUUID.getConstructor(ByteArray::class.java) - return SnapUUID(constructor.newInstance(bytes)) - } - fun fromUUID(uuid: UUID): SnapUUID { - val buffer = ByteBuffer.allocate(16) - buffer.putLong(uuid.mostSignificantBits) - buffer.putLong(uuid.leastSignificantBits) - return fromBytes(buffer.array()) - } + override fun hashCode(): Int { + return uuidBytes.contentHashCode() } } diff --git a/native/jni/src/hooks/duplex_hook.h b/native/jni/src/hooks/duplex_hook.h new file mode 100644 index 000000000..fecf8dccb --- /dev/null +++ b/native/jni/src/hooks/duplex_hook.h @@ -0,0 +1,26 @@ +#pragma once + + +namespace DuplexHook { + HOOK_DEF(jboolean, IsSameObject, JNIEnv * env, jobject obj1, jobject obj2) { + if (obj1 == nullptr || obj2 == nullptr) return IsSameObject_original(env, obj1, obj2); + + auto clazz = env->FindClass("java/lang/Class"); + if (!env->IsInstanceOf(obj1, clazz)) return IsSameObject_original(env, obj1, obj2); + + jstring obj1ClassName = (jstring) env->CallObjectMethod(obj1, env->GetMethodID(clazz, "getName", "()Ljava/lang/String;")); + const char* obj1ClassNameStr = env->GetStringUTFChars(obj1ClassName, nullptr); + + if (strstr(obj1ClassNameStr, "com.snapchat.client.duplex.MessageHandler") != 0) { + env->ReleaseStringUTFChars(obj1ClassName, obj1ClassNameStr); + return JNI_FALSE; + } + + env->ReleaseStringUTFChars(obj1ClassName, obj1ClassNameStr); + return IsSameObject_original(env, obj1, obj2); + } + + void init(JNIEnv* env) { + DobbyHook((void *)env->functions->IsSameObject, (void *)IsSameObject, (void **)&IsSameObject_original); + } +} \ No newline at end of file diff --git a/native/jni/src/library.cpp b/native/jni/src/library.cpp index a91bd5c8b..c320af5d9 100644 --- a/native/jni/src/library.cpp +++ b/native/jni/src/library.cpp @@ -9,6 +9,7 @@ #include "hooks/unary_call.h" #include "hooks/fstat_hook.h" #include "hooks/sqlite_mutex.h" +#include "hooks/duplex_hook.h" void JNICALL init(JNIEnv *env, jobject clazz) { LOGD("Initializing native"); @@ -29,6 +30,7 @@ void JNICALL init(JNIEnv *env, jobject clazz) { UnaryCallHook::init(env); FstatHook::init(); SqliteMutexHook::init(); + DuplexHook::init(env); LOGD("Native initialized"); }