Browse Source

Android stuff

Him188 6 years ago
parent
commit
62b3740a72

+ 54 - 0
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroid.kt

@@ -0,0 +1,54 @@
+package net.mamoe.mirai.qqandroid.network
+
+import net.mamoe.mirai.BotAccount
+import net.mamoe.mirai.utils.io.chunkedHexToBytes
+
+/**
+ * From QQAndroid 8.2.0
+ * `oicq.wlogin_sdk.tools.EcdhCrypt`
+ *
+ * Constant to avoid calculations
+ */
+interface ECDH {
+    object Default : ECDH {
+        override val publicKey: ByteArray = "020b03cf3d99541f29ffec281bebbd4ea211292ac1f53d7128".chunkedHexToBytes()
+        override val shareKey: ByteArray = "4da0f614fc9f29c2054c77048a6566d7".chunkedHexToBytes()
+        override val privateKey: ByteArray = ByteArray(16)
+    }
+
+    val publicKey: ByteArray
+
+    val shareKey: ByteArray
+
+    val privateKey: ByteArray
+}
+
+
+/*
+ APP ID:
+ GetStViaSMSVerifyLogin = 16
+ GetStWithoutPasswd = 16
+ */
+
+
+class QQAndroidDevice(
+    private val account: BotAccount,
+    /**
+     * 协议版本?, 8.2.0 的为 8001
+     */
+    @PublishedApi
+    internal val protocolVersion: Short = 8001,
+
+    @PublishedApi
+    internal val ecdh: ECDH = ECDH.Default,
+
+    @PublishedApi
+    internal val appClientVersion: Int
+) {
+    val uin: Long get() = account.id
+    val password: String get() = account.password
+
+    object Debugging {
+
+    }
+}

+ 241 - 0
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/io/JceOutput.kt

