Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions app/src/main/java/moe/ono/creator/JsonViewerDialog.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package moe.ono.creator

import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.widget.Button
import android.widget.RadioGroup
import com.lxj.xpopup.XPopup
import com.lxj.xpopup.core.BasePopupView
import com.lxj.xpopup.core.BottomPopupView
import com.lxj.xpopup.util.XPopupUtils
import moe.ono.R
import moe.ono.hooks.base.util.Toasts
import moe.ono.ui.CommonContextWrapper
import moe.ono.ui.view.JsonViewer
import moe.ono.util.AppRuntimeHelper
import moe.ono.util.analytics.ActionReporter
import org.json.JSONObject

@SuppressLint("ResourceType")
class JsonViewerDialog(context: Context) : BottomPopupView(context) {
@SuppressLint("SetTextI18n", "ServiceCast")
override fun onCreate() {
super.onCreate()
Handler(Looper.getMainLooper()).postDelayed({
val jsonViewer = findViewById<JsonViewer>(R.id.rv_json)
val btnCopy = findViewById<Button>(R.id.btn_copy)
val rgType = findViewById<RadioGroup>(R.id.rg_type)
var copyText: String

jsonViewer.setJson(content)

copyText = jsonViewer.getJSONString()

rgType.visibility = GONE

btnCopy.setOnClickListener {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("Copied Text", copyText))
Toasts.info(context, "已复制")
}


}, 100)
}

private fun formatMsgRecord(input: String): String {
var indentLevel = 0
val indentStep = " "
val result = StringBuilder()

input.forEach { char ->
when (char) {
'{' -> {
result.append(char)
result.append("\n")
indentLevel++
result.append(indentStep.repeat(indentLevel))
}
'}' -> {
result.append("\n")
indentLevel = if (indentLevel > 0) indentLevel - 1 else 0
result.append(indentStep.repeat(indentLevel))
result.append(char)
}
',' -> {
result.append(char)
result.append("\n")
result.append(indentStep.repeat(indentLevel))
}
else -> {
result.append(char)
}
}
}
return result.toString()
}




override fun getImplLayoutId(): Int {
return R.layout.layout_qq_message_fetcher_result
}

companion object {
private var popupView: BasePopupView? = null
private var content: JSONObject? = null

fun createView(context: Context, content: JSONObject) {
val fixContext = CommonContextWrapper.createAppCompatContext(context)
val newPop = XPopup.Builder(fixContext).moveUpToKeyboard(true).isDestroyOnDismiss(true)
newPop.maxHeight((XPopupUtils.getScreenHeight(context) * .90f).toInt())
newPop.popupHeight((XPopupUtils.getScreenHeight(context) * .90f).toInt())
Companion.content = content


ActionReporter.reportVisitor(
AppRuntimeHelper.getAccount(),
"CreateView-JsonViewerDialog"
)

popupView = newPop.asCustom(JsonViewerDialog(fixContext))
popupView?.show()
}
}
}



