diff --git a/mirai-core-api/compatibility-validation/android/api/android.api b/mirai-core-api/compatibility-validation/android/api/android.api index 44fed993df0..ec6fe9183bf 100644 --- a/mirai-core-api/compatibility-validation/android/api/android.api +++ b/mirai-core-api/compatibility-validation/android/api/android.api @@ -5588,8 +5588,9 @@ public final class net/mamoe/mirai/utils/BotConfiguration$MiraiProtocol : java/l public final class net/mamoe/mirai/utils/DeviceInfo { public static final field Companion Lnet/mamoe/mirai/utils/DeviceInfo$Companion; - public synthetic fun (I[B[B[B[B[B[B[B[B[B[B[BLnet/mamoe/mirai/utils/DeviceInfo$Version;[B[B[B[B[B[BLjava/lang/String;[BLkotlinx/serialization/internal/SerializationConstructorMarker;)V public fun ([B[B[B[B[B[B[B[B[B[B[BLnet/mamoe/mirai/utils/DeviceInfo$Version;[B[B[B[B[B[BLjava/lang/String;[B)V + public fun ([B[B[B[B[B[B[B[B[B[B[BLnet/mamoe/mirai/utils/DeviceInfo$Version;[B[B[B[B[B[BLjava/lang/String;[B[B)V + public static final fun deserializeFromString (Ljava/lang/String;)Lnet/mamoe/mirai/utils/DeviceInfo; public fun equals (Ljava/lang/Object;)Z public static final fun from (Ljava/io/File;)Lnet/mamoe/mirai/utils/DeviceInfo; public static final fun from (Ljava/io/File;Lkotlinx/serialization/json/Json;)Lnet/mamoe/mirai/utils/DeviceInfo; @@ -5619,26 +5620,26 @@ public final class net/mamoe/mirai/utils/DeviceInfo { public fun hashCode ()I public static final fun random ()Lnet/mamoe/mirai/utils/DeviceInfo; public static final fun random (Lkotlin/random/Random;)Lnet/mamoe/mirai/utils/DeviceInfo; - public static final fun write$Self (Lnet/mamoe/mirai/utils/DeviceInfo;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V + public static final fun serializeToString (Lnet/mamoe/mirai/utils/DeviceInfo;)Ljava/lang/String; } -public final class net/mamoe/mirai/utils/DeviceInfo$$serializer : kotlinx/serialization/internal/GeneratedSerializer { +public final class net/mamoe/mirai/utils/DeviceInfo$$serializer : kotlinx/serialization/KSerializer { public static final field INSTANCE Lnet/mamoe/mirai/utils/DeviceInfo$$serializer; - public fun childSerializers ()[Lkotlinx/serialization/KSerializer; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/utils/DeviceInfo; public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lnet/mamoe/mirai/utils/DeviceInfo;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } public final class net/mamoe/mirai/utils/DeviceInfo$Companion { + public final fun deserializeFromString (Ljava/lang/String;)Lnet/mamoe/mirai/utils/DeviceInfo; public final fun from (Ljava/io/File;)Lnet/mamoe/mirai/utils/DeviceInfo; public final fun from (Ljava/io/File;Lkotlinx/serialization/json/Json;)Lnet/mamoe/mirai/utils/DeviceInfo; public static synthetic fun from$default (Lnet/mamoe/mirai/utils/DeviceInfo$Companion;Ljava/io/File;Lkotlinx/serialization/json/Json;ILjava/lang/Object;)Lnet/mamoe/mirai/utils/DeviceInfo; public final fun random ()Lnet/mamoe/mirai/utils/DeviceInfo; public final fun random (Lkotlin/random/Random;)Lnet/mamoe/mirai/utils/DeviceInfo; + public final fun serializeToString (Lnet/mamoe/mirai/utils/DeviceInfo;)Ljava/lang/String; public final fun serializer ()Lkotlinx/serialization/KSerializer; } @@ -5674,6 +5675,7 @@ public final class net/mamoe/mirai/utils/DeviceInfo$Version$Companion { public final class net/mamoe/mirai/utils/DeviceInfoKt { public static final fun generateDeviceInfoData (Lnet/mamoe/mirai/utils/DeviceInfo;)[B + public static final synthetic fun serializeToString (Lnet/mamoe/mirai/utils/DeviceInfo;)Ljava/lang/String; } public abstract interface class net/mamoe/mirai/utils/DeviceVerificationRequests { diff --git a/mirai-core-api/compatibility-validation/jvm/api/jvm.api b/mirai-core-api/compatibility-validation/jvm/api/jvm.api index 19f1110e293..02343671f11 100644 --- a/mirai-core-api/compatibility-validation/jvm/api/jvm.api +++ b/mirai-core-api/compatibility-validation/jvm/api/jvm.api @@ -5588,8 +5588,9 @@ public final class net/mamoe/mirai/utils/BotConfiguration$MiraiProtocol : java/l public final class net/mamoe/mirai/utils/DeviceInfo { public static final field Companion Lnet/mamoe/mirai/utils/DeviceInfo$Companion; - public synthetic fun (I[B[B[B[B[B[B[B[B[B[B[BLnet/mamoe/mirai/utils/DeviceInfo$Version;[B[B[B[B[B[BLjava/lang/String;[BLkotlinx/serialization/internal/SerializationConstructorMarker;)V public fun ([B[B[B[B[B[B[B[B[B[B[BLnet/mamoe/mirai/utils/DeviceInfo$Version;[B[B[B[B[B[BLjava/lang/String;[B)V + public fun ([B[B[B[B[B[B[B[B[B[B[BLnet/mamoe/mirai/utils/DeviceInfo$Version;[B[B[B[B[B[BLjava/lang/String;[B[B)V + public static final fun deserializeFromString (Ljava/lang/String;)Lnet/mamoe/mirai/utils/DeviceInfo; public fun equals (Ljava/lang/Object;)Z public static final fun from (Ljava/io/File;)Lnet/mamoe/mirai/utils/DeviceInfo; public static final fun from (Ljava/io/File;Lkotlinx/serialization/json/Json;)Lnet/mamoe/mirai/utils/DeviceInfo; @@ -5619,26 +5620,26 @@ public final class net/mamoe/mirai/utils/DeviceInfo { public fun hashCode ()I public static final fun random ()Lnet/mamoe/mirai/utils/DeviceInfo; public static final fun random (Lkotlin/random/Random;)Lnet/mamoe/mirai/utils/DeviceInfo; - public static final fun write$Self (Lnet/mamoe/mirai/utils/DeviceInfo;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V + public static final fun serializeToString (Lnet/mamoe/mirai/utils/DeviceInfo;)Ljava/lang/String; } -public final class net/mamoe/mirai/utils/DeviceInfo$$serializer : kotlinx/serialization/internal/GeneratedSerializer { +public final class net/mamoe/mirai/utils/DeviceInfo$$serializer : kotlinx/serialization/KSerializer { public static final field INSTANCE Lnet/mamoe/mirai/utils/DeviceInfo$$serializer; - public fun childSerializers ()[Lkotlinx/serialization/KSerializer; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/utils/DeviceInfo; public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lnet/mamoe/mirai/utils/DeviceInfo;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } public final class net/mamoe/mirai/utils/DeviceInfo$Companion { + public final fun deserializeFromString (Ljava/lang/String;)Lnet/mamoe/mirai/utils/DeviceInfo; public final fun from (Ljava/io/File;)Lnet/mamoe/mirai/utils/DeviceInfo; public final fun from (Ljava/io/File;Lkotlinx/serialization/json/Json;)Lnet/mamoe/mirai/utils/DeviceInfo; public static synthetic fun from$default (Lnet/mamoe/mirai/utils/DeviceInfo$Companion;Ljava/io/File;Lkotlinx/serialization/json/Json;ILjava/lang/Object;)Lnet/mamoe/mirai/utils/DeviceInfo; public final fun random ()Lnet/mamoe/mirai/utils/DeviceInfo; public final fun random (Lkotlin/random/Random;)Lnet/mamoe/mirai/utils/DeviceInfo; + public final fun serializeToString (Lnet/mamoe/mirai/utils/DeviceInfo;)Ljava/lang/String; public final fun serializer ()Lkotlinx/serialization/KSerializer; } @@ -5674,6 +5675,7 @@ public final class net/mamoe/mirai/utils/DeviceInfo$Version$Companion { public final class net/mamoe/mirai/utils/DeviceInfoKt { public static final fun generateDeviceInfoData (Lnet/mamoe/mirai/utils/DeviceInfo;)[B + public static final synthetic fun serializeToString (Lnet/mamoe/mirai/utils/DeviceInfo;)Ljava/lang/String; } public abstract interface class net/mamoe/mirai/utils/DeviceVerificationRequests { diff --git a/mirai-core-api/src/commonMain/kotlin/utils/DeviceInfo.kt b/mirai-core-api/src/commonMain/kotlin/utils/DeviceInfo.kt index 2128a43d676..896a7be2893 100644 --- a/mirai-core-api/src/commonMain/kotlin/utils/DeviceInfo.kt +++ b/mirai-core-api/src/commonMain/kotlin/utils/DeviceInfo.kt @@ -12,20 +12,28 @@ package net.mamoe.mirai.utils import io.ktor.utils.io.core.* -import kotlinx.serialization.* -import kotlinx.serialization.builtins.serializer -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient import kotlinx.serialization.protobuf.ProtoBuf import kotlinx.serialization.protobuf.ProtoNumber -import net.mamoe.mirai.utils.DeviceInfoManager.Version.Companion.trans -import kotlin.jvm.JvmInline import kotlin.jvm.JvmStatic +import kotlin.jvm.JvmSynthetic import kotlin.random.Random -public expect class DeviceInfo( +internal const val DeviceInfoConstructorDeprecationMessage = + "Constructor and serializer of DeviceInfo is deprecated and will be removed in the future." + + "This is because new properties can be added and it requires too much effort to maintain public stability." + + "Please use DeviceInfo.serializeToString and DeviceInfo.deserializeFromString instead." + +/** + * 表示设备信息 + */ +public expect class DeviceInfo +@Deprecated(DeviceInfoConstructorDeprecationMessage, level = DeprecationLevel.WARNING) +@DeprecatedSinceMirai(warningSince = "2.15") // planned internal +public constructor( display: ByteArray, product: ByteArray, device: ByteArray, @@ -45,9 +53,9 @@ public expect class DeviceInfo( wifiSSID: ByteArray, imsiMd5: ByteArray, imei: String, - apn: ByteArray + apn: ByteArray, + androidId: ByteArray, ) { - public val display: ByteArray public val product: ByteArray public val device: ByteArray @@ -68,15 +76,14 @@ public expect class DeviceInfo( public val imsiMd5: ByteArray public val imei: String public val apn: ByteArray - public val androidId: ByteArray + public val ipAddress: ByteArray @Transient @MiraiInternalApi public val guid: ByteArray - // @Serializable: use DeviceInfoVersionSerializer in commonMain. public class Version( incremental: ByteArray = "5891938".toByteArray(), @@ -98,6 +105,10 @@ public expect class DeviceInfo( * @since 2.9 */ override fun hashCode(): Int + + internal companion object { + fun serializer(): KSerializer + } } public companion object { @@ -119,7 +130,27 @@ public expect class DeviceInfo( @JvmStatic public fun random(random: Random): DeviceInfo + @Deprecated(DeviceInfoConstructorDeprecationMessage, level = DeprecationLevel.WARNING) + @DeprecatedSinceMirai(warningSince = "2.15") // planned internal public fun serializer(): KSerializer + + /** + * 将此 [DeviceInfo] 序列化为字符串. 序列化的字符串可以在以后通过 [DeviceInfo.deserializeFromString] 反序列化为 [DeviceInfo]. + * + * 序列化的字符串有兼容性保证, 在旧版 mirai 序列化的字符串, 可以在新版 mirai 使用. 但新版 mirai 序列化的字符串不一定能在旧版使用. + * + * @since 2.15 + */ + @JvmStatic + public fun serializeToString(deviceInfo: DeviceInfo): String + + /** + * 将通过 [serializeToString] 序列化得到的字符串反序列化为 [DeviceInfo]. + * 此函数兼容旧版 mirai 序列化的字符串. + * @since 2.15 + */ + @JvmStatic + public fun deserializeFromString(string: String): DeviceInfo } /** @@ -134,7 +165,74 @@ public expect class DeviceInfo( override fun hashCode(): Int } +/** + * 将此 [DeviceInfo] 序列化为字符串. 序列化的字符串可以在以后通过 [DeviceInfo.deserializeFromString] 反序列化为 [DeviceInfo]. + * + * 序列化的字符串有兼容性保证, 在旧版 mirai 序列化的字符串, 可以在新版 mirai 使用. 但新版 mirai 序列化的字符串不一定能在旧版使用. + * + * @since 2.15 + */ +@JvmSynthetic +public fun DeviceInfo.serializeToString(): String = DeviceInfo.serializeToString(this) + +@Serializable +private class DevInfo @OptIn(ExperimentalSerializationApi::class) constructor( + @ProtoNumber(1) val bootloader: ByteArray, + @ProtoNumber(2) val procVersion: ByteArray, + @ProtoNumber(3) val codename: ByteArray, + @ProtoNumber(4) val incremental: ByteArray, + @ProtoNumber(5) val fingerprint: ByteArray, + @ProtoNumber(6) val bootId: ByteArray, + @ProtoNumber(7) val androidId: ByteArray, + @ProtoNumber(8) val baseBand: ByteArray, + @ProtoNumber(9) val innerVersion: ByteArray +) + +/** + * 不要使用这个 API, 此 API 在未来可能会被删除 + */ +public fun DeviceInfo.generateDeviceInfoData(): ByteArray { // ?? why is this public? + + @OptIn(ExperimentalSerializationApi::class) + return ProtoBuf.encodeToByteArray( + DevInfo.serializer(), DevInfo( + bootloader, + procVersion, + version.codename, + version.incremental, + fingerprint, + bootId, + androidId, + baseBand, + version.incremental + ) + ) +} + +/** + * Defaults "%4;7t>;28 by String.serializer().map( - String.serializer().descriptor.copy("HexString"), - deserialize = { HexString(it.hexToBytes()) }, - serialize = { it.data.toUHexString("").lowercase() } - ) - - // Note: property names must be kept intact during obfuscation process if applied. - @Serializable - class Wrapper( - @Suppress("unused") val deviceInfoVersion: Int, // used by plain jsonObject - val data: T - ) - - private object DeviceInfoVersionSerializer : KSerializer by SerialData.serializer().map( - resultantDescriptor = SerialData.serializer().descriptor, - deserialize = { - DeviceInfo.Version(incremental, release, codename, sdk) - }, - serialize = { - SerialData(incremental, release, codename, sdk) - } - ) { - @SerialName("Version") - @Serializable - private class SerialData( - val incremental: ByteArray = "5891938".toByteArray(), - val release: ByteArray = "10".toByteArray(), - val codename: ByteArray = "REL".toByteArray(), - val sdk: Int = 29 - ) - } - - @Serializable - class V1( - val display: ByteArray, - val product: ByteArray, - val device: ByteArray, - val board: ByteArray, - val brand: ByteArray, - val model: ByteArray, - val bootloader: ByteArray, - val fingerprint: ByteArray, - val bootId: ByteArray, - val procVersion: ByteArray, - val baseBand: ByteArray, - val version: @Serializable(DeviceInfoVersionSerializer::class) DeviceInfo.Version, - val simInfo: ByteArray, - val osType: ByteArray, - val macAddress: ByteArray, - val wifiBSSID: ByteArray, - val wifiSSID: ByteArray, - val imsiMd5: ByteArray, - val imei: String, - val apn: ByteArray - ) : Info { - override fun toDeviceInfo(): DeviceInfo { - return DeviceInfo( - display = display, - product = product, - device = device, - board = board, - brand = brand, - model = model, - bootloader = bootloader, - fingerprint = fingerprint, - bootId = bootId, - procVersion = procVersion, - baseBand = baseBand, - version = version, - simInfo = simInfo, - osType = osType, - macAddress = macAddress, - wifiBSSID = wifiBSSID, - wifiSSID = wifiSSID, - imsiMd5 = imsiMd5, - imei = imei, - apn = apn - ) - } - } - - - @Serializable - class V2( - val display: String, - val product: String, - val device: String, - val board: String, - val brand: String, - val model: String, - val bootloader: String, - val fingerprint: String, - val bootId: String, - val procVersion: String, - val baseBand: HexString, - val version: Version, - val simInfo: String, - val osType: String, - val macAddress: String, - val wifiBSSID: String, - val wifiSSID: String, - val imsiMd5: HexString, - val imei: String, - val apn: String - ) : Info { - override fun toDeviceInfo(): DeviceInfo = DeviceInfo( - this.display.toByteArray(), - this.product.toByteArray(), - this.device.toByteArray(), - this.board.toByteArray(), - this.brand.toByteArray(), - this.model.toByteArray(), - this.bootloader.toByteArray(), - this.fingerprint.toByteArray(), - this.bootId.toByteArray(), - this.procVersion.toByteArray(), - this.baseBand.data, - this.version.trans(), - this.simInfo.toByteArray(), - this.osType.toByteArray(), - this.macAddress.toByteArray(), - this.wifiBSSID.toByteArray(), - this.wifiSSID.toByteArray(), - this.imsiMd5.data, - this.imei, - this.apn.toByteArray() - ) - } - - @Serializable - class Version( - val incremental: String, - val release: String, - val codename: String, - val sdk: Int = 29 - ) { - companion object { - fun DeviceInfo.Version.trans(): Version { - return Version(incremental.decodeToString(), release.decodeToString(), codename.decodeToString(), sdk) - } - - fun Version.trans(): DeviceInfo.Version { - return DeviceInfo.Version(incremental.toByteArray(), release.toByteArray(), codename.toByteArray(), sdk) - } - } - } - - fun DeviceInfo.toCurrentInfo(): V2 = V2( - display.decodeToString(), - product.decodeToString(), - device.decodeToString(), - board.decodeToString(), - brand.decodeToString(), - model.decodeToString(), - bootloader.decodeToString(), - fingerprint.decodeToString(), - bootId.decodeToString(), - procVersion.decodeToString(), - HexString(baseBand), - version.trans(), - simInfo.decodeToString(), - osType.decodeToString(), - macAddress.decodeToString(), - wifiBSSID.decodeToString(), - wifiSSID.decodeToString(), - HexString(imsiMd5), - imei, - apn.decodeToString() - ) - - internal val format = Json { - ignoreUnknownKeys = true - isLenient = true - } - - @Throws(IllegalArgumentException::class, NumberFormatException::class) // in case malformed - fun deserialize(string: String, format: Json = this.format): DeviceInfo { - val element = format.parseToJsonElement(string) - - return when (val version = element.jsonObject["deviceInfoVersion"]?.jsonPrimitive?.content?.toInt() ?: 1) { - /** - * @since 2.0 - */ - 1 -> format.decodeFromJsonElement(V1.serializer(), element) - /** - * @since 2.9 - */ - 2 -> format.decodeFromJsonElement(Wrapper.serializer(V2.serializer()), element).data - else -> throw IllegalArgumentException("Unsupported deviceInfoVersion: $version") - }.toDeviceInfo() - } - - fun serialize(info: DeviceInfo, format: Json = this.format): String { - return format.encodeToString( - Wrapper.serializer(V2.serializer()), - Wrapper(2, info.toCurrentInfo()) - ) - } - - fun toJsonElement(info: DeviceInfo, format: Json = this.format): JsonElement { - return format.encodeToJsonElement( - Wrapper.serializer(V2.serializer()), - Wrapper(2, info.toCurrentInfo()) - ) - } -} - -/** - * Defaults "%4;7t>;28 by String.serializer().map( + String.serializer().descriptor.copy("HexString"), + deserialize = { + HexString(it.hexToBytes()) + }, + serialize = { it.data.toUHexString("").lowercase() } + ) + + // Note: property names must be kept intact during obfuscation process if applied. + @Serializable + class Wrapper( + @Suppress("unused") val deviceInfoVersion: Int, // used by plain jsonObject + val data: T + ) + + internal object DeviceInfoVersionSerializer : KSerializer by SerialData.serializer().map( + resultantDescriptor = SerialData.serializer().descriptor, + deserialize = { + DeviceInfo.Version(incremental, release, codename, sdk) + }, + serialize = { + SerialData(incremental, release, codename, sdk) + } + ) { + @SerialName("Version") + @Serializable + private class SerialData( + val incremental: ByteArray = "5891938".toByteArray(), + val release: ByteArray = "10".toByteArray(), + val codename: ByteArray = "REL".toByteArray(), + val sdk: Int = 29 + ) + } + + @Serializable + class V1( + val display: ByteArray, + val product: ByteArray, + val device: ByteArray, + val board: ByteArray, + val brand: ByteArray, + val model: ByteArray, + val bootloader: ByteArray, + val fingerprint: ByteArray, + val bootId: ByteArray, + val procVersion: ByteArray, + val baseBand: ByteArray, + val version: @Serializable(DeviceInfoVersionSerializer::class) DeviceInfo.Version, + val simInfo: ByteArray, + val osType: ByteArray, + val macAddress: ByteArray, + val wifiBSSID: ByteArray, + val wifiSSID: ByteArray, + val imsiMd5: ByteArray, + val imei: String, + val apn: ByteArray + ) : Info { + override fun toDeviceInfo(): DeviceInfo { + @Suppress("DEPRECATION", "DEPRECATION_ERROR") + return DeviceInfo( + display = display, + product = product, + device = device, + board = board, + brand = brand, + model = model, + bootloader = bootloader, + fingerprint = fingerprint, + bootId = bootId, + procVersion = procVersion, + baseBand = baseBand, + version = version, + simInfo = simInfo, + osType = osType, + macAddress = macAddress, + wifiBSSID = wifiBSSID, + wifiSSID = wifiSSID, + imsiMd5 = imsiMd5, + imei = imei, + apn = apn, + androidId = getRandomByteArray(8).toUHexString("").lowercase().encodeToByteArray() + ) + } + } + + @Serializable + class V2( + val display: String, + val product: String, + val device: String, + val board: String, + val brand: String, + val model: String, + val bootloader: String, + val fingerprint: String, + val bootId: String, + val procVersion: String, + val baseBand: HexString, + val version: Version, + val simInfo: String, + val osType: String, + val macAddress: String, + val wifiBSSID: String, + val wifiSSID: String, + val imsiMd5: HexString, + val imei: String, + val apn: String + ) : Info { + @Suppress("DEPRECATION", "DEPRECATION_ERROR") + override fun toDeviceInfo(): DeviceInfo = DeviceInfo( + this.display.toByteArray(), + this.product.toByteArray(), + this.device.toByteArray(), + this.board.toByteArray(), + this.brand.toByteArray(), + this.model.toByteArray(), + this.bootloader.toByteArray(), + this.fingerprint.toByteArray(), + this.bootId.toByteArray(), + this.procVersion.toByteArray(), + this.baseBand.data, + this.version.trans(), + this.simInfo.toByteArray(), + this.osType.toByteArray(), + this.macAddress.toByteArray(), + this.wifiBSSID.toByteArray(), + this.wifiSSID.toByteArray(), + this.imsiMd5.data, + this.imei, + this.apn.toByteArray(), + androidId = getRandomByteArray(8).toUHexString("").lowercase().encodeToByteArray() + ) + } + + + @Serializable + class V3( + val display: String, + val product: String, + val device: String, + val board: String, + val brand: String, + val model: String, + val bootloader: String, + val fingerprint: String, + val bootId: String, + val procVersion: String, + val baseBand: HexString, + val version: Version, + val simInfo: String, + val osType: String, + val macAddress: String, + val wifiBSSID: String, + val wifiSSID: String, + val imsiMd5: HexString, + val imei: String, + val apn: String, + val androidId: String, + ) : Info { + @Suppress("DEPRECATION", "DEPRECATION_ERROR") + override fun toDeviceInfo(): DeviceInfo = DeviceInfo( + this.display.toByteArray(), + this.product.toByteArray(), + this.device.toByteArray(), + this.board.toByteArray(), + this.brand.toByteArray(), + this.model.toByteArray(), + this.bootloader.toByteArray(), + this.fingerprint.toByteArray(), + this.bootId.toByteArray(), + this.procVersion.toByteArray(), + this.baseBand.data, + this.version.trans(), + this.simInfo.toByteArray(), + this.osType.toByteArray(), + this.macAddress.toByteArray(), + this.wifiBSSID.toByteArray(), + this.wifiSSID.toByteArray(), + this.imsiMd5.data, + this.imei, + this.apn.toByteArray(), + this.androidId.toByteArray() + ) + } + + @Serializable + class Version( + val incremental: String, + val release: String, + val codename: String, + val sdk: Int = 29 + ) { + companion object { + fun DeviceInfo.Version.trans(): Version { + return Version(incremental.decodeToString(), release.decodeToString(), codename.decodeToString(), sdk) + } + + fun Version.trans(): DeviceInfo.Version { + return DeviceInfo.Version(incremental.toByteArray(), release.toByteArray(), codename.toByteArray(), sdk) + } + } + } + + fun DeviceInfo.toCurrentInfo(): V3 = V3( + display.decodeToString(), + product.decodeToString(), + device.decodeToString(), + board.decodeToString(), + brand.decodeToString(), + model.decodeToString(), + bootloader.decodeToString(), + fingerprint.decodeToString(), + bootId.decodeToString(), + procVersion.decodeToString(), + HexString(baseBand), + version.trans(), + simInfo.decodeToString(), + osType.decodeToString(), + macAddress.decodeToString(), + wifiBSSID.decodeToString(), + wifiSSID.decodeToString(), + HexString(imsiMd5), + imei, + apn.decodeToString(), + androidId.decodeToString(), + ) + + internal val format = Json { + ignoreUnknownKeys = true + isLenient = true + } + + @Suppress("unused") + @Deprecated("ABI compatibility for device generator", level = DeprecationLevel.HIDDEN) + @JvmName("deserialize") + fun deserializeDeprecated( + string: String, + format: Json = this.format, + ): DeviceInfo = deserialize(string, format) + + @Throws(IllegalArgumentException::class, NumberFormatException::class) // in case malformed + fun deserialize( + string: String, + format: Json = this.format, + onUpgradeVersion: (DeviceInfo) -> Unit = { } + ): DeviceInfo { + val element = format.parseToJsonElement(string) + val version = element.jsonObject["deviceInfoVersion"]?.jsonPrimitive?.content?.toInt() ?: 1 + + val deviceInfo = when (version) { + /** + * @since 2.0 + */ + 1 -> format.decodeFromJsonElement(V1.serializer(), element) + /** + * @since 2.9 + */ + 2 -> format.decodeFromJsonElement(Wrapper.serializer(V2.serializer()), element).data + /** + * @since 2.15 + */ + 3 -> format.decodeFromJsonElement(Wrapper.serializer(V3.serializer()), element).data + else -> throw IllegalArgumentException("Unsupported deviceInfoVersion: $version") + }.toDeviceInfo() + + if (version < 3) onUpgradeVersion(deviceInfo) + + return deviceInfo + } + + fun serialize(info: DeviceInfo, format: Json = this.format): String { + return format.encodeToString( + Wrapper.serializer(V3.serializer()), + Wrapper(3, info.toCurrentInfo()) + ) + } + + fun toJsonElement(info: DeviceInfo, format: Json = this.format): JsonElement { + return format.encodeToJsonElement( + Wrapper.serializer(V3.serializer()), + Wrapper(3, info.toCurrentInfo()) + ) + } +} \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/utils/DeviceInfoV1LegacySerializer.kt b/mirai-core-api/src/commonMain/kotlin/utils/DeviceInfoV1LegacySerializer.kt new file mode 100644 index 00000000000..b7fc01f5e09 --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/utils/DeviceInfoV1LegacySerializer.kt @@ -0,0 +1,102 @@ +/* + * 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.utils + +import io.ktor.utils.io.core.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable + + +@Serializable +internal class DeviceInfoV1Legacy( + val product: ByteArray, + val display: ByteArray, + val device: ByteArray, + val board: ByteArray, + val brand: ByteArray, + val model: ByteArray, + val bootloader: ByteArray, + val fingerprint: ByteArray, + val bootId: ByteArray, + val procVersion: ByteArray, + val baseBand: ByteArray, + val version: DeviceInfoV1LegacyVersion, + val simInfo: ByteArray, + val osType: ByteArray, + val macAddress: ByteArray, + val wifiBSSID: ByteArray, + val wifiSSID: ByteArray, + val imsiMd5: ByteArray, + val imei: String, + val apn: ByteArray, + val androidId: ByteArray? = null +) + +@Serializable +internal class DeviceInfoV1LegacyVersion( + val incremental: ByteArray = "5891938".toByteArray(), + val release: ByteArray = "10".toByteArray(), + val codename: ByteArray = "REL".toByteArray(), + val sdk: Int = 29 +) + +internal object DeviceInfoV1LegacySerializer : KSerializer by DeviceInfoV1Legacy.serializer().map( + DeviceInfoV1Legacy.serializer().descriptor.copy("DeviceInfo"), + deserialize = { + @Suppress("DEPRECATION") + DeviceInfo( + display, + product, + device, + board, + brand, + model, + bootloader, + fingerprint, + bootId, + procVersion, + baseBand, + DeviceInfo.Version(version.incremental, version.release, version.codename, version.sdk), + simInfo, + osType, + macAddress, + wifiBSSID, + wifiSSID, + imsiMd5, + imei, + apn, + androidId = display + ) + }, + serialize = { + DeviceInfoV1Legacy( + display, + product, + device, + board, + brand, + model, + bootloader, + fingerprint, + bootId, + procVersion, + baseBand, + DeviceInfoV1LegacyVersion(version.incremental, version.release, version.codename, version.sdk), + simInfo, + osType, + macAddress, + wifiBSSID, + wifiSSID, + imsiMd5, + imei, + apn + ) + } +) \ No newline at end of file diff --git a/mirai-core-api/src/commonTest/kotlin/utils/DeviceInfoTest.kt b/mirai-core-api/src/commonTest/kotlin/utils/DeviceInfoTest.kt index af3c36c2cda..f520ed018eb 100644 --- a/mirai-core-api/src/commonTest/kotlin/utils/DeviceInfoTest.kt +++ b/mirai-core-api/src/commonTest/kotlin/utils/DeviceInfoTest.kt @@ -42,7 +42,7 @@ class CommonDeviceInfoTest { } @Test - fun `can serialize and deserialize v2`() { + fun `can serialize and deserialize v3`() { val device = DeviceInfo.random() assertEquals(device, DeviceInfoManager.deserialize(DeviceInfoManager.serialize(device))) } @@ -88,7 +88,7 @@ class CommonDeviceInfoTest { */ val element = DeviceInfoManager.toJsonElement(device) - assertEquals(2, element.jsonObject["deviceInfoVersion"]!!.jsonPrimitive.content.toInt()) + assertEquals(3, element.jsonObject["deviceInfoVersion"]!!.jsonPrimitive.content.toInt()) val imsiMd5 = element.jsonObject["data"]!!.jsonObject["imsiMd5"]!!.jsonPrimitive.content assertEquals( diff --git a/mirai-core-api/src/commonTest/resources/device/legacy-device-info-1.json b/mirai-core-api/src/commonTest/resources/device/legacy-device-info-1.json deleted file mode 100644 index 22a0ed933c6..00000000000 --- a/mirai-core-api/src/commonTest/resources/device/legacy-device-info-1.json +++ /dev/null @@ -1,354 +0,0 @@ -{ - "display" : [ - 77, - 73, - 82, - 65, - 73, - 46, - 55, - 56, - 49, - 56, - 55, - 57, - 46, - 48, - 48, - 49 - ], - "product" : [ - 109, - 105, - 114, - 97, - 105 - ], - "device" : [ - 109, - 105, - 114, - 97, - 105 - ], - "board" : [ - 109, - 105, - 114, - 97, - 105 - ], - "brand" : [ - 109, - 97, - 109, - 111, - 101 - ], - "model" : [ - 109, - 105, - 114, - 97, - 105 - ], - "bootloader" : [ - 117, - 110, - 107, - 110, - 111, - 119, - 110 - ], - "fingerprint" : [ - 109, - 97, - 109, - 111, - 101, - 47, - 109, - 105, - 114, - 97, - 105, - 47, - 109, - 105, - 114, - 97, - 105, - 58, - 49, - 48, - 47, - 77, - 73, - 82, - 65, - 73, - 46, - 50, - 48, - 48, - 49, - 50, - 50, - 46, - 48, - 48, - 49, - 47, - 53, - 56, - 52, - 54, - 51, - 56, - 49, - 58, - 117, - 115, - 101, - 114, - 47, - 114, - 101, - 108, - 101, - 97, - 115, - 101, - 45, - 107, - 101, - 121, - 115 - ], - "bootId" : [ - 56, - 53, - 57, - 67, - 67, - 54, - 52, - 65, - 45, - 57, - 65, - 69, - 57, - 45, - 56, - 48, - 67, - 51, - 45, - 66, - 51, - 68, - 52, - 45, - 51, - 49, - 70, - 49, - 49, - 67, - 56, - 67, - 54, - 66, - 56, - 52 - ], - "procVersion" : [ - 76, - 105, - 110, - 117, - 120, - 32, - 118, - 101, - 114, - 115, - 105, - 111, - 110, - 32, - 51, - 46, - 48, - 46, - 51, - 49, - 45, - 48, - 84, - 102, - 51, - 68, - 50, - 53, - 67, - 32, - 40, - 97, - 110, - 100, - 114, - 111, - 105, - 100, - 45, - 98, - 117, - 105, - 108, - 100, - 64, - 120, - 120, - 120, - 46, - 120, - 120, - 120, - 46, - 120, - 120, - 120, - 46, - 120, - 120, - 120, - 46, - 99, - 111, - 109, - 41 - ], - "baseBand" : [ - ], - "version" : { - "incremental" : [ - 53, - 56, - 57, - 49, - 57, - 51, - 56 - ], - "release" : [ - 49, - 48 - ], - "codename" : [ - 82, - 69, - 76 - ] - }, - "simInfo" : [ - 84, - 45, - 77, - 111, - 98, - 105, - 108, - 101 - ], - "osType" : [ - 97, - 110, - 100, - 114, - 111, - 105, - 100 - ], - "macAddress" : [ - 48, - 50, - 58, - 48, - 48, - 58, - 48, - 48, - 58, - 48, - 48, - 58, - 48, - 48, - 58, - 48, - 48 - ], - "wifiBSSID" : [ - 48, - 50, - 58, - 48, - 48, - 58, - 48, - 48, - 58, - 48, - 48, - 58, - 48, - 48, - 58, - 48, - 48 - ], - "wifiSSID" : [ - 60, - 117, - 110, - 107, - 110, - 111, - 119, - 110, - 32, - 115, - 115, - 105, - 100, - 62 - ], - "imsiMd5" : [ - 69, - 45, - 31, - 44, - 85, - 103, - -19, - 88, - 21, - -47, - 94, - -128, - 38, - -45, - 9, - 50 - ], - "imei" : "101633900250935", - "apn" : [ - 119, - 105, - 102, - 105 - ] -} \ No newline at end of file diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/utils/DeviceInfo.kt b/mirai-core-api/src/jvmBaseMain/kotlin/utils/DeviceInfo.kt index 5c56ccb8e15..92b733bf9fd 100644 --- a/mirai-core-api/src/jvmBaseMain/kotlin/utils/DeviceInfo.kt +++ b/mirai-core-api/src/jvmBaseMain/kotlin/utils/DeviceInfo.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. @@ -9,14 +9,18 @@ package net.mamoe.mirai.utils +import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import kotlinx.serialization.json.Json import java.io.File import kotlin.random.Random -@Serializable -public actual class DeviceInfo actual constructor( +@Serializable(DeviceInfoV1LegacySerializer::class) +public actual class DeviceInfo +@Deprecated(DeviceInfoConstructorDeprecationMessage, level = DeprecationLevel.WARNING) +@DeprecatedSinceMirai(warningSince = "2.15") // planned internal +public actual constructor( public actual val display: ByteArray, public actual val product: ByteArray, public actual val device: ByteArray, @@ -36,9 +40,48 @@ public actual class DeviceInfo actual constructor( public actual val wifiSSID: ByteArray, public actual val imsiMd5: ByteArray, public actual val imei: String, - public actual val apn: ByteArray + public actual val apn: ByteArray, + public actual val androidId: ByteArray, ) { - public actual val androidId: ByteArray get() = display + @Deprecated( + DeviceInfoConstructorDeprecationMessage, + replaceWith = ReplaceWith( + "net.mamoe.mirai.utils.DeviceInfo(display, product, device, board, brand, model, " + + "bootloader, fingerprint, bootId, procVersion, baseBand, version, simInfo, osType, " + + "macAddress, wifiBSSID, wifiSSID, imsiMd5, imei, apn, androidId)" + ), + level = DeprecationLevel.WARNING + ) + @DeprecatedSinceMirai(warningSince = "2.15") + @Suppress("DEPRECATION", "DEPRECATION_ERROR") + public constructor( + display: ByteArray, + product: ByteArray, + device: ByteArray, + board: ByteArray, + brand: ByteArray, + model: ByteArray, + bootloader: ByteArray, + fingerprint: ByteArray, + bootId: ByteArray, + procVersion: ByteArray, + baseBand: ByteArray, + version: Version, + simInfo: ByteArray, + osType: ByteArray, + macAddress: ByteArray, + wifiBSSID: ByteArray, + wifiSSID: ByteArray, + imsiMd5: ByteArray, + imei: String, + apn: ByteArray + ) : this( + display, product, device, board, brand, model, bootloader, + fingerprint, bootId, procVersion, baseBand, version, simInfo, + osType, macAddress, wifiBSSID, wifiSSID, imsiMd5, imei, apn, + androidId = display + ) + public actual val ipAddress: ByteArray get() = byteArrayOf(192.toByte(), 168.toByte(), 1, 123) init { @@ -100,7 +143,9 @@ public actual class DeviceInfo actual constructor( this.writeText(DeviceInfoManager.serialize(it, json)) } } - return DeviceInfoManager.deserialize(this.readText(), json) + return DeviceInfoManager.deserialize(this.readText(), json) { upg -> + this.writeText(DeviceInfoManager.serialize(upg, json)) + } } /** @@ -120,6 +165,24 @@ public actual class DeviceInfo actual constructor( public actual fun random(random: Random): DeviceInfo { return DeviceInfoCommonImpl.randomDeviceInfo(random) } + + /** + * 将此 [DeviceInfo] 序列化为字符串. 序列化的字符串可以在以后通过 [DeviceInfo.deserializeFromString] 反序列化为 [DeviceInfo]. + * + * 序列化的字符串有兼容性保证, 在旧版 mirai 序列化的字符串, 可以在新版 mirai 使用. 但新版 mirai 序列化的字符串不一定能在旧版使用. + * + * @since 2.15 + */ + @JvmStatic + public actual fun serializeToString(deviceInfo: DeviceInfo): String = DeviceInfoManager.serialize(deviceInfo) + + /** + * 将通过 [serializeToString] 序列化得到的字符串反序列化为 [DeviceInfo]. + * 此函数兼容旧版 mirai 序列化的字符串. + * @since 2.15 + */ + @JvmStatic + public actual fun deserializeFromString(string: String): DeviceInfo = DeviceInfoManager.deserialize(string) } /** @@ -137,4 +200,8 @@ public actual class DeviceInfo actual constructor( actual override fun hashCode(): Int { return DeviceInfoCommonImpl.hashCodeImpl(this) } + + @Suppress("ClassName") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public object `$serializer` : KSerializer by DeviceInfoV1LegacySerializer } diff --git a/mirai-core-api/src/jvmBaseTest/kotlin/utils/JvmDeviceInfoTest.kt b/mirai-core-api/src/jvmBaseTest/kotlin/utils/JvmDeviceInfoTest.kt index 7700a557874..b73cccd1a08 100644 --- a/mirai-core-api/src/jvmBaseTest/kotlin/utils/JvmDeviceInfoTest.kt +++ b/mirai-core-api/src/jvmBaseTest/kotlin/utils/JvmDeviceInfoTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 Mamoe Technologies and contributors. + * Copyright 2019-2022 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. @@ -11,10 +11,12 @@ package net.mamoe.mirai.utils import kotlinx.serialization.json.Json import net.mamoe.mirai.utils.DeviceInfo.Companion.loadAsDeviceInfo +import net.mamoe.mirai.utils.DeviceInfoManager.Version.Companion.trans import org.junit.jupiter.api.io.TempDir import java.io.File import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertTrue class JvmDeviceInfoTest { @@ -22,7 +24,7 @@ class JvmDeviceInfoTest { lateinit var dir: File @Test - fun `can write and read v2`() { + fun `can write and read`() { val device = DeviceInfo.random() val file = dir.resolve("device.json") @@ -31,11 +33,161 @@ class JvmDeviceInfoTest { } @Test - fun `can read legacy v1`() { + fun `can write read legacy v1`() { val device = DeviceInfo.random() val file = dir.resolve("device.json") - file.writeText(Json.encodeToString(DeviceInfo.serializer(), device)) - assertEquals(device, file.loadAsDeviceInfo()) + val encoded = Json.encodeToString( + DeviceInfoManager.V1.serializer(), DeviceInfoManager.V1( + display = device.display, + product = device.product, + device = device.device, + board = device.board, + brand = device.brand, + model = device.model, + bootloader = device.bootloader, + fingerprint = device.fingerprint, + bootId = device.bootId, + procVersion = device.procVersion, + baseBand = device.baseBand, + version = device.version, + simInfo = device.simInfo, + osType = device.osType, + macAddress = device.macAddress, + wifiBSSID = device.wifiBSSID, + wifiSSID = device.wifiSSID, + imsiMd5 = device.imsiMd5, + imei = device.imei, + apn = device.apn, + ) + ) + + file.writeText(encoded) + val fileDeviceInfo = file.loadAsDeviceInfo() + + assertTrue { isSameType(device, fileDeviceInfo) } + + assertTrue { device.display.contentEquals(fileDeviceInfo.display) } + assertTrue { device.product.contentEquals(fileDeviceInfo.product) } + assertTrue { device.device.contentEquals(fileDeviceInfo.device) } + assertTrue { device.board.contentEquals(fileDeviceInfo.board) } + assertTrue { device.brand.contentEquals(fileDeviceInfo.brand) } + assertTrue { device.model.contentEquals(fileDeviceInfo.model) } + assertTrue { device.bootloader.contentEquals(fileDeviceInfo.bootloader) } + assertTrue { device.fingerprint.contentEquals(fileDeviceInfo.fingerprint) } + assertTrue { device.bootId.contentEquals(fileDeviceInfo.bootId) } + assertTrue { device.procVersion.contentEquals(fileDeviceInfo.procVersion) } + assertTrue { device.baseBand.contentEquals(fileDeviceInfo.baseBand) } + assertEquals(device.version, fileDeviceInfo.version) + assertTrue { device.simInfo.contentEquals(fileDeviceInfo.simInfo) } + assertTrue { device.osType.contentEquals(fileDeviceInfo.osType) } + assertTrue { device.macAddress.contentEquals(fileDeviceInfo.macAddress) } + assertTrue { device.wifiBSSID.contentEquals(fileDeviceInfo.wifiBSSID) } + assertTrue { device.wifiSSID.contentEquals(fileDeviceInfo.wifiSSID) } + assertTrue { device.imsiMd5.contentEquals(fileDeviceInfo.imsiMd5) } + assertEquals(device.imei, fileDeviceInfo.imei) + assertTrue { device.apn.contentEquals(fileDeviceInfo.apn) } + assertTrue { device.androidId.size == fileDeviceInfo.androidId.size } + } + + @Test + fun `can write and read legacy v2`() { + val device = DeviceInfo.random() + val file = dir.resolve("device.json") + + val encoded = Json.encodeToString( + DeviceInfoManager.Wrapper.serializer(DeviceInfoManager.V2.serializer()), + DeviceInfoManager.Wrapper( + 2, DeviceInfoManager.V2( + display = device.display.decodeToString(), + product = device.product.decodeToString(), + device = device.device.decodeToString(), + board = device.board.decodeToString(), + brand = device.brand.decodeToString(), + model = device.model.decodeToString(), + bootloader = device.bootloader.decodeToString(), + fingerprint = device.fingerprint.decodeToString(), + bootId = device.bootId.decodeToString(), + procVersion = device.procVersion.decodeToString(), + baseBand = DeviceInfoManager.HexString(device.baseBand), + version = device.version.trans(), + simInfo = device.simInfo.decodeToString(), + osType = device.osType.decodeToString(), + macAddress = device.macAddress.decodeToString(), + wifiBSSID = device.wifiBSSID.decodeToString(), + wifiSSID = device.wifiSSID.decodeToString(), + imsiMd5 = DeviceInfoManager.HexString(device.imsiMd5), + imei = device.imei, + apn = device.apn.decodeToString(), + ) + ) + ) + + file.writeText(encoded) + val fileDeviceInfo = file.loadAsDeviceInfo() + + assertTrue { isSameType(device, fileDeviceInfo) } + + assertTrue { device.display.contentEquals(fileDeviceInfo.display) } + assertTrue { device.product.contentEquals(fileDeviceInfo.product) } + assertTrue { device.device.contentEquals(fileDeviceInfo.device) } + assertTrue { device.board.contentEquals(fileDeviceInfo.board) } + assertTrue { device.brand.contentEquals(fileDeviceInfo.brand) } + assertTrue { device.model.contentEquals(fileDeviceInfo.model) } + assertTrue { device.bootloader.contentEquals(fileDeviceInfo.bootloader) } + assertTrue { device.fingerprint.contentEquals(fileDeviceInfo.fingerprint) } + assertTrue { device.bootId.contentEquals(fileDeviceInfo.bootId) } + assertTrue { device.procVersion.contentEquals(fileDeviceInfo.procVersion) } + assertTrue { device.baseBand.contentEquals(fileDeviceInfo.baseBand) } + assertEquals(device.version, fileDeviceInfo.version) + assertTrue { device.simInfo.contentEquals(fileDeviceInfo.simInfo) } + assertTrue { device.osType.contentEquals(fileDeviceInfo.osType) } + assertTrue { device.macAddress.contentEquals(fileDeviceInfo.macAddress) } + assertTrue { device.wifiBSSID.contentEquals(fileDeviceInfo.wifiBSSID) } + assertTrue { device.wifiSSID.contentEquals(fileDeviceInfo.wifiSSID) } + assertTrue { device.imsiMd5.contentEquals(fileDeviceInfo.imsiMd5) } + assertEquals(device.imei, fileDeviceInfo.imei) + assertTrue { device.apn.contentEquals(fileDeviceInfo.apn) } + assertTrue { device.androidId.size == fileDeviceInfo.androidId.size } + } + + @Test + fun `can write and read v3`() { + val device = DeviceInfo.random() + val file = dir.resolve("device.json") + + val encoded = Json.encodeToString( + DeviceInfoManager.Wrapper.serializer(DeviceInfoManager.V3.serializer()), + DeviceInfoManager.Wrapper( + 3, DeviceInfoManager.V3( + display = device.display.decodeToString(), + product = device.product.decodeToString(), + device = device.device.decodeToString(), + board = device.board.decodeToString(), + brand = device.brand.decodeToString(), + model = device.model.decodeToString(), + bootloader = device.bootloader.decodeToString(), + fingerprint = device.fingerprint.decodeToString(), + bootId = device.bootId.decodeToString(), + procVersion = device.procVersion.decodeToString(), + baseBand = DeviceInfoManager.HexString(device.baseBand), + version = device.version.trans(), + simInfo = device.simInfo.decodeToString(), + osType = device.osType.decodeToString(), + macAddress = device.macAddress.decodeToString(), + wifiBSSID = device.wifiBSSID.decodeToString(), + wifiSSID = device.wifiSSID.decodeToString(), + imsiMd5 = DeviceInfoManager.HexString(device.imsiMd5), + imei = device.imei, + apn = device.apn.decodeToString(), + androidId = device.androidId.decodeToString() + ) + ) + ) + + file.writeText(encoded) + val fileDeviceInfo = file.loadAsDeviceInfo() + + assertEquals(device, fileDeviceInfo) } } \ No newline at end of file diff --git a/mirai-core-api/src/jvmTest/kotlin/utils/JvmDeviceInfoTestJvm.kt b/mirai-core-api/src/jvmTest/kotlin/utils/JvmDeviceInfoTestJvm.kt deleted file mode 100644 index 30072e279f0..00000000000 --- a/mirai-core-api/src/jvmTest/kotlin/utils/JvmDeviceInfoTestJvm.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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.utils - -import kotlin.test.Test - -class JvmDeviceInfoTestJvm { - @Test - fun `can deserialize legacy versions before 2_9_0`() { - // resources not available on android - - DeviceInfoManager.deserialize( - this::class.java.classLoader.getResourceAsStream("device/legacy-device-info-1.json")!! - .use { it.readBytes().decodeToString() }) - } -} \ No newline at end of file diff --git a/mirai-core-api/src/nativeMain/kotlin/utils/BotConfiguration.kt b/mirai-core-api/src/nativeMain/kotlin/utils/BotConfiguration.kt index e76828a2baa..9d5fbc47c8d 100644 --- a/mirai-core-api/src/nativeMain/kotlin/utils/BotConfiguration.kt +++ b/mirai-core-api/src/nativeMain/kotlin/utils/BotConfiguration.kt @@ -41,7 +41,10 @@ public actual abstract class AbstractBotConfiguration { // open for Java if (!file.exists()) { file.writeText(DeviceInfoManager.serialize(DeviceInfo.random(), BotConfiguration.json)) } - DeviceInfoManager.deserialize(file.readText(), BotConfiguration.json) + DeviceInfoManager.deserialize(file.readText(), BotConfiguration.json) { + file.writeText(DeviceInfoManager.serialize(it, BotConfiguration.json)) + } + } } diff --git a/mirai-core-api/src/nativeMain/kotlin/utils/DeviceInfo.kt b/mirai-core-api/src/nativeMain/kotlin/utils/DeviceInfo.kt index 58dfaecda9c..52d14f05b25 100644 --- a/mirai-core-api/src/nativeMain/kotlin/utils/DeviceInfo.kt +++ b/mirai-core-api/src/nativeMain/kotlin/utils/DeviceInfo.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. @@ -13,8 +13,11 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import kotlin.random.Random -@Serializable -public actual class DeviceInfo actual constructor( +@Serializable(DeviceInfoV1LegacySerializer::class) +public actual class DeviceInfo +@Deprecated(DeviceInfoConstructorDeprecationMessage, level = DeprecationLevel.WARNING) +@DeprecatedSinceMirai(warningSince = "2.15") // planned internal +public actual constructor( public actual val display: ByteArray, public actual val product: ByteArray, public actual val device: ByteArray, @@ -34,9 +37,9 @@ public actual class DeviceInfo actual constructor( public actual val wifiSSID: ByteArray, public actual val imsiMd5: ByteArray, public actual val imei: String, - public actual val apn: ByteArray + public actual val apn: ByteArray, + public actual val androidId: ByteArray, ) { - public actual val androidId: ByteArray get() = display public actual val ipAddress: ByteArray get() = byteArrayOf(192.toByte(), 168.toByte(), 1, 123) init { @@ -99,6 +102,22 @@ public actual class DeviceInfo actual constructor( public actual fun random(random: Random): DeviceInfo { return DeviceInfoCommonImpl.randomDeviceInfo(random) } + + /** + * 将此 [DeviceInfo] 序列化为字符串. 序列化的字符串可以在以后通过 [DeviceInfo.deserializeFromString] 反序列化为 [DeviceInfo]. + * + * 序列化的字符串有兼容性保证, 在旧版 mirai 序列化的字符串, 可以在新版 mirai 使用. 但新版 mirai 序列化的字符串不一定能在旧版使用. + * + * @since 2.15 + */ + public actual fun serializeToString(deviceInfo: DeviceInfo): String = DeviceInfoManager.serialize(deviceInfo) + + /** + * 将通过 [serializeToString] 序列化得到的字符串反序列化为 [DeviceInfo]. + * 此函数兼容旧版 mirai 序列化的字符串. + * @since 2.15 + */ + public actual fun deserializeFromString(string: String): DeviceInfo = DeviceInfoManager.deserialize(string) } /** diff --git a/mirai-core-utils/src/commonMain/kotlin/TimeUtils.kt b/mirai-core-utils/src/commonMain/kotlin/TimeUtils.kt index 599392ce1dd..6746f885023 100644 --- a/mirai-core-utils/src/commonMain/kotlin/TimeUtils.kt +++ b/mirai-core-utils/src/commonMain/kotlin/TimeUtils.kt @@ -25,7 +25,11 @@ public expect fun currentTimeMillis(): Long */ public fun currentTimeSeconds(): Long = currentTimeMillis() / 1000 -public expect fun currentTimeFormatted(format: String? = null): String +public fun currentTimeFormatted(format: String? = null): String { + return formatTime(currentTimeMillis(), format) +} + +public expect fun formatTime(epochTimeMillis: Long, format: String?): String // 临时使用, 待 Kotlin Duration 稳定后使用 Duration. diff --git a/mirai-core-utils/src/jvmBaseMain/kotlin/TimeUtils.kt b/mirai-core-utils/src/jvmBaseMain/kotlin/TimeUtils.kt index e487e9df128..6eed9b3fb2f 100644 --- a/mirai-core-utils/src/jvmBaseMain/kotlin/TimeUtils.kt +++ b/mirai-core-utils/src/jvmBaseMain/kotlin/TimeUtils.kt @@ -19,10 +19,10 @@ private val timeFormat: SimpleDateFormat by threadLocal { SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) } -public actual fun currentTimeFormatted(format: String?): String { +public actual fun formatTime(epochTimeMillis: Long, format: String?): String { return if (format == null) { - timeFormat.format(Date()) + timeFormat.format(Date(epochTimeMillis)) } else { - SimpleDateFormat(format, Locale.getDefault()).format(Date()) + SimpleDateFormat(format, Locale.getDefault()).format(Date(epochTimeMillis)) } } \ No newline at end of file diff --git a/mirai-core-utils/src/nativeMain/kotlin/TimeUtils.kt b/mirai-core-utils/src/nativeMain/kotlin/TimeUtils.kt index b758800f9d9..83fd24a4ac8 100644 --- a/mirai-core-utils/src/nativeMain/kotlin/TimeUtils.kt +++ b/mirai-core-utils/src/nativeMain/kotlin/TimeUtils.kt @@ -32,18 +32,26 @@ public actual fun currentTimeMillis(): Long { private val timeLock = ReentrantLock() -@OptIn(UnsafeNumber::class) -public actual fun currentTimeFormatted(format: String?): String = timeLock.withLock { +public actual fun formatTime(epochTimeMillis: Long, format: String?): String = timeLock.withLock { + val strftimeFormat = format + ?.replace("yyyy", "%Y") + ?.replace("MM", "%m") + ?.replace("dd", "%d") + ?.replace("HH", "%H") + ?.replace("mm", "%M") + ?.replace("ss", "%S") + ?: "%Y-%m-%d %H:%M:%S" memScoped { val timeT = alloc() - time(timeT.ptr) + timeT.value = epochTimeMillis / 1000 // http://www.cplusplus.com/reference/clibrary/ctime/localtime/ // tm returns a static pointer which doesn't need to free val tm = localtime(timeT.ptr) // localtime is not thread-safe val bb = allocArray(40) - strftime(bb, 40, "%Y-%m-%d %H:%M:%S", tm); + + strftime(bb, 40, strftimeFormat, tm); bb.toKString() } diff --git a/mirai-core-utils/src/nativeTest/kotlin/TimeUtilsTest.kt b/mirai-core-utils/src/nativeTest/kotlin/TimeUtilsTest.kt index 951a5e3df42..e356175e4cc 100644 --- a/mirai-core-utils/src/nativeTest/kotlin/TimeUtilsTest.kt +++ b/mirai-core-utils/src/nativeTest/kotlin/TimeUtilsTest.kt @@ -10,6 +10,8 @@ package net.mamoe.mirai.utils import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull import kotlin.test.assertTrue internal class TimeUtilsTest { @@ -23,6 +25,59 @@ internal class TimeUtilsTest { @Test fun `can get currentTimeFormatted`() { // 2022-28-26 18:28:28 - assertTrue { currentTimeFormatted().matches(Regex("""\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}""")) } + assertTrue { currentTimeFormatted().matches(Regex("""^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$""")) } + } + + @Test + fun `can parse explicit timestamp`() { + val epochMilli = 1681174590123 // 2023-04-11 00:56:30 GMT + val regex = Regex("""^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$""") + + val formatted = regex.find(formatTime(epochMilli, null)) + assertNotNull(formatted) + + formatted.groupValues.run { + assertEquals(get(1), "2023") + assertEquals(get(2), "04") + assertTrue { get(3) == "11" || get(3) == "10" } + assertTrue { get(4).toInt() in 0..23 } + assertEquals(get(5), "56") + assertEquals(get(6), "30") + } + } + + @Test + fun `can format with custom formatter`() { + fun formatTimeAndPrint(formatter: String?): String { + return formatTime(currentTimeMillis(), formatter).also { println("custom formatted time: $it") } + } + + assertTrue { + formatTimeAndPrint("MmMm").matches(Regex("""^MmMm$""")) + } + assertTrue { + formatTimeAndPrint("MM-mm").matches(Regex("""^\d{2}-\d{2}$""")) + } + assertTrue { + formatTimeAndPrint("yyyyMMddHHmmss").matches(Regex("""^\d{14}$""")) + } + assertTrue { + formatTimeAndPrint("yyyyMMddHHmmSS").matches(Regex("""^\d{12}SS$""")) + } + assertTrue { + formatTimeAndPrint(null).matches(Regex("""^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$""")) + } + assertTrue { + formatTimeAndPrint("yyyy-MM-dd 114514").matches(Regex("""^\d{4}-\d{2}-\d{2} 114514$""")) + } + assertTrue { + formatTimeAndPrint("yyyyMM-114 514--mm-SS").matches(Regex("""^\d{4}\d{2}-114 514--\d{2}-SS$""")) + } + assertTrue { + formatTimeAndPrint("yyyy-MM-dd HH-mm-ss").matches(Regex("""^\d{4}-\d{2}-\d{2} \d{2}-\d{2}-\d{2}$""")) + } + assertTrue { + formatTimeAndPrint("yyyy/MM\\dd HH:mm-ss").matches(Regex("""^\d{4}/\d{2}\\\d{2} \d{2}:\d{2}-\d{2}$""")) + } } } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt b/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt index b858aab1c3b..71b6e5037bf 100644 --- a/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt +++ b/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt @@ -102,7 +102,7 @@ internal open class QQAndroidClient( val apkVersionName: ByteArray get() = protocol.ver.toByteArray() //"8.4.18".toByteArray() - val buildVer: String get() = "8.4.18.4810" // 8.2.0.1296 // 8.4.8.4810 // 8.2.7.4410 + val buildVer: String get() = protocol.buildVer // 8.2.0.1296 // 8.4.8.4810 // 8.2.7.4410 private val sequenceId: AtomicInt = atomic(getRandomUnsignedInt()) @@ -166,7 +166,15 @@ internal open class QQAndroidClient( var reserveUinInfo: ReserveUinInfo? = null var t402: ByteArray? = null lateinit var t104: ByteArray + internal val t104Initialized get() = ::t104.isInitialized + var t543: ByteArray? = null var t547: ByteArray? = null + + /** + * t545 + */ + var qimei16: String? = null + var qimei36: String? = null } internal val QQAndroidClient.apkId: ByteArray get() = protocol.apkId.toByteArray() diff --git a/mirai-core/src/commonMain/kotlin/network/components/CacheValidator.kt b/mirai-core/src/commonMain/kotlin/network/components/CacheValidator.kt index 68094bd10e5..dc0eeda5acd 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/CacheValidator.kt +++ b/mirai-core/src/commonMain/kotlin/network/components/CacheValidator.kt @@ -10,7 +10,6 @@ package net.mamoe.mirai.internal.network.components import io.ktor.utils.io.core.* -import net.mamoe.mirai.internal.network.ProtoBufForCache import net.mamoe.mirai.internal.network.component.ComponentKey import net.mamoe.mirai.internal.utils.MiraiProtocolInternal import net.mamoe.mirai.internal.utils.io.writeShortLVString @@ -61,7 +60,7 @@ internal class CacheValidatorImpl( val device = ssoProcessorContext.device @Suppress("INVISIBLE_MEMBER") - writeFully(ProtoBufForCache.encodeToByteArray(DeviceInfo.serializer(), device)) + writeFully(device.serializeToString().encodeToByteArray()) }.let { pkg -> try { pkg.readBytes() diff --git a/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt b/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt index 3079085c065..21531d4fdf8 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt +++ b/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt @@ -32,6 +32,8 @@ import net.mamoe.mirai.internal.network.protocol.packet.login.UrlDeviceVerificat import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin.Login.LoginPacketResponse import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin.Login.LoginPacketResponse.Captcha import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.* +import net.mamoe.mirai.internal.network.qimei.requestQimei +import net.mamoe.mirai.internal.utils.subLogger import net.mamoe.mirai.network.* import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol @@ -140,6 +142,8 @@ internal open class SsoProcessorImpl( ssoContext.bot.components[BotClientHolder].client = value } + private val qimeiLogger by lazy { ssoContext.bot.network.logger.subLogger("QimeiApi") } + override val ssoSession: SsoSession get() = client private val components get() = ssoContext.bot.components @@ -199,6 +203,12 @@ internal open class SsoProcessorImpl( components[BdhSessionSyncer].loadServerListFromCache() + try { + ssoContext.bot.requestQimei(qimeiLogger) + } catch (exception: Throwable) { + qimeiLogger.warning("Cannot get qimei from server.", exception) + } + // try fast login if (client.wLoginSigInfoInitialized) { ssoContext.bot.components[EcdhInitialPublicKeyUpdater].refreshInitialPublicKeyAndApplyEcdh() diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/Tlv.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/Tlv.kt index 04eb68e0a53..f7a6e73167c 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/Tlv.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/Tlv.kt @@ -18,7 +18,10 @@ import net.mamoe.mirai.internal.utils.GuidSource import net.mamoe.mirai.internal.utils.MacOrAndroidIdChangeFlag import net.mamoe.mirai.internal.utils.NetworkType import net.mamoe.mirai.internal.utils.guidFlag -import net.mamoe.mirai.internal.utils.io.* +import net.mamoe.mirai.internal.utils.io.encryptAndWrite +import net.mamoe.mirai.internal.utils.io.writeShortLVByteArray +import net.mamoe.mirai.internal.utils.io.writeShortLVByteArrayLimitedLength +import net.mamoe.mirai.internal.utils.io.writeShortLVString import net.mamoe.mirai.utils.* import kotlin.jvm.JvmInline import kotlin.random.Random @@ -49,15 +52,15 @@ internal fun TlvMap.smartToString(leadingLineBreak: Boolean = true, sorted: Bool @JvmInline internal value class Tlv(val value: ByteArray) -internal fun TlvMapWriter.t1(uin: Long, ip: ByteArray) { - require(ip.size == 4) { "ip.size must == 4" } +internal fun TlvMapWriter.t1(uin: Long, timeSeconds: Int, ipv4: ByteArray) { + require(ipv4.size == 4) { "ip.size must == 4" } tlv(0x01) { writeShort(1) // _ip_ver writeInt(Random.nextInt()) writeInt(uin.toInt()) - writeInt(currentTimeSeconds().toInt()) - writeFully(ip) + writeInt(timeSeconds) + writeFully(ipv4) writeShort(0) } } @@ -192,6 +195,7 @@ internal fun TlvMapWriter.t106( client.subAppId /* maybe 1*/, client.appClientVersion, client.uin, + client.device.ipAddress, true, passwordMd5, 0, @@ -220,6 +224,7 @@ internal fun TlvMapWriter.t106( subAppId: Long, appClientVersion: Int = 0, uin: Long, + ipv4: ByteArray, isSavePassword: Boolean = true, passwordMd5: ByteArray, salt: Long, @@ -233,6 +238,7 @@ internal fun TlvMapWriter.t106( passwordMd5.requireSize(16) tgtgtKey.requireSize(16) guid?.requireSize(16) + ipv4.requireSize(4) tlv(0x106) { encryptAndWrite( @@ -252,7 +258,7 @@ internal fun TlvMapWriter.t106( } writeInt(currentTimeSeconds().toInt()) - writeFully(ByteArray(4)) // ip // no need to write actual ip + writeFully(ipv4) // writeByte(isSavePassword.toByte()) writeFully(passwordMd5) writeFully(tgtgtKey) @@ -368,16 +374,18 @@ internal fun TlvMapWriter.t174( internal fun TlvMapWriter.t17a( - value: Int = 0 + smsAppId: Int = 0 ) { tlv(0x17a) { - writeInt(value) + writeInt(smsAppId) } } -internal fun TlvMapWriter.t197() { +internal fun TlvMapWriter.t197( + devLockMobileType: Byte = 0 +) { tlv(0x197) { - writeByte(0) + writeByte(devLockMobileType) } } @@ -898,6 +906,61 @@ internal fun TlvMapWriter.t525( } } +internal fun TlvMapWriter.t542( + value: ByteArray +) { + tlv(0x542) { + writeFully(value) + } +} + +internal fun TlvMapWriter.t545( + qimei: String +) { + tlv(0x545) { + writeFully(qimei.toByteArray()) + } +} + +internal fun TlvMapWriter.t548( + nativeGetTestData: ByteArray = ( + "01 02 01 01 00 0A 00 00 00 80 5E C1 1A B0 39 A0 " + + "E0 5C 67 DF 44 F8 E5 86 91 A2 A4 5D 92 2B 25 3A " + + "B6 6E 2F F1 A1 E3 60 B8 36 1E 2F 6B 6F F7 2D F7 " + + "F8 21 F1 0B 75 7D 2A 4F 63 B8 83 9C 41 0B AA C7 " + + "C9 69 0D 70 AB F3 0F 46 28 C2 CD DB 81 CC 74 18 " + + "ED 97 CD 31 3E 1A 17 F1 94 96 AB 6C 6B 25 4F 83 " + + "5B 15 82 B0 8F 53 82 3F 59 FE 6E B5 EA B5 EA 7A " + + "0C E7 2B 31 CA 4C FD 43 9A DB 40 7A CA 51 D7 9A " + + "3C AD 6D 8F 3C C6 84 A5 4A 5F 00 20 BE FB 91 06 " + + "F0 67 42 8B CC 59 27 4E BC 91 78 55 4E E4 5C 98 " + + "4B 8B 0F C9 A3 83 56 06 E8 AE 5A 0D 00 AC 01 02 " + + "01 02 00 0A 00 00 00 80 5E C1 1A B0 39 A0 E0 5C " + + "67 DF 44 F8 E5 86 91 A2 A4 5D 92 2B 25 3A B6 6E " + + "2F F1 A1 E3 60 B8 36 1E 2F 6B 6F F7 2D F7 F8 21 " + + "F1 0B 75 7D 2A 4F 63 B8 83 9C 41 0B AA C7 C9 69 " + + "0D 70 AB F3 0F 46 28 C2 CD DB 81 CC 74 18 ED 97 " + + "CD 31 3E 1A 17 F1 94 96 AB 6C 6B 25 4F 83 5B 15 " + + "82 B0 8F 53 82 3F 59 FE 6E B5 EA B5 EA 7A 0C E7 " + + "2B 31 CA 4C FD 43 9A DB 40 7A CA 51 D7 9A 3C AD " + + "6D 8F 3C C6 84 A5 4A 5F 00 20 BE FB 91 06 F0 67 " + + "42 8B CC 59 27 4E BC 91 78 55 4E E4 5C 98 4B 8B " + + "0F C9 A3 83 56 06 E8 AE 5A 0D 00 80 5E C1 1A B0 " + + "39 A0 E0 5C 67 DF 44 F8 E5 86 91 A2 A4 5D 92 2B " + + "25 3A B6 6E 2F F1 A1 E3 60 B8 36 1E 2F 6B 6F F7 " + + "2D F7 F8 21 F1 0B 75 7D 2A 4F 63 B8 83 9C 41 0B " + + "AA C7 C9 69 0D 70 AB F3 0F 46 28 C2 CD DB 81 CC " + + "74 18 ED 97 CD 31 3E 1A 17 F1 94 96 AB 6C 6B 25 " + + "4F 83 5B 15 82 B0 8F 53 82 3F 59 FE 6E B5 EA B5 " + + "EA 7A 0C E7 2B 31 CA 4C FD 43 9A DB 40 7A CA 51 " + + "D7 9A 3C AD 6D 8F 3C C6 84 A5 71 6F 00 00 00 1F " + + "00 00 27 10").hexToBytes() +) { + tlv(0x548) { + writeFully(nativeGetTestData) + } +} + internal fun TlvMapWriter.t544( // 1334 ) { tlv(0x544) { diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/WtLogin.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/WtLogin.kt index cf0d59856e1..74a48caa2bc 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/WtLogin.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/WtLogin.kt @@ -216,6 +216,7 @@ internal class WtLogin { tlvMap[0x403]?.let { bot.client.randSeed = it } tlvMap[0x402]?.let { bot.client.t402 = it } tlvMap[0x546]?.let { bot.client.analysisTlv546(it) } + tlvMap[0x543]?.let { bot.client.t543 = it } // tlvMap[0x402]?.let { t402 -> // bot.client.G = buildPacket { // writeFully(bot.client.device.guid) diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin10.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin10.kt index fc5d390675d..e4a07f3b354 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin10.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin10.kt @@ -71,6 +71,7 @@ internal object WtLogin10 : WtLoginExt { ) //t112(client.account.phoneNumber.encodeToByteArray()) t143(client.wLoginSigInfo.d2.data) + t145(client.device.guid) t142(client.apkId) t154(sequenceId) t18(appId, uin = client.uin) diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin15.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin15.kt index f1b43ddd73b..961372856ff 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin15.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin15.kt @@ -14,6 +14,8 @@ import net.mamoe.mirai.internal.network.* import net.mamoe.mirai.internal.network.protocol.packet.* import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin import net.mamoe.mirai.utils._writeTlvMap +import net.mamoe.mirai.utils.currentTimeSeconds +import net.mamoe.mirai.utils.toByteArray import kotlin.math.abs import kotlin.random.Random @@ -26,7 +28,7 @@ internal object WtLogin15 : WtLoginExt { client: QQAndroidClient, ) = WtLogin.ExchangeEmp.buildOutgoingUniPacket( client, bodyType = 2, key = ByteArray(16), remark = "15:refresh-keys" - ) { + ) { sequenceId -> // writeSsoPacket(client, client.subAppId, WtLogin.ExchangeEmp.commandName, sequenceId = sequenceId) { writeOicqRequestPacket( client, @@ -53,10 +55,9 @@ internal object WtLogin15 : WtLoginExt { // "").hexToBytes()) // return@writeOicqRequestPacket - t18(appId, uin = client.uin) - t1(client.uin, ByteArray(4)) + t18(appId, client.appClientVersion, uin = client.uin) + t1(client.uin, (currentTimeSeconds() + client.timeDifference).toInt(), client.device.ipAddress) - // t106(client = client) t106(client.wLoginSigInfo.encryptA1!!) // kotlin.run { // val key = (client.account.passwordMd5 + ByteArray(4) + client.uin.toInt().toByteArray()).md5() @@ -82,7 +83,10 @@ internal object WtLogin15 : WtLoginExt { // } t116(client.miscBitMap, client.subSigMap) - //t116(0x08F7FF7C, 0x00010400) + if (client.miscBitMap and 128 != 0) { + t166(1) + client.rollbackSig?.let { t172(it) } + } //t100(appId, client.subAppId, client.appClientVersion, client.ssoVersion, client.mainSigMap) //t100(appId, 1, client.appClientVersion, client.ssoVersion, mainSigMap = 1048768) @@ -90,16 +94,21 @@ internal object WtLogin15 : WtLoginExt { t107(0) - //t108(client.ksid) // 第一次 exchange 没有 108 + if (client.ksid.isNotEmpty()) { + t108(client.ksid) + } t144(client) t142(client.apkId) + if (client.uin !in 10000L..4000000000L) { + t112(client.uin.toByteArray()) + } t145(client.device.guid) val noPicSig = client.wLoginSigInfo.noPicSig ?: error("Internal error: doing exchange emp 15 while noPicSig=null") t16a(noPicSig) - t154(0) + t154(sequenceId) t141(client.device.simInfo, client.networkType, client.device.apn) t8(2052) t511() @@ -112,20 +121,22 @@ internal object WtLogin15 : WtLoginExt { uin = client.uin, guid = client.device.guid, dpwd = client.dpwd, - appId = 1, - subAppId = 16, + appId = appId, + subAppId = 1, randomSeed = client.randSeed ) t187(client.device.macAddress) t188(client.device.androidId) t194(client.device.imsiMd5) + // ignored t201 cuz SetNeedForPayToken is never called. t202(client.device.wifiBSSID, client.device.wifiSSID) t516() t521() // new t525(client.loginExtraData) // new - //t544() // new + //t544() // new 810_f + t545(client.qimei16 ?: client.device.imei) } } diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin8.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin8.kt index 460df5534a0..724f3a152c4 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin8.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin8.kt @@ -43,7 +43,13 @@ internal object WtLogin8 : WtLoginExt { t174(t174) t17a(9) t197() + // Lcom/tencent/mobileqq/msf/core/auth/l;a(Ljava/lang/String;JLoicq/wlogin_sdk/request/WUserSigInfo;IIILoicq/wlogin_sdk/tools/ErrMsg;)V + // a2.addAttribute("smsExtraData", WtloginHelper.getLoginResultData(wUserSigInfo, 1347)); + // wUserSigInfo.loginResultTLVMap.get(new Integer(1347)).get_data() + // this.mUserSigInfo.loginResultTLVMap.put(new Integer(1347), async_contextVar._t543); + // toServiceMsg.getAttribute("smsExtraData")) + client.t543?.let { t542(it) } } } } diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin9.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin9.kt index 2e9da5fe1b9..8678df6342c 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin9.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin9.kt @@ -14,6 +14,8 @@ import net.mamoe.mirai.internal.network.* import net.mamoe.mirai.internal.network.protocol.packet.* import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin import net.mamoe.mirai.utils._writeTlvMap +import net.mamoe.mirai.utils.currentTimeSeconds +import net.mamoe.mirai.utils.toByteArray internal object WtLogin9 : WtLoginExt { private const val appId = 16L @@ -28,21 +30,16 @@ internal object WtLogin9 : WtLoginExt { writeSsoPacket(client, client.subAppId, WtLogin.Login.commandName, sequenceId = sequenceId) { writeOicqRequestPacket(client, commandId = 0x0810) { writeShort(9) // subCommand - var tlvCount = if (allowSlider) 0x18 else 0x17; val useEncryptA1AndNoPicSig = client.wLoginSigInfoInitialized && client.wLoginSigInfo.noPicSig != null && client.wLoginSigInfo.encryptA1 != null - if (useEncryptA1AndNoPicSig) { - tlvCount++; - } - // writeShort(tlvCount.toShort()) // count of TLVs, probably ignored by server? //writeShort(LoginType.PASSWORD.value.toShort()) _writeTlvMap { t18(appId, client.appClientVersion, client.uin) - t1(client.uin, client.device.ipAddress) + t1(client.uin, (currentTimeSeconds() + client.timeDifference).toInt(), client.device.ipAddress) if (useEncryptA1AndNoPicSig) { t106(client.wLoginSigInfo.encryptA1!!) @@ -63,25 +60,31 @@ internal object WtLogin9 : WtLoginExt { t116(client.miscBitMap, client.subSigMap) t100(appId, client.subAppId, client.appClientVersion, client.ssoVersion, client.mainSigMap) t107(0) - t108(client.device.imei.toByteArray()) + if (client.ksid.isNotEmpty()) { + t108(client.ksid) + } // t108(byteArrayOf()) - // ignored: t104() + if (client.t104Initialized) { + t104(client.t104) + } + t142(client.apkId) // if login with non-number uin - // t112() + if (client.uin !in 10000L..4000000000L) { + t112(client.uin.toByteArray()) + } t144(client) //this.build().debugPrint("傻逼") t145(client.device.guid) t147(appId, client.apkVersionName, client.apkSignatureMd5) - /* - if (client.miscBitMap and 0x80 != 0) { - t166(1) - } - */ + + if (client.miscBitMap and 0x80 != 0) { + t166(1) // com.tencent.luggage.wxa.me.e.CTRL_INDEX + } if (useEncryptA1AndNoPicSig) { t16a(client.wLoginSigInfo.noPicSig!!) } @@ -94,7 +97,17 @@ internal object WtLogin9 : WtLoginExt { // ignored t172 because rollbackSig is null // ignored t185 because loginType is not SMS - // ignored t400 because of first login + if (useEncryptA1AndNoPicSig) { + t400( + g = client.G, + uin = client.uin, + guid = client.device.guid, + dpwd = client.dpwd, + appId = appId, + subAppId = client.subAppId, + randomSeed = client.randSeed, + ) + } t187(client.device.macAddress) t188(client.device.androidId) @@ -103,8 +116,8 @@ internal object WtLogin9 : WtLoginExt { t191() } - /* - t201(N = byteArrayOf())*/ + + //t201(N = byteArrayOf()) t202(client.device.wifiBSSID, client.device.wifiSSID) @@ -116,6 +129,8 @@ internal object WtLogin9 : WtLoginExt { t521() t525() + t545(client.qimei16 ?: client.device.imei) + // t548() // this.build().debugPrint("傻逼") // ignored t318 because not logging in by QR @@ -138,7 +153,7 @@ internal object WtLogin9 : WtLoginExt { _writeTlvMap { t18(appId, client.appClientVersion, client.uin) - t1(client.uin, client.device.ipAddress) + t1(client.uin, (currentTimeSeconds() + client.timeDifference).toInt(), client.device.ipAddress) t106(data.tmpPwd) diff --git a/mirai-core/src/commonMain/kotlin/network/qimei/Qimei.kt b/mirai-core/src/commonMain/kotlin/network/qimei/Qimei.kt new file mode 100644 index 00000000000..3d63931b8f3 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/qimei/Qimei.kt @@ -0,0 +1,297 @@ +/* + * 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.qimei + +import io.ktor.client.plugins.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.utils.io.core.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import net.mamoe.mirai.internal.QQAndroidBot +import net.mamoe.mirai.internal.network.components.BotClientHolder +import net.mamoe.mirai.internal.network.components.HttpClientProvider +import net.mamoe.mirai.internal.network.components.SsoProcessorContext +import net.mamoe.mirai.internal.network.protocol +import net.mamoe.mirai.internal.utils.crypto.aesDecrypt +import net.mamoe.mirai.internal.utils.crypto.aesEncrypt +import net.mamoe.mirai.internal.utils.crypto.rsaEncryptWithX509PubKey +import net.mamoe.mirai.utils.* +import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol +import kotlin.random.Random + +private val secret = "ZdJqM15EeO2zWc08" +private val rsaPubKey = """ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDEIxgwoutfwoJxcGQeedgP7FG9 +qaIuS0qzfR8gWkrkTZKM2iWHn2ajQpBRZjMSoSf6+KJGvar2ORhBfpDXyVtZCKpq +LQ+FLkpncClKVIrBwv6PHyUvuCb0rIarmgDnzkfQAqVufEtR64iazGDKatvJ9y6B +9NMbHddGSAUmRTCrHQIDAQAB +-----END PUBLIC KEY----- +""".trimIndent() + +internal suspend fun QQAndroidBot.requestQimei(logger: MiraiLogger) { + val protocol = components[BotClientHolder].client.protocol + if (protocol.appKey.isEmpty()) return + + val deviceInfo = components[SsoProcessorContext].device + val httpClient = components[HttpClientProvider].getHttpClient() + + val seed = deviceInfo.guid.foldRight(0x6f4L) { curr, acc -> acc + curr.toLong() } + val random = Random(seed) + + val reservedData = Json.encodeToString( + ReservedData( + harmony = "0", + clone = "0", + containe = "", + oz = "UhYmelwouA+V2nPWbOvLTgN2/m8jwGB+yUB5v9tysQg=", + oo = "Xecjt+9S1+f8Pz2VLSxgpw==", + kelong = "0", + uptimes = formatTime(currentTimeMillis() - random.nextLong(14_400_000), null), + multiUser = "0", + bod = deviceInfo.board.decodeToString(), + brd = deviceInfo.brand.decodeToString(), + dv = deviceInfo.device.decodeToString(), + firstLevel = "", + manufact = deviceInfo.brand.decodeToString(), + name = deviceInfo.model.decodeToString(), + host = "se.infra", + kernel = deviceInfo.procVersion.decodeToString(), + ) + ) + + val yearMonthFormatted = formatTime(currentTimeMillis(), "yyyy-MM-01") + val rand1 = random.nextInt(899999) + 100000 + val rand2 = random.nextInt(899999999) + 100000000 + + val beaconId = buildString { + (1..40).forEach { i -> + when (i) { + 1, 2, 13, 14, 17, 18, 21, 22, 25, 26, 29, 30, 33, 34, 37, 38 -> { + append('k') + append(i) + append(':') + append(yearMonthFormatted) + append(rand1) + append('.') + append(rand2) + } + + 3 -> append("k3:0000000000000000") + 4 -> { + append("k4:") + append(getRandomString(16)) + } + + else -> { + append('k') + append(i) + append(':') + append(random.nextInt(10000)) + } + } + append(';') + } + } + + val payloadParam = Json.encodeToString( + DevicePayloadData( + androidId = deviceInfo.androidId.decodeToString(), + platformId = 1, + appKey = protocol.appKey, + appVersion = protocol.buildVer, + beaconIdSrc = beaconId, + brand = deviceInfo.brand.decodeToString(), + channelId = "2017", + cid = "", + imei = deviceInfo.imei, + imsi = "", + mac = deviceInfo.macAddress.decodeToString(), + model = deviceInfo.model.decodeToString(), + networkType = "unknown", + oaid = "", + osVersion = buildString { + append("Android ") + append(deviceInfo.version.release.toString()) + append(", level ") + append(deviceInfo.version.sdk.toString()) + }, + qimei = "", + qimei36 = "", + sdkVersion = "1.2.13.6", + audit = "", + userId = "{}", + packageId = protocol.apkId, + deviceType = if (configuration.protocol == MiraiProtocol.ANDROID_PAD) "Pad" else "Phone", + sdkName = "", + reserved = reservedData, + ) + ).toByteArray() + + val aesKey = getRandomString(16).toByteArray() + val nonce = getRandomString(16) + val timestamp = currentTimeSeconds() * 1000 + + val encodedAESKey = rsaEncryptWithX509PubKey(aesKey, rsaPubKey, timestamp).encodeBase64() + val encodedPayloadParam = aesEncrypt(payloadParam, aesKey, aesKey).encodeBase64() + + val payload = Json.encodeToString( + PostData( + key = encodedAESKey, + params = encodedPayloadParam, + time = timestamp, + nonce = nonce, + sign = buildString { + append(encodedAESKey) + append(encodedPayloadParam) + append(timestamp) + append(nonce) + append(secret) + }.md5().toUHexString(""), + extra = "" + ) + ) + + val resp = Json.decodeFromString( + OLAAndroidResp.serializer(), + httpClient.post("https://snowflake.qq.com/ola/android") { + userAgent(buildString { + append("Dalvik/") + append(dalvikVersions[deviceInfo.version.sdk] ?: "2.1.0") + append(" (Linux; U; Android ") + append(deviceInfo.version.release.decodeToString()) + append("; ") + append(deviceInfo.device.decodeToString()) + append(" Build/") + append(deviceInfo.display.decodeToString()) + append(")") + }) + contentType(ContentType.Application.Json) + header("Cookie", "") + setBody(payload.toByteArray()) + timeout { + connectTimeoutMillis = 5000 + requestTimeoutMillis = 5000 + socketTimeoutMillis = 5000 + } + }.bodyAsText() + ) + + if (resp.code != 0) { + logger.warning { "Cannot get qimei from server, return code = ${resp.code}" } + return + } + + val decryptedData = aesDecrypt(resp.data.decodeBase64(), aesKey, aesKey) + val qimeiData = Json.decodeFromString(QimeiData.serializer(), decryptedData.decodeToString()) + + client.qimei36 = qimeiData.q36 + client.qimei16 = qimeiData.q16 +} + +private val dalvikVersions = mapOf( + 14 to "1.6", + 15 to "1.6", + 16 to "1.6", + 17 to "1.6", + 18 to "1.6", + 19 to "2.0", + 20 to "2.0", + 21 to "2.1.0", + 22 to "2.1.0", + 23 to "2.1.0", + 24 to "2.1.0", + 25 to "2.1.0", + 26 to "2.1.0", + 27 to "2.1.0", + 28 to "2.1.0", + 29 to "2.1.0", + 30 to "2.1.0", + 31 to "2.1.0", + 32 to "2.1.0", + 33 to "2.1.0", + 34 to "2.1.0", +) + +@Serializable +private class OLAAndroidResp( + val code: Int, + val data: String, +) + +@Serializable +private class QimeiData( + val q16: String, + val q36: String, +) + +@Suppress("unused") +@Serializable +private class ReservedData( + val harmony: String, + val clone: String, + val containe: String, + val oz: String, + val oo: String, + val kelong: String, + val uptimes: String, + val multiUser: String, + val bod: String, + val brd: String, + val dv: String, + val firstLevel: String, + val manufact: String, + val name: String, + val host: String, + val kernel: String +) + +@Suppress("unused") +@Serializable +private class DevicePayloadData( + val androidId: String, + val platformId: Int, + val appKey: String, + val appVersion: String, + val beaconIdSrc: String, + val brand: String, + val channelId: String, + val cid: String, + val imei: String, + val imsi: String, + val mac: String, + val model: String, + val networkType: String, + val oaid: String, + val osVersion: String, + val qimei: String, + val qimei36: String, + val sdkVersion: String, + val audit: String, + val userId: String, + val packageId: String, + val deviceType: String, + val sdkName: String, + val reserved: String +) + +@Suppress("unused") +@Serializable +private class PostData( + val key: String, + val params: String, + val time: Long, + val nonce: String, + val sign: String, + val extra: String +) \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/utils/MiraiProtocolInternal.kt b/mirai-core/src/commonMain/kotlin/utils/MiraiProtocolInternal.kt index 884423937ea..581c89b5de9 100644 --- a/mirai-core/src/commonMain/kotlin/utils/MiraiProtocolInternal.kt +++ b/mirai-core/src/commonMain/kotlin/utils/MiraiProtocolInternal.kt @@ -18,6 +18,7 @@ internal class MiraiProtocolInternal( var apkId: String, var id: Long, var ver: String, + var buildVer: String, var sdkVer: String, var miscBitMap: Int, var subSigMap: Int, @@ -25,6 +26,7 @@ internal class MiraiProtocolInternal( var sign: String, var buildTime: Long, var ssoVersion: Int, + var appKey: String, var supportsQRLogin: Boolean, // don't change property signatures, used externally. @@ -38,25 +40,28 @@ internal class MiraiProtocolInternal( protocols[protocol] ?: error("Internal Error: Missing protocol $protocol") init { - //Updated from MiraiGo (2023/3/7) + //Updated from 8.9.35 (2023/4/9) protocols[MiraiProtocol.ANDROID_PHONE] = MiraiProtocolInternal( apkId = "com.tencent.mobileqq", - id = 537151682, - ver = "8.9.33.10335", - sdkVer = "6.0.0.2534", + id = 537153295, + ver = "8.9.35", + buildVer = "8.9.35.10440", + sdkVer = "6.0.0.2535", miscBitMap = 150470524, subSigMap = 0x10400, - mainSigMap = 16724722, + mainSigMap = 34869344 or 192, sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D", - buildTime = 1673599898L, + buildTime = 1676531414L, ssoVersion = 19, + appKey = "0S200MNJT807V3GE", supportsQRLogin = false, ) //Updated from MiraiGo (2023/3/7) protocols[MiraiProtocol.ANDROID_PAD] = MiraiProtocolInternal( apkId = "com.tencent.mobileqq", id = 537151218, - ver = "8.9.33.10335", + ver = "8.9.33", + buildVer = "8.9.33.10335", sdkVer = "6.0.0.2534", miscBitMap = 150470524, subSigMap = 0x10400, @@ -64,6 +69,7 @@ internal class MiraiProtocolInternal( sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D", buildTime = 1673599898L, ssoVersion = 19, + appKey = "0S200MNJT807V3GE", supportsQRLogin = false, ) //Updated from MiraiGo (2023/3/24) @@ -71,6 +77,7 @@ internal class MiraiProtocolInternal( apkId = "com.tencent.qqlite", id = 537065138, ver = "2.0.8", + buildVer = "2.0.8", sdkVer = "6.0.0.2365", miscBitMap = 16252796, subSigMap = 0x10400, @@ -78,12 +85,14 @@ internal class MiraiProtocolInternal( sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D", buildTime = 1559564731L, ssoVersion = 5, + appKey = "", supportsQRLogin = true, ) protocols[MiraiProtocol.IPAD] = MiraiProtocolInternal( apkId = "com.tencent.minihd.qq", id = 537151363, - ver = "8.9.33.614", + ver = "8.9.33", + buildVer = "8.9.33.614", sdkVer = "6.0.0.2433", miscBitMap = 150470524, subSigMap = 66560, @@ -91,12 +100,14 @@ internal class MiraiProtocolInternal( sign = "AA 39 78 F4 1F D9 6F F9 91 4A 66 9E 18 64 74 C7", buildTime = 1640921786L, ssoVersion = 12, + appKey = "", supportsQRLogin = false, ) protocols[MiraiProtocol.MACOS] = MiraiProtocolInternal( apkId = "com.tencent.qq", id = 0x2003ca32, ver = "6.7.9", + buildVer = "6.7.9", sdkVer = "6.2.0.1023", miscBitMap = 0x7ffc, subSigMap = 66560, @@ -104,6 +115,7 @@ internal class MiraiProtocolInternal( sign = "com.tencent.qq".encodeToByteArray().toUHexString(" "), buildTime = 0L, ssoVersion = 7, + appKey = "", supportsQRLogin = true, ) } diff --git a/mirai-core/src/commonMain/kotlin/utils/crypto/AES.kt b/mirai-core/src/commonMain/kotlin/utils/crypto/AES.kt new file mode 100644 index 00000000000..bf38fd80742 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/utils/crypto/AES.kt @@ -0,0 +1,14 @@ +/* + * 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.utils.crypto + +internal expect fun aesEncrypt(input: ByteArray, iv: ByteArray, key: ByteArray): ByteArray + +internal expect fun aesDecrypt(input: ByteArray, iv: ByteArray, key: ByteArray): ByteArray \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/utils/crypto/RSA.kt b/mirai-core/src/commonMain/kotlin/utils/crypto/RSA.kt new file mode 100644 index 00000000000..7209f5c63ec --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/utils/crypto/RSA.kt @@ -0,0 +1,21 @@ +/* + * 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.utils.crypto + +internal class RSAKeyPair( + val plainPubPemKey: String, + val plainPrivPemKey: String +) + +internal expect fun generateRSAKeyPair(keySize: Int): RSAKeyPair + +internal expect fun rsaEncryptWithX509PubKey(input: ByteArray, plainPubPemKey: String, seed: Long): ByteArray + +internal expect fun rsaDecryptWithPKCS8PrivKey(input: ByteArray, plainPrivPemKey: String, seed: Long): ByteArray \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/utils/crypto/AESTest.kt b/mirai-core/src/commonTest/kotlin/utils/crypto/AESTest.kt new file mode 100644 index 00000000000..7961a83ef80 --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/utils/crypto/AESTest.kt @@ -0,0 +1,40 @@ +/* + * 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.utils.crypto + +import net.mamoe.mirai.utils.currentTimeMillis +import net.mamoe.mirai.utils.getRandomString +import net.mamoe.mirai.utils.toUHexString +import kotlin.random.Random +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class AESTest { + @Test + fun `can do crypto`() { + val random = Random(currentTimeMillis()) + + val key = getRandomString(16, random).encodeToByteArray() + val iv = getRandomString(16, random).encodeToByteArray() + val currentTime = currentTimeMillis() + + val plainText = buildString { + append("Use of this source code is governed by the GNU AGPLv3 license ") + append("that can be found through the following link. ") + append(currentTime) + } + + println("AES crypto test: key = ${key.toUHexString()}, iv = ${iv.toUHexString()}, currentTimeMillis = $currentTime") + val encrypted = aesEncrypt(plainText.encodeToByteArray(), iv, key) + val decrypted = aesDecrypt(encrypted, iv, key) + + assertEquals(plainText, decrypted.decodeToString()) + } +} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/utils/crypto/RSATest.kt b/mirai-core/src/commonTest/kotlin/utils/crypto/RSATest.kt new file mode 100644 index 00000000000..4e821b1d922 --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/utils/crypto/RSATest.kt @@ -0,0 +1,112 @@ +/* + * 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.utils.crypto + +import net.mamoe.mirai.internal.testFramework.* +import net.mamoe.mirai.internal.testFramework.rules.DisabledOnJvmLikePlatform +import kotlin.math.pow +import kotlin.test.Test +import kotlin.test.assertEquals + +@DisabledOnJvmLikePlatform(Platform.AndroidUnitTest::class) +class RSATest { + @TestFactory + fun `can generate key pair`(): DynamicTestsResult { + return runDynamicTests(buildList { + repeat(4) { exp -> + val keySize = 2.0.pow(9 + exp).toInt() + add(dynamicTest("RSAKeyGenLength$keySize") { + val rsaKeyPair = generateRSAKeyPair(keySize) + println("RSA keygen test #${exp + 1}: keySize = $keySize") + println(rsaKeyPair.plainPubPemKey) + println(rsaKeyPair.plainPrivPemKey) + }) + } + }) + } + + @Test + fun `can do crypto with generated key`() { + val keyPair = generateRSAKeyPair(2048) + + val plainText = buildString { + append("Use of this source code is governed by the GNU AGPLv3 license ") + append("that can be found through the following link. ") + } + + println( + "RSA crypto test: plainTextLength: ${plainText.length}, " + + "pubKey = ${keyPair.plainPubPemKey}, " + + "privKey = ${keyPair.plainPrivPemKey}" + ) + val enc = rsaEncryptWithX509PubKey(plainText.encodeToByteArray(), keyPair.plainPubPemKey, 0) + println("rsa encrypt: data size=${enc.size}") + val decrypted = rsaDecryptWithPKCS8PrivKey(enc, keyPair.plainPrivPemKey, 0) + + assertEquals(plainText, decrypted.decodeToString()) + } + + @Test + fun `can do crypto with specific key`() { + val pubKey = """ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr0KIpsWPW2JA7ShJI18o +wPv3Ip3Y6a0OkJOozfVlQDOjjUG6niDcrPIm+OpL7pCAzwc+h8d9sFH5c/7/bY4i +wKK6CpSaOYgQQ03P31KhzmXGJ4LVSxUIV0bhuDYQr+sU5Gu97onF8Ko8MELtWTPw +KP1dfqZ3PrK8QBH6su0GlB8onYFtzDUckr2wCrrJ1cR4L1Dg5f2egE75l1cliAIM +4FH1WFU2musfdEuCo5oPgl8ZPPLrQwp8qm9w7xBvbgbmfPTjPBC0N4gcelVzvdfC +eU8vpIlLP/9W5nkdqF6CWzjE3dIx2btOH4QDDyogDSLRAvcKN5/1EIZeu2FTbw9k +3QIDAQAB +-----END PUBLIC KEY----- + """.trimIndent() + + val privKey = """ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCvQoimxY9bYkDt +KEkjXyjA+/cindjprQ6Qk6jN9WVAM6ONQbqeINys8ib46kvukIDPBz6Hx32wUflz +/v9tjiLAoroKlJo5iBBDTc/fUqHOZcYngtVLFQhXRuG4NhCv6xTka73uicXwqjww +Qu1ZM/Ao/V1+pnc+srxAEfqy7QaUHyidgW3MNRySvbAKusnVxHgvUODl/Z6ATvmX +VyWIAgzgUfVYVTaa6x90S4Kjmg+CXxk88utDCnyqb3DvEG9uBuZ89OM8ELQ3iBx6 +VXO918J5Ty+kiUs//1bmeR2oXoJbOMTd0jHZu04fhAMPKiANItEC9wo3n/UQhl67 +YVNvD2TdAgMBAAECggEAExREqRcfvJyNIeQ9Vg7xclTbuhaB+ypeSAnzGfzJeXxF +pUaPCNDeBSvVZ0qmWoG7rA4HViO3AJ9j7ydG6kfLa7orU6SKx5GS56jMZOzrdXsp +37pD+wj+n/W08+da2LPYUeeSxSmVdVYq+DwI96mKTwQKDhQULiyqBrWOW7Um/q/Z +JC8kJWKEmlNideDQHJxZViRyOdKvJtiwvoBLe1Jvbx7oMbpZnf20gV8C7UU7U38R +e0BKT6HBUHXuOOp2tFFpX6dySkJqW7Jijv04B/KnDYaSWD8TtaQfPfAhkiEVA17E +Ret04PnPMiYCkSVakO0MEeFpwb01vPca4Z64zgf7EQKBgQDyU6wsO3v8L1OlV7tx +7+T0PuOqeo7MWESSn18LeyOP2Y+fDtHKMUFULeYH1UZaGsZJvW+P+c8Mvyitbcvv +SZPTR0Dg+1HueXWkNTejs8Z2BKpPmIPaVLz5FxhV7hV2hKgII/yhyRoiWrTiawLg +ocOnYSostg+tt6kT8U2QPKhg9QKBgQC5Jho1nZ3pFPVu2rV/o9VvN7bGfn1M2o8k +9PQjLZQYiXJvPP0tNlvAOHk9cAqYecHJ3wDVacZWmLicU8xmNSFmmN6Vs9jj6km4 +CWq0/wuTUO/fiH+oHZb6+JM63RXbASWyNK+WwmZtDryNBGRB5zbeCAK8tFsRCJDw +19WQUzljSQKBgCWKkuzTVlTuXA4MdmyjVpwENi8OB5tevVjdudLEg/DgKqDgod2q +Hc3VwoJKJzkEVt3LrEHo2IvH/ZxIm0R56J3dtw5jwQCp7nC/EdyZmFBmTqBAJ4Um +hZQtYMbHOKoAySthr9y8lADofodpPqvgQ7hllCwTFIC8KER/qJ2E2C0VAoGATLUM +hsoWckrMpHDYYVlvQ/TBNNuS7hRe2eDihPCNOt03G/8YpXKv8KN1F48j1KgdMZXC +sqhwE9CSK7JMLMw2WltbXIp2gXa/tA+yteo00YPm3aWfvfcEZlY2KV0PgPyosXxC +gyNnbCd+1q3LG8K/aJ3JBIV0dUonQqEpSfIxBIECgYArO3Iw+LvoePjq4yHheyEM +rz6d6RB+i1Q7ExBK7lbZxN17HmKiOewwI772zEo28IY9sIHugV7rW1vQVs3bnzgk +ExDGjYWZSKHfs+3mvrLNRIx/IsVqqwlXt5oO9TspSh68ASvmXN51dmduxRrSuScq +8a49uOr675SyFCJTIdF/Ag== +-----END PRIVATE KEY----- +""".trimIndent() + + val plainText = buildString { + append("Use of this source code is governed by the GNU AGPLv3 license ") + append("that can be found through the following link. ") + } + + val enc = rsaEncryptWithX509PubKey(plainText.encodeToByteArray(), pubKey, 0) + val dec = rsaDecryptWithPKCS8PrivKey(enc, privKey, 0) + + assertEquals(plainText, dec.decodeToString()) + + } +} \ No newline at end of file diff --git a/mirai-core/src/jvmBaseMain/kotlin/utils/crypto/AES.kt b/mirai-core/src/jvmBaseMain/kotlin/utils/crypto/AES.kt new file mode 100644 index 00000000000..00565b5d938 --- /dev/null +++ b/mirai-core/src/jvmBaseMain/kotlin/utils/crypto/AES.kt @@ -0,0 +1,31 @@ +/* + * 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.utils.crypto + +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +internal actual fun aesEncrypt(input: ByteArray, iv: ByteArray, key: ByteArray): ByteArray { + return doAES(input, iv, key, Cipher.ENCRYPT_MODE) +} + +internal actual fun aesDecrypt(input: ByteArray, iv: ByteArray, key: ByteArray): ByteArray { + return doAES(input, iv, key, Cipher.DECRYPT_MODE) +} + +private fun doAES(input: ByteArray, iv: ByteArray, key: ByteArray, opMode: Int): ByteArray { + val keySpec = SecretKeySpec(key, "AES") + + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init(opMode, keySpec, IvParameterSpec(iv)) + + return cipher.doFinal(input) +} \ No newline at end of file diff --git a/mirai-core/src/jvmBaseMain/kotlin/utils/crypto/RSA.kt b/mirai-core/src/jvmBaseMain/kotlin/utils/crypto/RSA.kt new file mode 100644 index 00000000000..b2488b8dd0d --- /dev/null +++ b/mirai-core/src/jvmBaseMain/kotlin/utils/crypto/RSA.kt @@ -0,0 +1,74 @@ +/* + * 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.utils.crypto + +import net.mamoe.mirai.utils.decodeBase64 +import net.mamoe.mirai.utils.encodeBase64 +import java.security.KeyFactory +import java.security.KeyPairGenerator +import java.security.SecureRandom +import java.security.spec.PKCS8EncodedKeySpec +import java.security.spec.X509EncodedKeySpec +import javax.crypto.Cipher + +internal actual fun rsaEncryptWithX509PubKey(input: ByteArray, plainPubPemKey: String, seed: Long): ByteArray { + val encodedKey = plainPubPemKey + .replace("\n", "") + .removePrefix("-----BEGIN PUBLIC KEY-----") + .removeSuffix("-----END PUBLIC KEY-----") + .trim() + .decodeBase64() + + val keyFactory = KeyFactory.getInstance("RSA") + val rsaPubKey = keyFactory.generatePublic(X509EncodedKeySpec(encodedKey)) + + + val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding") + cipher.init(Cipher.ENCRYPT_MODE, rsaPubKey, SecureRandom().apply { setSeed(seed) }) + + return cipher.doFinal(input) +} + +internal actual fun rsaDecryptWithPKCS8PrivKey(input: ByteArray, plainPrivPemKey: String, seed: Long): ByteArray { + val encodedKey = plainPrivPemKey + .replace("\n", "") + .removePrefix("-----BEGIN PRIVATE KEY-----") + .removeSuffix("-----END PRIVATE KEY-----") + .trim() + .decodeBase64() + + val keyFactory = KeyFactory.getInstance("RSA") + val rsaPubKey = keyFactory.generatePrivate(PKCS8EncodedKeySpec(encodedKey)) + + + val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding") + cipher.init(Cipher.DECRYPT_MODE, rsaPubKey, SecureRandom().apply { setSeed(seed) }) + + return cipher.doFinal(input) +} + +internal actual fun generateRSAKeyPair(keySize: Int): RSAKeyPair { + val keyGen = KeyPairGenerator.getInstance("RSA") + keyGen.initialize(keySize) + + val keyPair = keyGen.generateKeyPair() + return RSAKeyPair( + plainPubPemKey = buildString { + appendLine("-----BEGIN PUBLIC KEY-----") + keyPair.public.encoded.encodeBase64().chunked(64).forEach(::appendLine) + appendLine("-----END PUBLIC KEY-----") + }, + plainPrivPemKey = buildString { + appendLine("-----BEGIN PRIVATE KEY-----") + keyPair.private.encoded.encodeBase64().chunked(64).forEach(::appendLine) + appendLine("-----END PRIVATE KEY-----") + } + ) +} \ No newline at end of file diff --git a/mirai-core/src/nativeMain/cinterop/OpenSSL.def b/mirai-core/src/nativeMain/cinterop/OpenSSL.def index 1294c1b8edd..bdadd36713b 100644 --- a/mirai-core/src/nativeMain/cinterop/OpenSSL.def +++ b/mirai-core/src/nativeMain/cinterop/OpenSSL.def @@ -1,4 +1,10 @@ -headers = openssl/ec.h openssl/ecdh.h openssl/evp.h +headers = openssl/ec.h \ + openssl/ecdh.h \ + openssl/evp.h \ + openssl/rsa.h \ + openssl/pem.h \ + openssl/x509.h \ + openssl/err.h # -L/usr/local/opt/openssl@1.1/1.1.1o/lib is for GitHub actions. See https://github.com/actions/virtual-environments/blob/main/images/macos/macos-12-Readme.md @@ -46,3 +52,22 @@ compilerOpts = -I/opt/openssl/include \ -I/usr/local/include/ \ -IC:/openssl/include/ \ -IC:/vcpkg/installed/x64-windows/include \ + +--- + +#include +#include + +static int _evpCipherCtxGetBlockSize(const EVP_CIPHER_CTX *ctx) { +#ifdef EVP_CIPHER_CTX_block_size + return EVP_CIPHER_CTX_get_block_size(ctx); +#endif + return EVP_CIPHER_CTX_block_size(ctx); +} + +static int _evpPkeyCtxSetRSAKeygenBits(EVP_PKEY_CTX *ctx, int bits) { +#ifdef EVP_PKEY_CTX_set_rsa_keygen_bits + return RSA_pkey_ctx_ctrl(ctx, EVP_PKEY_OP_KEYGEN, EVP_PKEY_CTRL_RSA_KEYGEN_BITS, bits, NULL); +#endif + return EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits); +} \ No newline at end of file diff --git a/mirai-core/src/nativeMain/kotlin/utils/crypto/AESNative.kt b/mirai-core/src/nativeMain/kotlin/utils/crypto/AESNative.kt new file mode 100644 index 00000000000..b9b52d46536 --- /dev/null +++ b/mirai-core/src/nativeMain/kotlin/utils/crypto/AESNative.kt @@ -0,0 +1,101 @@ +/* + * 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.utils.crypto + +import kotlinx.cinterop.* +import net.mamoe.mirai.internal.utils.free +import net.mamoe.mirai.internal.utils.getOpenSSLError +import openssl.* + +private val aes256CBC by lazy { EVP_aes_256_cbc() } + +internal actual fun aesEncrypt(input: ByteArray, iv: ByteArray, key: ByteArray): ByteArray { + return doAES(input, iv, key, true) +} + +internal actual fun aesDecrypt(input: ByteArray, iv: ByteArray, key: ByteArray): ByteArray { + return doAES(input, iv, key, false) +} + +/** + * reference: + * - https://wiki.openssl.org/index.php/EVP_Symmetric_Encryption_and_Decryption + */ +private fun doAES(input: ByteArray, iv: ByteArray, key: ByteArray, doEncrypt: Boolean): ByteArray { + memScoped { + val evpCipherCtx = EVP_CIPHER_CTX_new() ?: error("Failed to create evp cipher context: ${getOpenSSLError()}") + + val pinnedKey = key.pin() + val pinnedIv = iv.pin() + val pinnedInput = input.pin() + + if (1 != EVP_CipherInit( + ctx = evpCipherCtx, + cipher = aes256CBC, + key = pinnedKey.addressOf(0).reinterpret(), + iv = pinnedIv.addressOf(0).reinterpret(), + enc = if (doEncrypt) 1 else 0 + ) + ) { + pinnedKey.unpin() + pinnedIv.unpin() + pinnedInput.unpin() + EVP_CIPHER_CTX_free(evpCipherCtx) + error("Failed to init aes-256-cbc cipher: ${getOpenSSLError()}") + } + + pinnedKey.unpin() + pinnedIv.unpin() + + val blockSize = _evpCipherCtxGetBlockSize(evpCipherCtx) + val cipherBufferSize = pinnedInput.get().size + blockSize - (pinnedInput.get().size % blockSize) + val pinnedCipherBuffer = ByteArray(cipherBufferSize.convert()).pin() + + + val tempLen = alloc() + val cipherSize = alloc() + + if (1 != EVP_CipherUpdate( + ctx = evpCipherCtx, + out = pinnedCipherBuffer.addressOf(0).reinterpret(), + outl = tempLen.ptr, + `in` = pinnedInput.addressOf(0).reinterpret(), + inl = pinnedInput.get().size.convert() + ) + ) { + pinnedInput.unpin() + pinnedCipherBuffer.unpin() + free(tempLen.ptr, cipherSize.ptr) + EVP_CIPHER_CTX_free(evpCipherCtx) + error("Failed do aes-256-cbc cipher update: ${getOpenSSLError()}") + } + cipherSize.value = tempLen.value + + if (1 != EVP_CipherFinal( + ctx = evpCipherCtx, + outm = pinnedCipherBuffer.addressOf(tempLen.value).reinterpret(), + outl = tempLen.ptr + ) + ) { + pinnedInput.unpin() + pinnedCipherBuffer.unpin() + free(tempLen.ptr, cipherSize.ptr) + EVP_CIPHER_CTX_free(evpCipherCtx) + error("Failed do aes-256-cbc cipher final: ${getOpenSSLError()}") + } + cipherSize.value += tempLen.value + + return pinnedCipherBuffer.get().copyOf(cipherSize.value).also { + pinnedInput.unpin() + pinnedCipherBuffer.unpin() + EVP_CIPHER_CTX_free(evpCipherCtx) + } + } +} diff --git a/mirai-core/src/nativeMain/kotlin/utils/crypto/RSANative.kt b/mirai-core/src/nativeMain/kotlin/utils/crypto/RSANative.kt new file mode 100644 index 00000000000..10568dbfc93 --- /dev/null +++ b/mirai-core/src/nativeMain/kotlin/utils/crypto/RSANative.kt @@ -0,0 +1,299 @@ +/* + * 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.utils.crypto + +import kotlinx.cinterop.* +import net.mamoe.mirai.internal.utils.getOpenSSLError +import net.mamoe.mirai.internal.utils.ref +import openssl.* + +/** + * reference: + * - https://stackoverflow.com/questions/70535625/openssl-rsa-encryption-decryption-with-evp-methods + * - https://www.openssl.org/docs/man3.1/man3/ + */ + +/** + * Generate RSA key pair with size of [keySize]. + * The public key pair is encoded with x.509, and the private key pair is encoded with PKCS8 + */ +internal actual fun generateRSAKeyPair(keySize: Int): RSAKeyPair { + memScoped { + val evpPkeyCtx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, null) + ?: error("Failed to create evp pkey context: ${getOpenSSLError()}") + + if (EVP_PKEY_keygen_init(evpPkeyCtx) <= 0) { + error("Failed to init evp pkey context: ${getOpenSSLError()}") + } + + // libcrypto 3 move EVP_PKEY_CTX_set_rsa_keygen_bits from macro to function + if (_evpPkeyCtxSetRSAKeygenBits(evpPkeyCtx, keySize) <= 0) { + EVP_PKEY_CTX_free(evpPkeyCtx) + error("Failed to set key bit for rsa evp pkey: ${getOpenSSLError()}") + } + + val evpPKey = EVP_PKEY_new() ?: kotlin.run { + EVP_PKEY_CTX_free(evpPkeyCtx) + error("Failed to create evp pkey: ${getOpenSSLError()}") + } + + if (EVP_PKEY_keygen(evpPkeyCtx, ref(evpPKey)) <= 0) { + EVP_PKEY_free(evpPKey) + EVP_PKEY_CTX_free(evpPkeyCtx) + error("Failed to generate rsa key pair: ${getOpenSSLError()}") + } + + val publicPemKey = dumpPKey(evpPKey) { b, k -> PEM_write_bio_PUBKEY(b, k) } + ?: kotlin.run { + EVP_PKEY_free(evpPKey) + EVP_PKEY_CTX_free(evpPkeyCtx) + error("Failed to dump rsa public key: ${getOpenSSLError()}") + } + val privatePemKey = dumpPKey(evpPKey) { b, k -> + PEM_write_bio_PKCS8PrivateKey(b, k, null, null, 0, null, null) + } ?: kotlin.run { + EVP_PKEY_free(evpPKey) + EVP_PKEY_CTX_free(evpPkeyCtx) + error("Failed to dump rsa public key: ${getOpenSSLError()}") + } + + EVP_PKEY_free(evpPKey) + EVP_PKEY_CTX_free(evpPkeyCtx) + + return RSAKeyPair(publicPemKey, privatePemKey) + } +} + +@OptIn(UnsafeNumber::class) +private inline fun MemScope.dumpPKey( + evpPKey: CPointer, + dumper: (CPointer, CPointer) -> Unit +): String? { + val bio = BIO_new(BIO_s_mem()) ?: error("Failed to init mem BIO: ${getOpenSSLError()}") + + dumper(bio, evpPKey) + BIO_ctrl(bio, BIO_CTRL_FLUSH, 0, null) + + val pKeyBuf = allocPointerTo() + BIO_ctrl(bio, BIO_CTRL_INFO, 0, pKeyBuf.ptr) + + return pKeyBuf.value?.toKString().also { BIO_free(bio) } +} + +private fun MemScope.loadPKey( + plainPemKey: String, + reader: (CPointer) -> CPointer? +): CPointer? { + val bio = BIO_new(BIO_s_mem()) ?: error("Failed to init mem BIO: ${getOpenSSLError()}") + + return plainPemKey.encodeToByteArray().usePinned { + BIO_write(bio, it.addressOf(0), it.get().size) + reader(bio) + } +} + +internal actual fun rsaEncryptWithX509PubKey(input: ByteArray, plainPubPemKey: String, seed: Long): ByteArray { + memScoped { + val pubPKey = loadPKey(plainPubPemKey) { + PEM_read_bio_RSA_PUBKEY(it, null, null, null) + } ?: error("Failed to read pem key from BIO: ${getOpenSSLError()}") + + val pinnedInput = input.pin() + val encMsg = ByteArray(4096).pin() + + val encMsgLen = RSA_public_encrypt( + flen = pinnedInput.get().size, + from = pinnedInput.addressOf(0).reinterpret(), + to = encMsg.addressOf(0).reinterpret(), + rsa = pubPKey, + padding = RSA_PKCS1_PADDING + ) + if (encMsgLen <= 0) { + pinnedInput.unpin() + encMsg.unpin() + RSA_free(pubPKey) + error("Failed to do rsa decrypt: ${getOpenSSLError()}") + } + + return encMsg.get().copyOf(encMsgLen).also { + pinnedInput.unpin() + encMsg.unpin() + RSA_free(pubPKey) + } + + /*if (1 != EVP_SealInit( + ctx = evpCipherCtx, + type = aes256CBC, + ek = encKey.ptr, + ekl = encKeyLen.ptr, + pubk = ref(pubPKey), + iv = iv.ptr, + npubk = 1, + ) + ) { + free(encKey.ptr, encKeyLen.ptr, iv.ptr) + EVP_CIPHER_CTX_free(evpCipherCtx) + error("Failed to init evp seal: ${getOpenSSLError()}") + } + println("total size: ${pinnedInput.get().size + 1 + EVP_MAX_IV_LENGTH}") + val encMsgLen = alloc().apply { value = 0u } + val blockSize = alloc().apply { value = 0u } + + if (1 != EVP_EncryptUpdate( + ctx = evpCipherCtx, + out = encMsg.addressOf(encMsgLen.value.convert()).reinterpret(), + outl = blockSize.ptr.reinterpret(), + `in` = pinnedInput.addressOf(0).reinterpret(), + inl = pinnedInput.get().size + ) + ) { + pinnedInput.unpin() + encMsg.unpin() + free(encMsgLen.ptr, blockSize.ptr, encKey.ptr, encKeyLen.ptr, iv.ptr) + EVP_CIPHER_CTX_free(evpCipherCtx) + error("Failed to update evp seal: ${getOpenSSLError()}") + } + println("${encMsgLen.value}, ${blockSize.value}") + encMsgLen.value += blockSize.value + println("${encMsg.addressOf(0)}, ${encMsg.addressOf(encMsgLen.value.convert())}") + + if (1 != EVP_SealFinal( + ctx = evpCipherCtx, + out = encMsg.addressOf(encMsgLen.value.convert()).reinterpret(), + outl = blockSize.ptr.reinterpret() + ) + ) { + pinnedInput.unpin() + encMsg.unpin() + free(encMsgLen.ptr, blockSize.ptr, encKey.ptr, encKeyLen.ptr, iv.ptr) + EVP_CIPHER_CTX_free(evpCipherCtx) + error("Failed to do final evp seal: ${getOpenSSLError()}") + } + println("${encMsgLen.value}, ${blockSize.value}") + encMsgLen.value += blockSize.value + + return encMsg.get().copyOf(encMsgLen.value.convert()).also { + encMsg.unpin() + pinnedInput.unpin() + EVP_CIPHER_CTX_free(evpCipherCtx) + }.toByteArray()*/ + } +} + +internal actual fun rsaDecryptWithPKCS8PrivKey(input: ByteArray, plainPrivPemKey: String, seed: Long): ByteArray { + memScoped { + val evpCipherCtx = EVP_CIPHER_CTX_new() + ?: error("Failed to create evp cipher context: ${getOpenSSLError()}") + + val privKey = loadPKey(plainPrivPemKey) { + PEM_read_bio_RSAPrivateKey(it, null, null, null) + } ?: kotlin.run { + EVP_CIPHER_CTX_free(evpCipherCtx) + error("Failed to read pem key from BIO: ${getOpenSSLError()}") + } + + val pinnedInput = input.pin() + val encMsg = UByteArray(4096).pin() + + val encMsgLen = RSA_private_decrypt( + flen = pinnedInput.get().size, + from = pinnedInput.addressOf(0).reinterpret(), + to = encMsg.addressOf(0).reinterpret(), + rsa = privKey, + padding = RSA_PKCS1_PADDING + ) + if (encMsgLen <= 0) { + pinnedInput.unpin() + encMsg.unpin() + RSA_free(privKey) + error("Failed to do rsa decrypt: ${getOpenSSLError()}") + } + + return encMsg.get().copyOf(encMsgLen).toByteArray().also { + pinnedInput.unpin() + encMsg.unpin() + RSA_free(privKey) + } + + /*println(dumpPKey(privKey) { b, k -> PEM_write_bio_PKCS8PrivateKey(b, k, null, null, 0, null, null) }) + + val decKeyLen = EVP_PKEY_get_size(privKey) + println("evp_pkey_size: $decKeyLen") + val decKey = ByteArray(decKeyLen).pin() + val pinnedIv = ByteArray(16).pin() + + if (1 != EVP_OpenInit( + ctx = evpCipherCtx, + type = aes256CBC, + ek = decKey.addressOf(0).reinterpret(), + ekl = decKeyLen, + iv = pinnedIv.addressOf(0).reinterpret(), + priv = privKey + ) + ) { + pinnedIv.unpin() + decKey.unpin() + EVP_CIPHER_CTX_free(evpCipherCtx) + error("Failed to init evp open: ${getOpenSSLError()}") + } + println("init") + + val pinnedInput = input.pin() + val decMsg = ByteArray( + pinnedInput.get().size + EVP_CIPHER_CTX_get_block_size(evpCipherCtx) + ).pin() + val decMsgLen = alloc().apply { value = 0u } + val blockSize = alloc().apply { value = 0u } + + if (1 != EVP_DecryptUpdate( + ctx = evpCipherCtx, + out = decMsg.addressOf(0).reinterpret(), + outl = blockSize.ptr.reinterpret(), + `in` = pinnedInput.addressOf(0).reinterpret(), + inl = pinnedInput.get().size + ) + ) { + pinnedInput.unpin() + decMsg.unpin() + pinnedIv.unpin() + decKey.unpin() + free(decMsgLen.ptr, blockSize.ptr) + EVP_CIPHER_CTX_free(evpCipherCtx) + error("Failed to update evp open: ${getOpenSSLError()}") + } + decMsgLen.value += blockSize.value + println("update") + + if (1 != EVP_OpenFinal( + ctx = evpCipherCtx, + out = decMsg.addressOf(decMsgLen.value.convert()).reinterpret(), + outl = blockSize.ptr.reinterpret() + ) + ) { + pinnedInput.unpin() + decMsg.unpin() + pinnedIv.unpin() + decKey.unpin() + free(decMsgLen.ptr, blockSize.ptr) + EVP_CIPHER_CTX_free(evpCipherCtx) + error("Failed to do final evp open: ${getOpenSSLError()}") + } + decMsgLen.value += blockSize.value + println("final") + + return decMsg.get().copyOf(decMsgLen.value.convert()).also { + decMsg.unpin() + pinnedInput.unpin() + pinnedIv.unpin() + decKey.unpin() + EVP_CIPHER_CTX_free(evpCipherCtx) + }*/ + } +} \ No newline at end of file diff --git a/mirai-core/src/nativeMain/kotlin/utils/freePointer.kt b/mirai-core/src/nativeMain/kotlin/utils/freePointer.kt new file mode 100644 index 00000000000..ce78b70b9c3 --- /dev/null +++ b/mirai-core/src/nativeMain/kotlin/utils/freePointer.kt @@ -0,0 +1,19 @@ +/* + * 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.utils + +import kotlinx.cinterop.CValuesRef +import platform.posix.free + +internal fun free( + vararg refs: CValuesRef<*> +) { + refs.forEach(::free) +} \ No newline at end of file diff --git a/mirai-core/src/nativeMain/kotlin/utils/getOpenSSLError.kt b/mirai-core/src/nativeMain/kotlin/utils/getOpenSSLError.kt new file mode 100644 index 00000000000..d129d52b186 --- /dev/null +++ b/mirai-core/src/nativeMain/kotlin/utils/getOpenSSLError.kt @@ -0,0 +1,27 @@ +/* + * 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.utils + +import kotlinx.cinterop.* +import openssl.* + +@OptIn(UnsafeNumber::class) +internal fun getOpenSSLError(): String { + memScoped { + val bio = BIO_new(BIO_s_mem()) + val errBuffer = allocPointerTo() + + ERR_print_errors(bio) + BIO_ctrl(bio, BIO_CTRL_FLUSH, 0, null) + BIO_ctrl(bio, BIO_CTRL_INFO, 0, errBuffer.ptr) + + return errBuffer.value?.toKString()?.also { BIO_free(bio) } ?: "openssl error: no message" + } +} \ No newline at end of file diff --git a/mirai-core/src/nativeMain/kotlin/utils/ref.kt b/mirai-core/src/nativeMain/kotlin/utils/ref.kt new file mode 100644 index 00000000000..b774b17c10a --- /dev/null +++ b/mirai-core/src/nativeMain/kotlin/utils/ref.kt @@ -0,0 +1,19 @@ +/* + * 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.utils + +import kotlinx.cinterop.* + +/** + * returns reference to a pointer(**variable), equivalent to `my_type **a = &myTypePtr` + */ +internal inline fun NativePlacement.ref(ptr: CPointer): CPointer> { + return allocPointerTo().apply { value = ptr }.ptr +} \ No newline at end of file