@@ -0,0 +1,241 @@
+package net.mamoe.mirai.qqandroid.network.io
+
+import kotlinx.io.charsets.Charset
+import kotlinx.io.core.BytePacketBuilder
+import kotlinx.io.core.ExperimentalIoApi
+import kotlinx.io.core.toByteArray
+import kotlinx.io.core.writeFully
+import kotlin.reflect.KClass
+
+/**
+ *
+ * From: com.qq.taf.jce.JceOutputStream
+ */
+@Suppress("unused", "MemberVisibilityCanBePrivate")
+@UseExperimental(ExperimentalIoApi::class)
+class JceOutput(
+    private val stringCharset: Charset = Charset.forName("GBK")
+) {
+    private val output: BytePacketBuilder = BytePacketBuilder()
+
+    fun close() = output.close()
+    fun flush() = output.flush()
+
+    fun writeByte(v: Byte, tag: Int) {
+        if (v.toInt() == 0) {
+            writeHead(ZERO_TAG, tag)
+        } else {
+            writeHead(BYTE, tag)
+            output.writeByte(v)
+        }
+    }
+
+    fun writeDouble(v: Double, tag: Int) {
+        writeHead(DOUBLE, tag)
+        output.writeDouble(v)
+    }
+
+    fun writeFloat(v: Float, tag: Int) {
+        writeHead(FLOAT, tag)
+        output.writeFloat(v)
+    }
+
+    fun writeFully(src: ByteArray, tag: Int) {
+        writeHead(SIMPLE_LIST, tag)
+        writeHead(BYTE, 0)
+        writeInt(src.size, 0)
+        output.writeFully(src)
+    }
+
+    fun writeFully(src: DoubleArray, tag: Int) {
+        writeHead(LIST, tag)
+        writeInt(src.size, 0)
+        src.forEach {
+            writeDouble(it, 0)
+        }
+    }
+
+    fun writeFully(src: FloatArray, tag: Int) {
+        writeHead(LIST, tag)
+        writeInt(src.size, 0)
+        src.forEach {
+            writeFloat(it, 0)
+        }
+    }
+
+    fun writeFully(src: IntArray, tag: Int) {
+        writeHead(LIST, tag)
+        writeInt(src.size, 0)
+        src.forEach {
+            writeInt(it, 0)
+        }
+    }
+
+    fun writeFully(src: LongArray, tag: Int) {
+        writeHead(LIST, tag)
+        writeInt(src.size, 0)
+        src.forEach {
+            writeLong(it, 0)
+        }
+    }
+
+    fun writeFully(src: ShortArray, tag: Int) {
+        writeHead(LIST, tag)
+        writeInt(src.size, 0)
+        src.forEach {
+            writeShort(it, 0)
+        }
+    }
+
+    fun writeFully(src: BooleanArray, tag: Int) {
+        writeHead(LIST, tag)
+        writeInt(src.size, 0)
+        src.forEach {
+            writeBoolean(it, 0)
+        }
+    }
+
+    fun <T> writeFully(src: Array<T>, tag: Int) {
+        writeHead(LIST, tag)
+        writeInt(src.size, 0)
+        src.forEach {
+            writeObject(it, 0)
+        }
+    }
+
+    fun writeInt(v: Int, tag: Int) {
+        if (v in Short.MIN_VALUE..Short.MAX_VALUE) {
+            writeShort(v.toShort(), tag)
+        } else {
+            writeHead(INT, tag)
+            output.writeInt(v)
+        }
+    }
+
+    fun writeLong(v: Long, tag: Int) {
+        if (v in Int.MIN_VALUE..Int.MAX_VALUE) {
+            writeInt(v.toInt(), tag)
+        } else {
+            writeHead(LONG, tag)
+            output.writeLong(v)
+        }
+    }
+
+    fun writeShort(v: Short, tag: Int) {
+        if (v in Byte.MIN_VALUE..Byte.MAX_VALUE) {
+            writeByte(v.toByte(), tag)
+        } else {
+            writeHead(BYTE, tag)
+            output.writeShort(v)
+        }
+    }
+
+    fun writeBoolean(v: Boolean, tag: Int) {
+        this.writeByte(if (v) 1 else 0, tag)
+    }
+
+    fun writeString(v: String, tag: Int) {
+        val array = v.toByteArray(stringCharset)
+        if (array.size > 255) {
+            writeHead(STRING4, tag)
+            output.writeInt(array.size)
+            output.writeFully(array)
+        } else {
+            writeHead(STRING1, tag)
+            output.writeByte(array.size.toByte())
+            output.writeFully(array)
+        }
+    }
+
+    fun <K, V> writeMap(map: Map<K, V>, tag: Int) {
+        writeHead(MAP, tag)
+        if (map.isEmpty()) {
+            writeInt(0, 0)
+        } else {
+            writeInt(map.size, 0)
+            map.forEach { (key, value) ->
+                writeObject(key, 0)
+                writeObject(value, 0)
+            }
+        }
+    }
+
+    fun writeCollection(collection: Collection<*>?, tag: Int) {
+        writeHead(LIST, tag)
+        if (collection == null || collection.isEmpty()) {
+            writeInt(0, 0)
+        } else {
+            writeInt(collection.size, 0)
+            collection.forEach {
+                writeObject(it, 0)
+            }
+        }
+    }
+
+    fun writeJceStruct(v: JceStruct, tag: Int) {
+        writeHead(STRUCT_BEGIN, tag)
+        v.writeTo(this)
+        writeHead(STRUCT_END, 0)
+    }
+
+    fun <T> writeObject(v: T, tag: Int) {
+        when (v) {
+            is Byte -> writeByte(v, tag)
+            is Boolean -> writeBoolean(v, tag)
+            is Short -> writeShort(v, tag)
+            is Int -> writeInt(v, tag)
+            is Long -> writeLong(v, tag)
+            is Float -> writeFloat(v, tag)
+            is Double -> writeDouble(v, tag)
+            is Map<*, *> -> writeMap(v, tag)
+            is Collection<*> -> writeCollection(v, tag)
+            is JceStruct -> writeJceStruct(v, tag)
+            is ByteArray -> writeFully(v, tag)
+            is IntArray -> writeFully(v, tag)
+            is ShortArray -> writeFully(v, tag)
+            is BooleanArray -> writeFully(v, tag)
+            is LongArray -> writeFully(v, tag)
+            is FloatArray -> writeFully(v, tag)
+            is DoubleArray -> writeFully(v, tag)
+            is Array<*> -> writeFully(v, tag)
+            else -> error("unsupported type: ${v.getClassName()}")
+        }
+    }
+
+    @PublishedApi
+    internal companion object {
+        const val BYTE: Int = 0
+        const val DOUBLE: Int = 5
+        const val FLOAT: Int = 4
+        const val INT: Int = 2
+        const val JCE_MAX_STRING_LENGTH = 104857600
+        const val LIST: Int = 9
+        const val LONG: Int = 3
+        const val MAP: Int = 8
+        const val SHORT: Int = 1
+        const val SIMPLE_LIST: Int = 13
+        const val STRING1: Int = 6
+        const val STRING4: Int = 7
+        const val STRUCT_BEGIN: Int = 10
+        const val STRUCT_END: Int = 11
+        const val ZERO_TAG: Int = 12
+
+        private fun Any?.getClassName(): KClass<out Any> = if (this == null) Unit::class else this::class
+    }
+
+    @PublishedApi
+    internal fun writeHead(type: Int, tag: Int) {
+        if (tag < 15) {
+            this.output.writeByte((tag shl 4 or type).toByte())
+            return
+        }
+        if (tag < 256) {
+            this.output.writeByte((type or 0xF0).toByte())
+            this.output.writeByte(tag.toByte())
+            return
+        }
+        throw JceEncodeException("tag is too large: $tag")
+    }
+}
+
+class JceEncodeException(message: String) : RuntimeException(message)

+ 5 - 0
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/io/JceStruct.kt

@@ -0,0 +1,5 @@
+package net.mamoe.mirai.qqandroid.network.io
+
+abstract class JceStruct {
+    abstract fun writeTo(p0: JceOutput)
+}

+ 0 - 95
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/packet/OutgoingPacketHelperAndroid.kt