77 changes: 77 additions & 0 deletions app/src/main/java/moe/ono/hooks/base/api/QQMsgRespHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import com.tencent.qphone.base.remote.FromServiceMsg
import com.tencent.qphone.base.remote.ToServiceMsg
import com.tencent.qqnt.kernel.nativeinterface.PushExtraInfo
import de.robv.android.xposed.XposedHelpers.callMethod
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.readBytes
import kotlinx.serialization.json.*
import moe.ono.R
import moe.ono.bridge.ntapi.RelationNTUinAndUidApi.getUinFromUid
Expand All @@ -12,19 +14,26 @@ import moe.ono.config.CacheConfig
import moe.ono.config.CacheConfig.getIQQntWrapperSessionInstance
import moe.ono.config.CacheConfig.setRKeyGroup
import moe.ono.config.CacheConfig.setRKeyPrivate
import moe.ono.config.ConfigManager
import moe.ono.config.ONOConf
import moe.ono.constants.Constants
import moe.ono.creator.JsonViewerDialog
import moe.ono.creator.PacketHelperDialog
import moe.ono.creator.QQMessageFetcherResultDialog
import moe.ono.hooks._base.ApiHookItem
import moe.ono.hooks._core.annotation.HookItem
import moe.ono.hooks._core.factory.HookItemFactory.getItem
import moe.ono.hooks.base.util.Toasts
import moe.ono.hooks.item.chat.MessageEncryptor
import moe.ono.hooks.item.developer.QQPacketHelperC2CDisplayFixer
import moe.ono.hooks.item.entertainment.ModifyTextMessage
import moe.ono.hooks.protocol.buildMessage
import moe.ono.reflex.XField
import moe.ono.reflex.XMethod
import moe.ono.service.QQInterfaces
import moe.ono.service.inject.ServletPool.injectServlet
import moe.ono.ui.CommonContextWrapper
import moe.ono.util.AesUtils
import moe.ono.util.ContextUtils
import moe.ono.util.FunProtoData
import moe.ono.util.FunProtoData.getUnpPackage
Expand Down Expand Up @@ -265,6 +274,74 @@ class QQMsgRespHandler : ApiHookItem() {
Logger.d("on MessageSvc.PbGetGroupMsg")

Logger.d("obj: " + obj.toString(4))

if (MessageEncryptor.decryptMsg) {
MessageEncryptor.decryptMsg = false
val key = "${MessageEncryptor.peerUid}:${MessageEncryptor.msgSeq}:${MessageEncryptor.senderUin}"
MessageEncryptor.peerUid = ""
MessageEncryptor.senderUin = ""
MessageEncryptor.msgSeq = ""

val encryptKey = ConfigManager.dGetString(
Constants.PrekCfgXXX + getItem(MessageEncryptor::class.java).path,
"ono"
)
if (encryptKey.isBlank()) {
return@hookBefore
}

val aesKey = AesUtils.md5(encryptKey)
val encryptMsg = obj.optJSONObject("6")?.optJSONObject("3")?.optJSONObject("1")
?.optJSONArray("2")?.optJSONObject(2)?.optJSONObject("1")?.optJSONObject("12")
?.optString("1")

if (encryptMsg == null) {
Toasts.error(ContextUtils.getCurrentActivity(), "非加密消息")
return@hookBefore
}

val hexStr = encryptMsg.replace("hex->", "")
val byteArray = MessageEncryptor.hexToBytes(hexStr)
val encryptBuffer = ByteReadPacket(byteArray)

if (0x114514 != encryptBuffer.readInt()) {
Toasts.error(ContextUtils.getCurrentActivity(), "非加密消息")
return@hookBefore
}

if (encryptKey.hashCode() != encryptBuffer.readInt()) {
Toasts.error(ContextUtils.getCurrentActivity(), "密钥不匹配")
return@hookBefore
}

val decryptMsg = AesUtils.aesDecrypt(encryptBuffer.readBytes(), aesKey)
val decryptData = FunProtoData()
decryptData.fromBytes(getUnpPackage(decryptMsg))

val decryptMsgData = decryptData.toJSON().optJSONObject("1")
?.optJSONArray("2")
?.optJSONObject(2)
?.optJSONObject("1")
?.optString("1")

if (decryptMsgData != null) {
ModifyTextMessage.modifyMap.put(key, decryptMsgData)
Toasts.success(ContextUtils.getCurrentActivity(), "解密成功重新进入本界面生效")
return@hookBefore
}

SyncUtils.runOnUiThread {
JsonViewerDialog.createView(
CommonContextWrapper.createAppCompatContext(ContextUtils.getCurrentActivity()),
decryptData.toJSON()
)

Toasts.success(ContextUtils.getCurrentActivity(), "不支持的消息类型, 已打开原 PB")
}

return@hookBefore
}

SyncUtils.runOnUiThread { QQMessageFetcherResultDialog.createView(
CommonContextWrapper.createAppCompatContext(ContextUtils.getCurrentActivity()), obj) }

Expand Down
9 changes: 9 additions & 0 deletions app/src/main/java/moe/ono/hooks/base/api/QQOnLoadWebView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import moe.ono.hooks._core.annotation.HookItem
import moe.ono.hooks.clazz
import moe.ono.hooks.item.chat.QQBubbleRedirect.Companion.injectJavaScriptLogic
import moe.ono.hooks.item.chat.QQBubbleRedirect.Companion.injectWebViewForBubble
import moe.ono.hooks.item.developer.InjectVConsole
import moe.ono.util.Logger
import moe.ono.util.SyncUtils
import moe.ono.util.Utils
Expand Down Expand Up @@ -35,6 +36,12 @@ class QQOnLoadWebView : ApiHookItem() {
Logger.d("QQOnLoadWebView -> onPageFinished")

try {
InjectVConsole.injectJavaScriptLogic(
x5WebView = it.args[0],
x5WebViewClass = CacheConfig.getX5WebViewClass(),
x5ValueCallbackClass = CacheConfig.getX5ValueCallbackClass()
)

injectJavaScriptLogic(
x5WebView = it.args[0],
itemId = CacheConfig.getItemID(),
Expand All @@ -51,6 +58,8 @@ class QQOnLoadWebView : ApiHookItem() {
val url = it.args[0] as String
Logger.d("loadUrl: $url")

InjectVConsole.injectWebViewForVConsole(it.thisObject)

if (url.startsWith("https://zb.vip.qq.com/mall/item-detail?appid=2&")) {
val webView = it.thisObject
val pattern = Pattern.compile("itemid=(\\d+)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package moe.ono.hooks.dispatcher
import de.robv.android.xposed.XC_MethodHook
import moe.ono.hooks._base.ApiHookItem
import moe.ono.hooks._core.annotation.HookItem
import moe.ono.hooks.item.chat.MessageEncryptor
import moe.ono.hooks.item.chat.StickerPanelEntry
import moe.ono.hooks.item.developer.QQMessageFetcher
import moe.ono.hooks.item.entertainment.ModifyTextMessage
Expand All @@ -18,7 +19,8 @@ class MenuBuilderHook : ApiHookItem() {
QQMessageTracker(),
QQMessageFetcher(),
ModifyTextMessage(),
RespondFace()
RespondFace(),
MessageEncryptor(),
)

override fun entry(classLoader: ClassLoader) {
Expand Down
88 changes: 86 additions & 2 deletions app/src/main/java/moe/ono/hooks/item/chat/MessageEncryptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,53 @@ import kotlinx.io.core.writeFully
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.encodeToByteArray
import kotlinx.serialization.protobuf.ProtoBuf
import moe.ono.R
import moe.ono.bridge.ntapi.ChatTypeConstants.C2C
import moe.ono.bridge.ntapi.ChatTypeConstants.GROUP
import moe.ono.bridge.ntapi.MsgServiceHelper
import moe.ono.config.ConfigManager
import moe.ono.config.DailySaying
import moe.ono.ext.getUnknownObject
import moe.ono.ext.getUnknownObjects
import moe.ono.ext.toInnerValuesString
import moe.ono.hooks._base.BaseClickableFunctionHookItem
import moe.ono.hooks._core.annotation.HookItem
import moe.ono.hooks._core.factory.HookItemFactory.getItem
import moe.ono.hooks.base.util.Toasts
import moe.ono.hooks.dispatcher.OnMenuBuilder
import moe.ono.hooks.item.developer.QQHookCodec
import moe.ono.hooks.item.developer.QQMessageFetcher
import moe.ono.hooks.protocol.entries.QQSsoSecureInfo
import moe.ono.hooks.protocol.entries.TextMsgExtPbResvAttr
import moe.ono.hostInfo
import moe.ono.loader.hookapi.IHijacker
import moe.ono.reflex.Reflex
import moe.ono.util.AesUtils.aesEncrypt
import moe.ono.util.AesUtils.md5
import moe.ono.util.AppRuntimeHelper
import moe.ono.util.ContextUtils
import moe.ono.util.CustomMenu
import moe.ono.util.Logger
import moe.ono.util.Session
import moe.ono.util.SyncUtils

@SuppressLint("DiscouragedApi")
@HookItem(path = "聊天与消息/群聊加密消息", description = "发送的消息将加密抄送,仅针对群聊\n* 开启需重启\n* 禁止用于非法用途,违者后果自负\n* 需开启 '开发者选项/注入 CodecWarpper'")
class MessageEncryptor : BaseClickableFunctionHookItem() {
@HookItem(path = "聊天与消息/群聊加密消息", description = "发送的消息将加密抄送,仅针对群聊\n* 长按文本消息可手动解密\n* 手动解密功能依赖 娱乐功能/修改文本消息\n* 开启需重启\n* 禁止用于非法用途,违者后果自负\n* 需开启 '开发者选项/注入 CodecWarpper'")
class MessageEncryptor : BaseClickableFunctionHookItem(), OnMenuBuilder {
companion object {
var decryptMsg = false
var msgSeq = ""
var peerUid = ""
var senderUin = ""

fun hexToBytes(hex: String): ByteArray {
require(hex.length % 2 == 0) { "Invalid hex string length." }
return ByteArray(hex.length / 2) { i ->
hex.substring(i * 2, i * 2 + 2).toInt(16).toByte()
}
}
}

override fun entry(classLoader: ClassLoader) {
QQHookCodec.hijackers.add(object: IHijacker {
override fun onHandle(
Expand Down Expand Up @@ -168,6 +194,64 @@ class MessageEncryptor : BaseClickableFunctionHookItem() {
return newMsgBody.build()
}

override val targetTypes = arrayOf(
"com.tencent.mobileqq.aio.msglist.holder.component.text.AIOTextContentComponent",
)

override fun onGetMenu(aioMsgItem: Any, targetType: String, param: XC_MethodHook.MethodHookParam) {
if (!getItem(this.javaClass).isEnabled) {
return
}

val item: Any = CustomMenu.createItemIconNt(
aioMsgItem,
"解密",
R.drawable.ic_telegram,
R.id.item_message_encryptor
) {
try {
val msgID = Reflex.invokeVirtual(aioMsgItem, "getMsgId") as Long
val msgIDs = java.util.ArrayList<Long>()
msgIDs.add(msgID)
AppRuntimeHelper.getAppRuntime()
?.let {
MsgServiceHelper.getKernelMsgService(
it
)
}?.getMsgsByMsgId(
Session.getContact(),
msgIDs
) { _, _, msgList ->
SyncUtils.runOnUiThread {
for (msgRecord in msgList) {
// if (msgRecord.msgType != 2 || msgRecord.subMsgType != 1) {
// Toasts.error(ContextUtils.getCurrentActivity(), "仅支持文本消息")
// return@runOnUiThread
// }

when (msgRecord.chatType) {
C2C -> {
Toasts.error(ContextUtils.getCurrentActivity(), "仅支持群聊消息")
break
}
GROUP -> {
decryptMsg = true
msgSeq = msgRecord.msgSeq.toString()
peerUid = msgRecord.peerUid
senderUin = msgRecord.senderUin.toString()
QQMessageFetcher.pullGroupMsg(msgRecord)
}
}
}
}
}
} catch (e: Exception) {
Logger.e("QQPullMsgEntry.msgLongClick", e)
}
Unit
}
param.result = listOf(item) + param.result as List<*>
}

override fun alwaysRun(): Boolean {
return true
Expand Down
Loading