From 62b3740a7212dbdf28023c22dec9ca45ee80c127 Mon Sep 17 00:00:00 2001 From: Him188 Date: Sat, 28 Dec 2019 16:55:17 +0800 Subject: [PATCH] Android stuff --- .../mirai/qqandroid/network/QQAndroid.kt | 54 +++ .../mirai/qqandroid/network/io/JceOutput.kt | 241 +++++++++++ .../mirai/qqandroid/network/io/JceStruct.kt | 5 + .../packet/OutgoingPacketHelperAndroid.kt | 95 ----- .../mirai/qqandroid/network/packet/tlv/Tlv.kt | 158 ------- .../protocol/packet/OutgoingPacketAndroid.kt | 134 ++++++ .../network/protocol/packet/PacketFactory.kt | 76 ++++ .../{ => protocol}/packet/TouchPacket.kt | 16 +- .../protocol/packet/login/LoginPacket.kt | 52 +++ .../network/protocol/packet/tlv/Tlv.kt | 401 ++++++++++++++++++ 10 files changed, 972 insertions(+), 260 deletions(-) create mode 100644 mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroid.kt create mode 100644 mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/io/JceOutput.kt create mode 100644 mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/io/JceStruct.kt delete mode 100644 mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/packet/OutgoingPacketHelperAndroid.kt delete mode 100644 mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/packet/tlv/Tlv.kt create mode 100644 mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/OutgoingPacketAndroid.kt create mode 100644 mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt rename mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/{ => protocol}/packet/TouchPacket.kt (61%) create mode 100644 mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/LoginPacket.kt create mode 100644 mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/tlv/Tlv.kt diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroid.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroid.kt new file mode 100644 index 000000000..e0c7b1363 --- /dev/null +++ b/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 { + + } +} \ No newline at end of file diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/io/JceOutput.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/io/JceOutput.kt new file mode 100644 index 000000000..b3956aaa2 --- /dev/null +++ b/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 writeFully(src: Array, 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 writeMap(map: Map, 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 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 = 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) \ No newline at end of file diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/io/JceStruct.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/io/JceStruct.kt new file mode 100644 index 000000000..6c84e678c --- /dev/null +++ b/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) +} \ No newline at end of file diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/packet/OutgoingPacketHelperAndroid.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/packet/OutgoingPacketHelperAndroid.kt deleted file mode 100644 index 63299fdd1..000000000 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/packet/OutgoingPacketHelperAndroid.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/packet/tlv/Tlv.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/packet/tlv/Tlv.kt deleted file mode 100644 index 0d8a79bc3..000000000 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/packet/tlv/Tlv.kt +++ /dev/null @@ -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" \ No newline at end of file diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/OutgoingPacketAndroid.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/OutgoingPacketAndroid.kt new file mode 100644 index 000000000..24602eba8 --- /dev/null +++ b/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 + }) +} \ No newline at end of file diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt new file mode 100644 index 000000000..37a0e877b --- /dev/null +++ b/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(val decrypterType: DecrypterType) { + + @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 ByteReadPacket.decodeProtoPacket( + deserializer: DeserializationStrategy, + 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() + } + } +} \ No newline at end of file diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/packet/TouchPacket.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/TouchPacket.kt similarity index 61% rename from mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/packet/TouchPacket.kt rename to mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/TouchPacket.kt index e4e33277d..a21f2843d 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/packet/TouchPacket.kt +++ b/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 { +object TouchKey : DecrypterByteArray, + DecrypterType { override val value: ByteArray get() = TODO("not implemented") } -object TouchPacket : PacketFactory(TouchKey) { +object TouchPacket : PacketFactory( + TouchKey +) { @UseExperimental(ExperimentalUnsignedTypes::class) override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): TouchPacketResponse { TODO("not implemented") diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/LoginPacket.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/LoginPacket.kt new file mode 100644 index 000000000..d3ce46b2b --- /dev/null +++ b/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 { + + } +} + +@UseExperimental(ExperimentalUnsignedTypes::class) +object LoginPacket : PacketFactory(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") +} diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/tlv/Tlv.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/tlv/Tlv.kt new file mode 100644 index 000000000..63e124d41 --- /dev/null +++ b/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;28