@@ -1,95 +0,0 @@
-package net.mamoe.mirai.qqandroid.network.packet
-
-import kotlinx.atomicfu.AtomicInt
-import kotlinx.atomicfu.atomic
-import kotlinx.io.core.BytePacketBuilder
-
-
-/*
-private open fun writeHead(
-    always_8001: Int,
-    command: Int,
-    uin: Long,
-    encryptType: Int,
-    const8_always_0: Int,
-    appClientVersion: Int,
-    constp_always_0: Int,
-    bodyLength: Int
-) {
-    val j: Int = this.j + 1
-    this.j = j
-    this.pos = 0
-    util.int8_to_buf(this.buffer, this.pos, 2)
-    ++this.pos
-    util.int16_to_buf(this.buffer, this.pos, this.d + 2 + bodyLength)
-    this.pos += 2
-    util.int16_to_buf(this.buffer, this.pos, always_8001)
-    this.pos += 2
-    util.int16_to_buf(this.buffer, this.pos, command)
-    this.pos += 2
-    util.int16_to_buf(this.buffer, this.pos, j)
-    this.pos += 2
-    util.int32_to_buf(this.buffer, this.pos, uin.toInt())
-    this.pos += 4
-    util.int8_to_buf(this.buffer, this.pos, 3)
-    ++this.pos
-    util.int8_to_buf(this.buffer, this.pos, encryptType)
-    ++this.pos
-    util.int8_to_buf(this.buffer, this.pos, const8_always_0)
-    ++this.pos
-    util.int32_to_buf(this.buffer, this.pos, 2)
-    this.pos += 4
-    util.int32_to_buf(this.buffer, this.pos, appClientVersion)
-    this.pos += 4
-    util.int32_to_buf(this.buffer, this.pos, constp_always_0)
-    this.pos += 4
-}
-*/
-
-@UseExperimental(ExperimentalUnsignedTypes::class)
-private fun BytePacketBuilder.writeHead(
-    always_8001: Short = 8001,
-    command: Short,
-    uin: Long,
-    encryptType: Int, //
-    sequenceId: Int = SequenceIdCounter.nextSequenceId(),
-    const8_always_0: Byte = 0,
-    appClientVersion: Int,
-    constp_always_0: Int = 0,
-    bodyLength: Int
-) {
-    writeByte(2)
-    writeShort((27 + 2 + bodyLength).toShort())
-    writeShort(always_8001)
-    writeShort(command)
-    writeShort(sequenceId.toShort())
-    writeInt(uin.toInt())
-    writeByte(3)
-    writeByte(encryptType.toByte())
-    writeByte(const8_always_0)
-    writeInt(2)
-    writeInt(appClientVersion)
-    writeInt(constp_always_0)
-}
-
-fun buildOutgoingPacket(
-    command: Short
-    ///uin: Long,
-) {
-
-}
-
-//private b
-
-private object SequenceIdCounter {
-    private val sequenceId: AtomicInt = atomic(0)
-
-    fun nextSequenceId(): Int {
-        val id = sequenceId.getAndAdd(1)
-        if (id > Short.MAX_VALUE.toInt() * 2) {
-            sequenceId.value = 0
-            return nextSequenceId()
-        }
-        return id
-    }
-}

+ 0 - 158
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/packet/tlv/Tlv.kt

@@ -1,158 +0,0 @@
-package net.mamoe.mirai.qqandroid.network.packet.tlv
-
-import kotlinx.io.core.BytePacketBuilder
-import kotlinx.io.core.buildPacket
-import kotlinx.io.core.readBytes
-import kotlinx.io.core.writeFully
-import net.mamoe.mirai.utils.io.*
-import net.mamoe.mirai.utils.md5
-import kotlin.random.Random
-
-object Tlv {
-    fun BytePacketBuilder.t1(qq: Long, ip: ByteArray) {
-        require(ip.size == 4)
-        writeShort(0x0001)
-        writeShortLVPacket {
-            writeShort(1) // ip_ver
-            writeInt(Random.nextInt())
-            writeInt(qq.toInt())
-            writeTime()
-            writeFully(ip)
-            writeShort(0)
-        }
-    }
-
-    fun BytePacketBuilder.t2(captchaCode: String, captchaToken: ByteArray, sigVer: Short = 0) {
-        writeShort(0x0002)
-        writeShortLVPacket {
-            writeShort(sigVer)
-            writeShortLVString(captchaCode)
-            writeShortLVByteArray(captchaToken)
-        }
-    }
-
-    fun BytePacketBuilder.t8() {
-        writeShort(0x0008)
-        writeShortLVPacket {
-            writeShort(0)
-            writeInt(2052) // localId
-            writeShort(0)
-        }
-    }
-
-    fun BytePacketBuilder.t18(appId: Long, appClientVersion: Int, uin: Long, constant1_always_0: Int) {
-        writeShort(0x18)
-        writeShortLVPacket {
-            writeShort(1) //ping_version
-            writeInt(1536) //sso_version
-            writeInt(appId.toInt())
-            writeInt(appClientVersion)
-            writeInt(uin.toInt())
-            writeShort(constant1_always_0.toShort())
-            writeShort(0)
-        }
-    }
-
-    fun BytePacketBuilder.t106(
-        appId: Long,
-        subAppId: Long,
-        appClientVersion: Int,
-        uin: Long,
-        ipAddress: ByteArray,
-        n5_always_1: Int = 1,
-        temp_pwd: ByteArray,
-        salt: Long,
-        uinAccount: ByteArray,
-        tgtgtKey: ByteArray,
-        n7: Int,
-        array_6_may_be_null: ByteArray?,
-        ret_is_0_or_4: Int
-    ) {
-        writeShort(0x106)
-
-        writeShortLVPacket {
-            encryptAndWrite(
-                if (salt == 0L) {
-                    md5(buildPacket { writeFully(temp_pwd); writeInt(uin.toInt()) }.readBytes())
-                } else {
-                    md5(buildPacket { writeFully(temp_pwd); writeInt(salt.toInt()) }.readBytes())
-                }
-            ) {
-                writeShort(4)//TGTGTVer
-                writeInt(Random.nextInt())
-                writeInt(5)//ssoVer
-                writeInt(appId.toInt())
-                writeInt(appClientVersion)
-
-                if (uin == 0L) {
-                    writeLong(salt)
-                } else {
-                    writeLong(uin)
-                }
-
-                writeTime()
-                writeFully(ipAddress)
-                writeByte(n5_always_1.toByte())
-                writeFully(temp_pwd)
-                writeFully(tgtgtKey)
-                writeInt(0)
-                writeByte(n7.toByte())
-                if (array_6_may_be_null == null) {
-                    repeat(4) {
-                        writeInt(Random.nextInt())
-                    }
-                } else {
-                    writeFully(array_6_may_be_null)
-                }
-                writeInt(subAppId.toInt())
-                writeInt(ret_is_0_or_4)
-                writeShortLVByteArray(uinAccount)
-            }
-        }
-    }
-
-    fun BytePacketBuilder.t100(
-        appId: Long,
-        subAppId: Long,
-        appClientVersion: Int,
-        mainSigMap: Int
-    ) {
-        writeShort(0x100)
-        writeShortLVPacket {
-            writeShort(1)//db_buf_ver
-            writeInt(5)//sso_ver
-            writeInt(appId.toInt())
-            writeInt(subAppId.toInt())
-            writeInt(appClientVersion)
-            writeInt(mainSigMap)
-        } shouldEqualsTo 22
-    }
-
-    fun BytePacketBuilder.t107(
-        picType: Int,
-        const1_always_0: Int = 0,
-        const2_always_0: Int = 0,
-        const3_always_1: Int = 1
-    ) {
-        writeShort(0x107)
-        writeShortLVPacket {
-            writeShort(picType.toShort())
-            writeByte(const1_always_0.toByte())
-            writeShort(const2_always_0.toShort())
-            writeByte(const3_always_1.toByte())
-        } shouldEqualsTo 6
-    }
-}
-
-
-private infix fun Int.shouldEqualsTo(int: Int) = require(this == int)
-
-fun randomAndroidId(): String = buildString(15) {
-    repeat(15) { append(Random.nextInt(10)) }
-}
-
-fun generateGuid(androidId: String, macAddress: String): ByteArray {
-    return md5(androidId + macAddress)
-}
-
-fun getMacAddr(): String = "02:00:00:00:00:00"

