From 9de13ca6fd58eeed14cc63d84ccc9e08c10580f9 Mon Sep 17 00:00:00 2001 From: Him188 Date: Wed, 19 Feb 2020 14:15:38 +0800 Subject: [PATCH] Fix ECDH --- .../network/protocol/packet/EncryptMethod.kt | 34 +++++++++---- .../network/protocol/packet/login/WtLogin.kt | 10 ++-- .../mamoe/mirai/utils/cryptor/ECDHAndroid.kt | 22 +++++--- .../net.mamoe.mirai/utils/cryptor/ECDH.kt | 18 +++++-- .../net/mamoe/mirai/utils/cryptor/ECDHJvm.kt | 50 ++++++++----------- 5 files changed, 81 insertions(+), 53 deletions(-) diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/EncryptMethod.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/EncryptMethod.kt index 344c9dba2..fe32747ff 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/EncryptMethod.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/EncryptMethod.kt @@ -14,7 +14,9 @@ import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.buildPacket import kotlinx.io.core.writeFully import net.mamoe.mirai.qqandroid.network.QQAndroidClient +import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.cryptor.ECDH +import net.mamoe.mirai.utils.cryptor.ECDHKeyPair import net.mamoe.mirai.utils.io.encryptAndWrite import net.mamoe.mirai.utils.io.writeShortLVByteArray @@ -65,17 +67,25 @@ inline class EncryptMethodSessionKeyLoginState3(override val sessionKey: ByteArr override val currentLoginState: Int get() = 3 } -inline class EncryptMethodECDH135(override val ecdh: ECDH) : +internal inline class EncryptMethodECDH135(override val ecdh: ECDH) : EncryptMethodECDH { override val id: Int get() = 135 } -inline class EncryptMethodECDH7(override val ecdh: ECDH) : +internal inline class EncryptMethodECDH7(override val ecdh: ECDH) : EncryptMethodECDH { override val id: Int get() = 7 } internal interface EncryptMethodECDH : EncryptMethod { + companion object { + operator fun invoke(ecdh: ECDH): EncryptMethodECDH { + return if (ecdh.keyPair === ECDHKeyPair.DefaultStub) { + EncryptMethodECDH135(ecdh) + } else EncryptMethodECDH7(ecdh) + } + } + val ecdh: ECDH /** @@ -97,13 +107,19 @@ internal interface EncryptMethodECDH : EncryptMethod { // writeShortLVByteArray("04 CB 36 66 98 56 1E 93 6E 80 C1 57 E0 74 CA B1 3B 0B B6 8D DE B2 82 45 48 A1 B1 8D D4 FB 61 22 AF E1 2F E4 8C 52 66 D8 D7 26 9D 76 51 A8 EB 6F E7".hexToBytes()) - writeShortLVByteArray(ecdh.keyPair.publicKey.getEncoded().drop(23).take(49).toByteArray().also { - // it.toUHexString().debugPrint("PUBLIC KEY") - check(it[0].toInt() == 0x04) { "Bad publicKey generated. Expected first element=0x04, got${it[0]}" } - //check(ecdh.calculateShareKeyByPeerPublicKey(it.adjustToPublicKey()).contentEquals(ecdh.keyPair.shareKey)) { "PublicKey Validation failed" } - }) + if (ecdh.keyPair === ECDHKeyPair.DefaultStub) { + writeShortLVByteArray(ECDHKeyPair.DefaultStub.defaultPublicKey) + encryptAndWrite(ECDHKeyPair.DefaultStub.defaultShareKey, body) + } else { + MiraiLogger.info("Using custom") + writeShortLVByteArray(ecdh.keyPair.publicKey.getEncoded().drop(23).take(49).toByteArray().also { + // it.toUHexString().debugPrint("PUBLIC KEY") + check(it[0].toInt() == 0x04) { "Bad publicKey generated. Expected first element=0x04, got${it[0]}" } + //check(ecdh.calculateShareKeyByPeerPublicKey(it.adjustToPublicKey()).contentEquals(ecdh.keyPair.shareKey)) { "PublicKey Validation failed" } + }) - // encryptAndWrite("26 33 BA EC 86 EB 79 E6 BC E0 20 06 5E A9 56 6C".hexToBytes(), body) - encryptAndWrite(ecdh.keyPair.initialShareKey, body) + // encryptAndWrite("26 33 BA EC 86 EB 79 E6 BC E0 20 06 5E A9 56 6C".hexToBytes(), body) + encryptAndWrite(ecdh.keyPair.initialShareKey, body) + } } } \ No newline at end of file diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/WtLogin.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/WtLogin.kt index 6bca8ade1..29ea3b873 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/WtLogin.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/WtLogin.kt @@ -47,7 +47,7 @@ internal class WtLogin { ticket: String ): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId -> writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) { - writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) { + writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) { writeShort(2) // subCommand writeShort(4) // count of TLVs t193(ticket) @@ -64,7 +64,7 @@ internal class WtLogin { captchaAnswer: String ): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId -> writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) { - writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) { + writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) { writeShort(2) // subCommand writeShort(4) // count of TLVs t2(captchaAnswer, captchaSign, 0) @@ -83,7 +83,7 @@ internal class WtLogin { t402: ByteArray ): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId -> writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) { - writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) { + writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) { writeShort(20) // subCommand writeShort(4) // count of TLVs, probably ignored by server? t8(2052) @@ -103,7 +103,7 @@ internal class WtLogin { client: QQAndroidClient ): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId -> writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId, unknownHex = "01 00 00 00 00 00 00 00 00 00 01 00") { - writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) { + writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) { writeShort(8) // subCommand writeShort(6) // count of TLVs, probably ignored by server?TODO t8(2052) @@ -131,7 +131,7 @@ internal class WtLogin { client: QQAndroidClient ): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId -> writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) { - writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) { + writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) { writeShort(9) // subCommand writeShort(17) // count of TLVs, probably ignored by server? //writeShort(LoginType.PASSWORD.value.toShort()) diff --git a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHAndroid.kt b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHAndroid.kt index 93d59f082..89f582f46 100644 --- a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHAndroid.kt +++ b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHAndroid.kt @@ -19,13 +19,13 @@ import javax.crypto.KeyAgreement actual typealias ECDHPrivateKey = PrivateKey actual typealias ECDHPublicKey = PublicKey -actual class ECDHKeyPair( +internal actual class ECDHKeyPairImpl( private val delegate: KeyPair -) { - actual val privateKey: ECDHPrivateKey get() = delegate.private - actual val publicKey: ECDHPublicKey get() = delegate.public +) : ECDHKeyPair { + override val privateKey: ECDHPrivateKey get() = delegate.private + override val publicKey: ECDHPublicKey get() = delegate.public - actual val initialShareKey: ByteArray = ECDH.calculateShareKey(privateKey, initialPublicKey) + override val initialShareKey: ByteArray = ECDH.calculateShareKey(privateKey, initialPublicKey) } @Suppress("FunctionName") @@ -33,6 +33,10 @@ actual fun ECDH() = ECDH(ECDH.generateKeyPair()) actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) { actual companion object { + @Suppress("ObjectPropertyName") + private var _isECDHAvailable: Boolean = false // because `runCatching` has no contract. + actual val isECDHAvailable: Boolean get() = _isECDHAvailable + init { kotlin.runCatching { @SuppressLint("PrivateApi") @@ -48,13 +52,19 @@ actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) { Security.removeProvider(providerName) } Security.addProvider(clazz.newInstance() as Provider) + generateKeyPair() + _isECDHAvailable = true }.exceptionOrNull()?.let { throw IllegalStateException("cannot init BouncyCastle", it) } + _isECDHAvailable = false } actual fun generateKeyPair(): ECDHKeyPair { - return ECDHKeyPair(KeyPairGenerator.getInstance("ECDH").genKeyPair()) + if (!isECDHAvailable) { + return ECDHKeyPair.DefaultStub + } + return ECDHKeyPairImpl(KeyPairGenerator.getInstance("ECDH").genKeyPair()) } actual fun calculateShareKey( diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/ECDH.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/ECDH.kt index 2e93e0385..0b93f83b5 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/ECDH.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/ECDH.kt @@ -19,7 +19,9 @@ expect interface ECDHPublicKey { fun getEncoded(): ByteArray } -expect class ECDHKeyPair { +internal expect class ECDHKeyPairImpl : ECDHKeyPair + +interface ECDHKeyPair { val privateKey: ECDHPrivateKey val publicKey: ECDHPublicKey @@ -27,6 +29,15 @@ expect class ECDHKeyPair { * 私匙和固定公匙([initialPublicKey]) 计算得到的 shareKey */ val initialShareKey: ByteArray + + object DefaultStub : ECDHKeyPair { + val defaultPublicKey = "020b03cf3d99541f29ffec281bebbd4ea211292ac1f53d7128".chunkedHexToBytes() + val defaultShareKey = "4da0f614fc9f29c2054c77048a6566d7".chunkedHexToBytes() + + override val privateKey: Nothing get() = error("stub!") + override val publicKey: Nothing get() = error("stub!") + override val initialShareKey: ByteArray get() = defaultShareKey + } } /** @@ -41,6 +52,8 @@ expect class ECDH(keyPair: ECDHKeyPair) { fun calculateShareKeyByPeerPublicKey(peerPublicKey: ECDHPublicKey): ByteArray companion object { + val isECDHAvailable: Boolean + /** * 由完整的 publicKey ByteArray 得到 [ECDHPublicKey] */ @@ -60,9 +73,6 @@ expect class ECDH(keyPair: ECDHKeyPair) { override fun toString(): String } -/** - * - */ @Suppress("FunctionName") expect fun ECDH(): ECDH diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHJvm.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHJvm.kt index f24f61c72..c261cb80e 100644 --- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHJvm.kt +++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHJvm.kt @@ -9,11 +9,9 @@ package net.mamoe.mirai.utils.cryptor -import net.mamoe.mirai.utils.io.chunkedHexToBytes import net.mamoe.mirai.utils.md5 import org.bouncycastle.jce.provider.BouncyCastleProvider import java.security.* -import java.security.spec.ECGenParameterSpec import java.security.spec.X509EncodedKeySpec import javax.crypto.KeyAgreement @@ -21,20 +19,13 @@ import javax.crypto.KeyAgreement actual typealias ECDHPrivateKey = PrivateKey actual typealias ECDHPublicKey = PublicKey -actual class ECDHKeyPair( - private val delegate: KeyPair? -) { - actual val privateKey: ECDHPrivateKey get() = delegate?.private ?: error("ECDH is not available") - actual val publicKey: ECDHPublicKey get() = delegate?.public ?: defaultPublicKey +internal actual class ECDHKeyPairImpl( + private val delegate: KeyPair +) : ECDHKeyPair { + override val privateKey: ECDHPrivateKey get() = delegate.private + override val publicKey: ECDHPublicKey get() = delegate.public - actual val initialShareKey: ByteArray = if (delegate == null) { - defaultShareKey - } else ECDH.calculateShareKey(privateKey, initialPublicKey) - - companion object { - internal val defaultPublicKey = "020b03cf3d99541f29ffec281bebbd4ea211292ac1f53d7128".chunkedHexToBytes().adjustToPublicKey() - internal val defaultShareKey = "4da0f614fc9f29c2054c77048a6566d7".chunkedHexToBytes() - } + override val initialShareKey: ByteArray = ECDH.calculateShareKey(privateKey, initialPublicKey) } @Suppress("FunctionName") @@ -42,33 +33,35 @@ actual fun ECDH() = ECDH(ECDH.generateKeyPair()) actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) { actual companion object { - private var isECDHAvailable = true + @Suppress("ObjectPropertyName") + private val _isECDHAvailable: Boolean + actual val isECDHAvailable: Boolean get() = _isECDHAvailable init { - isECDHAvailable = kotlin.runCatching { + _isECDHAvailable = kotlin.runCatching { + if (Security.getProvider("BouncyCastle") != null) { + Security.removeProvider("BouncyCastle") + } Security.addProvider(BouncyCastleProvider()) generateKeyPair() // try if it is working }.isSuccess } actual fun generateKeyPair(): ECDHKeyPair { - return if (!isECDHAvailable) { - ECDHKeyPair(null) - } else ECDHKeyPair(KeyPairGenerator.getInstance("EC", "BC").apply { initialize(ECGenParameterSpec("secp192k1")) }.genKeyPair()) + if (!isECDHAvailable) { + return ECDHKeyPair.DefaultStub + } + return ECDHKeyPairImpl(KeyPairGenerator.getInstance("ECDH").genKeyPair()) } actual fun calculateShareKey( privateKey: ECDHPrivateKey, publicKey: ECDHPublicKey ): ByteArray { - return if (!isECDHAvailable) { - ECDHKeyPair.defaultShareKey - } else { - val instance = KeyAgreement.getInstance("ECDH", "BC") - instance.init(privateKey) - instance.doPhase(publicKey, true) - md5(instance.generateSecret()) - } + val instance = KeyAgreement.getInstance("ECDH", "BC") + instance.init(privateKey) + instance.doPhase(publicKey, true) + return md5(instance.generateSecret()) } actual fun constructPublicKey(key: ByteArray): ECDHPublicKey { @@ -77,7 +70,6 @@ actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) { } actual fun calculateShareKeyByPeerPublicKey(peerPublicKey: ECDHPublicKey): ByteArray { - if (!isECDHAvailable) return keyPair.initialShareKey return calculateShareKey(keyPair.privateKey, peerPublicKey) }