mirror of
https://github.com/mamoe/mirai.git
synced 2025-03-23 05:40:10 +08:00
[core] sso reserve field and encrypt spi
This commit is contained in:
parent
67dd471fdb
commit
ff96dce35d
@ -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))
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -180,6 +180,9 @@ internal abstract class AbstractRealNetworkHandlerTest<H : NetworkHandler> : Abs
|
||||
override suspend fun refreshInitialPublicKeyAndApplyEcdh() {
|
||||
}
|
||||
|
||||
override suspend fun initializeSsoSecureEcdh() {
|
||||
}
|
||||
|
||||
override fun getQQEcdh(): QQEcdh = QQEcdh()
|
||||
})
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user