+ 134 - 0
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/OutgoingPacketAndroid.kt

@@ -0,0 +1,134 @@
+package net.mamoe.mirai.qqandroid.network.protocol.packet
+
+
+import kotlinx.io.core.*
+import net.mamoe.mirai.data.Packet
+import net.mamoe.mirai.qqandroid.network.QQAndroidDevice
+import net.mamoe.mirai.qqandroid.network.protocol.packet.login.PacketId
+import net.mamoe.mirai.utils.MiraiInternalAPI
+import net.mamoe.mirai.utils.io.writeQQ
+
+/**
+ * 待发送给服务器的数据包. 它代表着一个 [ByteReadPacket],
+ */
+@UseExperimental(ExperimentalUnsignedTypes::class)
+class OutgoingPacket constructor(
+    name: String?,
+    val packetId: PacketId,
+    val sequenceId: UShort,
+    val delegate: ByteReadPacket
+) : Packet {
+    val name: String by lazy {
+        name ?: packetId.toString()
+    }
+}
+
+/*
+private open fun writeHead(
+    always_8001: Int,
+    command: Int,
+    uin: Long,
+    encryptType: Int,
+    const8_always_0: Int,
+    appClientVersion: Int,
+    constp_always_0: Int,
+    bodyLength: Int
+) {
+    val j: Int = this.j + 1
+    this.j = j
+    this.pos = 0
+    util.int8_to_buf(this.buffer, this.pos, 2)
+    ++this.pos
+    util.int16_to_buf(this.buffer, this.pos, this.d + 2 + bodyLength)
+    this.pos += 2
+    util.int16_to_buf(this.buffer, this.pos, always_8001)
+    this.pos += 2
+    util.int16_to_buf(this.buffer, this.pos, command)
+    this.pos += 2
+    util.int16_to_buf(this.buffer, this.pos, j)
+    this.pos += 2
+    util.int32_to_buf(this.buffer, this.pos, uin.toInt())
+    this.pos += 4
+    util.int8_to_buf(this.buffer, this.pos, 3)
+    ++this.pos
+    util.int8_to_buf(this.buffer, this.pos, encryptType)
+    ++this.pos
+    util.int8_to_buf(this.buffer, this.pos, const8_always_0)
+    ++this.pos
+    util.int32_to_buf(this.buffer, this.pos, 2)
+    this.pos += 4
+    util.int32_to_buf(this.buffer, this.pos, appClientVersion)
+    this.pos += 4
+    util.int32_to_buf(this.buffer, this.pos, constp_always_0)
+    this.pos += 4
+}
+*/
+
+@UseExperimental(ExperimentalUnsignedTypes::class)
+private fun BytePacketBuilder.writeHead(
+    always_8001: Short = 8001,
+    command: Short,
+    uin: Long,
+    encryptType: Int, //
+    sequenceId: UShort = PacketFactory.atomicNextSequenceId(),
+    const8_always_0: Byte = 0,
+    appClientVersion: Int,
+    constp_always_0: Int = 0,
+    bodyLength: Int
+) {
+    writeByte(2)
+    writeShort((27 + 2 + bodyLength).toShort())
+    writeShort(always_8001)
+    writeShort(command)
+    writeUShort(sequenceId)
+    writeInt(uin.toInt())
+    writeByte(3)
+    writeByte(encryptType.toByte())
+    writeByte(const8_always_0)
+    writeInt(2)
+    writeInt(appClientVersion)
+    writeInt(constp_always_0)
+}
+
+@UseExperimental(ExperimentalUnsignedTypes::class)
+inline class EncryptMethod(val value: UByte) {
+    companion object {
+        val BySessionToken = EncryptMethod(69u)
+        val ByECDH7 = EncryptMethod(7u)
+        // 登录都使用 135
+        val ByECDH135 = EncryptMethod(135u)
+    }
+}
+
+@UseExperimental(ExperimentalUnsignedTypes::class, MiraiInternalAPI::class)
+inline fun PacketFactory<*, *>.buildOutgoingPacket(
+    device: QQAndroidDevice,
+    encryptMethod: EncryptMethod,
+    name: String? = null,
+    id: PacketId = this.id,
+    sequenceId: UShort = PacketFactory.atomicNextSequenceId(),
+    bodyBlock: BytePacketBuilder.() -> Unit
+): OutgoingPacket {
+    val body = buildPacket { bodyBlock() }
+    return OutgoingPacket(name, id, sequenceId, buildPacket {
+        // Head
+        writeByte(0x02) // head
+        writeShort((27 + 2 + body.remaining).toShort()) // orthodox algorithm
+        writeShort(device.protocolVersion)
+        writeShort(id.commandId.toShort())
+        writeShort(sequenceId.toShort())
+        writeQQ(device.uin)
+        writeByte(3) // originally const
+        writeUByte(encryptMethod.value)
+        writeByte(0) // const8_always_0
+        writeInt(2) // originally const
+        writeInt(device.appClientVersion)
+        writeInt(0) // constp_always_0
+
+        // Body
+        writePacket(body)
+
+        // Tail
+        writeByte(0x03) // tail
+    })
+}

