瀏覽代碼

[core] enhance(ECDH): reconstruct ECDH (#2161)

* enhance(ECDH): reconstruct ECDH
common: 移动特定于 QQ 平台的实现到 OicqECDH,重新设计 ECDH,使 ECDH 类只针对算法本身,而不过多包括 QQ 协议的使用细节
jvm: 尝试优先使用平台加密实现,可能改善性能
jvm & android: 使用 curveName `secp256r1` 代替 `prime256v1`,前者在 Java 中更常用,可以被更多的 JCE 实现所识别(虽然都是指同一条曲线)
android: 使用系统自带的实现以减少依赖,并尝试兼容 Android P+ 版本
native: 中间储存时保留OpenSSL内部结构而不反复 new & free,提高性能
    (为了实现智能指针,需要用到 `@ExperimentalStdlibApi` 的 `createCleaner`,但这种风险应该可以接受)
native: 直接使用 point/bignum 到 bytes 的转换,避免了 hex string 作为中间层,提高效率

* test(ECDH): fix AndroidTest

* style(Ecdh): obey official Kotlin coding conventions
> When using an acronym as part of a declaration name, capitalize it if it consists of two letters (IOStream); capitalize only the first letter if it is longer (XmlFormatter, HttpInputStream).
> [View origin](https://kotlinlang.org/docs/coding-conventions.html)

Co-authored-by: ArcticLampyrid <arcticlampyrid@outlook.com>
AdoptOSS 2 年之前
父節點
當前提交
397d824d33
共有 24 個文件被更改,包括 538 次插入699 次删除
  1. 2 3
      mirai-core/build.gradle.kts
  2. 0 118
      mirai-core/src/androidMain/kotlin/utils/crypto/ECDHAndroid.kt
  3. 28 0
      mirai-core/src/androidMain/kotlin/utils/crypto/EcdhAndroid.kt
  4. 1 1
      mirai-core/src/androidTest/kotlin/test/initPlatform.android.kt
  5. 4 5
      mirai-core/src/commonMain/kotlin/network/components/AccountSecretsManager.kt
  6. 19 28
      mirai-core/src/commonMain/kotlin/network/components/EcdhInitialPublicKeyUpdater.kt
  7. 6 6
      mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt
  8. 2 2
      mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt
  9. 14 21
      mirai-core/src/commonMain/kotlin/network/protocol/packet/EncryptMethod.kt
  10. 1 1
      mirai-core/src/commonMain/kotlin/network/protocol/packet/OutgoingPacket.kt
  11. 0 128
      mirai-core/src/commonMain/kotlin/utils/crypto/ECDH.kt
  12. 44 0
      mirai-core/src/commonMain/kotlin/utils/crypto/Ecdh.kt
  13. 60 0
      mirai-core/src/commonMain/kotlin/utils/crypto/QQEcdh.kt
  14. 13 11
      mirai-core/src/commonTest/kotlin/utils/crypto/EcdhTest.kt
  15. 0 49
      mirai-core/src/jvmBaseMain/kotlin/utils/crypto/ECDH.kt
  16. 91 0
      mirai-core/src/jvmBaseMain/kotlin/utils/crypto/JceEcdh.kt
  17. 23 0
      mirai-core/src/jvmBaseMain/kotlin/utils/crypto/JceEcdhWithProvider.kt
  18. 28 0
      mirai-core/src/jvmBaseMain/kotlin/utils/crypto/QQEcdhJvm.kt
  19. 0 93
      mirai-core/src/jvmMain/kotlin/utils/crypto/ECDHJvmDesktop.kt
  20. 27 0
      mirai-core/src/jvmMain/kotlin/utils/crypto/EcdhJvmDesktop.kt
  21. 0 233
      mirai-core/src/nativeMain/kotlin/utils/crypto/ECDH.kt
  22. 13 0
      mirai-core/src/nativeMain/kotlin/utils/crypto/EcdhNative.kt
  23. 147 0
      mirai-core/src/nativeMain/kotlin/utils/crypto/OpenSslEcdh.kt
  24. 15 0
      mirai-core/src/nativeMain/kotlin/utils/crypto/QQEcdhNative.kt

+ 2 - 3
mirai-core/build.gradle.kts

@@ -58,7 +58,6 @@ kotlin {
 
         findByName("jvmBaseMain")?.apply {
             dependencies {
-                implementation(bouncycastle)
                 implementation(`log4j-api`)
                 implementation(`netty-all`)
                 implementation(`ktor-client-okhttp`)
@@ -84,13 +83,13 @@ kotlin {
                 implementation(kotlin("test-junit5", Versions.kotlinCompiler))
                 implementation(kotlin("test-annotations-common"))
                 implementation(kotlin("test-common"))
-                //implementation("org.bouncycastle:bcprov-jdk15on:1.64")
+                implementation(bouncycastle)
             }
         }
 
         findByName("jvmMain")?.apply {
             dependencies {
-                //implementation("org.bouncycastle:bcprov-jdk15on:1.64")
+                implementation(bouncycastle)
                 // api(kotlinx("coroutines-debug", Versions.coroutines))
             }
         }

+ 0 - 118
mirai-core/src/androidMain/kotlin/utils/crypto/ECDHAndroid.kt

@@ -1,118 +0,0 @@
-/*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * 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.md5
-import net.mamoe.mirai.utils.recoverCatchingSuppressed
-import java.security.KeyFactory
-import java.security.KeyPairGenerator
-import java.security.Provider
-import java.security.Signature
-import java.security.spec.ECGenParameterSpec
-import java.security.spec.X509EncodedKeySpec
-import javax.crypto.KeyAgreement
-
-/**
- * 绕过在Android P之后的版本无法使用EC的限制
- * https://cs.android.com/android/platform/superproject/+/master:libcore/ojluni/src/main/java/sun/security/jca/Providers.java;l=371;bpv=1;bpt=1
- * https://android-developers.googleblog.com/2018/03/cryptography-changes-in-android-p.html
- * */
-@Suppress("DEPRECATION") // since JDK 9
-private class AndroidProvider : Provider("sbAndroid", 1.0, "") {
-    override fun getService(type: String?, algorithm: String?): Service? {
-        if (type == "KeyFactory" && algorithm == "EC") {
-            return object : Service(this, type, algorithm, "", emptyList(), emptyMap()) {
-                override fun newInstance(constructorParameter: Any?): Any {
-                    return org.bouncycastle.jcajce.provider.asymmetric.ec.KeyFactorySpi.EC()
-                }
-            }
-        }
-        return super.getService(type, algorithm)
-    }
-}
-
-private val ANDROID_PROVIDER by lazy { AndroidProvider() }
-private val ecKf by lazy {
-    runCatching { KeyFactory.getInstance("EC", "BC") }
-        .recoverCatchingSuppressed { KeyFactory.getInstance("EC", ANDROID_PROVIDER) }
-        .getOrThrow()
-}
-
-internal actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
-    actual companion object {
-        private const val curveName = "prime256v1" // p-256
-
-        actual val isECDHAvailable: Boolean
-
-        init {
-            isECDHAvailable = kotlin.runCatching {
-                ecKf // init
-
-                fun testECDH() {
-                    ECDHKeyPairImpl(
-                        KeyPairGenerator.getInstance("ECDH")
-                            .also { it.initialize(ECGenParameterSpec(curveName)) }
-                            .genKeyPair()).let {
-                        calculateShareKey(it.privateKey, it.publicKey)
-                    }
-                }
-
-                if (kotlin.runCatching { testECDH() }.isSuccess) {
-                    return@runCatching
-                }
-
-                testECDH()
-            }.onFailure {
-                it.printStackTrace()
-            }.isSuccess
-        }
-
-        actual fun generateKeyPair(initialPublicKey: ECDHPublicKey): ECDHKeyPair {
-            if (!isECDHAvailable) {
-                return ECDHKeyPair.DefaultStub
-            }
-            return ECDHKeyPairImpl(
-                KeyPairGenerator.getInstance("ECDH")
-                    .also { it.initialize(ECGenParameterSpec(curveName)) }
-                    .genKeyPair(), initialPublicKey)
-        }
-
-        actual fun calculateShareKey(
-            privateKey: ECDHPrivateKey,
-            publicKey: ECDHPublicKey
-        ): ByteArray {
-            val instance = KeyAgreement.getInstance("ECDH", "BC")
-            instance.init(privateKey)
-            instance.doPhase(publicKey, true)
-            return instance.generateSecret().copyOf(16).md5()
-        }
-
-        actual fun verifyPublicKey(version: Int, publicKey: String, publicKeySign: String): Boolean {
-            val arrayForVerify = "305$version$publicKey".toByteArray()
-            val signInstance = Signature.getInstance("SHA256WithRSA")
-            signInstance.initVerify(publicKeyForVerify)
-            signInstance.update(arrayForVerify)
-            return signInstance.verify(publicKeySign.decodeBase64())
-        }
-
-        actual fun constructPublicKey(key: ByteArray): ECDHPublicKey {
-            return ecKf.generatePublic(X509EncodedKeySpec(key))
-        }
-    }
-
-    actual fun calculateShareKeyByPeerPublicKey(peerPublicKey: ECDHPublicKey): ByteArray {
-        return calculateShareKey(keyPair.privateKey, peerPublicKey)
-    }
-
-    actual override fun toString(): String {
-        return "ECDH(keyPair=$keyPair)"
-    }
-}

+ 28 - 0
mirai-core/src/androidMain/kotlin/utils/crypto/EcdhAndroid.kt

@@ -0,0 +1,28 @@
+/*
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.utils.crypto
+
+import java.security.Security
+
+internal actual fun Ecdh.Companion.create(): Ecdh<*, *> =
+    if (kotlin.runCatching {
+            // When running tests on JVM desktop, `ClassNotFoundException` will be got
+            android.os.Build.VERSION.SDK_INT >= 23
+        }.getOrDefault(false)) {
+        // For newer Android, BC is deprecated, but AndroidKeyStore (default) handles ECDH well
+        // Do not specify a provider as Google recommends
+        JceEcdh()
+    } else {
+        // For older Android, AndroidKeyStore (default) is buggy and cannot handle EC key generation without tricks
+        // See https://developer.android.com/training/articles/keystore#SupportedKeyPairGenerators for details
+
+        // Let's use BC instead, BC is bundled into older Android
+        JceEcdhWithProvider(Security.getProvider("BC"))
+    }

+ 1 - 1
mirai-core/src/androidTest/kotlin/test/initPlatform.android.kt

@@ -35,4 +35,4 @@ internal actual class PlatformInitializationTest : AbstractTest() {
         @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
         assertIs<net.mamoe.mirai.internal.utils.StdoutLogger>(MiraiLogger.Factory.create(this::class, "1"))
     }
-}
+}

+ 4 - 5
mirai-core/src/commonMain/kotlin/network/components/AccountSecretsManager.kt

@@ -19,9 +19,8 @@ import net.mamoe.mirai.internal.network.component.ComponentKey
 import net.mamoe.mirai.internal.network.getRandomByteArray
 import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.get_mpasswd
 import net.mamoe.mirai.internal.utils.accountSecretsFile
-import net.mamoe.mirai.internal.utils.crypto.ECDHInitialPublicKey
+import net.mamoe.mirai.internal.utils.crypto.QQEcdhInitialPublicKey
 import net.mamoe.mirai.internal.utils.crypto.TEA
-import net.mamoe.mirai.internal.utils.crypto.defaultInitialPublicKey
 import net.mamoe.mirai.internal.utils.io.ProtoBuf
 import net.mamoe.mirai.internal.utils.io.serialization.loadAs
 import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
@@ -73,7 +72,7 @@ internal interface AccountSecrets {
 
     var tgtgtKey: ByteArray
     val randomKey: ByteArray
-    var ecdhInitialPublicKey: ECDHInitialPublicKey
+    var ecdhInitialPublicKey: QQEcdhInitialPublicKey
 }
 
 
@@ -87,7 +86,7 @@ internal data class AccountSecretsImpl(
     override var ksid: ByteArray,
     override var tgtgtKey: ByteArray,
     override val randomKey: ByteArray,
-    override var ecdhInitialPublicKey: ECDHInitialPublicKey,
+    override var ecdhInitialPublicKey: QQEcdhInitialPublicKey,
 ) : AccountSecrets, ProtoBuf {
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
@@ -141,7 +140,7 @@ internal fun AccountSecretsImpl(
         ksid = EMPTY_BYTE_ARRAY,
         tgtgtKey = (account.passwordMd5 + ByteArray(4) + account.id.toInt().toByteArray()).md5(),
         randomKey = getRandomByteArray(16),
-        ecdhInitialPublicKey = defaultInitialPublicKey
+        ecdhInitialPublicKey = QQEcdhInitialPublicKey.default
     )
 }
 

+ 19 - 28
mirai-core/src/commonMain/kotlin/network/components/EcdhInitialPublicKeyUpdater.kt

@@ -17,25 +17,24 @@ import kotlinx.serialization.Serializable
 import kotlinx.serialization.json.Json
 import net.mamoe.mirai.internal.QQAndroidBot
 import net.mamoe.mirai.internal.network.component.ComponentKey
-import net.mamoe.mirai.internal.utils.crypto.ECDH
-import net.mamoe.mirai.internal.utils.crypto.ECDHInitialPublicKey
-import net.mamoe.mirai.internal.utils.crypto.ECDHWithPublicKey
-import net.mamoe.mirai.internal.utils.crypto.defaultInitialPublicKey
+import net.mamoe.mirai.internal.utils.crypto.QQEcdh
+import net.mamoe.mirai.internal.utils.crypto.QQEcdhInitialPublicKey
+import net.mamoe.mirai.internal.utils.crypto.verify
 import net.mamoe.mirai.utils.MiraiLogger
 import net.mamoe.mirai.utils.currentTimeSeconds
 import kotlin.time.Duration.Companion.seconds
 
 
 /**
- * Updater for updating [ECDHInitialPublicKey].
+ * Updater for updating [QQEcdhInitialPublicKey].
  */
 internal interface EcdhInitialPublicKeyUpdater {
     /**
-     * Refresh the [ECDHInitialPublicKey]
+     * Refresh the [QQEcdhInitialPublicKey]
      */
-    suspend fun refreshInitialPublicKeyAndApplyECDH()
+    suspend fun refreshInitialPublicKeyAndApplyEcdh()
 
-    fun getECDHWithPublicKey(): ECDHWithPublicKey
+    fun getQQEcdh(): QQEcdh
 
     companion object : ComponentKey<EcdhInitialPublicKeyUpdater>
 }
@@ -67,19 +66,15 @@ internal class EcdhInitialPublicKeyUpdaterImpl(
         val keyVer: Int
     )
 
-    companion object {
-        val json = Json {}
-    }
-
-    var ecdhWithPublicKey: ECDHWithPublicKey? = null
-    override fun getECDHWithPublicKey(): ECDHWithPublicKey {
-        if (ecdhWithPublicKey == null) {
-            error("Calling getECDHWithPublicKey without calling refreshInitialPublicKeyAndApplyECDH")
+    var qqEcdh: QQEcdh? = null
+    override fun getQQEcdh(): QQEcdh {
+        if (qqEcdh == null) {
+            error("Calling getQQEcdh without calling refreshInitialPublicKeyAndApplyEcdh")
         }
-        return ecdhWithPublicKey!!
+        return qqEcdh!!
     }
 
-    override suspend fun refreshInitialPublicKeyAndApplyECDH() {
+    override suspend fun refreshInitialPublicKeyAndApplyEcdh() {
 
         val initialPublicKey = kotlin.runCatching {
             val currentPublicKey = bot.client.ecdhInitialPublicKey
@@ -94,24 +89,20 @@ internal class EcdhInitialPublicKeyUpdaterImpl(
                             .get("https://keyrotate.qq.com/rotate_key?cipher_suite_ver=305&uin=${bot.client.uin}")
                             .bodyAsText()
                     }
-                val resp = json.decodeFromString(ServerRespPOJO.serializer(), respStr)
+                val resp = Json.decodeFromString(ServerRespPOJO.serializer(), respStr)
                 resp.pubKeyMeta.let { meta ->
-                    val isValid = ECDH.verifyPublicKey(
-                        version = meta.keyVer,
-                        publicKey = meta.pubKey,
-                        publicKeySign = meta.pubKeySign
-                    )
-                    check(isValid) { "Ecdh public key which from server is invalid" }
+                    val key = QQEcdhInitialPublicKey(meta.keyVer, meta.pubKey, currentTimeSeconds() + resp.querySpan)
+                    check(key.verify(meta.pubKeySign)) { "Ecdh public key which from server is invalid" }
                     logger.info("Successfully fetched ecdh public key from server.")
-                    ECDHInitialPublicKey(meta.keyVer, meta.pubKey, currentTimeSeconds() + resp.querySpan)
+                    key
                 }
             }
         }.getOrElse {
             logger.error("Failed to fetch ECDH public key from server, using default key instead", it)
-            defaultInitialPublicKey
+            QQEcdhInitialPublicKey.default
         }
         bot.client.ecdhInitialPublicKey = initialPublicKey
-        ecdhWithPublicKey = ECDHWithPublicKey(initialPublicKey)
+        qqEcdh = QQEcdh(initialPublicKey)
     }
 
 

+ 6 - 6
mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt

@@ -17,8 +17,8 @@ import net.mamoe.mirai.internal.network.components.PacketCodec.Companion.PacketL
 import net.mamoe.mirai.internal.network.components.PacketCodecException.Kind.*
 import net.mamoe.mirai.internal.network.handler.selector.NetworkException
 import net.mamoe.mirai.internal.network.protocol.packet.*
+import net.mamoe.mirai.internal.utils.crypto.Ecdh
 import net.mamoe.mirai.internal.utils.crypto.TEA
-import net.mamoe.mirai.internal.utils.crypto.adjustToPublicKey
 import net.mamoe.mirai.utils.*
 
 
@@ -254,20 +254,20 @@ internal class PacketCodecImpl : PacketCodec {
         val encryptionMethod = this.readUShort().toInt()
 
         this.discardExact(1)
-        val ecdhWithPublicKey =
-            (client as QQAndroidClient).bot.components[EcdhInitialPublicKeyUpdater].getECDHWithPublicKey()
+        val qqEcdh =
+            (client as QQAndroidClient).bot.components[EcdhInitialPublicKeyUpdater].getQQEcdh()
         return when (encryptionMethod) {
             4 -> {
                 val size = (this.remaining - 1).toInt()
                 val data =
                     TEA.decrypt(
                         this.readBytes(),
-                        ecdhWithPublicKey.keyPair.maskedShareKey,
+                        qqEcdh.initialQQShareKey,
                         length = size
                     )
 
                 val peerShareKey =
-                    ecdhWithPublicKey.calculateShareKeyByPeerPublicKey(readUShortLVByteArray().adjustToPublicKey())
+                    qqEcdh.calculateQQShareKey(Ecdh.Instance.importPublicKey(readUShortLVByteArray()))
                 TEA.decrypt(data, peerShareKey)
             }
             3 -> {
@@ -285,7 +285,7 @@ internal class PacketCodecImpl : PacketCodec {
                     val byteArrayBuffer = this.readBytes(size)
 
                     runCatching {
-                        TEA.decrypt(byteArrayBuffer, ecdhWithPublicKey.keyPair.maskedShareKey, length = size)
+                        TEA.decrypt(byteArrayBuffer, qqEcdh.initialQQShareKey, length = size)
                     }.getOrElse {
                         TEA.decrypt(byteArrayBuffer, client.randomKey, length = size)
                     }

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

@@ -135,7 +135,7 @@ internal class SsoProcessorImpl(
         components[BdhSessionSyncer].loadServerListFromCache()
         try {
             if (client.wLoginSigInfoInitialized) {
-                ssoContext.bot.components[EcdhInitialPublicKeyUpdater].refreshInitialPublicKeyAndApplyECDH()
+                ssoContext.bot.components[EcdhInitialPublicKeyUpdater].refreshInitialPublicKeyAndApplyEcdh()
                 kotlin.runCatching {
                     FastLoginImpl(handler).doLogin()
                 }.onFailure { e ->
@@ -144,7 +144,7 @@ internal class SsoProcessorImpl(
                 }
             } else {
                 client = createClient(ssoContext.bot)
-                ssoContext.bot.components[EcdhInitialPublicKeyUpdater].refreshInitialPublicKeyAndApplyECDH()
+                ssoContext.bot.components[EcdhInitialPublicKeyUpdater].refreshInitialPublicKeyAndApplyEcdh()
                 SlowLoginImpl(handler).doLogin()
             }
         } catch (e: Exception) {

+ 14 - 21
mirai-core/src/commonMain/kotlin/network/protocol/packet/EncryptMethod.kt

@@ -11,8 +11,7 @@ package net.mamoe.mirai.internal.network.protocol.packet
 
 import io.ktor.utils.io.core.*
 import net.mamoe.mirai.internal.network.QQAndroidClient
-import net.mamoe.mirai.internal.utils.crypto.ECDHKeyPair
-import net.mamoe.mirai.internal.utils.crypto.ECDHWithPublicKey
+import net.mamoe.mirai.internal.utils.crypto.QQEcdh
 import net.mamoe.mirai.internal.utils.io.encryptAndWrite
 import net.mamoe.mirai.internal.utils.io.writeShortLVByteArray
 
@@ -62,26 +61,26 @@ internal class EncryptMethodSessionKeyLoginState3(override val sessionKey: ByteA
     override val currentLoginState: Int get() = 3
 }
 
-internal class EncryptMethodECDH135(override val ecdh: ECDHWithPublicKey) :
-    EncryptMethodECDH {
+internal class EncryptMethodEcdh135(override val ecdh: QQEcdh) :
+    EncryptMethodEcdh {
     override val id: Int get() = 135
 }
 
-internal class EncryptMethodECDH7(override val ecdh: ECDHWithPublicKey) :
-    EncryptMethodECDH {
+internal class EncryptMethodEcdh7(override val ecdh: QQEcdh) :
+    EncryptMethodEcdh {
     override val id: Int get() = 7 // 135
 }
 
-internal interface EncryptMethodECDH : EncryptMethod {
+internal interface EncryptMethodEcdh : EncryptMethod {
     companion object {
-        operator fun invoke(ecdh: ECDHWithPublicKey): EncryptMethodECDH {
-            return if (ecdh.keyPair === ECDHKeyPair.DefaultStub) {
-                EncryptMethodECDH135(ecdh)
-            } else EncryptMethodECDH7(ecdh)
+        operator fun invoke(ecdh: QQEcdh): EncryptMethodEcdh {
+            return if (ecdh.fallbackMode) {
+                EncryptMethodEcdh135(ecdh)
+            } else EncryptMethodEcdh7(ecdh)
         }
     }
 
-    val ecdh: ECDHWithPublicKey
+    val ecdh: QQEcdh
 
     override fun makeBody(client: QQAndroidClient, body: BytePacketBuilder.() -> Unit): ByteReadPacket = buildPacket {
         /* //new curve p-256
@@ -97,14 +96,8 @@ internal interface EncryptMethodECDH : EncryptMethod {
         writeFully(client.randomKey)
         writeShort(0x0131)
         writeShort(ecdh.version.toShort())// public key version
-        if (ecdh.keyPair === ECDHKeyPair.DefaultStub) {
-            writeShortLVByteArray(ECDHKeyPair.DefaultStub.defaultPublicKey)
-            encryptAndWrite(ECDHKeyPair.DefaultStub.defaultShareKey, body)
-        } else {
-            // for p-256, drop(26). // but not really sure.
-            writeShortLVByteArray(ecdh.keyPair.maskedPublicKey)
-
-            encryptAndWrite(ecdh.keyPair.maskedShareKey, body)
-        }
+        // for p-256, drop(26). // but not really sure.
+        writeShortLVByteArray(ecdh.publicKey)
+        encryptAndWrite(ecdh.initialQQShareKey, body)
     }
 }

+ 1 - 1
mirai-core/src/commonMain/kotlin/network/protocol/packet/OutgoingPacket.kt

@@ -274,7 +274,7 @@ internal inline fun BytePacketBuilder.writeSsoPacket(
 
 internal fun BytePacketBuilder.writeOicqRequestPacket(
     client: QQAndroidClient,
-    encryptMethod: EncryptMethod = EncryptMethodECDH(client.bot.components[EcdhInitialPublicKeyUpdater].getECDHWithPublicKey()),
+    encryptMethod: EncryptMethod = EncryptMethodEcdh(client.bot.components[EcdhInitialPublicKeyUpdater].getQQEcdh()),
     commandId: Int,
     bodyBlock: BytePacketBuilder.() -> Unit
 ) {

+ 0 - 128
mirai-core/src/commonMain/kotlin/utils/crypto/ECDH.kt

@@ -1,128 +0,0 @@
-/*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/dev/LICENSE
- */
-
-package net.mamoe.mirai.internal.utils.crypto
-
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.Transient
-import net.mamoe.mirai.utils.hexToBytes
-
-internal expect interface ECDHPrivateKey
-
-internal expect interface ECDHPublicKey
-
-internal expect class ECDHKeyPairImpl : ECDHKeyPair
-
-internal interface ECDHKeyPair {
-    val privateKey: ECDHPrivateKey
-    val publicKey: ECDHPublicKey
-
-    /**
-     * 私匙和动态公匙([ECDHInitialPublicKey]) 计算得到的 shareKey
-     */
-    val maskedShareKey: ByteArray
-
-    /**
-     * 私匙和动态公匙([ECDHInitialPublicKey]) 计算得到的 publicKey
-     */
-    val maskedPublicKey: ByteArray
-
-    object DefaultStub : ECDHKeyPair {
-        val defaultPublicKey =
-            "04edb8906046f5bfbe9abbc5a88b37d70a6006bfbabc1f0cd49dfb33505e63efc5d78ee4e0a4595033b93d02096dcd3190279211f7b4f6785079e19004aa0e03bc".hexToBytes()
-        val defaultShareKey = "c129edba736f4909ecc4ab8e010f46a3".hexToBytes()
-
-        override val privateKey: Nothing get() = error("stub!")
-        override val publicKey: Nothing get() = error("stub!")
-        override val maskedShareKey: ByteArray get() = defaultShareKey
-        override val maskedPublicKey: ByteArray
-            get() = defaultPublicKey
-    }
-}
-
-internal expect class ECDH(keyPair: ECDHKeyPair) {
-    val keyPair: ECDHKeyPair
-
-    /**
-     * 由 [keyPair] 的私匙和 [peerPublicKey] 计算 shareKey
-     */
-    fun calculateShareKeyByPeerPublicKey(peerPublicKey: ECDHPublicKey): ByteArray
-
-    companion object {
-        val isECDHAvailable: Boolean
-
-
-        /**
-         * This API is platform dependent.
-         * On JVM you need to add `signHead`,
-         * but on Native you need to provide a key with initial byte value 0x04 and of 65 bytes' length.
-         */
-        fun constructPublicKey(key: ByteArray): ECDHPublicKey
-
-        /**
-         * 由完整的 rsaKey 校验 publicKey
-         */
-        fun verifyPublicKey(version: Int, publicKey: String, publicKeySign: String): Boolean
-
-        /**
-         * 生成随机密匙对
-         */
-        fun generateKeyPair(initialPublicKey: ECDHPublicKey = defaultInitialPublicKey.key): ECDHKeyPair
-
-        /**
-         * 由一对密匙计算服务器需要的 shareKey
-         */
-        fun calculateShareKey(privateKey: ECDHPrivateKey, publicKey: ECDHPublicKey): ByteArray
-    }
-
-    override fun toString(): String
-}
-
-@Suppress("FunctionName")
-internal fun ecdhWithPublicKey(initialPublicKey: ECDHInitialPublicKey = defaultInitialPublicKey): ECDHWithPublicKey =
-    ECDHWithPublicKey(initialPublicKey)
-
-internal data class ECDHWithPublicKey(private val initialPublicKey: ECDHInitialPublicKey = defaultInitialPublicKey) {
-    private val ecdhInstance: ECDH = ECDH(ECDH.generateKeyPair(initialPublicKey.key))
-    val version: Int = initialPublicKey.version
-    val keyPair: ECDHKeyPair = ecdhInstance.keyPair
-
-    /**
-     * 由 [keyPair] 的私匙和 [peerPublicKey] 计算 shareKey
-     */
-    fun calculateShareKeyByPeerPublicKey(peerPublicKey: ECDHPublicKey): ByteArray =
-        ecdhInstance.calculateShareKeyByPeerPublicKey(peerPublicKey)
-
-}
-// gen by p-256
-//3059301306072A8648CE3D020106082A8648CE3D03010703420004FA540CB3F755D0A6572338777A4D0BEAFA86664D53040B27331CBF1B7F3C226CE8A1C05EFA9028F85510B103D8175172895C9F9FE4C80A47894BCA2BE569BFCB
-//3059301306072A8648CE3D020106082A8648CE3D03010703420004949D41D7C14B92F0CB94B6232FB87BA51B0D5AB661FBAF95599A97472FFC4F50BC8CEC5865E79DB3782459A6E9A2298954CD198A25274CEEA8F925342D763D62
-
-/*
-// p-256
-    get() = ECDH.constructPublicKey(
-        ("3059301306072A8648CE3D020106082A8648CE3D03010703420004" +
-                "EBCA94D733E399B2DB96EACDD3F69A8BB0F74224E2B44E3357812211D2E62EFB" +
-                "C91BB553098E25E33A799ADC7F76FEB208DA7C6522CDB0719A305180CC54A82E"
-                ).chunkedHexToBytes()
-    )
-* */
-
-@Serializable
-internal data class ECDHInitialPublicKey(val version: Int = 1, val keyStr: String, val expireTime: Long = 0) {
-    @Transient
-    internal val key: ECDHPublicKey = keyStr.hexToBytes().adjustToPublicKey()
-}
-
-internal val defaultInitialPublicKey: ECDHInitialPublicKey by lazy { ECDHInitialPublicKey(keyStr = "04EBCA94D733E399B2DB96EACDD3F69A8BB0F74224E2B44E3357812211D2E62EFBC91BB553098E25E33A799ADC7F76FEB208DA7C6522CDB0719A305180CC54A82E") }
-
-
-internal expect fun ByteArray.adjustToPublicKey(): ECDHPublicKey
-
-internal val ECDH.Companion.curveName get() = "prime256v1" // p-256

+ 44 - 0
mirai-core/src/commonMain/kotlin/utils/crypto/Ecdh.kt

@@ -0,0 +1,44 @@
+/*
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.utils.crypto
+
+import kotlin.jvm.JvmStatic
+
+internal data class EcdhKeyPair<TPublicKey, TPrivate>(val public: TPublicKey, val private: TPrivate)
+
+internal interface Ecdh<TPublicKey, TPrivate> {
+    fun generateKeyPair(): EcdhKeyPair<TPublicKey, TPrivate>
+    fun calculateShareKey(peerKey: TPublicKey, privateKey: TPrivate): ByteArray
+
+    /**
+     * @param encoded The encoding should conform with
+     * Sec. 2.3.3 of the SECG SEC 1 ("Elliptic Curve Cryptography") standard,
+     * with compression is off.
+     * @see <a href="https://www.secg.org/sec1-v2.pdf">SECG SEC 1: Elliptic Curve Cryptography</a>
+     */
+    fun importPublicKey(encoded: ByteArray): TPublicKey
+
+    /**
+     * @return The encoding conforms with
+     * Sec. 2.3.3 of the SECG SEC 1 ("Elliptic Curve Cryptography") standard,
+     * with compression is off.
+     * @see <a href="https://www.secg.org/sec1-v2.pdf">SECG SEC 1: Elliptic Curve Cryptography</a>
+     */
+    fun exportPublicKey(key: TPublicKey): ByteArray
+
+    companion object {
+        @JvmStatic
+        val Instance by lazy {
+            @Suppress("UNCHECKED_CAST")
+            Ecdh.create() as Ecdh<Any, Any>
+        }
+    }
+}
+internal expect fun Ecdh.Companion.create() : Ecdh<*, *>

+ 60 - 0
mirai-core/src/commonMain/kotlin/utils/crypto/QQEcdh.kt

@@ -0,0 +1,60 @@
+/*
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+package net.mamoe.mirai.internal.utils.crypto
+
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+import net.mamoe.mirai.utils.cast
+import net.mamoe.mirai.utils.hexToBytes
+import net.mamoe.mirai.utils.md5
+
+private val defaultPublicKey =
+    "04edb8906046f5bfbe9abbc5a88b37d70a6006bfbabc1f0cd49dfb33505e63efc5d78ee4e0a4595033b93d02096dcd3190279211f7b4f6785079e19004aa0e03bc".hexToBytes()
+private val defaultQQShareKey = "c129edba736f4909ecc4ab8e010f46a3".hexToBytes()
+
+@Serializable
+internal data class QQEcdhInitialPublicKey(val version: Int = 1, val keyStr: String, val expireTime: Long = 0) {
+    @Transient
+    internal val key = Ecdh.Instance.importPublicKey(keyStr.hexToBytes())
+    companion object {
+        internal val default: QQEcdhInitialPublicKey by lazy {
+            QQEcdhInitialPublicKey(keyStr = "04EBCA94D733E399B2DB96EACDD3F69A8BB0F74224E2B44E3357812211D2E62EFBC91BB553098E25E33A799ADC7F76FEB208DA7C6522CDB0719A305180CC54A82E")
+        }
+    }
+}
+
+internal expect fun QQEcdhInitialPublicKey.verify(sign: String): Boolean
+
+internal data class QQEcdh(private val initialPublicKey: QQEcdhInitialPublicKey = QQEcdhInitialPublicKey.default) {
+    val version: Int = initialPublicKey.version
+    private val keyPair = try {
+        Ecdh.Instance.generateKeyPair()
+    } catch (e:Throwable){
+        null
+    }
+    val publicKey: ByteArray = keyPair?.let {
+        Ecdh.Instance.exportPublicKey(it.public)
+    } ?: defaultPublicKey
+
+    val initialQQShareKey: ByteArray = keyPair?.let {
+        Ecdh.Instance.calculateShareKey(initialPublicKey.key, it.private).copyOf(16).md5()
+    } ?: defaultQQShareKey
+
+    val fallbackMode : Boolean = keyPair == null
+
+    /**
+     * 由 [keyPair] 的私匙和 [peerKey] 计算 shareKey
+     */
+    fun calculateQQShareKey(peerKey: Any): ByteArray {
+        check (keyPair != null) {
+            "cannot calculate QQShareKey in fallback mode"
+        }
+        return Ecdh.Instance.calculateShareKey(peerKey.cast(), keyPair.private).copyOf(16).md5()
+    }
+}

+ 13 - 11
mirai-core/src/commonTest/kotlin/utils/crypto/ECDHTest.kt → mirai-core/src/commonTest/kotlin/utils/crypto/EcdhTest.kt

@@ -15,29 +15,31 @@ import kotlin.test.Test
 import kotlin.test.assertContentEquals
 import kotlin.test.assertEquals
 
-internal class ECDHTest : AbstractTest() {
+internal class EcdhTest : AbstractTest() {
 
     @Test
     fun `can generate key pair`() {
-        val alice = ECDH.generateKeyPair()
-        val bob = ECDH.generateKeyPair()
+        val alice = Ecdh.Instance.generateKeyPair()
+        val bob = Ecdh.Instance.generateKeyPair()
 
-        val aliceSecret = ECDH.calculateShareKey(alice.privateKey, bob.publicKey)
-        val bobSecret = ECDH.calculateShareKey(bob.privateKey, alice.publicKey)
+        val aliceSecret = Ecdh.Instance.calculateShareKey(bob.public, alice.private)
+        val bobSecret = Ecdh.Instance.calculateShareKey(alice.public, bob.private)
 
         println(aliceSecret.toUHexString())
         assertContentEquals(aliceSecret, bobSecret)
     }
 
     @Test
-    fun `can get masked keys`() {
-        val alice = ECDH.generateKeyPair()
+    fun `can export and import public keys`() {
+        val alice = Ecdh.Instance.generateKeyPair()
 
         println(alice)
-        val maskedPublicKey = alice.maskedPublicKey
-        println(maskedPublicKey.toUHexString())
-        assertEquals(0x04, maskedPublicKey.first())
-        println(alice.maskedShareKey.toUHexString())
+        val publicKey = Ecdh.Instance.exportPublicKey(alice.public)
+        println(publicKey.toUHexString())
+        assertEquals(0x04, publicKey.first())
+
+        val importedAlicePubKey = Ecdh.Instance.importPublicKey(publicKey)
+        assertEquals(alice.public, importedAlicePubKey)
     }
 
     /*

+ 0 - 49
mirai-core/src/jvmBaseMain/kotlin/utils/crypto/ECDH.kt

@@ -1,49 +0,0 @@
-/*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/dev/LICENSE
- */
-
-@file:JvmName("ECDHKt_jvmBase")
-
-package net.mamoe.mirai.internal.utils.crypto
-
-import net.mamoe.mirai.utils.decodeBase64
-import net.mamoe.mirai.utils.hexToBytes
-import java.security.KeyFactory
-import java.security.KeyPair
-import java.security.PrivateKey
-import java.security.PublicKey
-import java.security.spec.X509EncodedKeySpec
-
-
-
-@Suppress("ACTUAL_WITHOUT_EXPECT")
-internal actual typealias ECDHPrivateKey = PrivateKey
-@Suppress("ACTUAL_WITHOUT_EXPECT")
-internal actual typealias ECDHPublicKey = PublicKey
-
-internal actual class ECDHKeyPairImpl(
-    private val delegate: KeyPair,
-    initialPublicKey: ECDHPublicKey = defaultInitialPublicKey.key
-) : ECDHKeyPair {
-    override val privateKey: ECDHPrivateKey get() = delegate.private
-    override val publicKey: ECDHPublicKey get() = delegate.public
-    override val maskedPublicKey: ByteArray by lazy { publicKey.encoded.copyOfRange(26, 91) }
-    override val maskedShareKey: ByteArray by lazy { ECDH.calculateShareKey(privateKey, initialPublicKey) }
-}
-
-
-internal val publicKeyForVerify: ECDHPublicKey by lazy {
-    KeyFactory.getInstance("RSA")
-        .generatePublic(X509EncodedKeySpec("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuJTW4abQJXeVdAODw1CamZH4QJZChyT08ribet1Gp0wpSabIgyKFZAOxeArcCbknKyBrRY3FFI9HgY1AyItH8DOUe6ajDEb6c+vrgjgeCiOiCVyum4lI5Fmp38iHKH14xap6xGaXcBccdOZNzGT82sPDM2Oc6QYSZpfs8EO7TYT7KSB2gaHz99RQ4A/Lel1Vw0krk+DescN6TgRCaXjSGn268jD7lOO23x5JS1mavsUJtOZpXkK9GqCGSTCTbCwZhI33CpwdQ2EHLhiP5RaXZCio6lksu+d8sKTWU1eEiEb3cQ7nuZXLYH7leeYFoPtbFV4RicIWp0/YG+RP7rLPCwIDAQAB".decodeBase64()))
-}
-
-private val signHead = "3059301306072a8648ce3d020106082a8648ce3d030107034200".hexToBytes()
-
-internal actual fun ByteArray.adjustToPublicKey(): ECDHPublicKey {
-    return ECDH.constructPublicKey(signHead + this)
-}

+ 91 - 0
mirai-core/src/jvmBaseMain/kotlin/utils/crypto/JceEcdh.kt

@@ -0,0 +1,91 @@
+/*
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.utils.crypto
+
+import java.math.BigInteger
+import java.security.AlgorithmParameters
+import java.security.KeyFactory
+import java.security.KeyPairGenerator
+import java.security.interfaces.ECPrivateKey
+import java.security.interfaces.ECPublicKey
+import java.security.spec.ECGenParameterSpec
+import java.security.spec.ECParameterSpec
+import java.security.spec.ECPoint
+import java.security.spec.ECPublicKeySpec
+import javax.crypto.KeyAgreement
+
+internal open class JceEcdh : Ecdh<ECPublicKey, ECPrivateKey> {
+    protected open fun newECKeyPairGenerator() = KeyPairGenerator.getInstance("EC")
+    protected open fun newECKeyFactory() = KeyFactory.getInstance("EC")
+    protected open fun newECAlgorithmParameters() = AlgorithmParameters.getInstance("EC")
+    protected open fun newECDHKeyAgreement() = KeyAgreement.getInstance("ECDH")
+
+    override fun generateKeyPair(): EcdhKeyPair<ECPublicKey, ECPrivateKey> {
+        return newECKeyPairGenerator()
+            .apply {
+                // AKA. prime256v1
+                // But `secp256r1` is more common
+                initialize(ECGenParameterSpec("secp256r1"))
+            }
+            .genKeyPair()
+            .let {
+                EcdhKeyPair(it.public as ECPublicKey, it.private as ECPrivateKey)
+            }
+    }
+
+    override fun calculateShareKey(peerKey: ECPublicKey, privateKey: ECPrivateKey): ByteArray {
+        return newECDHKeyAgreement().apply {
+            init(privateKey)
+            doPhase(peerKey, true)
+        }.generateSecret()
+    }
+
+    override fun importPublicKey(encoded: ByteArray): ECPublicKey {
+        val params: ECParameterSpec = newECAlgorithmParameters().apply {
+            init(ECGenParameterSpec("secp256r1"))
+        }.getParameterSpec(ECParameterSpec::class.java)
+
+        require(encoded[0] == 0x04.toByte()) { "Only uncompressed format is supported" }
+        val fieldSize = params.curve.field.fieldSize
+        val elementSize = (fieldSize + 7) / 8
+        val affineXBytes = ByteArray(elementSize)
+        val affineYBytes = ByteArray(elementSize)
+        System.arraycopy(encoded, 1, affineXBytes, 0, elementSize)
+        System.arraycopy(encoded, elementSize + 1, affineYBytes, 0, elementSize)
+        val point = ECPoint(BigInteger(1, affineXBytes), BigInteger(1, affineYBytes))
+
+        val keySpec = ECPublicKeySpec(point, params)
+        return newECKeyFactory().generatePublic(keySpec) as ECPublicKey
+    }
+
+    override fun exportPublicKey(key: ECPublicKey): ByteArray {
+        val point = key.w
+        val fieldSize = key.params.curve.field.fieldSize
+        val elementSize = (fieldSize + 7) / 8
+        val x = point.affineX.toByteArray()
+        val y = point.affineY.toByteArray()
+        val startOfX = countLeadingZeros(x)
+        val startOfY = countLeadingZeros(y)
+        val encoded = ByteArray(elementSize * 2 + 1)
+        encoded[0] = 0x04 // uncompressed
+        System.arraycopy(x, startOfX, encoded, elementSize - x.size + startOfX + 1, x.size - startOfX)
+        System.arraycopy(y, startOfY, encoded, encoded.size - y.size + startOfY, y.size - startOfY)
+        return encoded
+    }
+
+    private fun countLeadingZeros(bytes: ByteArray): Int {
+        for (i in bytes.indices) {
+            if (bytes[i] != 0.toByte()) {
+                return i
+            }
+        }
+        return bytes.size
+    }
+}

+ 23 - 0
mirai-core/src/jvmBaseMain/kotlin/utils/crypto/JceEcdhWithProvider.kt

@@ -0,0 +1,23 @@
+/*
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.utils.crypto
+
+import java.security.AlgorithmParameters
+import java.security.KeyFactory
+import java.security.KeyPairGenerator
+import java.security.Provider
+import javax.crypto.KeyAgreement
+
+internal class JceEcdhWithProvider(val provider: Provider): JceEcdh() {
+    override fun newECKeyPairGenerator() = KeyPairGenerator.getInstance("EC", provider)
+    override fun newECKeyFactory() = KeyFactory.getInstance("EC", provider)
+    override fun newECAlgorithmParameters() = AlgorithmParameters.getInstance("EC", provider)
+    override fun newECDHKeyAgreement() = KeyAgreement.getInstance("ECDH", provider)
+}

+ 28 - 0
mirai-core/src/jvmBaseMain/kotlin/utils/crypto/QQEcdhJvm.kt

@@ -0,0 +1,28 @@
+/*
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+package net.mamoe.mirai.internal.utils.crypto
+
+import net.mamoe.mirai.utils.decodeBase64
+import java.security.KeyFactory
+import java.security.Signature
+import java.security.spec.X509EncodedKeySpec
+
+internal val publicKeyForVerify by lazy {
+    KeyFactory.getInstance("RSA")
+        .generatePublic(X509EncodedKeySpec("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuJTW4abQJXeVdAODw1CamZH4QJZChyT08ribet1Gp0wpSabIgyKFZAOxeArcCbknKyBrRY3FFI9HgY1AyItH8DOUe6ajDEb6c+vrgjgeCiOiCVyum4lI5Fmp38iHKH14xap6xGaXcBccdOZNzGT82sPDM2Oc6QYSZpfs8EO7TYT7KSB2gaHz99RQ4A/Lel1Vw0krk+DescN6TgRCaXjSGn268jD7lOO23x5JS1mavsUJtOZpXkK9GqCGSTCTbCwZhI33CpwdQ2EHLhiP5RaXZCio6lksu+d8sKTWU1eEiEb3cQ7nuZXLYH7leeYFoPtbFV4RicIWp0/YG+RP7rLPCwIDAQAB".decodeBase64()))
+}
+
+internal actual fun QQEcdhInitialPublicKey.verify(sign: String): Boolean {
+    val arrayForVerify = "305$version$keyStr".toByteArray()
+    val signInstance = Signature.getInstance("SHA256WithRSA").apply {
+        initVerify(publicKeyForVerify)
+        update(arrayForVerify)
+    }
+    return signInstance.verify(sign.decodeBase64())
+}

+ 0 - 93
mirai-core/src/jvmMain/kotlin/utils/crypto/ECDHJvmDesktop.kt

@@ -1,93 +0,0 @@
-/*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * 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.md5
-import org.bouncycastle.jce.provider.BouncyCastleProvider
-import java.security.KeyFactory
-import java.security.KeyPairGenerator
-import java.security.Security
-import java.security.Signature
-import java.security.spec.ECGenParameterSpec
-import java.security.spec.X509EncodedKeySpec
-import javax.crypto.KeyAgreement
-
-internal actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
-    actual companion object {
-
-        actual val isECDHAvailable: Boolean
-
-        init {
-            isECDHAvailable = kotlin.runCatching {
-                fun testECDH() {
-                    ECDHKeyPairImpl(
-                        KeyPairGenerator.getInstance("ECDH")
-                            .also { it.initialize(ECGenParameterSpec(curveName)) }
-                            .genKeyPair()).let {
-                        calculateShareKey(it.privateKey, it.publicKey)
-                    }
-                }
-
-                if (kotlin.runCatching { testECDH() }.isSuccess) {
-                    return@runCatching
-                }
-
-                if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) != null) {
-                    Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
-                }
-                Security.addProvider(BouncyCastleProvider())
-                testECDH()
-            }.onFailure {
-                it.printStackTrace()
-            }.isSuccess
-        }
-
-        actual fun generateKeyPair(initialPublicKey: ECDHPublicKey): ECDHKeyPair {
-            if (!isECDHAvailable) {
-                return ECDHKeyPair.DefaultStub
-            }
-            return ECDHKeyPairImpl(
-                KeyPairGenerator.getInstance("ECDH")
-                    .also { it.initialize(ECGenParameterSpec(curveName)) }
-                    .genKeyPair(), initialPublicKey)
-        }
-
-        actual fun verifyPublicKey(version: Int, publicKey: String, publicKeySign: String): Boolean {
-            val arrayForVerify = "305$version$publicKey".toByteArray()
-            val signInstance = Signature.getInstance("SHA256WithRSA")
-            signInstance.initVerify(publicKeyForVerify)
-            signInstance.update(arrayForVerify)
-            return signInstance.verify(publicKeySign.decodeBase64())
-        }
-
-        actual fun calculateShareKey(
-            privateKey: ECDHPrivateKey,
-            publicKey: ECDHPublicKey,
-        ): ByteArray {
-            val instance = KeyAgreement.getInstance("ECDH", "BC")
-            instance.init(privateKey)
-            instance.doPhase(publicKey, true)
-            return instance.generateSecret().copyOf(16).md5()
-        }
-
-        actual fun constructPublicKey(key: ByteArray): ECDHPublicKey {
-            return KeyFactory.getInstance("EC", "BC").generatePublic(X509EncodedKeySpec(key))
-        }
-    }
-
-    actual fun calculateShareKeyByPeerPublicKey(peerPublicKey: ECDHPublicKey): ByteArray {
-        return calculateShareKey(keyPair.privateKey, peerPublicKey)
-    }
-
-    actual override fun toString(): String {
-        return "ECDH(keyPair=$keyPair)"
-    }
-}

+ 27 - 0
mirai-core/src/jvmMain/kotlin/utils/crypto/EcdhJvmDesktop.kt

@@ -0,0 +1,27 @@
+/*
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.utils.crypto
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+
+internal actual fun Ecdh.Companion.create(): Ecdh<*, *> =
+    kotlin.runCatching {
+        // try platform default EC/ECDH implementations first, which may have better performance
+        // note that they may not work properly but being created successfully
+        JceEcdh().apply {
+            val keyPair = generateKeyPair()
+            calculateShareKey(keyPair.public, keyPair.private)
+            val encoded = exportPublicKey(keyPair.public)
+            importPublicKey(encoded)
+        }
+    }.getOrElse {
+        // fallback to BouncyCastle
+        JceEcdhWithProvider(BouncyCastleProvider())
+    }

+ 0 - 233
mirai-core/src/nativeMain/kotlin/utils/crypto/ECDH.kt

@@ -1,233 +0,0 @@
-/*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/dev/LICENSE
- */
-
-package net.mamoe.mirai.internal.utils.crypto
-
-import kotlinx.cinterop.*
-import net.mamoe.mirai.utils.hexToBytes
-import net.mamoe.mirai.utils.md5
-import net.mamoe.mirai.utils.toUHexString
-import openssl.*
-import platform.posix.errno
-
-private const val curveId = NID_X9_62_prime256v1
-
-// shared, not freed!
-private val group by lazy { EC_GROUP_new_by_curve_name(curveId) ?: error("Failed to get EC_GROUP") }
-
-private val convForm by lazy { EC_GROUP_get_point_conversion_form(group) }
-
-// shared, not freed!
-private val bnCtx by lazy { BN_CTX_new() }
-
-// ====ATTENTION====
-// Do not use [platform.posix.free] easily
-// For anything allocated by OpenSSL, <type>_free or CRYPTO_free
-// (the underlying of OPENSSL_free macro) should be called.
-// It's more than dangerous to assume OpenSSL uses the same memory manager as general posix functions,
-// easily causing memory leaking (usually on *nix) or crash (usually on Windows)
-
-internal actual interface ECDHPublicKey : OpenSSLKey {
-    val encoded: ByteArray
-
-    /**
-     * @return It is the caller's responsibility to free this memory with a subsequent call to [EC_POINT_free]
-     */
-    fun toPoint(): CPointer<EC_POINT>
-}
-
-internal actual interface ECDHPrivateKey : OpenSSLKey {
-    /**
-     * @return It is the caller's responsibility to free this memory with a subsequent call to [BN_free]
-     */
-    fun toBignum(): CPointer<BIGNUM>
-}
-
-internal class OpenSslPrivateKey(
-    override val hex: String, // use Kotlin's memory
-) : ECDHPrivateKey {
-
-    override fun toBignum(): CPointer<BIGNUM> {
-        val bn = BN_new() ?: error("Failed BN_new")
-        val values = cValuesOf(bn)
-        BN_hex2bn(values, hex).let { r ->
-            if (r <= 0) error("Failed BN_hex2bn: $r")
-        }
-        return bn
-    }
-
-    companion object {
-        fun fromKey(key: CPointer<EC_KEY>): OpenSslPrivateKey {
-            // Note that the private key (bignum) is associated with the key
-            // We can't free it, or it'll crash when EC_KEY_free
-            val bn = EC_KEY_get0_private_key(key) ?: error("Failed EC_KEY_get0_private_key")
-            val ptr = BN_bn2hex(bn) ?: error("Failed EC_POINT_bn2point")
-            val hex = try {
-                ptr.toKString()
-            } finally {
-                CRYPTO_free(ptr, "OpenSslPrivateKey.Companion.fromKey(key: CPointer<EC_KEY>)", -1)
-            }
-            return OpenSslPrivateKey(hex)
-        }
-    }
-}
-
-internal interface OpenSSLKey {
-    val hex: String
-}
-
-internal class OpenSslPublicKey(override val hex: String) : ECDHPublicKey {
-    override val encoded: ByteArray = hex.hexToBytes()
-
-    override fun toPoint(): CPointer<EC_POINT> {
-        val point = EC_POINT_new(group)
-        EC_POINT_hex2point(group, hex, point, bnCtx) ?: error("Failed EC_POINT_hex2point")
-        return point!!
-    }
-
-    companion object {
-        fun fromKey(key: CPointer<EC_KEY>): OpenSslPublicKey =
-            fromPoint(EC_KEY_get0_public_key(key) ?: error("Failed to get private key"))
-
-        fun fromPoint(point: CPointer<EC_POINT>): OpenSslPublicKey {
-            return OpenSslPublicKey(point.toKtHex())
-        }
-    }
-}
-
-internal actual class ECDHKeyPairImpl(
-    override val privateKey: OpenSslPrivateKey,
-    override val publicKey: OpenSslPublicKey,
-    initialPublicKey: ECDHPublicKey
-) : ECDHKeyPair {
-
-    override val maskedPublicKey: ByteArray by lazy { publicKey.encoded }
-    override val maskedShareKey: ByteArray by lazy { ECDH.calculateShareKey(privateKey, initialPublicKey) }
-
-    companion object {
-        fun fromKey(
-            key: CPointer<EC_KEY>,
-            initialPublicKey: ECDHPublicKey = defaultInitialPublicKey.key
-        ): ECDHKeyPairImpl {
-            return ECDHKeyPairImpl(OpenSslPrivateKey.fromKey(key), OpenSslPublicKey.fromKey(key), initialPublicKey)
-        }
-    }
-}
-
-private fun CPointer<EC_POINT>.toKtHex(): String {
-    val ptr = EC_POINT_point2hex(group, this, convForm, bnCtx) ?: error("Failed EC_POINT_point2hex")
-    return try {
-        ptr.toKString()
-    } finally {
-        CRYPTO_free(ptr, "CPointer<EC_POINT>.toKtHex()", -1)
-    }
-}
-
-
-internal actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
-
-    /**
-     * 由 [keyPair] 的私匙和 [peerPublicKey] 计算 shareKey
-     */
-    actual fun calculateShareKeyByPeerPublicKey(peerPublicKey: ECDHPublicKey): ByteArray {
-        return calculateShareKey(keyPair.privateKey, peerPublicKey)
-    }
-
-    actual companion object {
-        actual val isECDHAvailable: Boolean get() = true
-
-        /**
-         * 由完整的 publicKey ByteArray 得到 [ECDHPublicKey]
-         */
-        actual fun constructPublicKey(key: ByteArray): ECDHPublicKey {
-            val p = EC_POINT_new(group) ?: error("Failed to create EC_POINT")
-
-            // TODO: 2022/6/1 native: check memory
-            EC_POINT_hex2point(group, key.toUHexString("").lowercase(), p, bnCtx)
-
-            return OpenSslPublicKey.fromPoint(p)
-        }
-
-        /**
-         * 由完整的 rsaKey 校验 publicKey
-         */
-        actual fun verifyPublicKey(
-            version: Int,
-            publicKey: String,
-            publicKeySign: String
-        ): Boolean = true
-
-        /**
-         * 生成随机密匙对
-         */
-        actual fun generateKeyPair(initialPublicKey: ECDHPublicKey): ECDHKeyPair {
-            val key: CPointer<EC_KEY> = EC_KEY_new_by_curve_name(curveId)
-                ?: throw IllegalStateException("Failed to create key curve, $errno")
-            try {
-                if (1 != EC_KEY_generate_key(key)) {
-                    throw IllegalStateException("Failed to generate key, $errno")
-                }
-                return ECDHKeyPairImpl.fromKey(key, initialPublicKey)
-            } finally {
-                EC_KEY_free(key)
-            }
-        }
-
-        fun calculateCanonicalShareKey(privateKey: ECDHPrivateKey, publicKey: ECDHPublicKey): ByteArray {
-            check(publicKey is OpenSslPublicKey)
-            check(privateKey is OpenSslPrivateKey)
-
-            val k = EC_KEY_new_by_curve_name(curveId) ?: error("Failed to create EC key")
-            try {
-                val privateBignum = privateKey.toBignum()
-                try {
-                    EC_KEY_set_private_key(k, privateBignum).let { r ->
-                        if (r != 1) error("Failed EC_KEY_set_private_key: $r")
-                    }
-
-                    val fieldSize = EC_GROUP_get_degree(group)
-                    if (fieldSize <= 0) {
-                        error("Failed EC_GROUP_get_degree: $fieldSize")
-                    }
-
-                    var secretLen = (fieldSize + 7) / 8
-
-                    val publicPoint = publicKey.toPoint()
-                    try {
-                        ByteArray(secretLen.convert()).usePinned { pin ->
-                            secretLen = ECDH_compute_key(pin.addressOf(0), secretLen.convert(), publicPoint, k, null)
-                            if (secretLen <= 0) {
-                                error("Failed to compute secret")
-                            }
-
-                            return pin.get().copyOf(secretLen)
-                        }
-                    } finally {
-                        EC_POINT_free(publicPoint)
-                    }
-                } finally {
-                    BN_free(privateBignum)
-                }
-            } finally {
-                EC_KEY_free(k)
-            }
-        }
-
-        actual fun calculateShareKey(
-            privateKey: ECDHPrivateKey,
-            publicKey: ECDHPublicKey
-        ): ByteArray = calculateCanonicalShareKey(privateKey, publicKey).copyOf(16).md5()
-    }
-
-    actual override fun toString(): String = "ECDH($keyPair)"
-}
-
-internal actual fun ByteArray.adjustToPublicKey(): ECDHPublicKey {
-    return ECDH.constructPublicKey(this)
-}

+ 13 - 0
mirai-core/src/nativeMain/kotlin/utils/crypto/EcdhNative.kt

@@ -0,0 +1,13 @@
+/*
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.utils.crypto
+
+
+internal actual fun Ecdh.Companion.create(): Ecdh<*, *> = OpenSslEcdh()

+ 147 - 0
mirai-core/src/nativeMain/kotlin/utils/crypto/OpenSslEcdh.kt

@@ -0,0 +1,147 @@
+/*
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.utils.crypto
+
+import kotlinx.cinterop.*
+import openssl.*
+import platform.posix.errno
+import kotlin.native.internal.createCleaner
+
+private const val curveId = NID_X9_62_prime256v1
+private val group by lazy { EC_GROUP_new_by_curve_name(curveId) ?: error("Failed to get EC_GROUP") }
+private val convForm by lazy { EC_GROUP_get_point_conversion_form(group) }
+private val bnCtx by lazy { BN_CTX_new() }
+
+
+internal class OpenSslECPublicKey private constructor(val point: CPointer<EC_POINT>) {
+    @Suppress("unused")
+    @OptIn(ExperimentalStdlibApi::class)
+    private val cleaner = createCleaner(point) {
+        EC_POINT_free(it)
+    }
+
+    fun export(): ByteArray {
+        val len = EC_POINT_point2oct(group, point, convForm, null, 0, null)
+        val bytes = ByteArray(len.convert())
+        bytes.usePinned {
+            EC_POINT_point2oct(group, point, convForm, it.addressOf(0).reinterpret(), len, bnCtx)
+        }
+        return bytes
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        return (other as? OpenSslECPublicKey)?.let {
+            EC_POINT_cmp(group, point, it.point, bnCtx) == 0
+        } ?: false
+    }
+
+    override fun hashCode(): Int {
+        return export().hashCode()
+    }
+
+    companion object {
+        fun copyFrom(source: CPointer<EC_POINT>): OpenSslECPublicKey {
+            return OpenSslECPublicKey(EC_POINT_dup(source, group) ?: error("Failed to dup a EC_POINT"))
+        }
+
+        fun import(encoded: ByteArray): OpenSslECPublicKey {
+            val point = EC_POINT_new(group) ?: error("Failed to create EC_POINT")
+            encoded.usePinned {
+                EC_POINT_oct2point(group, point, it.addressOf(0).reinterpret(), it.get().size.convert(), bnCtx)
+            }
+            return OpenSslECPublicKey(point)
+        }
+    }
+}
+
+internal class OpenSslECPrivateKey private constructor(val bn: CPointer<BIGNUM>) {
+    @Suppress("unused")
+    @OptIn(ExperimentalStdlibApi::class)
+    private val cleaner = createCleaner(bn) {
+        BN_free(it)
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        return (other as? OpenSslECPrivateKey)?.let {
+            BN_cmp(bn, other.bn) == 0
+        } ?: false
+    }
+
+    fun export(): ByteArray {
+        val len = (BN_num_bits(bn)+7)/8
+        val bytes = ByteArray(len)
+        bytes.usePinned {
+            BN_bn2bin(bn, it.addressOf(0).reinterpret())
+        }
+        return bytes
+    }
+
+    override fun hashCode(): Int {
+        return export().hashCode()
+    }
+
+    companion object {
+        fun copyFrom(source: CPointer<BIGNUM>): OpenSslECPrivateKey {
+            return OpenSslECPrivateKey(BN_dup(source) ?: error("Failed to dup a BIGNUM"))
+        }
+    }
+}
+
+internal class OpenSslEcdh : Ecdh<OpenSslECPublicKey, OpenSslECPrivateKey> {
+    override fun generateKeyPair(): EcdhKeyPair<OpenSslECPublicKey, OpenSslECPrivateKey> {
+        val key: CPointer<EC_KEY> = EC_KEY_new_by_curve_name(curveId)
+            ?: throw IllegalStateException("Failed to create key curve, $errno")
+        try {
+            if (1 != EC_KEY_generate_key(key)) {
+                throw IllegalStateException("Failed to generate key, $errno")
+            }
+            val public =
+                OpenSslECPublicKey.copyFrom(EC_KEY_get0_public_key(key) ?: error("Failed EC_key_get0_public_key"))
+            val private =
+                OpenSslECPrivateKey.copyFrom(EC_KEY_get0_private_key(key) ?: error("Failed EC_KEY_get0_private_key"))
+            return EcdhKeyPair(public, private)
+        } finally {
+            EC_KEY_free(key)
+        }
+    }
+
+    override fun calculateShareKey(peerKey: OpenSslECPublicKey, privateKey: OpenSslECPrivateKey): ByteArray {
+        val k = EC_KEY_new_by_curve_name(curveId) ?: error("Failed to create EC key")
+        try {
+            EC_KEY_set_private_key(k, privateKey.bn).let { r ->
+                if (r != 1) error("Failed EC_KEY_set_private_key: $r")
+            }
+            val fieldSize = EC_GROUP_get_degree(group)
+            if (fieldSize <= 0) {
+                error("Failed EC_GROUP_get_degree: $fieldSize")
+            }
+            var secretLen = (fieldSize + 7) / 8
+            ByteArray(secretLen.convert()).usePinned { pin ->
+                secretLen = ECDH_compute_key(pin.addressOf(0), secretLen.convert(), peerKey.point, k, null)
+                if (secretLen <= 0) {
+                    error("Failed to compute secret")
+                }
+                return pin.get().copyOf(secretLen)
+            }
+        } finally {
+            EC_KEY_free(k)
+        }
+    }
+
+    override fun importPublicKey(encoded: ByteArray): OpenSslECPublicKey {
+        return OpenSslECPublicKey.import(encoded)
+    }
+
+    override fun exportPublicKey(key: OpenSslECPublicKey): ByteArray {
+        return key.export()
+    }
+}

+ 15 - 0
mirai-core/src/nativeMain/kotlin/utils/crypto/QQEcdhNative.kt

@@ -0,0 +1,15 @@
+/*
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.utils.crypto
+
+internal actual fun QQEcdhInitialPublicKey.verify(sign: String): Boolean {
+    // FIXME
+    return true
+}