commit b1f73042404af2205106789965bb59888a3871ce
parent b46139c3ad8d5aac95689171a501e45f13d94a47
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Sun, 24 Sep 2023 17:28:29 +0200

feat: experimental chat encryption

Diffstat:
Mcore/src/main/assets/lang/en_US.json | 10+++++++++-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Experimental.kt | 1+
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Rules.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt | 1+
Acore/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/AESMessageEncryption.kt | 162+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt | 1+
6 files changed, 175 insertions(+), 2 deletions(-)

diff --git a/core/src/main/assets/lang/en_US.json b/core/src/main/assets/lang/en_US.json @@ -98,6 +98,9 @@ "hide_chat_feed": { "name": "Hide from Chat feed" }, + "aes_encryption": { + "name": "Use AES Encryption" + }, "pin_conversation": { "name": "Pin Conversation" } @@ -516,6 +519,10 @@ "name": "No Friend Score Delay", "description": "Removes the delay when viewing a Friends Score" }, + "use_message_aes_encryption": { + "name": "Use AES Encryption", + "description": "Encrypts your messages using AES\nMessages are only readable by other SnapEnhance users" + }, "add_friend_source_spoof": { "name": "Add Friend Source Spoof", "description": "Spoofs the source of a Friend Request" @@ -557,7 +564,8 @@ "auto_download": "\u2B07\uFE0F Auto Download", "auto_save": "\uD83D\uDCAC Auto Save Messages", "stealth": "\uD83D\uDC7B Stealth Mode", - "conversation_info": "\uD83D\uDC64 Conversation Info" + "conversation_info": "\uD83D\uDC64 Conversation Info", + "aes_encryption": "\uD83D\uDD12 Use AES Encryption" }, "path_format": { "create_author_folder": "Create folder for each author", 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 @@ -12,6 +12,7 @@ class Experimental : ConfigContainer() { val meoPasscodeBypass = boolean("meo_passcode_bypass") val unlimitedMultiSnap = boolean("unlimited_multi_snap") { addNotices(FeatureNotice.BAN_RISK)} val noFriendScoreDelay = boolean("no_friend_score_delay") + val useMessageAESEncryption = boolean("use_message_aes_encryption") 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/config/impl/Rules.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Rules.kt @@ -14,7 +14,7 @@ class Rules : ConfigContainer() { } init { - MessagingRuleType.values().filter { it.listMode }.forEach { ruleType -> + MessagingRuleType.values().toList().filter { it.listMode }.forEach { ruleType -> rules[ruleType] = unique(ruleType.key,"whitelist", "blacklist") { customTranslationPath = "rules.properties.${ruleType.key}" customOptionTranslationPath = "rules.modes" diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt @@ -35,6 +35,7 @@ enum class MessagingRuleType( STEALTH("stealth", true), AUTO_SAVE("auto_save", true), HIDE_CHAT_FEED("hide_chat_feed", false, showInFriendMenu = false), + AES_ENCRYPTION("aes_encryption", false), PIN_CONVERSATION("pin_conversation", false, showInFriendMenu = false); fun translateOptionKey(optionKey: String): String { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/AESMessageEncryption.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/AESMessageEncryption.kt @@ -0,0 +1,161 @@ +package me.rhunk.snapenhance.features.impl.experiments + +import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent +import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent +import me.rhunk.snapenhance.core.messaging.MessagingRuleType +import me.rhunk.snapenhance.core.messaging.RuleState +import me.rhunk.snapenhance.core.util.protobuf.ProtoEditor +import me.rhunk.snapenhance.core.util.protobuf.ProtoReader +import me.rhunk.snapenhance.core.util.protobuf.ProtoWriter +import me.rhunk.snapenhance.data.ContentType +import me.rhunk.snapenhance.data.wrapper.impl.Message +import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID +import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.features.MessagingRuleFeature +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.hookConstructor +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec +import kotlin.random.Random + +/* + To prevent snapchat from using fidelius, snaps are spoofed to external media and chats into status before it's sent to the native. + When the CreateContentMessage request is sent to the server, the content is encrypted + */ + +//TODO: RSA encryption +class AESMessageEncryption : MessagingRuleFeature( + "AESMessageEncryption", + MessagingRuleType.AES_ENCRYPTION, + loadParams = FeatureLoadParams.INIT_ASYNC or FeatureLoadParams.INIT_SYNC +) { + private val key = intArrayOf( + 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, + 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, + 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, + 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f + ).map { it.toByte() }.toByteArray() + + private val isEnabled get() = context.config.experimental.useMessageAESEncryption.get() + + private fun useCipher(input: ByteArray, iv: ByteArray, decrypt: Boolean = false): ByteArray { + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init(if (decrypt) Cipher.DECRYPT_MODE else Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) + return cipher.doFinal(input) + } + + private fun fixContentType(contentType: ContentType, message: ProtoReader): ContentType { + return when { + contentType == ContentType.EXTERNAL_MEDIA && message.containsPath(11) -> { + ContentType.SNAP + } + contentType == ContentType.SHARE && message.containsPath(2) -> { + ContentType.CHAT + } + else -> contentType + } + } + + override fun asyncInit() { + // trick to disable fidelius encryption + context.event.subscribe(SendMessageWithContentEvent::class, { isEnabled }) { 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 (messageContent.contentType == ContentType.SNAP) { + messageContent.contentType = ContentType.EXTERNAL_MEDIA + } + + if (messageContent.contentType == ContentType.CHAT) { + messageContent.contentType = ContentType.SHARE + } + } + } + + override fun init() { + context.event.subscribe(UnaryCallEvent::class, { isEnabled }) { event -> + if (event.uri != "/messagingcoreservice.MessagingCoreService/CreateContentMessage") return@subscribe + val protoReader = ProtoReader(event.buffer) + + val conversationIds = mutableListOf<SnapUUID>() + protoReader.eachBuffer(3) { + conversationIds.add(SnapUUID.fromBytes(getByteArray(1, 1, 1) ?: return@eachBuffer)) + } + + if (conversationIds.size != 1) return@subscribe + + if (!getState(conversationIds.first().toString())) return@subscribe + + val generatedIv = ByteArray(16).also { Random.nextBytes(it) } + + event.buffer = ProtoEditor(event.buffer).apply { + protoReader.followPath(4) { + val contentType = fixContentType(ContentType.fromId(getVarInt(2)?.toInt() ?: -1), followPath(4) ?: return@followPath) + + runCatching { + val encryptedMessage = useCipher(getByteArray(4) ?: return@followPath, generatedIv, false) + edit(4) { + //set message content type + remove(2) + addVarInt(2, contentType.id) + + //set encrypted content + remove(4) + add(4) { + from(2) { + from(1) { + addBuffer(1, encryptedMessage) + addBuffer(2, generatedIv) + } + addVarInt(2, 1) + } + } + } + }.onFailure { + event.canceled = true + context.log.error("Failed to encrypt message", it) + context.longToast("Failed to encrypt message! Check logcat for more details.") + } + } + }.toByteArray() + } + + context.classCache.message.hookConstructor(HookStage.AFTER, { isEnabled }) { param -> + val message = Message(param.thisObject()) + val reader = ProtoReader(message.messageContent.content) + + // fix content type + message.messageContent.contentType?.also { + message.messageContent.contentType = fixContentType(it, reader) + } + + reader.followPath(2) { + if (getVarInt(2) != 1L) return@followPath + + runCatching { + followPath(1) path@{ + val encryptedMessage = getByteArray(1) ?: return@path + val iv = getByteArray(2) ?: return@path + + val decryptedMessage = useCipher(encryptedMessage, iv, decrypt = true) + message.messageContent.content = decryptedMessage + } + }.onFailure { + context.log.error("Failed to decrypt message id: ${message.messageDescriptor.messageId}", it) + message.messageContent.contentType = ContentType.CHAT + message.messageContent.content = ProtoWriter().apply { + from(2) { + addString(1, "Failed to decrypt message, id=${message.messageDescriptor.messageId}. Check logcat for more details.") + } + }.toByteArray() + } + } + } + } + + override fun getRuleState() = RuleState.WHITELIST +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt @@ -54,6 +54,7 @@ class FeatureManager(private val context: ModContext) : Manager { override fun init() { register( + AESMessageEncryption::class, ScopeSync::class, Messaging::class, MediaDownloader::class,