+ 76 - 0
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt

@@ -0,0 +1,76 @@
+package net.mamoe.mirai.qqandroid.network.protocol.packet
+
+import kotlinx.atomicfu.AtomicInt
+import kotlinx.atomicfu.atomic
+import kotlinx.io.core.ByteReadPacket
+import kotlinx.io.core.discardExact
+import kotlinx.io.core.readBytes
+import kotlinx.serialization.DeserializationStrategy
+import kotlinx.serialization.protobuf.ProtoBuf
+import net.mamoe.mirai.data.Packet
+import net.mamoe.mirai.network.BotNetworkHandler
+import net.mamoe.mirai.qqandroid.network.protocol.packet.login.NullPacketId
+import net.mamoe.mirai.qqandroid.network.protocol.packet.login.PacketId
+import net.mamoe.mirai.utils.cryptor.Decrypter
+import net.mamoe.mirai.utils.cryptor.DecrypterType
+import net.mamoe.mirai.utils.cryptor.readProtoMap
+import net.mamoe.mirai.utils.io.debugPrint
+import net.mamoe.mirai.utils.io.read
+
+/**
+ * 一种数据包的处理工厂. 它可以解密解码服务器发来的这个包, 也可以编码加密要发送给服务器的这个包
+ * 应由一个 `object` 实现, 且实现 `operator fun invoke`
+ *
+ * @param TPacket 服务器回复包解析结果
+ * @param TDecrypter 服务器回复包解密器
+ */
+@UseExperimental(ExperimentalUnsignedTypes::class)
+abstract class PacketFactory<out TPacket : Packet, TDecrypter : Decrypter>(val decrypterType: DecrypterType<TDecrypter>) {
+
+    @Suppress("PropertyName")
+    internal var _id: PacketId = NullPacketId
+
+    /**
+     * 包 ID.
+     */
+    open val id: PacketId get() = _id
+
+    /**
+     * **解码**服务器的回复数据包
+     */
+    abstract suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): TPacket
+
+    fun <T> ByteReadPacket.decodeProtoPacket(
+        deserializer: DeserializationStrategy<T>,
+        debuggingTag: String? = null
+    ): T {
+        val headLength = readInt()
+        val protoLength = readInt()
+        if (debuggingTag != null) {
+            readBytes(headLength).debugPrint("$debuggingTag head")
+        } else {
+            discardExact(headLength)
+        }
+        val bytes = readBytes(protoLength)
+        // println(ByteReadPacket(bytes).readProtoMap())
+
+        if (debuggingTag != null) {
+            bytes.read { readProtoMap() }.toString().debugPrint("$debuggingTag proto")
+        }
+
+        return ProtoBuf.load(deserializer, bytes)
+    }
+
+    companion object {
+        private val sequenceId: AtomicInt = atomic(1)
+
+        fun atomicNextSequenceId(): UShort {
+            val id = sequenceId.getAndAdd(1)
+            if (id > Short.MAX_VALUE.toInt() * 2) {
+                sequenceId.value = 0
+                return atomicNextSequenceId()
+            }
+            return id.toUShort()
+        }
+    }
+}

+ 9 - 7
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/packet/TouchPacket.kt → mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/TouchPacket.kt

@@ -1,20 +1,22 @@
-package net.mamoe.mirai.qqandroid.network.packet
+package net.mamoe.mirai.qqandroid.network.protocol.packet
 
 import kotlinx.io.core.ByteReadPacket
 import net.mamoe.mirai.data.Packet
 import net.mamoe.mirai.network.BotNetworkHandler
