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:
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,