Support key refresh (#901)

* Add skey tests

* t106

* Fix some protocol errors. skey refresh

* login extra

* Support Key refresh, close #833

* apiDump

* Merge with dev
This commit is contained in:
Him188 2021-01-26 19:32:27 +08:00 committed by GitHub
parent cc87f6833f
commit 7a6e930fc7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 552 additions and 284 deletions

View File

@ -179,6 +179,7 @@ public abstract interface class net/mamoe/mirai/LowLevelApiAccessor {
public abstract fun recallGroupMessageRaw (Lnet/mamoe/mirai/Bot;J[I[ILkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun recallGroupTempMessageRaw (Lnet/mamoe/mirai/Bot;JJ[I[II)Z
public abstract fun recallGroupTempMessageRaw (Lnet/mamoe/mirai/Bot;JJ[I[IILkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun refreshKeys (Lnet/mamoe/mirai/Bot;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun sendGroupAnnouncement (Lnet/mamoe/mirai/Bot;JLnet/mamoe/mirai/data/GroupAnnouncement;)Ljava/lang/String;
public abstract fun sendGroupAnnouncement (Lnet/mamoe/mirai/Bot;JLnet/mamoe/mirai/data/GroupAnnouncement;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public synthetic fun solveBotInvitedJoinGroupRequestEvent (Lnet/mamoe/mirai/Bot;JJJZ)Lkotlin/Unit;
@ -5409,6 +5410,7 @@ public final class net/mamoe/mirai/utils/DeviceInfo {
public final fun getDevice ()[B
public final fun getDisplay ()[B
public final fun getFingerprint ()[B
public final fun getGuid ()[B
public final fun getImei ()Ljava/lang/String;
public final fun getImsiMd5 ()[B
public final fun getIpAddress ()[B

View File

@ -37,6 +37,16 @@ public annotation class LowLevelApi
*/
@LowLevelApi
public interface LowLevelApiAccessor {
/**
* 主动刷新 keys, SKey, PSKey .
*
* 通常 mirai 会自动刷新, 不需要手动刷新.
*
* @since 2.2
*/
@MiraiExperimentalApi
public suspend fun refreshKeys(bot: Bot)
/**
* 构造一个 [Friend] 对象. 它持有对 [Bot] 的弱引用([WeakRef]).
*

View File

@ -11,6 +11,7 @@ package net.mamoe.mirai.utils
import kotlinx.io.core.toByteArray
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.json.Json
import kotlinx.serialization.protobuf.ProtoBuf
import kotlinx.serialization.protobuf.ProtoNumber
@ -42,6 +43,10 @@ public class DeviceInfo(
public val androidId: ByteArray get() = display
public val ipAddress: ByteArray get() = byteArrayOf(192.toByte(), 168.toByte(), 1, 123)
@Transient
@MiraiInternalApi
public val guid: ByteArray = generateGuid(androidId, macAddress)
@Serializable
public class Version(
public val incremental: ByteArray = "5891938".toByteArray(),
@ -127,6 +132,14 @@ public fun DeviceInfo.generateDeviceInfoData(): ByteArray {
)
}
/**
* Defaults "%4;7t>;28<fc.5*6".toByteArray()
*/
@Suppress("RemoveRedundantQualifierName") // bug
private fun generateGuid(androidId: ByteArray, macAddress: ByteArray): ByteArray =
(androidId + macAddress).md5()
/*
fun DeviceInfo.toOidb0x769DeviceInfo() : Oidb0x769.DeviceInfo = Oidb0x769.DeviceInfo(
brand = brand.encodeToString(),

View File

@ -142,6 +142,10 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
}
}
override suspend fun refreshKeys(bot: Bot) {
bot.asQQAndroidBot().network.refreshKeys()
}
override suspend fun rejectNewFriendRequest(event: NewFriendRequestEvent, blackList: Boolean) {
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
check(event.responded.compareAndSet(false, true)) {

View File

@ -23,8 +23,11 @@ import net.mamoe.mirai.internal.contact.StrangerInfoImpl
import net.mamoe.mirai.internal.contact.checkIsGroupImpl
import net.mamoe.mirai.internal.contact.uin
import net.mamoe.mirai.internal.message.*
import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.internal.network.QQAndroidBotNetworkHandler
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType
import net.mamoe.mirai.internal.network.protocol.packet.chat.*
import net.mamoe.mirai.internal.network.useNextServers
import net.mamoe.mirai.message.data.*
@ -118,6 +121,14 @@ internal class QQAndroidBot constructor(
}
suspend inline fun <E : Packet> OutgoingPacketWithRespType<E>.sendAndExpect(
timeoutMillis: Long = 5000,
retry: Int = 2
): E = network.run { sendAndExpect(timeoutMillis, retry) }
suspend inline fun <E : Packet> OutgoingPacket.sendAndExpect(timeoutMillis: Long = 5000, retry: Int = 2): E =
network.run { sendAndExpect(timeoutMillis, retry) }
/**
* 获取 获取群公告 所需的 bkn 参数
* */

View File

@ -0,0 +1,14 @@
/*
* Copyright 2019-2021 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.internal.network
internal object DebuggingProperties {
const val SHOW_TLV_MAP_ON_LOGIN_SUCCESS = false
}

View File

@ -44,6 +44,7 @@ import net.mamoe.mirai.internal.network.protocol.packet.login.ConfigPushSvc
import net.mamoe.mirai.internal.network.protocol.packet.login.Heartbeat
import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.WtLogin15
import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.WtLogin2
import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.WtLogin20
import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.WtLogin9
@ -55,6 +56,8 @@ import java.util.concurrent.ConcurrentLinkedQueue
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.coroutines.CoroutineContext
import kotlin.time.minutes
import kotlin.time.seconds
@Suppress("MemberVisibilityCanBePrivate")
internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bot: QQAndroidBot) : BotNetworkHandler() {
@ -252,8 +255,7 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
is WtLogin.Login.LoginPacketResponse.DeviceLockLogin -> {
response = WtLogin20(
bot.client,
response.t402
bot.client
).sendAndExpect()
continue@mainloop
}
@ -279,6 +281,25 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
bot.otherClientsLock.withLock {
updateOtherClientsList()
}
launch {
while (isActive) {
bot.client.wLoginSigInfo.sKey.run {
val delay = (expireTime - creationTime).seconds - 5.minutes
logger.info { "Scheduled key refresh in ${delay.toHumanReadableString()}." }
delay(delay)
}
runCatching {
refreshKeys()
}.onFailure {
logger.error("Failed to refresh key.", it)
}
}
}
}
suspend fun refreshKeys() {
WtLogin15(bot.client).sendAndExpect()
}
private suspend fun registerClientOnline() {
@ -778,7 +799,8 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
return retryCatchingExceptions(
retry + 1,
except = CancellationException::class.cast() // CancellationException means network closed so don't retry
except = CancellationException::class.cast() // explicit cast due for stupid IDE.
// CancellationException means network closed so don't retry
) {
withPacketListener(commandName, sequenceId) { listener ->
return withTimeout(timeoutMillis) { // may throw CancellationException

View File

@ -15,8 +15,10 @@ import kotlinx.atomicfu.AtomicBoolean
import kotlinx.atomicfu.AtomicInt
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CompletableDeferred
import kotlinx.io.core.BytePacketBuilder
import kotlinx.io.core.String
import kotlinx.io.core.toByteArray
import kotlinx.io.core.writeFully
import net.mamoe.mirai.data.OnlineStatus
import net.mamoe.mirai.internal.BotAccount
import net.mamoe.mirai.internal.QQAndroidBot
@ -24,6 +26,7 @@ import net.mamoe.mirai.internal.network.protocol.SyncingCacheList
import net.mamoe.mirai.internal.network.protocol.data.jce.FileStoragePushFSSvcList
import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY
import net.mamoe.mirai.internal.network.protocol.packet.Tlv
import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.get_mpasswd
import net.mamoe.mirai.internal.utils.MiraiProtocolInternal
import net.mamoe.mirai.internal.utils.NetworkType
import net.mamoe.mirai.internal.utils.crypto.ECDH
@ -31,14 +34,8 @@ import net.mamoe.mirai.utils.*
import java.util.concurrent.CopyOnWriteArraySet
import kotlin.random.Random
internal val DeviceInfo.guid: ByteArray get() = generateGuid(androidId, macAddress)
/**
* Defaults "%4;7t>;28<fc.5*6".toByteArray()
*/
@Suppress("RemoveRedundantQualifierName") // bug
private fun generateGuid(androidId: ByteArray, macAddress: ByteArray): ByteArray =
(androidId + macAddress).md5()
internal val DEFAULT_GUID = "%4;7t>;28<fc.5*6".toByteArray()
/**
* 生成长度为 [length], 元素为随机 `0..255` [ByteArray]
@ -89,8 +86,9 @@ internal open class QQAndroidClient(
val bot: QQAndroidBot by bot.unsafeWeakRef()
internal var tgtgtKey: ByteArray = generateTgtgtKey(device.guid)
internal var randomKey: ByteArray = getRandomByteArray(16)
internal var tgtgtKey: ByteArray = (account.passwordMd5 + ByteArray(4) + uin.toInt().toByteArray()).md5()
internal val randomKey: ByteArray = getRandomByteArray(16)
internal val miscBitMap: Int = protocol.miscBitMap // 184024956 // 也可能是 150470524 ?
internal val mainSigMap: Int = protocol.mainSigMap
@ -142,7 +140,7 @@ internal open class QQAndroidClient(
internal fun nextHighwayDataTransSequenceIdForApplyUp(): Int = highwayDataTransSequenceIdForApplyUp.getAndAdd(2)
val appClientVersion: Int = 0
val ssoVersion: Int = 13
val ssoVersion: Int = 15
var networkType: NetworkType = NetworkType.WIFI
@ -260,21 +258,27 @@ internal open class QQAndroidClient(
/**
* t537
*/
var loginExtraData: LoginExtraData? = null
var loginExtraData: MutableSet<LoginExtraData> = CopyOnWriteArraySet()
lateinit var wFastLoginInfo: WFastLoginInfo
var reserveUinInfo: ReserveUinInfo? = null
lateinit var wLoginSigInfo: WLoginSigInfo
val wLoginSigInfoInitialized get() = ::wLoginSigInfo.isInitialized
/**
* from tlvMap119
*/
var tlv16a: ByteArray? = null
var G: ByteArray = device.guid // sigInfo[2]
var dpwd: ByteArray = get_mpasswd().toByteArray()
var randSeed: ByteArray = EMPTY_BYTE_ARRAY // t403
var tlv113: ByteArray? = null
var t402: ByteArray? = null
lateinit var qrPushSig: ByteArray
lateinit var mainDisplayName: ByteArray
var transportSequenceId = 1
var lastT106Full: ByteArray? = null
lateinit var t104: ByteArray
/**
@ -284,6 +288,16 @@ internal open class QQAndroidClient(
val bdhSession: CompletableDeferred<BdhSession> = CompletableDeferred()
}
internal fun BytePacketBuilder.writeLoginExtraData(loginExtraData: LoginExtraData) {
loginExtraData.run {
writeLong(uin)
writeByte(ip.size.toByte())
writeFully(ip)
writeInt(time)
writeInt(version)
}
}
internal class BdhSession(
val sigSession: ByteArray,
val sessionKey: ByteArray,

View File

@ -45,17 +45,13 @@ internal class WFastLoginInfo(
internal class WLoginSimpleInfo(
val uin: Long, // uin
val face: Int, // ubyte actually
val age: Int, // ubyte
val gender: Int, // ubyte
val nick: String, // ubyte lv string
val imgType: ByteArray,
val imgFormat: ByteArray,
val imgUrl: ByteArray,
val mainDisplayName: ByteArray
) {
override fun toString(): String {
return "WLoginSimpleInfo(uin=$uin, face=$face, age=$age, gender=$gender, nick='$nick', imgType=${imgType.toUHexString()}, imgFormat=${imgFormat.toUHexString()}, imgUrl=${imgUrl.toUHexString()}, mainDisplayName=${mainDisplayName.toUHexString()})"
return "WLoginSimpleInfo(uin=$uin, imgType=${imgType.toUHexString()}, imgFormat=${imgFormat.toUHexString()}, imgUrl=${imgUrl.toUHexString()}, mainDisplayName=${mainDisplayName.toUHexString()})"
}
}
@ -77,51 +73,48 @@ internal class WLoginSigInfo(
* WARNING, please check [QQAndroidClient.tlv16a]
*/
val noPicSig: ByteArray?, // sigInfo[1]
val G: ByteArray, // sigInfo[2]
val dpwd: ByteArray,
val randSeed: ByteArray,
val simpleInfo: WLoginSimpleInfo,
val appPri: Long,
val a2ExpiryTime: Long,
val loginBitmap: Long,
val tgt: ByteArray,
val a2CreationTime: Long,
val tgtKey: ByteArray,
val userStSig: UserStSig,
var appPri: Long,
var a2ExpiryTime: Long,
var loginBitmap: Long,
var tgt: ByteArray,
var a2CreationTime: Long,
var tgtKey: ByteArray,
var userStSig: UserStSig,
/**
* TransEmpPacket 加密使用
*/
val userStKey: ByteArray,
val userStWebSig: UserStWebSig,
val userA5: UserA5,
val userA8: UserA8,
val lsKey: LSKey,
val sKey: SKey,
val userSig64: UserSig64,
val openId: ByteArray,
val openKey: OpenKey,
val vKey: VKey,
val accessToken: AccessToken,
val d2: D2,
val d2Key: ByteArray,
val sid: Sid,
val aqSig: AqSig,
val psKeyMap: PSKeyMap,
val pt4TokenMap: Pt4TokenMap,
val superKey: ByteArray,
val payToken: ByteArray,
val pf: ByteArray,
val pfKey: ByteArray,
val da2: ByteArray,
// val pt4Token: ByteArray,
val wtSessionTicket: WtSessionTicket,
val wtSessionTicketKey: ByteArray,
val deviceToken: ByteArray
var userStKey: ByteArray,
var userStWebSig: UserStWebSig,
var userA5: UserA5,
var userA8: UserA8,
var lsKey: LSKey,
var sKey: SKey,
var userSig64: UserSig64,
var openId: ByteArray,
var openKey: OpenKey,
var vKey: VKey,
var accessToken: AccessToken,
var d2: D2,
var d2Key: ByteArray,
var sid: Sid,
var aqSig: AqSig,
var psKeyMap: PSKeyMap,
var pt4TokenMap: Pt4TokenMap,
var superKey: ByteArray,
var payToken: ByteArray,
var pf: ByteArray,
var pfKey: ByteArray,
var da2: ByteArray,
// val pt4Token: ByteArray,
var wtSessionTicket: WtSessionTicket,
var wtSessionTicketKey: ByteArray,
var deviceToken: ByteArray
) {
override fun toString(): String {
return "WLoginSigInfo(uin=$uin, encryptA1=${encryptA1?.toUHexString()}, noPicSig=${noPicSig?.toUHexString()}, G=${G.toUHexString()}, dpwd=${dpwd.toUHexString()}, randSeed=${randSeed.toUHexString()}, simpleInfo=$simpleInfo, appPri=$appPri, a2ExpiryTime=$a2ExpiryTime, loginBitmap=$loginBitmap, tgt=${tgt.toUHexString()}, a2CreationTime=$a2CreationTime, tgtKey=${tgtKey.toUHexString()}, userStSig=$userStSig, userStKey=${userStKey.toUHexString()}, userStWebSig=$userStWebSig, userA5=$userA5, userA8=$userA8, lsKey=$lsKey, sKey=$sKey, userSig64=$userSig64, openId=${openId.toUHexString()}, openKey=$openKey, vKey=$vKey, accessToken=$accessToken, d2=$d2, d2Key=${d2Key.toUHexString()}, sid=$sid, aqSig=$aqSig, psKey=$psKeyMap, superKey=${superKey.toUHexString()}, payToken=${payToken.toUHexString()}, pf=${pf.toUHexString()}, pfKey=${pfKey.toUHexString()}, da2=${da2.toUHexString()}, wtSessionTicket=$wtSessionTicket, wtSessionTicketKey=${wtSessionTicketKey.toUHexString()}, deviceToken=${deviceToken.toUHexString()})"
return "WLoginSigInfo(uin=$uin, encryptA1=${encryptA1?.toUHexString()}, noPicSig=${noPicSig?.toUHexString()}, simpleInfo=$simpleInfo, appPri=$appPri, a2ExpiryTime=$a2ExpiryTime, loginBitmap=$loginBitmap, tgt=${tgt.toUHexString()}, a2CreationTime=$a2CreationTime, tgtKey=${tgtKey.toUHexString()}, userStSig=$userStSig, userStKey=${userStKey.toUHexString()}, userStWebSig=$userStWebSig, userA5=$userA5, userA8=$userA8, lsKey=$lsKey, sKey=$sKey, userSig64=$userSig64, openId=${openId.toUHexString()}, openKey=$openKey, vKey=$vKey, accessToken=$accessToken, d2=$d2, d2Key=${d2Key.toUHexString()}, sid=$sid, aqSig=$aqSig, psKey=$psKeyMap, superKey=${superKey.toUHexString()}, payToken=${payToken.toUHexString()}, pf=${pf.toUHexString()}, pfKey=${pfKey.toUHexString()}, da2=${da2.toUHexString()}, wtSessionTicket=$wtSessionTicket, wtSessionTicketKey=${wtSessionTicketKey.toUHexString()}, deviceToken=${deviceToken.toUHexString()})"
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
* Copyright 2019-2021 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@ -42,6 +42,19 @@ internal interface EncryptMethodSessionKey : EncryptMethod {
}
}
internal class EncryptMethodSessionKeyNew(
val wtSessionTicket: ByteArray, // t133
val wtSessionTicketKey: ByteArray, // t134
) : EncryptMethod {
override val id: Int get() = 69
override fun makeBody(client: QQAndroidClient, body: BytePacketBuilder.() -> Unit): ByteReadPacket =
buildPacket {
writeShortLVByteArray(wtSessionTicket)
encryptAndWrite(wtSessionTicketKey, body)
}
}
internal class EncryptMethodSessionKeyLoginState2(override val sessionKey: ByteArray) :
EncryptMethodSessionKey {
override val currentLoginState: Int get() = 2

View File

@ -135,7 +135,7 @@ internal val NO_ENCRYPT: ByteArray = ByteArray(0)
/**
* com.tencent.qphone.base.util.CodecWarpper#encodeRequest(int, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, byte[], int, int, java.lang.String, byte, byte, byte, byte[], byte[], boolean)
*/
internal inline fun OutgoingPacketFactory<*>.buildLoginOutgoingPacket(
internal inline fun <R : Packet?> OutgoingPacketFactory<R>.buildLoginOutgoingPacket(
client: QQAndroidClient,
bodyType: Byte,
extraData: ByteArray = EMPTY_BYTE_ARRAY,
@ -143,10 +143,10 @@ internal inline fun OutgoingPacketFactory<*>.buildLoginOutgoingPacket(
commandName: String = this.commandName,
key: ByteArray = KEY_16_ZEROS,
body: BytePacketBuilder.(sequenceId: Int) -> Unit
): OutgoingPacket {
): OutgoingPacketWithRespType<R> {
val sequenceId: Int = client.nextSsoSequenceId()
return OutgoingPacket(name, commandName, sequenceId, buildPacket {
return OutgoingPacketWithRespType(name, commandName, sequenceId, buildPacket {
writeIntLVPacket(lengthOffset = { it + 4 }) {
writeInt(0x00_00_00_0A)
writeByte(bodyType)

View File

@ -394,7 +394,8 @@ internal object KnownPacketFactories {
this.discardExact(1)
val packet = when (encryptionMethod) {
4 -> {
var data = TEA.decrypt(this, bot.client.ecdh.keyPair.initialShareKey, (this.remaining - 1).toInt())
var data =
TEA.decrypt(this, bot.client.ecdh.keyPair.initialShareKey, length = (this.remaining - 1).toInt())
val peerShareKey =
bot.client.ecdh.calculateShareKeyByPeerPublicKey(readUShortLVByteArray().adjustToPublicKey())
@ -402,19 +403,26 @@ internal object KnownPacketFactories {
packetFactory.decode(bot, data)
}
3 -> {
// session
val data = TEA.decrypt(
this,
bot.client.wLoginSigInfo.wtSessionTicketKey,
length = (this.remaining - 1).toInt()
)
packetFactory.decode(bot, data)
}
0 -> {
val data = if (bot.client.loginState == 0) {
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
ByteArrayPool.useInstance(this.remaining.toInt()) { byteArrayBuffer ->
val size = (this.remaining - 1).toInt()
this.readFully(byteArrayBuffer, 0, size)
val size = (this.remaining - 1).toInt()
val byteArrayBuffer = this.readBytes(size)
runCatching {
TEA.decrypt(byteArrayBuffer, bot.client.ecdh.keyPair.initialShareKey, size)
}.getOrElse {
TEA.decrypt(byteArrayBuffer, bot.client.randomKey, size)
}.toReadPacket()
}
runCatching {
TEA.decrypt(byteArrayBuffer, bot.client.ecdh.keyPair.initialShareKey, size)
}.getOrElse {
TEA.decrypt(byteArrayBuffer, bot.client.randomKey, size)
}.toReadPacket()
} else {
TEA.decrypt(this, bot.client.randomKey, 0, (this.remaining - 1).toInt())
}

View File

@ -11,24 +11,39 @@
package net.mamoe.mirai.internal.network.protocol.packet
import kotlinx.io.core.BytePacketBuilder
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.toByteArray
import kotlinx.io.core.writeFully
import kotlinx.io.core.*
import net.mamoe.mirai.internal.network.LoginExtraData
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.guid
import net.mamoe.mirai.internal.network.protocol.LoginType
import net.mamoe.mirai.internal.network.writeLoginExtraData
import net.mamoe.mirai.internal.utils.GuidSource
import net.mamoe.mirai.internal.utils.MacOrAndroidIdChangeFlag
import net.mamoe.mirai.internal.utils.NetworkType
import net.mamoe.mirai.internal.utils.guidFlag
import net.mamoe.mirai.internal.utils.io.*
import net.mamoe.mirai.utils.currentTimeMillis
import net.mamoe.mirai.utils.generateDeviceInfoData
import net.mamoe.mirai.utils.md5
import net.mamoe.mirai.utils.toByteArray
import net.mamoe.mirai.utils.*
import kotlin.random.Random
private val Char.isHumanReadable get() = this in '0'..'9' || this in 'a'..'z' || this in 'A'..'Z' || this in """ <>?,.";':/\][{}~!@#$%^&*()_+-=`""" || this in "\n\r"
internal fun TlvMap.smartToString(leadingLineBreak: Boolean = true, sorted: Boolean = true): String {
fun ByteArray.valueToString(): String {
val str = this.encodeToString()
return if (str.all { it.isHumanReadable }) str
else this.toUHexString()
}
val map = if (sorted) entries.sortedBy { it.key } else this.entries
return buildString {
if (leadingLineBreak) appendLine()
appendLine("count=${map.size}")
appendLine(map.joinToString("\n") { (key, value) ->
"0x" + key.toShort().toUHexString("") + " = " + value.valueToString()
})
}
}
/**
* 显式表示一个 [ByteArray] 是一个 tlv body
*/
@ -41,7 +56,7 @@ internal fun BytePacketBuilder.t1(uin: Long, ip: ByteArray) {
writeShort(1) // _ip_ver
writeInt(Random.nextInt())
writeInt(uin.toInt())
writeInt(currentTimeMillis().toInt())
writeInt(currentTimeSeconds().toInt())
writeFully(ip)
writeShort(0)
} shouldEqualsTo 20
@ -76,7 +91,7 @@ internal fun BytePacketBuilder.t18(
writeShort(0x18)
writeShortLVPacket {
writeShort(1) //_ping_version
writeInt(1536) //_sso_version
writeInt(0x00_00_06_00) //_sso_version=1536
writeInt(appId.toInt())
writeInt(appClientVersion)
writeInt(uin.toInt())
@ -106,6 +121,9 @@ internal fun BytePacketBuilder.t106(
)
}
/**
* A1
*/
internal fun BytePacketBuilder.t106(
appId: Long = 16L,
subAppId: Long,
@ -143,7 +161,7 @@ internal fun BytePacketBuilder.t106(
writeLong(uin)
}
writeInt(currentTimeMillis().toInt())
writeInt(currentTimeSeconds().toInt())
writeFully(ByteArray(4)) // ip // no need to write actual ip
writeByte(isSavePassword.toByte())
writeFully(passwordMd5)
@ -607,6 +625,11 @@ internal fun BytePacketBuilder.t185() {
}
internal fun BytePacketBuilder.t400(
/**
* if (var1[2] != null && var1[2].length > 0) {
this._G = (byte[])var1[2].clone();
}
*/
g: ByteArray, // 用于加密这个 tlv
uin: Long,
guid: ByteArray,
@ -617,15 +640,14 @@ internal fun BytePacketBuilder.t400(
) {
writeShort(0x400)
writeShortLVPacket {
writeByte(1) // version
writeLong(uin)
encryptAndWrite(g) {
writeByte(1) // version
writeLong(uin)
writeFully(guid)
writeFully(dpwd)
writeInt(appId.toInt())
writeInt(subAppId.toInt())
writeLong(currentTimeMillis())
writeInt(currentTimeSeconds().toInt())
writeFully(randomSeed)
}
}
@ -757,8 +779,38 @@ internal fun BytePacketBuilder.t536( // 1334
}
}
internal fun BytePacketBuilder.t536( // 1334
loginExtraData: Collection<LoginExtraData>
) {
writeShort(0x536)
writeShortLVPacket {
//com.tencent.loginsecsdk.ProtocolDet#packExtraData
writeByte(1) // const
writeByte(loginExtraData.size.toByte()) // data count
for (extraData in loginExtraData) {
writeLoginExtraData(extraData)
}
}
}
internal fun BytePacketBuilder.t525(
t536: ByteReadPacket
loginExtraData: Collection<LoginExtraData>,
) {
writeShort(0x525)
writeShortLVPacket {
writeShort(1)
t536(loginExtraData)
}
}
internal fun BytePacketBuilder.t525(
t536: ByteReadPacket = buildPacket {
t536(buildPacket {
//com.tencent.loginsecsdk.ProtocolDet#packExtraData
writeByte(1) // const
writeByte(0) // data count
}.readBytes())
}
) {
writeShort(0x525)
writeShortLVPacket {
@ -767,6 +819,14 @@ internal fun BytePacketBuilder.t525(
}
}
internal fun BytePacketBuilder.t544( // 1334
) {
writeShort(0x544)
writeShortLVPacket {
writeFully(byteArrayOf(0, 0, 0, 11)) // means native throws exception
}
}
internal fun BytePacketBuilder.t318(
tgtQR: ByteArray // unknown
) {

View File

@ -27,7 +27,6 @@ import net.mamoe.mirai.internal.message.contextualBugReportException
import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.getRandomByteArray
import net.mamoe.mirai.internal.network.guid
import net.mamoe.mirai.internal.network.protocol.data.jce.*
import net.mamoe.mirai.internal.network.protocol.data.proto.Oidb0x769
import net.mamoe.mirai.internal.network.protocol.data.proto.StatSvcGetOnline

File diff suppressed because one or more lines are too long

View File

@ -10,9 +10,12 @@
package net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.guid
import net.mamoe.mirai.internal.network.protocol.packet.*
import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
import net.mamoe.mirai.internal.utils.io.writeShortLVByteArray
import java.security.SecureRandom
import java.util.*
import kotlin.math.abs
internal object WtLogin15 : WtLoginExt {
private const val subCommand = 15.toShort()
@ -21,34 +24,134 @@ internal object WtLogin15 : WtLoginExt {
operator fun invoke(
client: QQAndroidClient,
) = WtLogin.ExchangeEmp.buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
writeSsoPacket(client, client.subAppId, WtLogin.ExchangeEmp.commandName, sequenceId = sequenceId) {
writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) {
writeShort(subCommand) // subCommand
writeShort(21)
) = WtLogin.ExchangeEmp.buildOutgoingUniPacket(client, bodyType = 2, key = ByteArray(16)) { sequenceId ->
// writeSsoPacket(client, client.subAppId, WtLogin.ExchangeEmp.commandName, sequenceId = sequenceId) {
writeOicqRequestPacket(
client,
EncryptMethodSessionKeyNew(
client.wLoginSigInfo.wtSessionTicket.data,
client.wLoginSigInfo.wtSessionTicketKey
),
0x0810
) {
writeShort(subCommand) // subCommand
writeShort(24)
t18(16, uin = client.uin)
t1(client.uin, client.device.ipAddress)
t106(appId, client)
t116(client.miscBitMap, client.subSigMap)
t100(appId, client.subAppId, client.appClientVersion, client.ssoVersion, client.mainSigMap)
t107(0)
t142(client.apkId)
t144(client)
t145(client.device.guid)
t16a(client.tlv16a ?: byteArrayOf()) // new
t154(sequenceId)
t141(client.device.simInfo, client.networkType, client.device.apn)
t8(2052)
t511()
t147(appId, client.apkVersionName, client.apkSignatureMd5)
t177(buildTime = client.buildTime, buildVersion = client.sdkVersion)
t187(client.device.macAddress)
t188(client.device.androidId)
t194(client.device.imsiMd5)
t202(client.device.wifiBSSID, client.device.wifiSSID)
t516()
// writeFully(("00 18 00 16 00 01 00 00 06 00 00 00 00 10 00 00 00 00 76 E4 B8 DD 00 00 00 00 00 01 00 14 00 01 5A 11 60 11 76 E4 B8 DD 60 0D 44 35 90 E8 11 1B 00 00 " +
// //"01 06 " +
// //client.wLoginSigInfo.encryptA1!!.size.toShort().toUHexString() + " " + client.wLoginSigInfo.encryptA1!!.toUHexString() + " "+
// "01 06 00 78 CD 52 75 7E B7 CF A0 FC 58 15 1E 79 29 86 F0 28 06 AE 4D E4 EF 39 6B 8B 64 19 90 7E DF 2B 71 48 53 E6 71 19 4A 7F 81 06 EB 19 A2 CA 8B B3 47 D9 07 73 04 71 99 B3 F2 7E 2B F0 93 6B A0 FA 94 D3 15 41 55 6C EF 87 96 47 22 5C 6B 10 0F EC 27 3A 8F 83 0A 75 8B 95 EA 81 02 AA C6 E7 C8 6C 7B C3 93 4E EC 08 2E DB 9B 3E 4B 17 7C 06 1C 66 31 F4 EE 70 55 87 49 A2 5B 25 " +
// "01 16 00 0E 00 0A F7 FF 7C 00 01 " +
// "04 00 01 5F 5E 10 E2 01 00 00 16 00 01 00 00 00 0F 00 00 00 10 20 02 FD E2 00 00 00 00 02 10 10 E0 01 07 00 06 00 00 00 00 00 01 01 44 01 B8 20 3F 4F ED A0 CD 3B CC 07 47 A3 91 8B B5 C8 18 EA D1 45 8A 0F F3 1F CD 95 85 00 7C AD 7F 18 7C 43 B6 A6 C3 FF 53 D5 F6 F4 E6 75 1D 4F AF A4 DF C5 EB 3D 6F C5 C3 21 E9 C7 66 8A EF 4A BD 45 CC D2 A5 34 E4 1D F5 9B 07 9B 65 6A 35 BD B4 0B D1 94 43 18 1C 48 1D B5 2C B5 62 FA E9 E1 35 14 13 BC EE BF FA FD 98 A4 72 A8 1A 71 9C 77 2D 4D BF 58 3A 0A 72 D4 B0 A2 DA 6C FC 04 49 F5 05 3D EF 60 E5 92 9A 04 96 AA A4 22 4E 13 8F 63 3B D9 54 B8 DC CC 2D A0 0B 8B B9 9A D7 8A D0 E4 A4 EE 4F 2E 8D 52 86 35 74 70 93 A0 21 0D 98 DE A4 5B B9 79 A5 8E 31 8D A5 AC 0B DB A8 65 6C 93 1C EE FB E9 A2 FD 22 90 0B A5 41 3D 1F 2B A9 84 FF D8 64 DF 94 48 B3 D6 20 47 F3 12 D3 63 F0 84 1C 6A 1E 0A 8B 13 02 EA F2 C3 3E 41 87 59 5A 65 80 E4 7C 3E FC 52 70 20 26 ED 27 91 01 C0 4D 50 23 B9 1F 59 77 AE D5 4D C8 57 DB 86 E9 5B 98 0C 95 A2 15 AB 01 3E 10 D5 3B 01 57 FE C8 88 80 4D 1A 8A 4D 64 89 C3 7F E6 73 D3 04 C8 EA 98 E2 F3 82 48 7D FC D7 CF 07 " +
// "F4 33 F1 1E D3 1C 0D 48 37 3A 50 0B 39 28 AB F3 4F BF C9 D8 70 6F B9 F2 FB 46 5A 4B 21 DE D8 B8 30 A1 FC C6 09 60 4E 07 21 28 F7 CC 7A A0 07 1C 87 42 90 D3 40 07 1B 35 52 56 31 E2 C0 6D 1F 79 43 6C 63 46 D4 92 EE 30 A2 D2 D6 0F 79 46 2A EF C7 C6 CF 54 1D 03 FE 80 D4 28 87 AF 2D 1A FF 71 99 FC 23 09 79 B0 9D B4 E9 0F 4E E3 D2 79 10 2C C7 6E 30 34 A5 66 2E 33 00 08 D0 58 2B 7F D8 E6 21 2E 7E 30 01 42 00 18 00 00 00 14 63 6F 6D 2E 74 65 6E 63 65 6E 74 2E 6D 6F 62 69 6C 65 71 71 01 45 00 10 E2 9A BF 20 0D 00 CA F0 4D 28 CA 66 BF 31 6E AF 01 6A 00 38 CA 78 6A 94 A7 A4 F3 BC 28 58 3C FF 53 41 4C A3 5B 98 AB 7C 21 FB 34 D9 28 59 91 D5 15 12 04 A0 7E 27 25 DF 7A A0 06 87 EB 13 12 10 CD 80 85 78 17 F9 1C D6 21 A7 AF 8C 01 54 00 04 00 01 38 E4 01 41 00 1C 00 01 00 10 43 68 69 6E 61 20 4D 6F 62 69 6C 65 20 47 53 4D 00 02 00 04 77 69 66 69 00 08 00 08 00 00 00 00 08 04 00 00 05 11 00 D3 00 0E 01 00 0A 74 65 6E 70 61 79 2E 63 6F 6D 01 00 11 6F 70 65 6E 6D 6F 62 69 6C 65 2E 71 71 2E 63 6F 6D 01 00 0B 64 6F 63 73 2E 71 71 2E 63 6F 6D 01 00 0E 63 6F 6E 6E 65 63 74 2E 71 71 2E 63 6F 6D 01 00 0C 71 7A 6F 6E 65 2E 71 71 2E 63 6F 6D 01 00 0A 76 69 70 2E 71 71 2E 63 6F 6D 01 00 11 67 61 6D 65 63 65 6E 74 65 72 2E 71 71 2E 63 6F 6D 01 00 0A 71 75 6E 2E 71 71 2E 63 6F 6D 01 00 0B 67 61 6D 65 2E 71 71 2E 63 6F 6D 01 00 0C 71 71 77 65 62 2E 71 71 2E 63 6F 6D 01 00 09 74 69 2E 71 71 2E 63 6F 6D 01 00 0D 6F 66 66 69 63 65 2E 71 71 2E 63 6F 6D 01 00 0B 6D 61 69 6C 2E 71 71 2E 63 6F 6D 01 00 0A 6D 6D 61 2E 71 71 2E 63 6F 6D 01 47 00 1D 00 00 00 10 00 05 38 2E 35 2E 35 00 10 A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D 01 77 00 11 01 5F EC 50 93 00 0A 36 2E 30 2E 30 2E 32 34 36 33 " +
// "04 00 00 48 2F C5 1F 51 62 56 7E A0 39 92 E9 DC 0D 55 00 E5 BA 24 8D 38 47 8F 1C 7F 6A 91 54 62 D9 B3 25 B0 2D 10 A2 15 5E B4 C7 5F 76 CA AC EF 76 88 92 F4 FF 16 55 98 EB 01 BB 4D 34 95 9E 8E 80 59 4E B3 99 1E 80 CD 4E 27 A4 8E" +
// " 01 87 00 10 30 86 EC F4 ED 94 5D D2 6F 88 8A 39 46 6C 22 7D 01 88 00 10 9D B6 A8 DF CB C1 79 06 EB 5D 95 FA 20 5A 9E 11 01 94 00 10 73 A1 26 09 B8 99 62 29 04 95 B9 9E 5C DA 22 8C 02 02 00 1B 00 10 36 85 4A A7 02 92 35 CE 9F B5 A2 D0 7A E4 88 F8 00 07 22 38 32 45 46 45 22 05 16 00 04 00 00 00 00 05 21 00 06 00 00 00 00 00 00 05 25 00 47 00 01 05 36 00 41 01 03 00 00 00 00 76 E4 B8 DD 04 1B 11 E8 90 60 0D 3C 28 20 02 FD E2 00 00 00 00 76 E4 B8 DD 04 1B 11 E8 90 60 0D 3C 28 20 02 FD E2 00 00 00 00 76 E4 B8 DD 04 1B 11 E8 90 60 0D 3C 95 20 02 FD E2 " +
// "").hexToBytes())
// return@writeOicqRequestPacket
t18(appId, uin = client.uin)
t1(client.uin, ByteArray(4))
// t106(client = client)
writeShort(0x106)
val encryptA1 = client.wLoginSigInfo.encryptA1!!
writeShortLVByteArray(encryptA1)
// kotlin.run {
// val key = (client.account.passwordMd5 + ByteArray(4) + client.uin.toInt().toByteArray()).md5()
// kotlin.runCatching {
// TEA.decrypt(encryptA1, key).toUHexString()
// }.soutv("DEC") // success
// }
// val a1 = kotlin.runCatching {
// TEA.decrypt(encryptA1, buildPacket {
// writeFully(client.device.guid)
// writeFully(client.dpwd)
// writeFully(client.randSeed)
// }.readBytes().md5())
// }.recoverCatching {
// client.tryDecryptOrNull(encryptA1) { it }!!
// }.getOrElse {
// encryptA1.soutv("ENCRYPT A1")
// client.soutv("CLIENT")
// // exitProcess(1)
// // error("Failed to decrypt A1")
// encryptA1
// }
t116(client.miscBitMap, client.subSigMap)
//t116(0x08F7FF7C, 0x00010400)
//t100(appId, client.subAppId, client.appClientVersion, client.ssoVersion, client.mainSigMap)
//t100(appId, 1, client.appClientVersion, client.ssoVersion, mainSigMap = 1048768)
t100(appId, 2, client.appClientVersion, client.ssoVersion, client.mainSigMap)
t107(0)
// t108(client.ksid) // 第一次 exchange 没有 108
t144(client)
t142(client.apkId)
t145(client.device.guid)
val noPicSig =
client.wLoginSigInfo.noPicSig ?: error("Internal error: doing exchange emp 15 while noPicSig=null")
t16a(noPicSig)
t154(0)
t141(client.device.simInfo, client.networkType, client.device.apn)
t8(2052)
t511()
t147(appId, client.apkVersionName, client.apkSignatureMd5)
t177(buildTime = client.buildTime, buildVersion = client.sdkVersion)
// new
t400(
g = client.G,
uin = client.uin,
guid = client.device.guid,
dpwd = client.dpwd,
appId = 1,
subAppId = 16,
randomSeed = client.randSeed
)
t187(client.device.macAddress)
t188(client.device.androidId)
t194(client.device.imsiMd5)
t202(client.device.wifiBSSID, client.device.wifiSSID)
t516()
t521() // new
t525(client.loginExtraData) // new
//t544() // new
}
// }
}
}
internal fun get_mpasswd(): String {
var var5: String
run label41@{
val var6: ByteArray = SecureRandom.getSeed(16)
var var0 = 0
var var4 = ""
while (true) {
var5 = var4
if (var0 >= var6.size) {
return var5
}
val var3: Boolean = Random().nextBoolean()
val var2: Int = abs(var6[var0] % 26)
val var1: Byte = if (var3) {
97
} else {
65
}
var4 += ((var1 + var2).toChar()).toString()
++var0
}
}
}
}

View File

@ -9,18 +9,14 @@
package net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin
import kotlinx.io.core.toByteArray
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.guid
import net.mamoe.mirai.internal.network.protocol.packet.*
import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
import net.mamoe.mirai.utils.md5
internal object WtLogin20 : WtLoginExt {
operator fun invoke(
client: QQAndroidClient,
t402: ByteArray
): OutgoingPacket = WtLogin.Login.buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
client: QQAndroidClient
) = WtLogin.Login.buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
writeSsoPacket(client, client.subAppId, WtLogin.Login.commandName, sequenceId = sequenceId) {
writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) {
writeShort(20) // subCommand
@ -28,7 +24,7 @@ internal object WtLogin20 : WtLoginExt {
t8(2052)
t104(client.t104)
t116(client.miscBitMap, client.subSigMap)
t401((client.device.guid + "stMNokHgxZUGhsYp".toByteArray() + t402).md5())
t401(client.G) // (client.device.guid + "stMNokHgxZUGhsYp".toByteArray() + t402).md5()
}
}
}

View File

@ -9,11 +9,8 @@
package net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin
import kotlinx.io.core.buildPacket
import kotlinx.io.core.readBytes
import kotlinx.io.core.toByteArray
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.guid
import net.mamoe.mirai.internal.network.protocol.packet.*
import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
@ -32,6 +29,7 @@ internal object WtLogin9 : WtLoginExt {
t18(appId, client.appClientVersion, client.uin)
t1(client.uin, client.device.ipAddress)
t106(appId, client)
/* // from GetStWithPasswd
@ -98,13 +96,7 @@ internal object WtLogin9 : WtLoginExt {
t516()
t521()
t525(buildPacket {
t536(buildPacket {
//com.tencent.loginsecsdk.ProtocolDet#packExtraData
writeByte(1) // const
writeByte(0) // data count
}.readBytes())
})
t525()
// this.build().debugPrint("傻逼")
// ignored t318 because not logging in by QR

View File

@ -12,11 +12,12 @@ package net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin
import kotlinx.io.core.*
import net.mamoe.mirai.internal.network.LoginExtraData
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.guid
import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY
import net.mamoe.mirai.internal.network.protocol.packet.Tlv
import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
import net.mamoe.mirai.internal.network.protocol.packet.t145
import net.mamoe.mirai.internal.network.readUShortLVByteArray
import net.mamoe.mirai.internal.utils.crypto.TEA
import net.mamoe.mirai.internal.utils.io.writeShortLVByteArray
import net.mamoe.mirai.utils.*
@ -97,15 +98,22 @@ internal interface WtLoginExt { // so as not to register to global extension
/**
* login extra data
*
* oicq/wlogin_sdk/request/oicq_request.java:1445
*/
fun QQAndroidClient.analysisTlv537(t537: ByteArray) = t537.read {
//discardExact(2)
loginExtraData = LoginExtraData( // args are to correct order
uin = readUInt().toLong(),
ip = readBytes(readByte().toInt() and 0xff),
time = readInt(), // correct
version = readInt()
)
discardExact(1)
repeat(readByte().toInt()) {
loginExtraData.add(
LoginExtraData( // args are to correct order
uin = readLong(),
ip = readBytes(readByte().toInt() and 0xff),
time = readInt(), // correct
version = readInt()
)
)
}
}
/**
@ -180,5 +188,23 @@ internal interface WtLoginExt { // so as not to register to global extension
}
}
fun QQAndroidClient.analyzeTlv106(t106: ByteArray) {
val tgtgtKey = decodeA1(t106) {
discardExact(51)
readBytes(16)
}
this.tgtgtKey = tgtgtKey
}
fun Input.readUShortLVString(): String = String(this.readUShortLVByteArray())
}
internal inline fun <R> QQAndroidClient.decodeA1(a1: ByteArray, block: ByteReadPacket.() -> R): R {
val key = (account.passwordMd5 + ByteArray(4) + uin.toInt().toByteArray()).md5()
val v = TEA.decrypt(a1, key)
return v.toReadPacket().withUse(block)
}
internal fun ByteArray?.orEmpty(size: Int = 0): ByteArray {
return this ?: if (size == 0) EMPTY_BYTE_ARRAY else ByteArray(size)
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
* Copyright 2019-2021 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@ -34,8 +34,8 @@ internal class MiraiProtocolInternal(
init {
protocols[MiraiProtocol.ANDROID_PHONE] = MiraiProtocolInternal(
"com.tencent.mobileqq",
537066419,
"8.4.18",
537066978,
"8.5.5",
"6.0.0.2454",
184024956,
0x10400,