-import net.mamoe.mirai.network.packet.DecrypterByteArray
-import net.mamoe.mirai.network.packet.DecrypterType
-import net.mamoe.mirai.network.packet.PacketFactory
-import net.mamoe.mirai.network.packet.PacketId
+import net.mamoe.mirai.qqandroid.network.protocol.packet.login.PacketId
+import net.mamoe.mirai.utils.cryptor.DecrypterByteArray
+import net.mamoe.mirai.utils.cryptor.DecrypterType
 
 
-object TouchKey : DecrypterByteArray, DecrypterType<TouchKey> {
+object TouchKey : DecrypterByteArray,
+    DecrypterType<TouchKey> {
     override val value: ByteArray
         get() = TODO("not implemented")
 }
 
-object TouchPacket : PacketFactory<TouchPacketResponse, TouchKey>(TouchKey) {
+object TouchPacket : PacketFactory<TouchPacketResponse, TouchKey>(
+    TouchKey
+) {
     @UseExperimental(ExperimentalUnsignedTypes::class)
     override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): TouchPacketResponse {
         TODO("not implemented")

+ 52 - 0
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/LoginPacket.kt

@@ -0,0 +1,52 @@
+package net.mamoe.mirai.qqandroid.network.protocol.packet.login
+
+
+import kotlinx.io.core.ByteReadPacket
+import net.mamoe.mirai.data.Packet
+import net.mamoe.mirai.network.BotNetworkHandler
+import net.mamoe.mirai.qqandroid.network.QQAndroidDevice
+import net.mamoe.mirai.qqandroid.network.protocol.packet.EncryptMethod
+import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket
+import net.mamoe.mirai.qqandroid.network.protocol.packet.PacketFactory
+import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingPacket
+import net.mamoe.mirai.qqandroid.network.protocol.packet.tlv.writeTLVList
+import net.mamoe.mirai.utils.cryptor.DecrypterByteArray
+import net.mamoe.mirai.utils.cryptor.DecrypterType
+
+class LoginPacketDecrypter(override val value: ByteArray) : DecrypterByteArray {
+    companion object : DecrypterType<LoginPacketDecrypter> {
+
+    }
+}
+
+@UseExperimental(ExperimentalUnsignedTypes::class)
+object LoginPacket : PacketFactory<LoginPacket.LoginPacketResponse, LoginPacketDecrypter>(LoginPacketDecrypter) {
+
+    fun invoke(
+        device: QQAndroidDevice
+    ): OutgoingPacket = buildOutgoingPacket(device, EncryptMethod.ByECDH135) {
+        writeTLVList {
+
+        }
+    }
+
+
+    class LoginPacketResponse : Packet
+
+    override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): LoginPacketResponse {
+
+        TODO()
+    }
+}
+
+interface PacketId {
+    val commandId: Int // ushort actually
+    val subCommandId: Int // ushort actually
+}
+
+object NullPacketId : PacketId {
+    override val commandId: Int
+        get() = error("uninitialized")
+    override val subCommandId: Int
+        get() = error("uninitialized")
+}

+ 401 - 0
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/tlv/Tlv.kt

@@ -0,0 +1,401 @@
+package net.mamoe.mirai.qqandroid.network.protocol.packet.tlv
+
+import kotlinx.io.core.*
+import net.mamoe.mirai.utils.io.*
+import net.mamoe.mirai.utils.md5
+import kotlin.random.Random
+
+
+fun BytePacketBuilder.writeTLVList(block: TlvBuilder.() -> Unit) {
+    var tlvCount = 0
+    val tlvList = buildPacket { block(TlvBuilder { tlvCount++ }) }
+    writeShort(tlvCount.toShort())
+    writePacket(tlvList)
+}
+
+
+inline class LoginType(
+    val value: Int
+) {
+    companion object {
+        val SMS = LoginType(3)
+        val PASSWORD = LoginType(1)
+        val WE_CHAT = LoginType(4)
+    }
+}
+
+inline class TlvBuilder(
+    val counter: () -> Unit
+) {
+    fun BytePacketBuilder.t1(uin: Long, ip: ByteArray) {
+        require(ip.size == 4)
+        writeShort(0x1)
+        writeShortLVPacket {
+            writeShort(1) // _ip_ver
+            writeInt(Random.nextInt())
+            writeInt(uin.toInt())
+            writeTime()
+            writeFully(ip)
+            writeShort(0)
+        } shouldEqualsTo 20
+    }
+
+    fun BytePacketBuilder.t2(captchaCode: String, captchaToken: ByteArray, sigVer: Short = 0) {
+        writeShort(0x2)
+        writeShortLVPacket {
+            writeShort(sigVer)
+            writeShortLVString(captchaCode)
+            writeShortLVByteArray(captchaToken)
+        }
+    }
+
+    fun BytePacketBuilder.t8() {
+        writeShort(0x0008)
+        writeShortLVPacket {
+            writeShort(0)
+            writeInt(2052) // localId
+            writeShort(0)
+        }
+    }
+
+    fun BytePacketBuilder.t18(appId: Long, appClientVersion: Int, uin: Long, constant1_always_0: Int) {
+        writeShort(0x18)
+        writeShortLVPacket {
+            writeShort(1) //_ping_version
+            writeInt(1536) //_sso_version
+            writeInt(appId.toInt())
+            writeInt(appClientVersion)
+            writeInt(uin.toInt())
+            writeShort(constant1_always_0.toShort())
+            writeShort(0)
+        } shouldEqualsTo 22
+    }
+
+    fun BytePacketBuilder.t106(
+        appId: Long,
+        subAppId: Long,
+        appClientVersion: Int,
+        uin: Long,
+        ipAddress: ByteArray,
+        n5_always_1: Int = 1,
+        temp_pwd: ByteArray,
+        salt: Long,
+        uinAccount: ByteArray,
+        tgtgtKey: ByteArray,
+        n7: Int,
+        array_6_may_be_null: ByteArray?,
+        loginType: LoginType
+    ) {
+        writeShort(0x106)
+
+        writeShortLVPacket {
+            encryptAndWrite(
+                if (salt == 0L) {
+                    md5(buildPacket { writeFully(temp_pwd); writeInt(uin.toInt()) }.readBytes())
+                } else {
+                    md5(buildPacket { writeFully(temp_pwd); writeInt(salt.toInt()) }.readBytes())
+                }
+            ) {
+                writeShort(4)//TGTGTVer
+                writeInt(Random.nextInt())
+                writeInt(5)//ssoVer
+                writeInt(appId.toInt())
+                writeInt(appClientVersion)
+
+                if (uin == 0L) {
+                    writeLong(salt)
+                } else {
+                    writeLong(uin)
+                }
+
+                writeTime()
+                writeFully(ipAddress)
+                writeByte(n5_always_1.toByte())
+                writeFully(temp_pwd)
+                writeFully(tgtgtKey)
+                writeInt(0)
+                writeByte(n7.toByte())
+                if (array_6_may_be_null == null) {
+                    repeat(4) {
+                        writeInt(Random.nextInt())
+                    }
+                } else {
+                    writeFully(array_6_may_be_null)
+                }
+                writeInt(subAppId.toInt())
+                writeInt(loginType.value)
+                writeShortLVByteArray(uinAccount)
+            }
+        } shouldEqualsTo 98
+    }
+
+    fun BytePacketBuilder.t116(
+        miscBitmap: Int,
+        subSigMap: Int,
+        appIdList: LongArray
+    ) {
+        writeShort(0x116)
+        writeShortLVPacket {
+            writeByte(0) // _ver
+            writeInt(miscBitmap)
+            writeInt(subSigMap)
+            writeByte(appIdList.size.toByte())
+            appIdList.forEach {
+                writeInt(it.toInt())
+            }
+        }
+    }
+
+    fun BytePacketBuilder.t100(
+        appId: Long,
+        subAppId: Long,
+        appClientVersion: Int,
+        mainSigMap: Int
+    ) {
+        writeShort(0x100)
+        writeShortLVPacket {
+            writeShort(1)//db_buf_ver
+            writeInt(5)//sso_ver
+            writeInt(appId.toInt())
+            writeInt(subAppId.toInt())
+            writeInt(appClientVersion)
+            writeInt(mainSigMap)
+        } shouldEqualsTo 22
+    }
+
+    fun BytePacketBuilder.t107(
+        picType: Int,
+        const1_always_0: Int = 0,
+        const2_always_0: Int = 0,
+        const3_always_1: Int = 1
+    ) {
+        writeShort(0x107)
+        writeShortLVPacket {
+            writeShort(picType.toShort())
+            writeByte(const1_always_0.toByte())
+            writeShort(const2_always_0.toShort())
+            writeByte(const3_always_1.toByte())
+        } shouldEqualsTo 6
+    }
+
+    fun BytePacketBuilder.t108(
+        to_verify_passwd_img: ByteArray
+    ) {
+        writeShort(0x108)
+        writeShortLVPacket {
+            writeFully(to_verify_passwd_img)
+        }
+    }
+
+    fun BytePacketBuilder.t104(
+        t104Data: ByteArray
+    ) {
+        writeShort(0x104)
+        writeShortLVPacket {
+            writeFully(t104Data)
+        }
+    }
+
+    /**
+     * @param apkId application.getPackageName().getBytes()
+     */
+    fun BytePacketBuilder.t142(
+        apkId: ByteArray
+    ) {
+        writeShort(0x142)
+        writeShortLVPacket {
+            writeShort(0) //_version
+            writeShortLVByteArrayLimitedLength(apkId, 32)
+        }
+    }
+
+    fun BytePacketBuilder.t112(
+        nonNumberUin: ByteArray
+    ) {
+        writeShort(0x112)
+        writeShortLVPacket {
+            writeFully(nonNumberUin)
+        }
+    }
+
+    fun BytePacketBuilder.t144(
+        // t109
+        androidId: ByteArray,
+
+        // t52d
+        androidDevInfo: ByteArray,
+
+        // t124
+        osType: ByteArray = "android".toByteArray(),
+        osVersion: ByteArray,
+        ipv6NetType: Int,
+        simInfo: ByteArray,
+        unknown: ByteArray,
+        apn: ByteArray = "wifi".toByteArray(),
+
+        // t128
+        isGuidFromFileNull: Boolean = false,
+        isGuidAvailable: Boolean = true,
+        isGuidChanged: Boolean = false,
+        guidFlag: Int,
+        buildModel: ByteArray,
+        guid: ByteArray,
+        buildBrand: ByteArray,
+
+        // encrypt
+        tgtgtKey: ByteArray
+    ) {
+        writeShort(0x144)
+        writeShortLVPacket {
+            encryptAndWrite(tgtgtKey) {
+                t109(androidId)
+                t52d(androidDevInfo)
+                t124(osType, osVersion, ipv6NetType, simInfo, unknown, apn)
+                t128(isGuidFromFileNull, isGuidAvailable, isGuidChanged, guidFlag, buildModel, guid, buildBrand)
+                t16e(buildModel)
+            }
+        }
+    }
+
+    fun BytePacketBuilder.t109(
+        androidId: ByteArray
+    ) {
+        writeShort(0x109)
+        writeShortLVPacket {
+            writeFully(androidId)
+        }
+    }
+
+    fun BytePacketBuilder.t52d(
+        androidDevInfo: ByteArray // oicq.wlogin_sdk.tools.util#get_android_dev_info
+    ) {
+        writeShort(0x52d)
+        writeShortLVPacket {
+            writeFully(androidDevInfo)
+        }
+    }
+
+    fun BytePacketBuilder.t124(
+        osType: ByteArray = "android".toByteArray(),
+        osVersion: ByteArray, // Build.VERSION.RELEASE.toByteArray()
+        ipv6NetType: Int,  //oicq.wlogin_sdk.tools.util#get_network_type
+        simInfo: ByteArray, // oicq.wlogin_sdk.tools.util#get_sim_operator_name
+        unknown: ByteArray,
+        apn: ByteArray = "wifi".toByteArray() // oicq.wlogin_sdk.tools.util#get_apn_string
+    ) {
+        writeShort(0x124)
+        writeShortLVPacket {
+            writeShortLVByteArrayLimitedLength(osType, 16)
+            writeShortLVByteArrayLimitedLength(osVersion, 16)
+            writeShort(ipv6NetType.toShort())
+            writeShortLVByteArrayLimitedLength(simInfo, 16)
+            writeShortLVByteArrayLimitedLength(unknown, 32)
+            writeShortLVByteArrayLimitedLength(apn, 16)
+        }
+    }
+
+    fun BytePacketBuilder.t128(
+        isGuidFromFileNull: Boolean = false, // 保存到文件的 GUID 是否为 null
+        isGuidAvailable: Boolean = true, // GUID 是否可用(计算/读取成功)
+        isGuidChanged: Boolean = false, // GUID 是否有变动
+        /**
+         * guidFlag:
+         * ```java
+         * GUID_FLAG |= GUID_SRC << 24 & 0xFF000000;
+         * GUID_FLAG |= FLAG_MAC_ANDROIDID_GUID_CHANGE << 8 & 0xFF00;
+         * ```
+         *
+         * FLAG_MAC_ANDROIDID_GUID_CHANGE:
+         * ```java
+         * if (!Arrays.equals(currentMac, get_last_mac)) {
+         *     oicq.wlogin_sdk.request.t.FLAG_MAC_ANDROIDID_GUID_CHANGEMENT |= 0x1;
+         * }
+         * if (!Arrays.equals(currentAndroidId, get_last_android_id)) {
+         *     oicq.wlogin_sdk.request.t.FLAG_MAC_ANDROIDID_GUID_CHANGEMENT |= 0x2;
+         * }
+         * if (!Arrays.equals(currentGuid, get_last_guid)) {
+         *     oicq.wlogin_sdk.request.t.FLAG_MAC_ANDROIDID_GUID_CHANGEMENT |= 0x4;
+         * }
+         * ```
+         */
+        guidFlag: Int,
+        buildModel: ByteArray,  // android.os.Build.MODEL
+        /**
+         * [generateGuid] or `"%4;7t>;28<fc.5*6".toByteArray()`
+         */
+        guid: ByteArray,
+        buildBrand: ByteArray // android.os.Build.BRAND
+    ) {
+        writeShort(0x128)
+        writeShortLVPacket {
+            writeShort(0)
+            writeByte(isGuidFromFileNull.toByte())
+            writeByte(isGuidAvailable.toByte())
+            writeByte(isGuidChanged.toByte())
+            writeInt(guidFlag)
+            writeShortLVByteArrayLimitedLength(buildModel, 32)
+            writeShortLVByteArrayLimitedLength(guid, 16)
+            writeShortLVByteArrayLimitedLength(buildBrand, 16)
+        }
+    }
+
+    fun BytePacketBuilder.t16e(
+        buildModel: ByteArray
+    ) {
+        writeShort(0x16e)
+        writeShortLVPacket {
+            writeFully(buildModel)
+        }
+    }
+
+    fun BytePacketBuilder.t145(
+        guid: ByteArray
+    ) {
+        writeShort(0x145)
+        writeShortLVPacket {
+            writeFully(guid)
+        }
+    }
+
+    fun BytePacketBuilder.t147(
+        appId: Long,
+        apkVersionName: ByteArray,
+        apkSignatureMd5: ByteArray
+    ) {
+        writeShort(0x147)
+        writeShortLVPacket {
+            writeLong(appId)
+            writeShortLVByteArrayLimitedLength(apkVersionName, 32)
+            writeShortLVByteArrayLimitedLength(apkSignatureMd5, 32)
+        }
+    }
+
+    fun BytePacketBuilder.t166(
+        imageType: Int
+    ) {
+        writeShort(0x166)
+        writeShortLVPacket {
+            writeByte(imageType.toByte())
+        }
+    }
+}
+
+private fun Boolean.toByte(): Byte = if (this) 1 else 0
+
+private infix fun Int.shouldEqualsTo(int: Int) = require(this == int)
+
+fun randomAndroidId(): String = buildString(15) {
+    repeat(15) { append(Random.nextInt(10)) }
+}
+
+/**
+ * Defaults "%4;7t>;28<fc.5*6".toByteArray()
+ */
+fun generateGuid(androidId: String, macAddress: String): ByteArray {
+    return md5(androidId + macAddress)
+}
+
+fun getMacAddr(): String = "02:00:00:00:00:00"
+
+
+// AndroidDevInfo: oicq.wlogin_sdk.tools.util#get_android_dev_info