diff --git a/mirai-core/build.gradle.kts b/mirai-core/build.gradle.kts index da87f5b12..99056e26c 100644 --- a/mirai-core/build.gradle.kts +++ b/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)) } } diff --git a/mirai-core/src/androidMain/kotlin/utils/crypto/ECDHAndroid.kt b/mirai-core/src/androidMain/kotlin/utils/crypto/ECDHAndroid.kt deleted file mode 100644 index b3558bd5d..000000000 --- a/mirai-core/src/androidMain/kotlin/utils/crypto/ECDHAndroid.kt +++ /dev/null @@ -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)" - } -} \ No newline at end of file diff --git a/mirai-core/src/androidMain/kotlin/utils/crypto/EcdhAndroid.kt b/mirai-core/src/androidMain/kotlin/utils/crypto/EcdhAndroid.kt new file mode 100644 index 000000000..b5da7f669 --- /dev/null +++ b/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")) + } \ No newline at end of file diff --git a/mirai-core/src/androidTest/kotlin/test/initPlatform.android.kt b/mirai-core/src/androidTest/kotlin/test/initPlatform.android.kt index ff718a53d..b5f4db808 100644 --- a/mirai-core/src/androidTest/kotlin/test/initPlatform.android.kt +++ b/mirai-core/src/androidTest/kotlin/test/initPlatform.android.kt @@ -35,4 +35,4 @@ internal actual class PlatformInitializationTest : AbstractTest() { @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") assertIs(MiraiLogger.Factory.create(this::class, "1")) } -} \ No newline at end of file +} diff --git a/mirai-core/src/commonMain/kotlin/network/components/AccountSecretsManager.kt b/mirai-core/src/commonMain/kotlin/network/components/AccountSecretsManager.kt index 5bec626dc..e71d075cb 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/AccountSecretsManager.kt +++ b/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 ) } diff --git a/mirai-core/src/commonMain/kotlin/network/components/EcdhInitialPublicKeyUpdater.kt b/mirai-core/src/commonMain/kotlin/network/components/EcdhInitialPublicKeyUpdater.kt index cea70d255..77d9f3af4 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/EcdhInitialPublicKeyUpdater.kt +++ b/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 } @@ -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) } diff --git a/mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt b/mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt index 797f9fedb..dd9fc306d 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt +++ b/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) } diff --git a/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt b/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt index 482213cf9..52d113d63 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt +++ b/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) { diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/EncryptMethod.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/EncryptMethod.kt index 967ca5942..c13d9d032 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/EncryptMethod.kt +++ b/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) } } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/OutgoingPacket.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/OutgoingPacket.kt index c32e932f5..82ae95dfb 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/OutgoingPacket.kt +++ b/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 ) { diff --git a/mirai-core/src/commonMain/kotlin/utils/crypto/ECDH.kt b/mirai-core/src/commonMain/kotlin/utils/crypto/ECDH.kt deleted file mode 100644 index c546f2614..000000000 --- a/mirai-core/src/commonMain/kotlin/utils/crypto/ECDH.kt +++ /dev/null @@ -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 diff --git a/mirai-core/src/commonMain/kotlin/utils/crypto/Ecdh.kt b/mirai-core/src/commonMain/kotlin/utils/crypto/Ecdh.kt new file mode 100644 index 000000000..48da6d981 --- /dev/null +++ b/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(val public: TPublicKey, val private: TPrivate) + +internal interface Ecdh { + fun generateKeyPair(): EcdhKeyPair + 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 SECG SEC 1: Elliptic Curve Cryptography + */ + 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 SECG SEC 1: Elliptic Curve Cryptography + */ + fun exportPublicKey(key: TPublicKey): ByteArray + + companion object { + @JvmStatic + val Instance by lazy { + @Suppress("UNCHECKED_CAST") + Ecdh.create() as Ecdh + } + } +} +internal expect fun Ecdh.Companion.create() : Ecdh<*, *> \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/utils/crypto/QQEcdh.kt b/mirai-core/src/commonMain/kotlin/utils/crypto/QQEcdh.kt new file mode 100644 index 000000000..495c55f93 --- /dev/null +++ b/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() + } +} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/utils/crypto/ECDHTest.kt b/mirai-core/src/commonTest/kotlin/utils/crypto/EcdhTest.kt similarity index 68% rename from mirai-core/src/commonTest/kotlin/utils/crypto/ECDHTest.kt rename to mirai-core/src/commonTest/kotlin/utils/crypto/EcdhTest.kt index 81662f7f4..f2cb1fb55 100644 --- a/mirai-core/src/commonTest/kotlin/utils/crypto/ECDHTest.kt +++ b/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) } /* diff --git a/mirai-core/src/jvmBaseMain/kotlin/utils/crypto/ECDH.kt b/mirai-core/src/jvmBaseMain/kotlin/utils/crypto/ECDH.kt deleted file mode 100644 index 2e146ffb7..000000000 --- a/mirai-core/src/jvmBaseMain/kotlin/utils/crypto/ECDH.kt +++ /dev/null @@ -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) -} diff --git a/mirai-core/src/jvmBaseMain/kotlin/utils/crypto/JceEcdh.kt b/mirai-core/src/jvmBaseMain/kotlin/utils/crypto/JceEcdh.kt new file mode 100644 index 000000000..8c39b1556 --- /dev/null +++ b/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 { + 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 { + 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 + } +} diff --git a/mirai-core/src/jvmBaseMain/kotlin/utils/crypto/JceEcdhWithProvider.kt b/mirai-core/src/jvmBaseMain/kotlin/utils/crypto/JceEcdhWithProvider.kt new file mode 100644 index 000000000..b7548cd87 --- /dev/null +++ b/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) +} \ No newline at end of file diff --git a/mirai-core/src/jvmBaseMain/kotlin/utils/crypto/QQEcdhJvm.kt b/mirai-core/src/jvmBaseMain/kotlin/utils/crypto/QQEcdhJvm.kt new file mode 100644 index 000000000..5a70da2eb --- /dev/null +++ b/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()) +} diff --git a/mirai-core/src/jvmMain/kotlin/utils/crypto/ECDHJvmDesktop.kt b/mirai-core/src/jvmMain/kotlin/utils/crypto/ECDHJvmDesktop.kt deleted file mode 100644 index cfa36b4b8..000000000 --- a/mirai-core/src/jvmMain/kotlin/utils/crypto/ECDHJvmDesktop.kt +++ /dev/null @@ -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)" - } -} \ No newline at end of file diff --git a/mirai-core/src/jvmMain/kotlin/utils/crypto/EcdhJvmDesktop.kt b/mirai-core/src/jvmMain/kotlin/utils/crypto/EcdhJvmDesktop.kt new file mode 100644 index 000000000..80c04594d --- /dev/null +++ b/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()) + } \ No newline at end of file diff --git a/mirai-core/src/nativeMain/kotlin/utils/crypto/ECDH.kt b/mirai-core/src/nativeMain/kotlin/utils/crypto/ECDH.kt deleted file mode 100644 index 987f68321..000000000 --- a/mirai-core/src/nativeMain/kotlin/utils/crypto/ECDH.kt +++ /dev/null @@ -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, _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 -} - -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 -} - -internal class OpenSslPrivateKey( - override val hex: String, // use Kotlin's memory -) : ECDHPrivateKey { - - override fun toBignum(): CPointer { - 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): 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)", -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 { - 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): OpenSslPublicKey = - fromPoint(EC_KEY_get0_public_key(key) ?: error("Failed to get private key")) - - fun fromPoint(point: CPointer): 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, - initialPublicKey: ECDHPublicKey = defaultInitialPublicKey.key - ): ECDHKeyPairImpl { - return ECDHKeyPairImpl(OpenSslPrivateKey.fromKey(key), OpenSslPublicKey.fromKey(key), initialPublicKey) - } - } -} - -private fun CPointer.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.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_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) -} diff --git a/mirai-core/src/nativeMain/kotlin/utils/crypto/EcdhNative.kt b/mirai-core/src/nativeMain/kotlin/utils/crypto/EcdhNative.kt new file mode 100644 index 000000000..84c6c1bc7 --- /dev/null +++ b/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() \ No newline at end of file diff --git a/mirai-core/src/nativeMain/kotlin/utils/crypto/OpenSslEcdh.kt b/mirai-core/src/nativeMain/kotlin/utils/crypto/OpenSslEcdh.kt new file mode 100644 index 000000000..9802129f8 --- /dev/null +++ b/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) { + @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): 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) { + @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): OpenSslECPrivateKey { + return OpenSslECPrivateKey(BN_dup(source) ?: error("Failed to dup a BIGNUM")) + } + } +} + +internal class OpenSslEcdh : Ecdh { + override fun generateKeyPair(): EcdhKeyPair { + val key: CPointer = 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() + } +} \ No newline at end of file diff --git a/mirai-core/src/nativeMain/kotlin/utils/crypto/QQEcdhNative.kt b/mirai-core/src/nativeMain/kotlin/utils/crypto/QQEcdhNative.kt new file mode 100644 index 000000000..750612f23 --- /dev/null +++ b/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 +} \ No newline at end of file