Pārlūkot izejas kodu

[core] Update login protocol, deprecate DeviceInfo constructor and serializer (#2613)

* [core] update protocol

* [core] fix t106 decryption key

* [core] handle t543

* [core] revert t106

* [core] revert native test pow data

* [core] split build version and apk version name

* [core] pass time to t1 instead of time diff

* [core] remove unused t106 writer & rename param ip to ipv4 of t1 and t106

* [core] implement qimei for t545
TODO: aes and rsa crypto for nativeMain

* [core] fix base64 decode for android in rsa

* [core] fix unix timestamp parsing on native and add more tests

* [api] DeviceInfo move `androidId` to main constructor and adjust test.

* [api] do not request qimei when protocol is neither  ANDROID_PHONE nor ANDROID_PAD

* [api] implement aes crypto for native target

* [api] rsa crypto for multi-platform

* [api] crypto test

* [api] move freePointer util

* [api] openssl api compatibility

* [core] add explicit `androidId` param

* [core] remove unused `tlvCount`

* [core] optimize crypto

* [core] move Qimei to network package

* [core] move appId to protocol

* [core] lazily initialize qimeiLogger

* [core] write byte array to BIO mem

* [core] move qimei to client, add direct serializer for DeviceInfo

* [core] optimize DeviceInfoDelegateSerializer

* [core] real user-agent when requesting qimei

* [core] use `DeviceInfo.version.release`

* [core] remove unused wtlogin packet

* [core] do what constructor serializer should do

* [core] fix endless cache validation caused by not upgrading device info file.

* [core] request qimei before fast-login

* [core] tlv order

* [core] remove wrong tests and print more detail when deserialize failed.

* [core] device info upgrade for native

* [core] request qimei after validating cache

* [core] DeviceInfo compatibility

* [core] DeviceInfo test name

* [core] compatibility serializer

* [core] disable rsa crypto test on android unit test

* [core] move rsa impl to jvmBase

* action

* import

* api dump

* api dump

* revert wrong api dumps

* [core] Deprecate DeviceInfo constructor and serializer, provide `serializeToString` and `deserializeFromString` for replacement

* rerun ci

* optimize

* use serializer directly

* optimize test

* revert

* [core] CacheValidator use `DeviceInfo.serializeToString()` instead of direct serializer

* Remove `println` in `DeviceInfoManager`

* Add legacy deserialize overload for ABI compatibility

* Remove uncompleted docs for DeviceInfo

* Suppress DeviceInfo deprecation warnings for internal usages

---------

Co-authored-by: Him188 <Him188@mamoe.net>
StageGuard 2 gadi atpakaļ
vecāks
revīzija
60d360baad
39 mainītis faili ar 2147 papildinājumiem un 748 dzēšanām
  1. 7 5
      mirai-core-api/compatibility-validation/android/api/android.api
  2. 7 5
      mirai-core-api/compatibility-validation/jvm/api/jvm.api
  3. 121 283
      mirai-core-api/src/commonMain/kotlin/utils/DeviceInfo.kt
  4. 319 0
      mirai-core-api/src/commonMain/kotlin/utils/DeviceInfoManager.kt
  5. 102 0
      mirai-core-api/src/commonMain/kotlin/utils/DeviceInfoV1LegacySerializer.kt
  6. 2 2
      mirai-core-api/src/commonTest/kotlin/utils/DeviceInfoTest.kt
  7. 0 354
      mirai-core-api/src/commonTest/resources/device/legacy-device-info-1.json
  8. 73 6
      mirai-core-api/src/jvmBaseMain/kotlin/utils/DeviceInfo.kt
  9. 157 5
      mirai-core-api/src/jvmBaseTest/kotlin/utils/JvmDeviceInfoTest.kt
  10. 0 23
      mirai-core-api/src/jvmTest/kotlin/utils/JvmDeviceInfoTestJvm.kt
  11. 4 1
      mirai-core-api/src/nativeMain/kotlin/utils/BotConfiguration.kt
  12. 24 5
      mirai-core-api/src/nativeMain/kotlin/utils/DeviceInfo.kt
  13. 5 1
      mirai-core-utils/src/commonMain/kotlin/TimeUtils.kt
  14. 3 3
      mirai-core-utils/src/jvmBaseMain/kotlin/TimeUtils.kt
  15. 12 4
      mirai-core-utils/src/nativeMain/kotlin/TimeUtils.kt
  16. 56 1
      mirai-core-utils/src/nativeTest/kotlin/TimeUtilsTest.kt
  17. 9 1
      mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt
  18. 1 2
      mirai-core/src/commonMain/kotlin/network/components/CacheValidator.kt
  19. 10 0
      mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt
  20. 73 10
      mirai-core/src/commonMain/kotlin/network/protocol/packet/Tlv.kt
  21. 1 0
      mirai-core/src/commonMain/kotlin/network/protocol/packet/login/WtLogin.kt
  22. 1 0
      mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin10.kt
  23. 21 10
      mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin15.kt
  24. 6 0
      mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin8.kt
  25. 33 18
      mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin9.kt
  26. 297 0
      mirai-core/src/commonMain/kotlin/network/qimei/Qimei.kt
  27. 20 8
      mirai-core/src/commonMain/kotlin/utils/MiraiProtocolInternal.kt
  28. 14 0
      mirai-core/src/commonMain/kotlin/utils/crypto/AES.kt
  29. 21 0
      mirai-core/src/commonMain/kotlin/utils/crypto/RSA.kt
  30. 40 0
      mirai-core/src/commonTest/kotlin/utils/crypto/AESTest.kt
  31. 112 0
      mirai-core/src/commonTest/kotlin/utils/crypto/RSATest.kt
  32. 31 0
      mirai-core/src/jvmBaseMain/kotlin/utils/crypto/AES.kt
  33. 74 0
      mirai-core/src/jvmBaseMain/kotlin/utils/crypto/RSA.kt
  34. 26 1
      mirai-core/src/nativeMain/cinterop/OpenSSL.def
  35. 101 0
      mirai-core/src/nativeMain/kotlin/utils/crypto/AESNative.kt
  36. 299 0
      mirai-core/src/nativeMain/kotlin/utils/crypto/RSANative.kt
  37. 19 0
      mirai-core/src/nativeMain/kotlin/utils/freePointer.kt
  38. 27 0
      mirai-core/src/nativeMain/kotlin/utils/getOpenSSLError.kt
  39. 19 0
      mirai-core/src/nativeMain/kotlin/utils/ref.kt

+ 7 - 5
mirai-core-api/compatibility-validation/android/api/android.api

@@ -5604,8 +5604,9 @@ public final class net/mamoe/mirai/utils/BotConfiguration$MiraiProtocol : java/l
 
 
 public final class net/mamoe/mirai/utils/DeviceInfo {
 public final class net/mamoe/mirai/utils/DeviceInfo {
 	public static final field Companion Lnet/mamoe/mirai/utils/DeviceInfo$Companion;
 	public static final field Companion Lnet/mamoe/mirai/utils/DeviceInfo$Companion;
-	public synthetic fun <init> (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 <init> ([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 <init> ([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 <init> ([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 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;)Lnet/mamoe/mirai/utils/DeviceInfo;
 	public static final fun from (Ljava/io/File;Lkotlinx/serialization/json/Json;)Lnet/mamoe/mirai/utils/DeviceInfo;
 	public static final fun from (Ljava/io/File;Lkotlinx/serialization/json/Json;)Lnet/mamoe/mirai/utils/DeviceInfo;
@@ -5635,26 +5636,26 @@ public final class net/mamoe/mirai/utils/DeviceInfo {
 	public fun hashCode ()I
 	public fun hashCode ()I
 	public static final fun random ()Lnet/mamoe/mirai/utils/DeviceInfo;
 	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 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 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 synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/utils/DeviceInfo;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/utils/DeviceInfo;
 	public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
 	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 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 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;)Lnet/mamoe/mirai/utils/DeviceInfo;
 	public final fun from (Ljava/io/File;Lkotlinx/serialization/json/Json;)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 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 ()Lnet/mamoe/mirai/utils/DeviceInfo;
 	public final fun random (Lkotlin/random/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;
 	public final fun serializer ()Lkotlinx/serialization/KSerializer;
 }
 }
 
 
@@ -5690,6 +5691,7 @@ public final class net/mamoe/mirai/utils/DeviceInfo$Version$Companion {
 
 
 public final class net/mamoe/mirai/utils/DeviceInfoKt {
 public final class net/mamoe/mirai/utils/DeviceInfoKt {
 	public static final fun generateDeviceInfoData (Lnet/mamoe/mirai/utils/DeviceInfo;)[B
 	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 {
 public abstract interface class net/mamoe/mirai/utils/DeviceVerificationRequests {

+ 7 - 5
mirai-core-api/compatibility-validation/jvm/api/jvm.api

@@ -5604,8 +5604,9 @@ public final class net/mamoe/mirai/utils/BotConfiguration$MiraiProtocol : java/l
 
 
 public final class net/mamoe/mirai/utils/DeviceInfo {
 public final class net/mamoe/mirai/utils/DeviceInfo {
 	public static final field Companion Lnet/mamoe/mirai/utils/DeviceInfo$Companion;
 	public static final field Companion Lnet/mamoe/mirai/utils/DeviceInfo$Companion;
-	public synthetic fun <init> (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 <init> ([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 <init> ([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 <init> ([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 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;)Lnet/mamoe/mirai/utils/DeviceInfo;
 	public static final fun from (Ljava/io/File;Lkotlinx/serialization/json/Json;)Lnet/mamoe/mirai/utils/DeviceInfo;
 	public static final fun from (Ljava/io/File;Lkotlinx/serialization/json/Json;)Lnet/mamoe/mirai/utils/DeviceInfo;
@@ -5635,26 +5636,26 @@ public final class net/mamoe/mirai/utils/DeviceInfo {
 	public fun hashCode ()I
 	public fun hashCode ()I
 	public static final fun random ()Lnet/mamoe/mirai/utils/DeviceInfo;
 	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 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 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 synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/utils/DeviceInfo;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/utils/DeviceInfo;
 	public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
 	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 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 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;)Lnet/mamoe/mirai/utils/DeviceInfo;
 	public final fun from (Ljava/io/File;Lkotlinx/serialization/json/Json;)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 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 ()Lnet/mamoe/mirai/utils/DeviceInfo;
 	public final fun random (Lkotlin/random/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;
 	public final fun serializer ()Lkotlinx/serialization/KSerializer;
 }
 }
 
 
@@ -5690,6 +5691,7 @@ public final class net/mamoe/mirai/utils/DeviceInfo$Version$Companion {
 
 
 public final class net/mamoe/mirai/utils/DeviceInfoKt {
 public final class net/mamoe/mirai/utils/DeviceInfoKt {
 	public static final fun generateDeviceInfoData (Lnet/mamoe/mirai/utils/DeviceInfo;)[B
 	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 {
 public abstract interface class net/mamoe/mirai/utils/DeviceVerificationRequests {

+ 121 - 283
mirai-core-api/src/commonMain/kotlin/utils/DeviceInfo.kt

@@ -12,20 +12,28 @@
 package net.mamoe.mirai.utils
 package net.mamoe.mirai.utils
 
 
 import io.ktor.utils.io.core.*
 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.ProtoBuf
 import kotlinx.serialization.protobuf.ProtoNumber
 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.JvmStatic
+import kotlin.jvm.JvmSynthetic
 import kotlin.random.Random
 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,
     display: ByteArray,
     product: ByteArray,
     product: ByteArray,
     device: ByteArray,
     device: ByteArray,
@@ -45,9 +53,9 @@ public expect class DeviceInfo(
     wifiSSID: ByteArray,
     wifiSSID: ByteArray,
     imsiMd5: ByteArray,
     imsiMd5: ByteArray,
     imei: String,
     imei: String,
-    apn: ByteArray
+    apn: ByteArray,
+    androidId: ByteArray,
 ) {
 ) {
-
     public val display: ByteArray
     public val display: ByteArray
     public val product: ByteArray
     public val product: ByteArray
     public val device: ByteArray
     public val device: ByteArray
@@ -68,15 +76,14 @@ public expect class DeviceInfo(
     public val imsiMd5: ByteArray
     public val imsiMd5: ByteArray
     public val imei: String
     public val imei: String
     public val apn: ByteArray
     public val apn: ByteArray
-
     public val androidId: ByteArray
     public val androidId: ByteArray
+
     public val ipAddress: ByteArray
     public val ipAddress: ByteArray
 
 
     @Transient
     @Transient
     @MiraiInternalApi
     @MiraiInternalApi
     public val guid: ByteArray
     public val guid: ByteArray
 
 
-
     // @Serializable: use DeviceInfoVersionSerializer in commonMain.
     // @Serializable: use DeviceInfoVersionSerializer in commonMain.
     public class Version(
     public class Version(
         incremental: ByteArray = "5891938".toByteArray(),
         incremental: ByteArray = "5891938".toByteArray(),
@@ -98,6 +105,10 @@ public expect class DeviceInfo(
          * @since 2.9
          * @since 2.9
          */
          */
         override fun hashCode(): Int
         override fun hashCode(): Int
+
+        internal companion object {
+            fun serializer(): KSerializer<Version>
+        }
     }
     }
 
 
     public companion object {
     public companion object {
@@ -119,7 +130,27 @@ public expect class DeviceInfo(
         @JvmStatic
         @JvmStatic
         public fun random(random: Random): DeviceInfo
         public fun random(random: Random): DeviceInfo
 
 
+        @Deprecated(DeviceInfoConstructorDeprecationMessage, level = DeprecationLevel.WARNING)
+        @DeprecatedSinceMirai(warningSince = "2.15") // planned internal
         public fun serializer(): KSerializer<DeviceInfo>
         public fun serializer(): KSerializer<DeviceInfo>
+
+        /**
+         * 将此 [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
     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<fc.5*6".toByteArray()
+ */
+internal fun generateGuid(androidId: ByteArray, macAddress: ByteArray): ByteArray =
+    (androidId + macAddress).md5()
+
+
+/*
+fun DeviceInfo.toOidb0x769DeviceInfo() : Oidb0x769.DeviceInfo = Oidb0x769.DeviceInfo(
+    brand = brand.encodeToString(),
+    model = model.encodeToString(),
+    os = Oidb0x769.OS(
+        version = version.release.encodeToString(),
+        sdk = version.sdk.toString(),
+        kernel = version.kernel
+    )
+)
+*/
+
+/**
+ * @see DeviceInfoManager
+ */
 internal object DeviceInfoCommonImpl {
 internal object DeviceInfoCommonImpl {
+    @Suppress("DEPRECATION")
     fun randomDeviceInfo(random: Random) = DeviceInfo(
     fun randomDeviceInfo(random: Random) = DeviceInfo(
         display = "MIRAI.${getRandomString(6, '0'..'9', random)}.001".toByteArray(),
         display = "MIRAI.${getRandomString(6, '0'..'9', random)}.001".toByteArray(),
         product = "mirai".toByteArray(),
         product = "mirai".toByteArray(),
@@ -159,7 +257,8 @@ internal object DeviceInfoCommonImpl {
         wifiSSID = "<unknown ssid>".toByteArray(),
         wifiSSID = "<unknown ssid>".toByteArray(),
         imsiMd5 = getRandomByteArray(16, random).md5(),
         imsiMd5 = getRandomByteArray(16, random).md5(),
         imei = "86${getRandomIntString(12, random)}".let { it + luhn(it) },
         imei = "86${getRandomIntString(12, random)}".let { it + luhn(it) },
-        apn = "wifi".toByteArray()
+        apn = "wifi".toByteArray(),
+        androidId = getRandomByteArray(8, random).toUHexString("").lowercase().encodeToByteArray()
     )
     )
 
 
     /**
     /**
@@ -187,6 +286,8 @@ internal object DeviceInfoCommonImpl {
         if (deviceInfo === other) return true
         if (deviceInfo === other) return true
         if (!isSameType(this, other)) return false
         if (!isSameType(this, other)) return false
 
 
+        // also remember to add equal compare to JvmDeviceInfoTest.`can read legacy v1`
+        // when adding new field compare here.
         if (!display.contentEquals(other.display)) return false
         if (!display.contentEquals(other.display)) return false
         if (!product.contentEquals(other.product)) return false
         if (!product.contentEquals(other.product)) return false
         if (!device.contentEquals(other.device)) return false
         if (!device.contentEquals(other.device)) return false
@@ -207,7 +308,10 @@ internal object DeviceInfoCommonImpl {
         if (!imsiMd5.contentEquals(other.imsiMd5)) return false
         if (!imsiMd5.contentEquals(other.imsiMd5)) return false
         if (imei != other.imei) return false
         if (imei != other.imei) return false
         if (!apn.contentEquals(other.apn)) return false
         if (!apn.contentEquals(other.apn)) return false
-        return guid.contentEquals(other.guid)
+        if (!guid.contentEquals(other.guid)) return false
+        if (!androidId.contentEquals(other.androidId)) return false
+
+        return true
     }
     }
 
 
     @OptIn(MiraiInternalApi::class)
     @OptIn(MiraiInternalApi::class)
@@ -234,273 +338,7 @@ internal object DeviceInfoCommonImpl {
         result = 31 * result + imei.hashCode()
         result = 31 * result + imei.hashCode()
         result = 31 * result + apn.contentHashCode()
         result = 31 * result + apn.contentHashCode()
         result = 31 * result + guid.contentHashCode()
         result = 31 * result + guid.contentHashCode()
+        result = 31 * result + androidId.contentHashCode()
         return result
         return result
     }
     }
-}
-
-@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
-)
-
-public fun DeviceInfo.generateDeviceInfoData(): ByteArray {
-    @OptIn(ExperimentalSerializationApi::class)
-    return ProtoBuf.encodeToByteArray(
-        DevInfo.serializer(), DevInfo(
-            bootloader,
-            procVersion,
-            version.codename,
-            version.incremental,
-            fingerprint,
-            bootId,
-            androidId,
-            baseBand,
-            version.incremental
-        )
-    )
-}
-
-internal object DeviceInfoManager {
-    sealed interface Info {
-        fun toDeviceInfo(): DeviceInfo
-    }
-
-    @Serializable(HexStringSerializer::class)
-    @JvmInline
-    value class HexString(
-        val data: ByteArray
-    )
-
-    object HexStringSerializer : KSerializer<HexString> 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<T : Info>(
-        @Suppress("unused") val deviceInfoVersion: Int, // used by plain jsonObject
-        val data: T
-    )
-
-    private object DeviceInfoVersionSerializer : KSerializer<DeviceInfo.Version> 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<fc.5*6".toByteArray()
- */
-internal fun generateGuid(androidId: ByteArray, macAddress: ByteArray): ByteArray =
-    (androidId + macAddress).md5()
-
-
-/*
-fun DeviceInfo.toOidb0x769DeviceInfo() : Oidb0x769.DeviceInfo = Oidb0x769.DeviceInfo(
-    brand = brand.encodeToString(),
-    model = model.encodeToString(),
-    os = Oidb0x769.OS(
-        version = version.release.encodeToString(),
-        sdk = version.sdk.toString(),
-        kernel = version.kernel
-    )
-)
-*/
+}

+ 319 - 0
mirai-core-api/src/commonMain/kotlin/utils/DeviceInfoManager.kt

@@ -0,0 +1,319 @@
+/*
+ * 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.SerialName
+import kotlinx.serialization.Serializable
+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 net.mamoe.mirai.utils.DeviceInfoManager.Version.Companion.trans
+import kotlin.jvm.JvmInline
+import kotlin.jvm.JvmName
+
+internal object DeviceInfoManager {
+    sealed interface Info {
+        fun toDeviceInfo(): DeviceInfo
+    }
+
+    @Serializable(HexStringSerializer::class)
+    @JvmInline
+    value class HexString(
+        val data: ByteArray
+    )
+
+    object HexStringSerializer : KSerializer<HexString> 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<T : Info>(
+        @Suppress("unused") val deviceInfoVersion: Int, // used by plain jsonObject
+        val data: T
+    )
+
+    internal object DeviceInfoVersionSerializer : KSerializer<DeviceInfo.Version> 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())
+        )
+    }
+}

+ 102 - 0
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<DeviceInfo> 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
+        )
+    }
+)

+ 2 - 2
mirai-core-api/src/commonTest/kotlin/utils/DeviceInfoTest.kt

@@ -42,7 +42,7 @@ class CommonDeviceInfoTest {
     }
     }
 
 
     @Test
     @Test
-    fun `can serialize and deserialize v2`() {
+    fun `can serialize and deserialize v3`() {
         val device = DeviceInfo.random()
         val device = DeviceInfo.random()
         assertEquals(device, DeviceInfoManager.deserialize(DeviceInfoManager.serialize(device)))
         assertEquals(device, DeviceInfoManager.deserialize(DeviceInfoManager.serialize(device)))
     }
     }
@@ -88,7 +88,7 @@ class CommonDeviceInfoTest {
          */
          */
 
 
         val element = DeviceInfoManager.toJsonElement(device)
         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
         val imsiMd5 = element.jsonObject["data"]!!.jsonObject["imsiMd5"]!!.jsonPrimitive.content
         assertEquals(
         assertEquals(

+ 0 - 354
mirai-core-api/src/commonTest/resources/device/legacy-device-info-1.json

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

+ 73 - 6
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 许可证的约束, 可以在以下链接找到该许可证.
  * 此源代码的使用受 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.
  * 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
 package net.mamoe.mirai.utils
 
 
+import kotlinx.serialization.KSerializer
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.Transient
 import kotlinx.serialization.Transient
 import kotlinx.serialization.json.Json
 import kotlinx.serialization.json.Json
 import java.io.File
 import java.io.File
 import kotlin.random.Random
 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 display: ByteArray,
     public actual val product: ByteArray,
     public actual val product: ByteArray,
     public actual val device: ByteArray,
     public actual val device: ByteArray,
@@ -36,9 +40,48 @@ public actual class DeviceInfo actual constructor(
     public actual val wifiSSID: ByteArray,
     public actual val wifiSSID: ByteArray,
     public actual val imsiMd5: ByteArray,
     public actual val imsiMd5: ByteArray,
     public actual val imei: String,
     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)
     public actual val ipAddress: ByteArray get() = byteArrayOf(192.toByte(), 168.toByte(), 1, 123)
 
 
     init {
     init {
@@ -100,7 +143,9 @@ public actual class DeviceInfo actual constructor(
                     this.writeText(DeviceInfoManager.serialize(it, json))
                     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 {
         public actual fun random(random: Random): DeviceInfo {
             return DeviceInfoCommonImpl.randomDeviceInfo(random)
             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 {
     actual override fun hashCode(): Int {
         return DeviceInfoCommonImpl.hashCodeImpl(this)
         return DeviceInfoCommonImpl.hashCodeImpl(this)
     }
     }
+
+    @Suppress("ClassName")
+    @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
+    public object `$serializer` : KSerializer<DeviceInfo> by DeviceInfoV1LegacySerializer
 }
 }

+ 157 - 5
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 许可证的约束, 可以在以下链接找到该许可证.
  * 此源代码的使用受 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.
  * 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 kotlinx.serialization.json.Json
 import net.mamoe.mirai.utils.DeviceInfo.Companion.loadAsDeviceInfo
 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 org.junit.jupiter.api.io.TempDir
 import java.io.File
 import java.io.File
 import kotlin.test.Test
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertEquals
+import kotlin.test.assertTrue
 
 
 class JvmDeviceInfoTest {
 class JvmDeviceInfoTest {
 
 
@@ -22,7 +24,7 @@ class JvmDeviceInfoTest {
     lateinit var dir: File
     lateinit var dir: File
 
 
     @Test
     @Test
-    fun `can write and read v2`() {
+    fun `can write and read`() {
         val device = DeviceInfo.random()
         val device = DeviceInfo.random()
         val file = dir.resolve("device.json")
         val file = dir.resolve("device.json")
 
 
@@ -31,11 +33,161 @@ class JvmDeviceInfoTest {
     }
     }
 
 
     @Test
     @Test
-    fun `can read legacy v1`() {
+    fun `can write read legacy v1`() {
         val device = DeviceInfo.random()
         val device = DeviceInfo.random()
         val file = dir.resolve("device.json")
         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)
     }
     }
 }
 }

+ 0 - 23
mirai-core-api/src/jvmTest/kotlin/utils/JvmDeviceInfoTestJvm.kt

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

+ 4 - 1
mirai-core-api/src/nativeMain/kotlin/utils/BotConfiguration.kt

@@ -41,7 +41,10 @@ public actual abstract class AbstractBotConfiguration { // open for Java
             if (!file.exists()) {
             if (!file.exists()) {
                 file.writeText(DeviceInfoManager.serialize(DeviceInfo.random(), BotConfiguration.json))
                 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))
+            }
+
         }
         }
     }
     }
 
 

+ 24 - 5
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 许可证的约束, 可以在以下链接找到该许可证.
  * 此源代码的使用受 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.
  * 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 kotlinx.serialization.Transient
 import kotlin.random.Random
 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 display: ByteArray,
     public actual val product: ByteArray,
     public actual val product: ByteArray,
     public actual val device: ByteArray,
     public actual val device: ByteArray,
@@ -34,9 +37,9 @@ public actual class DeviceInfo actual constructor(
     public actual val wifiSSID: ByteArray,
     public actual val wifiSSID: ByteArray,
     public actual val imsiMd5: ByteArray,
     public actual val imsiMd5: ByteArray,
     public actual val imei: String,
     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)
     public actual val ipAddress: ByteArray get() = byteArrayOf(192.toByte(), 168.toByte(), 1, 123)
 
 
     init {
     init {
@@ -99,6 +102,22 @@ public actual class DeviceInfo actual constructor(
         public actual fun random(random: Random): DeviceInfo {
         public actual fun random(random: Random): DeviceInfo {
             return DeviceInfoCommonImpl.randomDeviceInfo(random)
             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)
     }
     }
 
 
     /**
     /**

+ 5 - 1
mirai-core-utils/src/commonMain/kotlin/TimeUtils.kt

@@ -25,7 +25,11 @@ public expect fun currentTimeMillis(): Long
  */
  */
 public fun currentTimeSeconds(): Long = currentTimeMillis() / 1000
 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.
 // 临时使用, 待 Kotlin Duration 稳定后使用 Duration.

+ 3 - 3
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())
     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) {
     return if (format == null) {
-        timeFormat.format(Date())
+        timeFormat.format(Date(epochTimeMillis))
     } else {
     } else {
-        SimpleDateFormat(format, Locale.getDefault()).format(Date())
+        SimpleDateFormat(format, Locale.getDefault()).format(Date(epochTimeMillis))
     }
     }
 }
 }

+ 12 - 4
mirai-core-utils/src/nativeMain/kotlin/TimeUtils.kt

@@ -32,18 +32,26 @@ public actual fun currentTimeMillis(): Long {
 
 
 private val timeLock = ReentrantLock()
 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 {
     memScoped {
         val timeT = alloc<time_tVar>()
         val timeT = alloc<time_tVar>()
-        time(timeT.ptr)
+        timeT.value = epochTimeMillis / 1000
 
 
         // http://www.cplusplus.com/reference/clibrary/ctime/localtime/
         // http://www.cplusplus.com/reference/clibrary/ctime/localtime/
         // tm returns a static pointer which doesn't need to free
         // tm returns a static pointer which doesn't need to free
         val tm = localtime(timeT.ptr) // localtime is not thread-safe
         val tm = localtime(timeT.ptr) // localtime is not thread-safe
 
 
         val bb = allocArray<ByteVar>(40)
         val bb = allocArray<ByteVar>(40)
-        strftime(bb, 40, "%Y-%m-%d %H:%M:%S", tm);
+
+        strftime(bb, 40, strftimeFormat, tm);
 
 
         bb.toKString()
         bb.toKString()
     }
     }

+ 56 - 1
mirai-core-utils/src/nativeTest/kotlin/TimeUtilsTest.kt

@@ -10,6 +10,8 @@
 package net.mamoe.mirai.utils
 package net.mamoe.mirai.utils
 
 
 import kotlin.test.Test
 import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
 import kotlin.test.assertTrue
 import kotlin.test.assertTrue
 
 
 internal class TimeUtilsTest {
 internal class TimeUtilsTest {
@@ -23,6 +25,59 @@ internal class TimeUtilsTest {
     @Test
     @Test
     fun `can get currentTimeFormatted`() {
     fun `can get currentTimeFormatted`() {
         // 2022-28-26 18:28:28
         // 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}$"""))
+        }
     }
     }
 }
 }

+ 9 - 1
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 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())
     private val sequenceId: AtomicInt = atomic(getRandomUnsignedInt())
@@ -166,7 +166,15 @@ internal open class QQAndroidClient(
     var reserveUinInfo: ReserveUinInfo? = null
     var reserveUinInfo: ReserveUinInfo? = null
     var t402: ByteArray? = null
     var t402: ByteArray? = null
     lateinit var t104: ByteArray
     lateinit var t104: ByteArray
+    internal val t104Initialized get() = ::t104.isInitialized
+    var t543: ByteArray? = null
     var t547: ByteArray? = null
     var t547: ByteArray? = null
+
+    /**
+     * t545
+     */
+    var qimei16: String? = null
+    var qimei36: String? = null
 }
 }
 
 
 internal val QQAndroidClient.apkId: ByteArray get() = protocol.apkId.toByteArray()
 internal val QQAndroidClient.apkId: ByteArray get() = protocol.apkId.toByteArray()

+ 1 - 2
mirai-core/src/commonMain/kotlin/network/components/CacheValidator.kt

@@ -10,7 +10,6 @@
 package net.mamoe.mirai.internal.network.components
 package net.mamoe.mirai.internal.network.components
 
 
 import io.ktor.utils.io.core.*
 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.network.component.ComponentKey
 import net.mamoe.mirai.internal.utils.MiraiProtocolInternal
 import net.mamoe.mirai.internal.utils.MiraiProtocolInternal
 import net.mamoe.mirai.internal.utils.io.writeShortLVString
 import net.mamoe.mirai.internal.utils.io.writeShortLVString
@@ -61,7 +60,7 @@ internal class CacheValidatorImpl(
             val device = ssoProcessorContext.device
             val device = ssoProcessorContext.device
 
 
             @Suppress("INVISIBLE_MEMBER")
             @Suppress("INVISIBLE_MEMBER")
-            writeFully(ProtoBufForCache.encodeToByteArray(DeviceInfo.serializer(), device))
+            writeFully(device.serializeToString().encodeToByteArray())
         }.let { pkg ->
         }.let { pkg ->
             try {
             try {
                 pkg.readBytes()
                 pkg.readBytes()

+ 10 - 0
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
 import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin.Login.LoginPacketResponse.Captcha
 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.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.network.*
 import net.mamoe.mirai.utils.*
 import net.mamoe.mirai.utils.*
 import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol
 import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol
@@ -140,6 +142,8 @@ internal open class SsoProcessorImpl(
             ssoContext.bot.components[BotClientHolder].client = value
             ssoContext.bot.components[BotClientHolder].client = value
         }
         }
 
 
+    private val qimeiLogger by lazy { ssoContext.bot.network.logger.subLogger("QimeiApi") }
+
     override val ssoSession: SsoSession get() = client
     override val ssoSession: SsoSession get() = client
     private val components get() = ssoContext.bot.components
     private val components get() = ssoContext.bot.components
 
 
@@ -199,6 +203,12 @@ internal open class SsoProcessorImpl(
 
 
             components[BdhSessionSyncer].loadServerListFromCache()
             components[BdhSessionSyncer].loadServerListFromCache()
 
 
+            try {
+                ssoContext.bot.requestQimei(qimeiLogger)
+            } catch (exception: Throwable) {
+                qimeiLogger.warning("Cannot get qimei from server.", exception)
+            }
+
             // try fast login
             // try fast login
             if (client.wLoginSigInfoInitialized) {
             if (client.wLoginSigInfoInitialized) {
                 ssoContext.bot.components[EcdhInitialPublicKeyUpdater].refreshInitialPublicKeyAndApplyEcdh()
                 ssoContext.bot.components[EcdhInitialPublicKeyUpdater].refreshInitialPublicKeyAndApplyEcdh()

+ 73 - 10
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.MacOrAndroidIdChangeFlag
 import net.mamoe.mirai.internal.utils.NetworkType
 import net.mamoe.mirai.internal.utils.NetworkType
 import net.mamoe.mirai.internal.utils.guidFlag
 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 net.mamoe.mirai.utils.*
 import kotlin.jvm.JvmInline
 import kotlin.jvm.JvmInline
 import kotlin.random.Random
 import kotlin.random.Random
@@ -49,15 +52,15 @@ internal fun TlvMap.smartToString(leadingLineBreak: Boolean = true, sorted: Bool
 @JvmInline
 @JvmInline
 internal value class Tlv(val value: ByteArray)
 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) {
     tlv(0x01) {
         writeShort(1) // _ip_ver
         writeShort(1) // _ip_ver
         writeInt(Random.nextInt())
         writeInt(Random.nextInt())
         writeInt(uin.toInt())
         writeInt(uin.toInt())
-        writeInt(currentTimeSeconds().toInt())
-        writeFully(ip)
+        writeInt(timeSeconds)
+        writeFully(ipv4)
         writeShort(0)
         writeShort(0)
     }
     }
 }
 }
@@ -192,6 +195,7 @@ internal fun TlvMapWriter.t106(
         client.subAppId /* maybe 1*/,
         client.subAppId /* maybe 1*/,
         client.appClientVersion,
         client.appClientVersion,
         client.uin,
         client.uin,
+        client.device.ipAddress,
         true,
         true,
         passwordMd5,
         passwordMd5,
         0,
         0,
@@ -220,6 +224,7 @@ internal fun TlvMapWriter.t106(
     subAppId: Long,
     subAppId: Long,
     appClientVersion: Int = 0,
     appClientVersion: Int = 0,
     uin: Long,
     uin: Long,
+    ipv4: ByteArray,
     isSavePassword: Boolean = true,
     isSavePassword: Boolean = true,
     passwordMd5: ByteArray,
     passwordMd5: ByteArray,
     salt: Long,
     salt: Long,
@@ -233,6 +238,7 @@ internal fun TlvMapWriter.t106(
     passwordMd5.requireSize(16)
     passwordMd5.requireSize(16)
     tgtgtKey.requireSize(16)
     tgtgtKey.requireSize(16)
     guid?.requireSize(16)
     guid?.requireSize(16)
+    ipv4.requireSize(4)
 
 
     tlv(0x106) {
     tlv(0x106) {
         encryptAndWrite(
         encryptAndWrite(
@@ -252,7 +258,7 @@ internal fun TlvMapWriter.t106(
             }
             }
 
 
             writeInt(currentTimeSeconds().toInt())
             writeInt(currentTimeSeconds().toInt())
-            writeFully(ByteArray(4)) // ip // no need to write actual ip
+            writeFully(ipv4) //
             writeByte(isSavePassword.toByte())
             writeByte(isSavePassword.toByte())
             writeFully(passwordMd5)
             writeFully(passwordMd5)
             writeFully(tgtgtKey)
             writeFully(tgtgtKey)
@@ -368,16 +374,18 @@ internal fun TlvMapWriter.t174(
 
 
 
 
 internal fun TlvMapWriter.t17a(
 internal fun TlvMapWriter.t17a(
-    value: Int = 0
+    smsAppId: Int = 0
 ) {
 ) {
     tlv(0x17a) {
     tlv(0x17a) {
-        writeInt(value)
+        writeInt(smsAppId)
     }
     }
 }
 }
 
 
-internal fun TlvMapWriter.t197() {
+internal fun TlvMapWriter.t197(
+    devLockMobileType: Byte = 0
+) {
     tlv(0x197) {
     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
 internal fun TlvMapWriter.t544( // 1334
 ) {
 ) {
     tlv(0x544) {
     tlv(0x544) {

+ 1 - 0
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[0x403]?.let { bot.client.randSeed = it }
             tlvMap[0x402]?.let { bot.client.t402 = it }
             tlvMap[0x402]?.let { bot.client.t402 = it }
             tlvMap[0x546]?.let { bot.client.analysisTlv546(it) }
             tlvMap[0x546]?.let { bot.client.analysisTlv546(it) }
+            tlvMap[0x543]?.let { bot.client.t543 = it }
             // tlvMap[0x402]?.let { t402 ->
             // tlvMap[0x402]?.let { t402 ->
 //            bot.client.G = buildPacket {
 //            bot.client.G = buildPacket {
 //                writeFully(bot.client.device.guid)
 //                writeFully(bot.client.device.guid)

+ 1 - 0
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())
                     //t112(client.account.phoneNumber.encodeToByteArray())
                     t143(client.wLoginSigInfo.d2.data)
                     t143(client.wLoginSigInfo.d2.data)
+                    t145(client.device.guid)
                     t142(client.apkId)
                     t142(client.apkId)
                     t154(sequenceId)
                     t154(sequenceId)
                     t18(appId, uin = client.uin)
                     t18(appId, uin = client.uin)

+ 21 - 10
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.*
 import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
 import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
 import net.mamoe.mirai.utils._writeTlvMap
 import net.mamoe.mirai.utils._writeTlvMap
+import net.mamoe.mirai.utils.currentTimeSeconds
+import net.mamoe.mirai.utils.toByteArray
 import kotlin.math.abs
 import kotlin.math.abs
 import kotlin.random.Random
 import kotlin.random.Random
 
 
@@ -26,7 +28,7 @@ internal object WtLogin15 : WtLoginExt {
         client: QQAndroidClient,
         client: QQAndroidClient,
     ) = WtLogin.ExchangeEmp.buildOutgoingUniPacket(
     ) = WtLogin.ExchangeEmp.buildOutgoingUniPacket(
         client, bodyType = 2, key = ByteArray(16), remark = "15:refresh-keys"
         client, bodyType = 2, key = ByteArray(16), remark = "15:refresh-keys"
-    ) {
+    ) { sequenceId ->
 //        writeSsoPacket(client, client.subAppId, WtLogin.ExchangeEmp.commandName, sequenceId = sequenceId) {
 //        writeSsoPacket(client, client.subAppId, WtLogin.ExchangeEmp.commandName, sequenceId = sequenceId) {
         writeOicqRequestPacket(
         writeOicqRequestPacket(
             client,
             client,
@@ -53,10 +55,9 @@ internal object WtLogin15 : WtLoginExt {
 //                    "").hexToBytes())
 //                    "").hexToBytes())
 //            return@writeOicqRequestPacket
 //            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!!)
                 t106(client.wLoginSigInfo.encryptA1!!)
 //            kotlin.run {
 //            kotlin.run {
 //                val key = (client.account.passwordMd5 + ByteArray(4) + client.uin.toInt().toByteArray()).md5()
 //                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(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, client.subAppId, client.appClientVersion, client.ssoVersion, client.mainSigMap)
                 //t100(appId, 1, client.appClientVersion, client.ssoVersion, mainSigMap = 1048768)
                 //t100(appId, 1, client.appClientVersion, client.ssoVersion, mainSigMap = 1048768)
@@ -90,16 +94,21 @@ internal object WtLogin15 : WtLoginExt {
 
 
                 t107(0)
                 t107(0)
 
 
-                //t108(client.ksid) // 第一次 exchange 没有 108
+                if (client.ksid.isNotEmpty()) {
+                    t108(client.ksid)
+                }
                 t144(client)
                 t144(client)
                 t142(client.apkId)
                 t142(client.apkId)
+                if (client.uin !in 10000L..4000000000L) {
+                    t112(client.uin.toByteArray())
+                }
                 t145(client.device.guid)
                 t145(client.device.guid)
 
 
                 val noPicSig =
                 val noPicSig =
                     client.wLoginSigInfo.noPicSig ?: error("Internal error: doing exchange emp 15 while noPicSig=null")
                     client.wLoginSigInfo.noPicSig ?: error("Internal error: doing exchange emp 15 while noPicSig=null")
                 t16a(noPicSig)
                 t16a(noPicSig)
 
 
-                t154(0)
+                t154(sequenceId)
                 t141(client.device.simInfo, client.networkType, client.device.apn)
                 t141(client.device.simInfo, client.networkType, client.device.apn)
                 t8(2052)
                 t8(2052)
                 t511()
                 t511()
@@ -112,20 +121,22 @@ internal object WtLogin15 : WtLoginExt {
                     uin = client.uin,
                     uin = client.uin,
                     guid = client.device.guid,
                     guid = client.device.guid,
                     dpwd = client.dpwd,
                     dpwd = client.dpwd,
-                    appId = 1,
-                    subAppId = 16,
+                    appId = appId,
+                    subAppId = 1,
                     randomSeed = client.randSeed
                     randomSeed = client.randSeed
                 )
                 )
 
 
                 t187(client.device.macAddress)
                 t187(client.device.macAddress)
                 t188(client.device.androidId)
                 t188(client.device.androidId)
                 t194(client.device.imsiMd5)
                 t194(client.device.imsiMd5)
+                // ignored t201 cuz SetNeedForPayToken is never called.
                 t202(client.device.wifiBSSID, client.device.wifiSSID)
                 t202(client.device.wifiBSSID, client.device.wifiSSID)
                 t516()
                 t516()
 
 
                 t521() // new
                 t521() // new
                 t525(client.loginExtraData) // new
                 t525(client.loginExtraData) // new
-                //t544() // new
+                //t544() // new 810_f
+                t545(client.qimei16 ?: client.device.imei)
             }
             }
         }
         }
 
 

+ 6 - 0
mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin8.kt

@@ -43,7 +43,13 @@ internal object WtLogin8 : WtLoginExt {
                     t174(t174)
                     t174(t174)
                     t17a(9)
                     t17a(9)
                     t197()
                     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) }
                 }
                 }
             }
             }
         }
         }

+ 33 - 18
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.*
 import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
 import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
 import net.mamoe.mirai.utils._writeTlvMap
 import net.mamoe.mirai.utils._writeTlvMap
+import net.mamoe.mirai.utils.currentTimeSeconds
+import net.mamoe.mirai.utils.toByteArray
 
 
 internal object WtLogin9 : WtLoginExt {
 internal object WtLogin9 : WtLoginExt {
     private const val appId = 16L
     private const val appId = 16L
@@ -28,21 +30,16 @@ internal object WtLogin9 : WtLoginExt {
         writeSsoPacket(client, client.subAppId, WtLogin.Login.commandName, sequenceId = sequenceId) {
         writeSsoPacket(client, client.subAppId, WtLogin.Login.commandName, sequenceId = sequenceId) {
             writeOicqRequestPacket(client, commandId = 0x0810) {
             writeOicqRequestPacket(client, commandId = 0x0810) {
                 writeShort(9) // subCommand
                 writeShort(9) // subCommand
-                var tlvCount = if (allowSlider) 0x18 else 0x17;
                 val useEncryptA1AndNoPicSig =
                 val useEncryptA1AndNoPicSig =
                     client.wLoginSigInfoInitialized
                     client.wLoginSigInfoInitialized
                             && client.wLoginSigInfo.noPicSig != null
                             && client.wLoginSigInfo.noPicSig != null
                             && client.wLoginSigInfo.encryptA1 != null
                             && client.wLoginSigInfo.encryptA1 != null
-                if (useEncryptA1AndNoPicSig) {
-                    tlvCount++;
-                }
-                // writeShort(tlvCount.toShort()) // count of TLVs, probably ignored by server?
                 //writeShort(LoginType.PASSWORD.value.toShort())
                 //writeShort(LoginType.PASSWORD.value.toShort())
 
 
                 _writeTlvMap {
                 _writeTlvMap {
 
 
                     t18(appId, client.appClientVersion, client.uin)
                     t18(appId, client.appClientVersion, client.uin)
-                    t1(client.uin, client.device.ipAddress)
+                    t1(client.uin, (currentTimeSeconds() + client.timeDifference).toInt(), client.device.ipAddress)
 
 
                     if (useEncryptA1AndNoPicSig) {
                     if (useEncryptA1AndNoPicSig) {
                         t106(client.wLoginSigInfo.encryptA1!!)
                         t106(client.wLoginSigInfo.encryptA1!!)
@@ -63,25 +60,31 @@ internal object WtLogin9 : WtLoginExt {
                     t116(client.miscBitMap, client.subSigMap)
                     t116(client.miscBitMap, client.subSigMap)
                     t100(appId, client.subAppId, client.appClientVersion, client.ssoVersion, client.mainSigMap)
                     t100(appId, client.subAppId, client.appClientVersion, client.ssoVersion, client.mainSigMap)
                     t107(0)
                     t107(0)
-                    t108(client.device.imei.toByteArray())
+                    if (client.ksid.isNotEmpty()) {
+                        t108(client.ksid)
+                    }
 
 
                     // t108(byteArrayOf())
                     // t108(byteArrayOf())
-                    // ignored: t104()
+                    if (client.t104Initialized) {
+                        t104(client.t104)
+                    }
+
                     t142(client.apkId)
                     t142(client.apkId)
 
 
                     // if login with non-number uin
                     // if login with non-number uin
-                    // t112()
+                    if (client.uin !in 10000L..4000000000L) {
+                        t112(client.uin.toByteArray())
+                    }
                     t144(client)
                     t144(client)
 
 
                     //this.build().debugPrint("傻逼")
                     //this.build().debugPrint("傻逼")
                     t145(client.device.guid)
                     t145(client.device.guid)
                     t147(appId, client.apkVersionName, client.apkSignatureMd5)
                     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) {
                     if (useEncryptA1AndNoPicSig) {
                         t16a(client.wLoginSigInfo.noPicSig!!)
                         t16a(client.wLoginSigInfo.noPicSig!!)
                     }
                     }
@@ -94,7 +97,17 @@ internal object WtLogin9 : WtLoginExt {
 
 
                     // ignored t172 because rollbackSig is null
                     // ignored t172 because rollbackSig is null
                     // ignored t185 because loginType is not SMS
                     // 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)
                     t187(client.device.macAddress)
                     t188(client.device.androidId)
                     t188(client.device.androidId)
@@ -103,8 +116,8 @@ internal object WtLogin9 : WtLoginExt {
                         t191()
                         t191()
                     }
                     }
 
 
-                    /*
-                t201(N = byteArrayOf())*/
+
+                    //t201(N = byteArrayOf())
 
 
                     t202(client.device.wifiBSSID, client.device.wifiSSID)
                     t202(client.device.wifiBSSID, client.device.wifiSSID)
 
 
@@ -116,6 +129,8 @@ internal object WtLogin9 : WtLoginExt {
                     t521()
                     t521()
 
 
                     t525()
                     t525()
+                    t545(client.qimei16 ?: client.device.imei)
+                    // t548()
                     // this.build().debugPrint("傻逼")
                     // this.build().debugPrint("傻逼")
 
 
                     // ignored t318 because not logging in by QR
                     // ignored t318 because not logging in by QR
@@ -138,7 +153,7 @@ internal object WtLogin9 : WtLoginExt {
 
 
                 _writeTlvMap {
                 _writeTlvMap {
                     t18(appId, client.appClientVersion, client.uin)
                     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)
                     t106(data.tmpPwd)
 
 

+ 297 - 0
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
+)

+ 20 - 8
mirai-core/src/commonMain/kotlin/utils/MiraiProtocolInternal.kt

@@ -18,6 +18,7 @@ internal class MiraiProtocolInternal(
     var apkId: String,
     var apkId: String,
     var id: Long,
     var id: Long,
     var ver: String,
     var ver: String,
+    var buildVer: String,
     var sdkVer: String,
     var sdkVer: String,
     var miscBitMap: Int,
     var miscBitMap: Int,
     var subSigMap: Int,
     var subSigMap: Int,
@@ -25,6 +26,7 @@ internal class MiraiProtocolInternal(
     var sign: String,
     var sign: String,
     var buildTime: Long,
     var buildTime: Long,
     var ssoVersion: Int,
     var ssoVersion: Int,
+    var appKey: String,
     var supportsQRLogin: Boolean,
     var supportsQRLogin: Boolean,
 
 
     // don't change property signatures, used externally.
     // don't change property signatures, used externally.
@@ -38,25 +40,28 @@ internal class MiraiProtocolInternal(
             protocols[protocol] ?: error("Internal Error: Missing protocol $protocol")
             protocols[protocol] ?: error("Internal Error: Missing protocol $protocol")
 
 
         init {
         init {
-            //Updated from MiraiGo (2023/3/7)
+            //Updated from 8.9.35 (2023/4/9)
             protocols[MiraiProtocol.ANDROID_PHONE] = MiraiProtocolInternal(
             protocols[MiraiProtocol.ANDROID_PHONE] = MiraiProtocolInternal(
                 apkId = "com.tencent.mobileqq",
                 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,
                 miscBitMap = 150470524,
                 subSigMap = 0x10400,
                 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",
                 sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D",
-                buildTime = 1673599898L,
+                buildTime = 1676531414L,
                 ssoVersion = 19,
                 ssoVersion = 19,
+                appKey = "0S200MNJT807V3GE",
                 supportsQRLogin = false,
                 supportsQRLogin = false,
             )
             )
             //Updated from MiraiGo (2023/3/7)
             //Updated from MiraiGo (2023/3/7)
             protocols[MiraiProtocol.ANDROID_PAD] = MiraiProtocolInternal(
             protocols[MiraiProtocol.ANDROID_PAD] = MiraiProtocolInternal(
                 apkId = "com.tencent.mobileqq",
                 apkId = "com.tencent.mobileqq",
                 id = 537151218,
                 id = 537151218,
-                ver = "8.9.33.10335",
+                ver = "8.9.33",
+                buildVer = "8.9.33.10335",
                 sdkVer = "6.0.0.2534",
                 sdkVer = "6.0.0.2534",
                 miscBitMap = 150470524,
                 miscBitMap = 150470524,
                 subSigMap = 0x10400,
                 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",
                 sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D",
                 buildTime = 1673599898L,
                 buildTime = 1673599898L,
                 ssoVersion = 19,
                 ssoVersion = 19,
+                appKey = "0S200MNJT807V3GE",
                 supportsQRLogin = false,
                 supportsQRLogin = false,
             )
             )
             //Updated from MiraiGo (2023/3/24)
             //Updated from MiraiGo (2023/3/24)
@@ -71,6 +77,7 @@ internal class MiraiProtocolInternal(
                 apkId = "com.tencent.qqlite",
                 apkId = "com.tencent.qqlite",
                 id = 537065138,
                 id = 537065138,
                 ver = "2.0.8",
                 ver = "2.0.8",
+                buildVer = "2.0.8",
                 sdkVer = "6.0.0.2365",
                 sdkVer = "6.0.0.2365",
                 miscBitMap = 16252796,
                 miscBitMap = 16252796,
                 subSigMap = 0x10400,
                 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",
                 sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D",
                 buildTime = 1559564731L,
                 buildTime = 1559564731L,
                 ssoVersion = 5,
                 ssoVersion = 5,
+                appKey = "",
                 supportsQRLogin = true,
                 supportsQRLogin = true,
             )
             )
             protocols[MiraiProtocol.IPAD] = MiraiProtocolInternal(
             protocols[MiraiProtocol.IPAD] = MiraiProtocolInternal(
                 apkId = "com.tencent.minihd.qq",
                 apkId = "com.tencent.minihd.qq",
                 id = 537151363,
                 id = 537151363,
-                ver = "8.9.33.614",
+                ver = "8.9.33",
+                buildVer = "8.9.33.614",
                 sdkVer = "6.0.0.2433",
                 sdkVer = "6.0.0.2433",
                 miscBitMap = 150470524,
                 miscBitMap = 150470524,
                 subSigMap = 66560,
                 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",
                 sign = "AA 39 78 F4 1F D9 6F F9 91 4A 66 9E 18 64 74 C7",
                 buildTime = 1640921786L,
                 buildTime = 1640921786L,
                 ssoVersion = 12,
                 ssoVersion = 12,
+                appKey = "",
                 supportsQRLogin = false,
                 supportsQRLogin = false,
             )
             )
             protocols[MiraiProtocol.MACOS] = MiraiProtocolInternal(
             protocols[MiraiProtocol.MACOS] = MiraiProtocolInternal(
                 apkId = "com.tencent.qq",
                 apkId = "com.tencent.qq",
                 id = 0x2003ca32,
                 id = 0x2003ca32,
                 ver = "6.7.9",
                 ver = "6.7.9",
+                buildVer = "6.7.9",
                 sdkVer = "6.2.0.1023",
                 sdkVer = "6.2.0.1023",
                 miscBitMap = 0x7ffc,
                 miscBitMap = 0x7ffc,
                 subSigMap = 66560,
                 subSigMap = 66560,
@@ -104,6 +115,7 @@ internal class MiraiProtocolInternal(
                 sign = "com.tencent.qq".encodeToByteArray().toUHexString(" "),
                 sign = "com.tencent.qq".encodeToByteArray().toUHexString(" "),
                 buildTime = 0L,
                 buildTime = 0L,
                 ssoVersion = 7,
                 ssoVersion = 7,
+                appKey = "",
                 supportsQRLogin = true,
                 supportsQRLogin = true,
             )
             )
         }
         }

+ 14 - 0
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

+ 21 - 0
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

+ 40 - 0
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())
+    }
+}

+ 112 - 0
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())
+
+    }
+}

+ 31 - 0
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)
+}

+ 74 - 0
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-----")
+        }
+    )
+}

+ 26 - 1
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
 # -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/ \
                              -I/usr/local/include/ \
                              -IC:/openssl/include/ \
                              -IC:/openssl/include/ \
                              -IC:/vcpkg/installed/x64-windows/include \
                              -IC:/vcpkg/installed/x64-windows/include \
+
+---
+
+#include <openssl/evp.h>
+#include <openssl/rsa.h>
+
+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);
+}

+ 101 - 0
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<IntVar>()
+        val cipherSize = alloc<IntVar>()
+
+        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)
+        }
+    }
+}

+ 299 - 0
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<EVP_PKEY>,
+    dumper: (CPointer<BIO>, CPointer<EVP_PKEY>) -> 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<ByteVar>()
+    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<BIO>) -> CPointer<RSA>?
+): CPointer<RSA>? {
+    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<size_tVar>().apply { value = 0u }
+        val blockSize = alloc<size_tVar>().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<size_tVar>().apply { value = 0u }
+        val blockSize = alloc<size_tVar>().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)
+        }*/
+    }
+}

+ 19 - 0
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)
+}

+ 27 - 0
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<ByteVar>()
+
+        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"
+    }
+}

+ 19 - 0
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 <T : CPointed> NativePlacement.ref(ptr: CPointer<T>): CPointer<CPointerVar<T>> {
+    return allocPointerTo<T>().apply { value = ptr }.ptr
+}