Browse Source

Update login protocol (#2433)

* Update login protocol
Still need testing

* Turn off debug option and make t547 null when failed

* Fix wrong convert method and improve tips

* Remove unused part and improve tips

* Fix typo

* Inline resultStatus for performance

* Rename pow to PoW, the name should be "Proof of Work"

* Add shadow and deps-test for kt-bignum

* Try to fix deps-test

* Fix deps-test again
sandtechnology 2 năm trước cách đây
mục cha
commit
cc7f35519e

+ 3 - 0
buildSrc/src/main/kotlin/Versions.kt

@@ -267,6 +267,9 @@ const val `netty-transport` = "io.netty:netty-transport:${Versions.netty}"
 const val `netty-buffer` = "io.netty:netty-buffer:${Versions.netty}"
 const val `bouncycastle` = "org.bouncycastle:bcprov-jdk15on:${Versions.bouncycastle}"
 
+const val `kt-bignum` = "com.ionspin.kotlin:bignum:0.3.7"
+val `kt-bignum_relocated` = RelocatedDependency(`kt-bignum`, "com.ionspin.kotlin.bignum")
+
 const val `maven-resolver-api` = "org.apache.maven.resolver:maven-resolver-api:${Versions.mavenArtifactResolver}"
 const val `maven-resolver-impl` = "org.apache.maven.resolver:maven-resolver-impl:${Versions.mavenArtifactResolver}"
 const val `maven-resolver-connector-basic` =

+ 1 - 0
mirai-core-all/build.gradle.kts

@@ -25,6 +25,7 @@ dependencies {
     api(project(":mirai-core-utils"))
     implementation(`slf4j-api`) // Required by mirai-console
 
+    relocateImplementation(project, `kt-bignum_relocated`)
     relocateImplementation(project, `ktor-client-core_relocated`)
     relocateImplementation(project, `ktor-client-okhttp_relocated`)
     relocateImplementation(project, `ktor-io_relocated`)

+ 4 - 1
mirai-core-utils/src/commonMain/kotlin/ByteArrayOp.kt

@@ -24,6 +24,9 @@ public fun String.sha1(): ByteArray = toByteArray().sha1()
 
 public expect fun ByteArray.sha1(offset: Int = 0, length: Int = size - offset): ByteArray
 
+public fun String.sha256(): ByteArray = toByteArray().sha256()
+
+public expect fun ByteArray.sha256(offset: Int = 0, length: Int = size - offset): ByteArray
 
 ///////////////////////////////////////////////////////////////////////////
 // How to choose 'inflate', 'inflateAllAvailable', 'InflateInput'?
@@ -112,4 +115,4 @@ public expect fun DeflateInput(source: Input): Input
 /**
  * @see DeflateInput
  */
-public fun Input.deflateInput(): Input = DeflateInput(this)
+public fun Input.deflateInput(): Input = DeflateInput(this)

+ 11 - 1
mirai-core-utils/src/jvmBaseMain/kotlin/ByteArrayOp.kt

@@ -13,7 +13,7 @@
 package net.mamoe.mirai.utils
 
 import io.ktor.utils.io.core.*
-import io.ktor.utils.io.streams.asInput
+import io.ktor.utils.io.streams.*
 import java.io.ByteArrayInputStream
 import java.io.ByteArrayOutputStream
 import java.io.InputStream
@@ -50,6 +50,10 @@ public fun InputStream.sha1(): ByteArray {
     return digest("SHA-1")
 }
 
+public fun InputStream.sha256(): ByteArray {
+    return digest("SHA-256")
+}
+
 public actual fun ByteArray.md5(offset: Int, length: Int): ByteArray {
     checkOffsetAndLength(offset, length)
     return MessageDigest.getInstance("MD5").apply { update(this@md5, offset, length) }.digest()
@@ -62,6 +66,12 @@ public actual fun ByteArray.sha1(offset: Int, length: Int): ByteArray {
     return MessageDigest.getInstance("SHA-1").apply { update(this@sha1, offset, length) }.digest()
 }
 
+@JvmOverloads
+public actual fun ByteArray.sha256(offset: Int, length: Int): ByteArray {
+    checkOffsetAndLength(offset, length)
+    return MessageDigest.getInstance("SHA-256").apply { update(this@sha256, offset, length) }.digest()
+}
+
 @JvmOverloads
 public actual fun ByteArray.gzip(offset: Int, length: Int): ByteArray {
     ByteArrayOutputStream().use { buf ->

+ 5 - 0
mirai-core-utils/src/nativeMain/kotlin/ByteArrayOp.kt

@@ -31,6 +31,11 @@ public actual fun ByteArray.sha1(offset: Int, length: Int): ByteArray = SHA1.cre
     return digest().bytes
 }
 
+public actual fun ByteArray.sha256(offset: Int, length: Int): ByteArray = SHA256.create().run {
+    update(this@sha256, offset, length)
+    return digest().bytes
+}
+
 /**
  * WARNING: DO NOT SET THIS BUFFER TOO SMALL, OR YOU WILL SEE COMPRESSION ERROR.
  */

+ 76 - 0
mirai-core-utils/src/nativeMain/kotlin/CommonDigest.kt

@@ -272,3 +272,79 @@ internal class SHA1 : SHA(chunkSize = 64, digestSize = 20) {
         for (n in out.indices) out[n] = (h[n / 4] ushr (24 - 8 * (n % 4))).toByte()
     }
 }
+
+internal class SHA256 : SHA(chunkSize = 64, digestSize = 32) {
+    companion object : HasherFactory({ SHA256() }) {
+        private val H = intArrayOf(
+            0x6a09e667, -0x4498517b, 0x3c6ef372, -0x5ab00ac6,
+            0x510e527f, -0x64fa9774, 0x1f83d9ab, 0x5be0cd19
+        )
+
+        private val K = intArrayOf(
+            0x428a2f98, 0x71374491, -0x4a3f0431, -0x164a245b,
+            0x3956c25b, 0x59f111f1, -0x6dc07d5c, -0x54e3a12b,
+            -0x27f85568, 0x12835b01, 0x243185be, 0x550c7dc3,
+            0x72be5d74, -0x7f214e02, -0x6423f959, -0x3e640e8c,
+            -0x1b64963f, -0x1041b87a, 0x0fc19dc6, 0x240ca1cc,
+            0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
+            -0x67c1aeae, -0x57ce3993, -0x4ffcd838, -0x40a68039,
+            -0x391ff40d, -0x2a586eb9, 0x06ca6351, 0x14292967,
+            0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
+            0x650a7354, 0x766a0abb, -0x7e3d36d2, -0x6d8dd37b,
+            -0x5d40175f, -0x57e599b5, -0x3db47490, -0x3893ae5d,
+            -0x2e6d17e7, -0x2966f9dc, -0xbf1ca7b, 0x106aa070,
+            0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
+            0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
+            0x748f82ee, 0x78a5636f, -0x7b3787ec, -0x7338fdf8,
+            -0x6f410006, -0x5baf9315, -0x41065c09, -0x398e870e
+        )
+    }
+
+
+    private val h = IntArray(8)
+    private val r = IntArray(8)
+    private val w = IntArray(64)
+
+    init {
+        coreReset()
+    }
+
+    override fun coreReset() {
+        arraycopy(H, 0, h, 0, 8)
+    }
+
+    override fun coreUpdate(chunk: ByteArray) {
+        arraycopy(h, 0, r, 0, 8)
+
+        for (j in 0 until 16) w[j] = chunk.readS32_be(j * 4)
+        for (j in 16 until 64) {
+            val s0 = w[j - 15].rotateRight(7) xor w[j - 15].rotateRight(18) xor w[j - 15].ushr(3)
+            val s1 = w[j - 2].rotateRight(17) xor w[j - 2].rotateRight(19) xor w[j - 2].ushr(10)
+            w[j] = w[j - 16] + s0 + w[j - 7] + s1
+        }
+
+        for (j in 0 until 64) {
+            val s1 = r[4].rotateRight(6) xor r[4].rotateRight(11) xor r[4].rotateRight(25)
+            val ch = r[4] and r[5] xor (r[4].inv() and r[6])
+            val t1 = r[7] + s1 + ch + K[j] + w[j]
+            val s0 = r[0].rotateRight(2) xor r[0].rotateRight(13) xor r[0].rotateRight(22)
+            val maj = r[0] and r[1] xor (r[0] and r[2]) xor (r[1] and r[2])
+            val t2 = s0 + maj
+            r[7] = r[6]
+            r[6] = r[5]
+            r[5] = r[4]
+            r[4] = r[3] + t1
+            r[3] = r[2]
+            r[2] = r[1]
+            r[1] = r[0]
+            r[0] = t1 + t2
+
+        }
+        for (j in 0 until 8) h[j] += r[j]
+    }
+
+    override fun coreDigest(out: ByteArray) {
+        for (n in out.indices) out[n] = (h[n / 4] ushr (24 - 8 * (n % 4))).toByte()
+    }
+}
+

+ 9 - 0
mirai-core/build.gradle.kts

@@ -41,6 +41,7 @@ kotlin {
                 api(`kotlinx-serialization-json`)
                 api(`kotlinx-coroutines-core`)
 
+                implementation(`kt-bignum`)
                 implementation(project(":mirai-core-utils"))
                 implementation(`kotlinx-serialization-protobuf`)
                 implementation(`kotlinx-atomicfu`)
@@ -108,6 +109,14 @@ kotlin {
         }
 
 
+        // Kt bignum
+        findByName("jvmBaseMain")?.apply {
+            dependencies {
+                relocateImplementation(`kt-bignum_relocated`)
+            }
+        }
+
+
         // Ktor
 
         findByName("commonMain")?.apply {

+ 1 - 0
mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt

@@ -166,6 +166,7 @@ internal open class QQAndroidClient(
     var reserveUinInfo: ReserveUinInfo? = null
     var t402: ByteArray? = null
     lateinit var t104: ByteArray
+    var t547: ByteArray? = null
 }
 
 internal val QQAndroidClient.apkId: ByteArray get() = "com.tencent.mobileqq".toByteArray()

+ 9 - 0
mirai-core/src/commonMain/kotlin/network/protocol/packet/Tlv.kt

@@ -275,6 +275,15 @@ internal fun BytePacketBuilder.t104(
     }
 }
 
+internal fun BytePacketBuilder.t547(
+    t547Data: ByteArray
+) {
+    writeShort(0x547)
+    writeShortLVPacket {
+        writeFully(t547Data)
+    }
+}
+
 internal fun BytePacketBuilder.t174(
     t174Data: ByteArray
 ) {

+ 1 - 0
mirai-core/src/commonMain/kotlin/network/protocol/packet/login/WtLogin.kt

@@ -210,6 +210,7 @@ internal class WtLogin {
             tlvMap[0x161]?.let { bot.client.analysisTlv161(it) }
             tlvMap[0x403]?.let { bot.client.randSeed = it }
             tlvMap[0x402]?.let { bot.client.t402 = it }
+            tlvMap[0x546]?.let { bot.client.analysisTlv546(it) }
             // tlvMap[0x402]?.let { t402 ->
 //            bot.client.G = buildPacket {
 //                writeFully(bot.client.device.guid)

+ 2 - 1
mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin10.kt

@@ -43,7 +43,7 @@ internal object WtLogin10 : WtLoginExt {
                 0x0810
             ) {
                 writeShort(11) // subCommand
-                writeShort(17)
+                writeShort(18)
                 t100(appId, subAppId, client.appClientVersion, client.ssoVersion, mainSigMap)
                 t10a(client.wLoginSigInfo.tgt)
                 t116(client.miscBitMap, client.subSigMap)
@@ -80,6 +80,7 @@ internal object WtLogin10 : WtLoginExt {
                 t188(client.device.androidId)
                 t194(client.device.imsiMd5)
                 t511()
+                t202(client.device.wifiBSSID, client.device.wifiSSID)
                 //t544()
 
             }

+ 20 - 2
mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin2.kt

@@ -26,11 +26,20 @@ internal object WtLogin2 : WtLoginExt {
         writeSsoPacket(client, client.subAppId, WtLogin.Login.commandName, sequenceId = sequenceId) {
             writeOicqRequestPacket(client, commandId = 0x0810) {
                 writeShort(2) // subCommand
-                writeShort(4) // count of TLVs
+                writeShort(
+                    if (client.t547 == null) {
+                        4
+                    } else {
+                        5
+                    }
+                ) // count of TLVs
                 t193(ticket)
                 t8(2052)
                 t104(client.t104)
                 t116(client.miscBitMap, client.subSigMap)
+                client.t547?.let {
+                    t547(it)
+                }
             }
         }
     }
@@ -43,11 +52,20 @@ internal object WtLogin2 : WtLoginExt {
         writeSsoPacket(client, client.subAppId, WtLogin.Login.commandName, sequenceId = sequenceId) {
             writeOicqRequestPacket(client, commandId = 0x0810) {
                 writeShort(2) // subCommand
-                writeShort(4) // count of TLVs
+                writeShort(
+                    if (client.t547 == null) {
+                        4
+                    } else {
+                        5
+                    }
+                ) // count of TLVs
                 t2(captchaAnswer, captchaSign, 0)
                 t8(2052)
                 t104(client.t104)
                 t116(client.miscBitMap, client.subSigMap)
+                client.t547?.let {
+                    t547(it)
+                }
             }
         }
     }

+ 123 - 0
mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLoginExt.kt

@@ -9,6 +9,9 @@
 
 package net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin
 
+import com.ionspin.kotlin.bignum.integer.BigInteger
+import com.ionspin.kotlin.bignum.integer.util.fromTwosComplementByteArray
+import com.ionspin.kotlin.bignum.integer.util.toTwosComplementByteArray
 import io.ktor.utils.io.core.*
 import net.mamoe.mirai.internal.QQAndroidBot
 import net.mamoe.mirai.internal.network.LoginExtraData
@@ -171,6 +174,126 @@ internal interface WtLoginExt { // so as not to register to global extension
         this.t150 = Tlv(t150)
     }
 
+    fun QQAndroidClient.analysisTlv546(t546: ByteArray) {
+        val version: Byte
+        val algorithmType: Byte
+        val hashType: Byte
+        val maxIndex: Short
+        val reserveBytes: ByteArray
+        val inputBigNumArr: ByteArray
+        val targetHashArr: ByteArray
+        val reserveHashArr: ByteArray
+        var resultArr: ByteArray = EMPTY_BYTE_ARRAY;
+        var costTimeMS: Int = 0;
+        var recursiveDepth: Int = 0;
+        var failed = false
+
+        fun getPadRemaining(bigNumArr: ByteArray, bound: Short): Int {
+            if (bound > 32) {
+                return 1
+            }
+            var maxLoopCount = 255
+            var index = 0
+            while (maxLoopCount >= 0 && index < bound) {
+                if (bigNumArr[maxLoopCount / 8].toInt() and (1 shl maxLoopCount) % 8 != 0) {
+                    return 2
+                }
+                maxLoopCount--
+                index++
+            }
+            return 0
+        }
+
+        fun calcType1(bigNumArrIn: ByteArray, maxLength: Short) {
+            var bigIntArrClone = bigNumArrIn.copyOf()
+            val originLength = bigIntArrClone.size
+            var bigInteger = BigInteger.fromTwosComplementByteArray(bigIntArrClone)
+            while (true) {
+                if (getPadRemaining(bigIntArrClone.sha256().copyOf(32), maxLength) == 0) {
+                    resultArr = bigIntArrClone
+                    return
+                }
+                recursiveDepth++
+                bigInteger = bigInteger.add(BigInteger.ONE)
+                bigIntArrClone = bigInteger.toTwosComplementByteArray()
+                if (bigIntArrClone.size > originLength) {
+                    failed = true
+                    return
+                }
+            }
+        }
+
+        fun calcType2(bigNumArrIn: ByteArray, hashTarget: ByteArray) {
+            var bigIntArrClone = bigNumArrIn.copyOf()
+            val originLength = bigIntArrClone.size
+            var bigInteger = BigInteger.fromTwosComplementByteArray(bigIntArrClone)
+            while (true) {
+                if (bigIntArrClone.sha256().copyOf(32).contentEquals(hashTarget)) {
+                    resultArr = bigIntArrClone
+                    return
+                }
+                recursiveDepth++
+                bigInteger = bigInteger.add(BigInteger.ONE)
+                bigIntArrClone = bigInteger.toTwosComplementByteArray()
+                if (bigIntArrClone.size > originLength) {
+                    failed = true
+                    return
+                }
+
+            }
+        }
+
+        t546.toReadPacket().apply {
+            version = readByte()
+            algorithmType = readByte()
+            hashType = readByte()
+            readByte() // Ignore resultStatus since it's useless
+            maxIndex = readShort()
+            reserveBytes = readBytes(2)
+            inputBigNumArr = readBytes(readShort().toInt())
+            targetHashArr = readBytes(readShort().toInt())
+            reserveHashArr = readBytes(readShort().toInt())
+        }
+        val startTimeMS: Long = currentTimeMillis()
+        costTimeMS = 0
+        recursiveDepth = 0
+        if (hashType == 1.toByte()) {
+            bot.logger.info("Calculating type $algorithmType PoW, it can take some time....")
+            when (algorithmType.toInt()) {
+                1 -> calcType1(inputBigNumArr, maxIndex)
+                2 -> calcType2(inputBigNumArr, targetHashArr)
+                else -> {
+                    failed = true
+                    bot.logger.warning("Unsupported tlv546 algorithm type:${algorithmType}")
+                }
+            }
+        } else {
+            failed = true
+            bot.logger.warning("Unsupported tlv546 hash type:${hashType}")
+        }
+        if (!failed) {
+            costTimeMS = (currentTimeMillis() - startTimeMS).toInt()
+            bot.logger.info("Got PoW result, cost: $costTimeMS ms")
+            this.t547 = buildPacket {
+                writeByte(version)
+                writeByte(algorithmType)
+                writeByte(hashType)
+                writeByte(1) //resultStatus
+                writeShort(maxIndex)
+                writeFully(reserveBytes)
+                writeShortLVByteArray(inputBigNumArr)
+                writeShortLVByteArray(targetHashArr)
+                writeShortLVByteArray(reserveHashArr)
+                writeShortLVByteArray(resultArr)
+                writeInt(costTimeMS)
+                writeInt(recursiveDepth)
+            }.readBytes()
+        } else {
+            bot.logger.warning("Failed to get PoW result, login may fail with error 0x6!")
+        }
+
+    }
+
     fun QQAndroidClient.analysisTlv161(t161: ByteArray) {
         val tlv = t161.toReadPacket().apply { discardExact(2) }.withUse { _readTLVMap() }
 

+ 8 - 0
mirai-deps-test/test/CoreShadowRelocationTest.kt

@@ -23,6 +23,7 @@ class CoreShadowRelocationTest : AbstractTest() {
         private const val KtorOkHttp = "io.ktor.client.engine.okhttp.OkHttp"
         private const val OkHttp = "okhttp3.OkHttp"
         private const val OkIO = "okio.ByteString"
+        private const val BigInteger = "com.ionspin.kotlin.bignum.integer.BigInteger"
 
         fun relocated(string: String): String {
             return "net.mamoe.mirai.internal.deps.$string"
@@ -38,6 +39,7 @@ class CoreShadowRelocationTest : AbstractTest() {
             -both(`ktor-client-okhttp`)
             -both(`okhttp3-okhttp`)
             -both(okio)
+            -both(`kt-bignum`)
         }
         applyCodeFragment(fragment)
         buildFile.appendText(
@@ -59,6 +61,7 @@ class CoreShadowRelocationTest : AbstractTest() {
             -both(`ktor-client-okhttp`)
             -both(`okhttp3-okhttp`)
             -both(okio)
+            -both(`kt-bignum`)
             +relocated(`ExternalResource-input`)
         }
         applyCodeFragment(fragment)
@@ -82,6 +85,7 @@ class CoreShadowRelocationTest : AbstractTest() {
             +relocated(`okhttp3-okhttp`)
             +relocated(okio)
             +relocated(`ExternalResource-input`)
+            +relocated(`kt-bignum`)
         }
         applyCodeFragment(fragment)
         buildFile.appendText(
@@ -106,6 +110,7 @@ class CoreShadowRelocationTest : AbstractTest() {
             +relocated(`ktor-client-okhttp`)
             +relocated(`okhttp3-okhttp`)
             +relocated(okio)
+            +relocated(`kt-bignum`)
         }
         applyCodeFragment(fragment)
         buildFile.appendText(
@@ -130,6 +135,7 @@ class CoreShadowRelocationTest : AbstractTest() {
             -both(`ktor-client-okhttp`)
             -both(`okhttp3-okhttp`)
             -both(okio)
+            -both(`kt-bignum`)
 //            +relocated(`ExternalResource-input`) // Will fail with no class def found error because there is no runtime ktor-io
         }
         applyCodeFragment(fragment)
@@ -155,6 +161,7 @@ class CoreShadowRelocationTest : AbstractTest() {
             +relocated(`okhttp3-okhttp`)
             +relocated(okio)
             +relocated(`ExternalResource-input`)
+            +relocated(`kt-bignum`)
         }
         applyCodeFragment(fragment)
 
@@ -231,6 +238,7 @@ class CoreShadowRelocationTest : AbstractTest() {
         val `ktor-client-okhttp` = ClassTestCase("ktor-client-core OkHttp", KtorOkHttp)
         val `okhttp3-okhttp` = ClassTestCase("okhttp3 OkHttp", OkHttp)
         val okio = ClassTestCase("okio ByteString", OkIO)
+        val `kt-bignum` = ClassTestCase("kt-bignum BigInteger", BigInteger)
         val `ExternalResource-input` =
             FunctionTestCase(
                 "ExternalResource_input",