diff --git a/mirai-core/src/commonMain/kotlin/network/components/EcdhInitialPublicKeyUpdater.kt b/mirai-core/src/commonMain/kotlin/network/components/EcdhInitialPublicKeyUpdater.kt index 77d9f3af4..3d57dc221 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/EcdhInitialPublicKeyUpdater.kt +++ b/mirai-core/src/commonMain/kotlin/network/components/EcdhInitialPublicKeyUpdater.kt @@ -17,10 +17,14 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.internal.network.component.ComponentKey +import net.mamoe.mirai.internal.network.protocol.packet.createChannelProxy +import net.mamoe.mirai.internal.spi.EncryptService +import net.mamoe.mirai.internal.spi.EncryptServiceContext import net.mamoe.mirai.internal.utils.crypto.QQEcdh import net.mamoe.mirai.internal.utils.crypto.QQEcdhInitialPublicKey import net.mamoe.mirai.internal.utils.crypto.verify import net.mamoe.mirai.utils.MiraiLogger +import net.mamoe.mirai.utils.buildTypeSafeMap import net.mamoe.mirai.utils.currentTimeSeconds import kotlin.time.Duration.Companion.seconds @@ -34,6 +38,8 @@ internal interface EcdhInitialPublicKeyUpdater { */ suspend fun refreshInitialPublicKeyAndApplyEcdh() + suspend fun initializeSsoSecureEcdh() + fun getQQEcdh(): QQEcdh companion object : ComponentKey<EcdhInitialPublicKeyUpdater> @@ -105,5 +111,18 @@ internal class EcdhInitialPublicKeyUpdaterImpl( qqEcdh = QQEcdh(initialPublicKey) } + override suspend fun initializeSsoSecureEcdh() { + val encryptWorker = EncryptService.instance + + if (encryptWorker == null) { + logger.info("EncryptService SPI is not provided, sso secure ecdh will not be initialized.") + return + } + + encryptWorker.initialize(EncryptServiceContext(bot.id, buildTypeSafeMap { + set(EncryptServiceContext.KEY_CHANNEL_PROXY, createChannelProxy(bot.client)) + })) + } + } diff --git a/mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt b/mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt index 8f7ec0248..2b64d85a0 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt +++ b/mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt @@ -122,21 +122,25 @@ internal class PacketCodecImpl : PacketCodec { val raw = try { when (encryptMethod) { + // empty key 2 -> TEA.decrypt(buffer, DECRYPTER_16_ZERO, size) - 1 -> { - TEA.decrypt(buffer, kotlin.runCatching { client.wLoginSigInfo.d2Key }.getOrElse { - throw PacketCodecException( - "Received packet needed d2Key to decrypt but d2Key doesn't existed, ignoring. Please report to https://github.com/mamoe/mirai/issues/new/choose if you see anything abnormal", - PROTOCOL_UPDATED - ) - }, size) - } - + // d2 key + 1 -> { + TEA.decrypt(buffer, kotlin.runCatching { client.wLoginSigInfo.d2Key }.getOrElse { + throw PacketCodecException( + "Received packet needed d2Key to decrypt but d2Key doesn't existed, ignoring. Please report to https://github.com/mamoe/mirai/issues/new/choose if you see anything abnormal", + PROTOCOL_UPDATED + ) + }, size) + } + // no encrypt 0 -> buffer else -> throw PacketCodecException("Unknown encrypt type=$encryptMethod", PROTOCOL_UPDATED) }.let { decryptedData -> when (type) { + // login 0x0A -> parseSsoFrame(client, decryptedData) + // simple 0x0B -> parseSsoFrame(client, decryptedData) // 这里可能是 uni?? 但测试时候发现结构跟 sso 一样. else -> throw PacketCodecException( "unknown packet type: ${type.toByte().toUHexString()}", @@ -171,7 +175,7 @@ internal class PacketCodecImpl : PacketCodec { raw.sequenceId, raw.body.withUse { try { - parseOicqResponse(client, raw.commandName) + parseOicqResponse(client, raw.commandName) } catch (e: Throwable) { throw PacketCodecException(e, PacketCodecException.Kind.OTHER) } diff --git a/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt b/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt index dafc7d02d..355ffcb24 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt +++ b/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt @@ -39,7 +39,6 @@ import net.mamoe.mirai.network.* import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol import kotlin.coroutines.cancellation.CancellationException -import kotlin.jvm.Volatile /** * Handles login, and acts also as a mediator of [BotInitProcessor] @@ -213,8 +212,8 @@ internal open class SsoProcessorImpl( } components[CacheValidator].validate() - components[BdhSessionSyncer].loadServerListFromCache() + components[EcdhInitialPublicKeyUpdater].initializeSsoSecureEcdh() try { ssoContext.bot.requestQimei(qimeiLogger) diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/SSOReserveField.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/SSOReserveField.kt new file mode 100644 index 000000000..467050106 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/SSOReserveField.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2019-2023 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.internal.network.protocol.data.proto + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber +import net.mamoe.mirai.internal.utils.io.ProtoBuf +import net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY + +internal class SSOReserveField { + @Serializable + internal class ReserveFields( + @JvmField @ProtoNumber(1) val client_ipcookie: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(2) val flag: Int = 0, + @JvmField @ProtoNumber(3) val env_id: Int = 0, + @JvmField @ProtoNumber(4) val locale_id: Int = 0, + @JvmField @ProtoNumber(5) val qimei: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(6) val env: String = "", + @JvmField @ProtoNumber(7) val newconn_flag: Int = 0, + @JvmField @ProtoNumber(8) val trace_parent: String = "", + @JvmField @ProtoNumber(9) val uid: String = "", + @JvmField @ProtoNumber(10) val imsi: Int = 0, + @JvmField @ProtoNumber(11) val network_type: Int = 0, + @JvmField @ProtoNumber(12) val ip_stack_type: Int = 0, + @JvmField @ProtoNumber(13) val message_type: Int = 0, + @JvmField @ProtoNumber(14) val trpc_rsp: SsoTrpcResponse? = null, + @JvmField @ProtoNumber(15) val trans_info: List<SsoMapEntry>? = null, + @JvmField @ProtoNumber(16) val sec_info: SsoSecureInfo? = null, + @JvmField @ProtoNumber(17) val sec_sig_flag: Int = 0, + @JvmField @ProtoNumber(18) val nt_core_version: Int = 0, + @JvmField @ProtoNumber(19) val sso_route_cost: Int = 0, + @JvmField @ProtoNumber(20) val sso_ip_origin: Int = 0, + @JvmField @ProtoNumber(21) val presure_token: ByteArray = EMPTY_BYTE_ARRAY, + ) : ProtoBuf + + @Serializable + internal class SsoSecureInfo( + @JvmField @ProtoNumber(1) val sec_sig: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(2) val sec_device_token: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(3) val sec_extra: ByteArray = EMPTY_BYTE_ARRAY, + ) : ProtoBuf + + @Serializable + internal class SsoTrpcResponse( + @JvmField @ProtoNumber(1) val ret: Int = 0, + @JvmField @ProtoNumber(2) val func_ret: Int = 0, + @JvmField @ProtoNumber(3) val error_msg: ByteArray = EMPTY_BYTE_ARRAY, + ) : ProtoBuf + + @Serializable + internal class SsoMapEntry( + @JvmField @ProtoNumber(1) val key: String = "", + @JvmField @ProtoNumber(2) val value: ByteArray = EMPTY_BYTE_ARRAY, + ) : ProtoBuf +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/OutgoingPacket.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/OutgoingPacket.kt index f46bd6345..0f41833b7 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/OutgoingPacket.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/OutgoingPacket.kt @@ -11,18 +11,19 @@ package net.mamoe.mirai.internal.network.protocol.packet import io.ktor.utils.io.core.* -import net.mamoe.mirai.internal.network.Packet -import net.mamoe.mirai.internal.network.QQAndroidClient -import net.mamoe.mirai.internal.network.appClientVersion +import kotlinx.serialization.encodeToByteArray +import net.mamoe.mirai.internal.network.* import net.mamoe.mirai.internal.network.components.EcdhInitialPublicKeyUpdater +import net.mamoe.mirai.internal.network.protocol.data.proto.SSOReserveField +import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbSendMsg +import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin +import net.mamoe.mirai.internal.spi.EncryptService +import net.mamoe.mirai.internal.spi.EncryptServiceContext import net.mamoe.mirai.internal.utils.io.encryptAndWrite import net.mamoe.mirai.internal.utils.io.writeHex import net.mamoe.mirai.internal.utils.io.writeIntLVPacket -import net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY -import net.mamoe.mirai.utils.Either +import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.Either.Companion.fold -import net.mamoe.mirai.utils.KEY_16_ZEROS -import net.mamoe.mirai.utils.TestOnly import kotlin.random.Random @Suppress("unused") @@ -242,6 +243,37 @@ internal fun <R : Packet?> OutgoingPacketFactory<R>.buildLoginOutgoingPacket( private inline val BRP_STUB get() = ByteReadPacket.Empty +internal fun createChannelProxy(client: QQAndroidClient): EncryptService.ChannelProxy { + return object : EncryptService.ChannelProxy { + override suspend fun sendMessage( + remark: String, + commandName: String, + uin: Long, + data: ByteArray + ): EncryptService.ChannelResult? { + if (commandName == "trpc.o3.ecdh_access.EcdhAccess.SsoEstablishShareKey") { + val packet = client.bot.network.sendAndExpect<Packet>( + WtLogin.Login.buildLoginOutgoingPacket( + client = client, + encryptMethod = PacketEncryptType.Empty, + uin = uin.toString(), + remark = remark + ) { + writeSsoPacket( + client, + client.subAppId, + sequenceId = it, + commandName = commandName, + body = { writeFully(data) } + ) + } + ) + TODO("parse packet to ChannelResult") + } + return null + } + } +} internal inline fun BytePacketBuilder.writeSsoPacket( client: QQAndroidClient, @@ -269,6 +301,42 @@ internal inline fun BytePacketBuilder.writeSsoPacket( * * 00 00 00 04 */ + val encryptWorker = EncryptService.instance + + val reserveField = if ( + commandName.startsWith("wtlogin") + || commandName == MessageSvcPbSendMsg.commandName + || encryptWorker != null + ) { + + val signResult = encryptWorker?.qSecurityGetSign( + EncryptServiceContext(client.uin, buildTypeSafeMap { + set(EncryptServiceContext.KEY_APP_QUA, "V1_AND_SQ_8.9.58_4106_YYB_D") // 8.9.58 + set(EncryptServiceContext.KEY_CHANNEL_PROXY, createChannelProxy(client)) + }), + sequenceId, + commandName, + buildPacket(body).readBytes() + ) + if (signResult != null) ProtoBufForCache.encodeToByteArray( + SSOReserveField.ReserveFields( + flag = 0, + qimei = client.qimei16?.toByteArray() ?: EMPTY_BYTE_ARRAY, + newconn_flag = 0, + uid = client.uin.toString(), + imsi = 0, + network_type = 1, + ip_stack_type = 1, + message_type = 0, + sec_info = SSOReserveField.SsoSecureInfo( + sec_sig = signResult.sign, + sec_device_token = signResult.token, + sec_extra = signResult.extra + ) + ) + ) else EMPTY_BYTE_ARRAY + } else EMPTY_BYTE_ARRAY + writeIntLVPacket(lengthOffset = { it + 4 }) { writeInt(sequenceId) writeInt(subAppId.toInt()) @@ -281,27 +349,32 @@ internal inline fun BytePacketBuilder.writeSsoPacket( writeInt((extraData.remaining + 4).toInt()) writePacket(extraData) } - commandName.let { - writeInt(it.length + 4) - writeText(it) - } - writeInt(4 + 4) + writeInt(commandName.length + 4) + writeText(commandName) + + writeInt(client.outgoingPacketSessionId.size + 4) writeFully(client.outgoingPacketSessionId) // 02 B0 5B 8B - client.device.imei.let { - writeInt(it.length + 4) - writeText(it) + if (commandName.startsWith("wtlogin")) { + writeText(client.device.imei) + writeInt(0x4) + + writeShort((client.ksid.size + 2).toShort()) + writeFully(client.ksid) + + writeInt(reserveField.size + 4) + writeFully(reserveField) } - writeInt(4) - - client.ksid.let { - writeShort((it.size + 2).toShort()) - writeFully(it) + if (commandName == MessageSvcPbSendMsg.commandName && encryptWorker != null) { + writeInt(reserveField.size + 4) + writeFully(reserveField) } - writeInt(4) + val qimei16Bytes = client.qimei16?.toByteArray() ?: EMPTY_BYTE_ARRAY + writeInt(qimei16Bytes.size + 4) + writeFully(qimei16Bytes) } // body diff --git a/mirai-core/src/commonMain/kotlin/spi/EncryptService.kt b/mirai-core/src/commonMain/kotlin/spi/EncryptService.kt index a9c921b66..3c2657bb4 100644 --- a/mirai-core/src/commonMain/kotlin/spi/EncryptService.kt +++ b/mirai-core/src/commonMain/kotlin/spi/EncryptService.kt @@ -13,10 +13,7 @@ package net.mamoe.mirai.internal.spi import net.mamoe.mirai.Bot import net.mamoe.mirai.spi.BaseService import net.mamoe.mirai.spi.SpiServiceLoader -import net.mamoe.mirai.utils.BotConfiguration -import net.mamoe.mirai.utils.MiraiInternalApi -import net.mamoe.mirai.utils.TypeKey -import net.mamoe.mirai.utils.TypeSafeMap +import net.mamoe.mirai.utils.* /** @@ -32,6 +29,8 @@ public class EncryptServiceContext @MiraiInternalApi constructor( public companion object { public val KEY_COMMAND_STR: TypeKey<String> = TypeKey("KEY_COMMAND_STR") public val KEY_BOT_PROTOCOL: TypeKey<BotConfiguration.MiraiProtocol> = TypeKey("BOT_PROTOCOL") + public val KEY_APP_QUA: TypeKey<String> = TypeKey("KEY_APP_QUA") + public val KEY_CHANNEL_PROXY: TypeKey<EncryptService.ChannelProxy> = TypeKey("KEY_CHANNEL_PROXY") } } @@ -39,6 +38,7 @@ public class EncryptServiceContext @MiraiInternalApi constructor( * @since 2.15.0 */ public interface EncryptService : BaseService { + public fun initialize(context: EncryptServiceContext) /** * Returns `false` if not supported. @@ -56,9 +56,34 @@ public interface EncryptService : BaseService { payload: ByteArray, // Do not write to payload ): ByteArray? + public fun qSecurityGetSign( + context: EncryptServiceContext, + sequenceId: Int, + commandName: String, + payload: ByteArray + ): SignResult? + + public class SignResult( + public val sign: ByteArray = EMPTY_BYTE_ARRAY, + public val token: ByteArray = EMPTY_BYTE_ARRAY, + public val extra: ByteArray = EMPTY_BYTE_ARRAY, + ) + + public class ChannelResult( + public val cmd: String, + public val data: ByteArray, + public val success: Int, + public val callbackId: Long + ) + + public interface ChannelProxy { + public suspend fun sendMessage(remark: String, commandName: String, uin: Long, data: ByteArray): ChannelResult? + } + public companion object { private val loader = SpiServiceLoader(EncryptService::class) internal val instance: EncryptService? get() = loader.service } + } diff --git a/mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt b/mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt index 0312bf5e3..47cdd8946 100644 --- a/mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt +++ b/mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt @@ -180,6 +180,9 @@ internal abstract class AbstractRealNetworkHandlerTest<H : NetworkHandler> : Abs override suspend fun refreshInitialPublicKeyAndApplyEcdh() { } + override suspend fun initializeSsoSecureEcdh() { + } + override fun getQQEcdh(): QQEcdh = QQEcdh() })