feat: experimental chat encryption

This commit is contained in:
rhunk
2023-09-24 17:28:29 +02:00
parent b46139c3ad
commit b1f7304240
6 changed files with 174 additions and 2 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -54,6 +54,7 @@ class FeatureManager(private val context: ModContext) : Manager {
override fun init() {
register(
AESMessageEncryption::class,
ScopeSync::class,
Messaging::class,
MediaDownloader::class,