[core] sso reserve field and encrypt spi

This commit is contained in:
StageGuard 2023-06-16 20:25:07 +08:00 committed by Karlatemp
parent 67dd471fdb
commit ff96dce35d
No known key found for this signature in database
GPG Key ID: BA173CA2B9956C59
7 changed files with 222 additions and 37 deletions

View File

@ -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))
}))
}
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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
}

View File

@ -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

View File

@ -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
}
}

View File

@ -180,6 +180,9 @@ internal abstract class AbstractRealNetworkHandlerTest<H : NetworkHandler> : Abs
override suspend fun refreshInitialPublicKeyAndApplyEcdh() {
}
override suspend fun initializeSsoSecureEcdh() {
}
override fun getQQEcdh(): QQEcdh = QQEcdh()
})