1
0
mirror of https://github.com/mamoe/mirai.git synced 2025-04-02 05:00:35 +08:00

Implement ECDH on native with OpenSSL

This commit is contained in:
Him188 2022-06-01 19:44:24 +01:00
parent 8655f44a50
commit 29c8d13795
No known key found for this signature in database
GPG Key ID: BA439CDDCF652375
12 changed files with 378 additions and 117 deletions
buildSrc/src/main/kotlin
gradle.properties
mirai-core
build.gradle.kts
src
commonMain/kotlin
message/protocol
pipeline
utils/crypto
commonTest/kotlin/utils/crypto
jvmBaseMain/kotlin/utils/crypto
jvmMain/kotlin/utils/crypto
nativeMain
cinterop
kotlin/utils/crypto

View File

@ -81,6 +81,8 @@ val LINUX_TARGETS = setOf("linuxX64")
val UNIX_LIKE_TARGETS by lazy { LINUX_TARGETS + MAC_TARGETS }
val NATIVE_TARGETS by lazy { UNIX_LIKE_TARGETS + WIN_TARGETS }
fun Project.configureHMPP() {
extensions.getByType(KotlinMultiplatformExtension::class.java).apply {

View File

@ -24,4 +24,5 @@ mirai.android.target.api.level=24
# Enable if you want to use mavenLocal for both Gradle plugin and project dependencies resolutions.
systemProp.use.maven.local=false
org.gradle.caching=true
kotlin.native.ignoreIncorrectDependencies=true
kotlin.native.ignoreIncorrectDependencies=true
kotlin.mpp.enableCInteropCommonization=true

View File

@ -10,6 +10,7 @@
@file:Suppress("UNUSED_VARIABLE")
import BinaryCompatibilityConfigurator.configureBinaryValidators
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
plugins {
kotlin("multiplatform")
@ -105,6 +106,14 @@ kotlin {
}
}
NATIVE_TARGETS.forEach { target ->
(targets.getByName(target) as KotlinNativeTarget).compilations.getByName("main").cinterops.create("OpenSSL")
.apply {
defFile = projectDir.resolve("src/nativeMain/cinterop/OpenSSL.def")
packageName("openssl")
}
}
configure(WIN_TARGETS.map { getByName(it + "Main") }) {
dependencies {
implementation(`ktor-client-curl`)

View File

@ -199,10 +199,8 @@ internal class MessageProtocolFacadeImpl(
val instances = protocols
.sortedWith(MessageProtocol.PriorityComparator.reversed())
for (instance in instances) {
println("instance: $instance, class=${instance::class}")
instance.collectProcessors(object : ProcessorCollector() {
override fun <T : SingleMessage> add(encoder: MessageEncoder<T>, elementType: KClass<T>) {
println("add: $encoder, $elementType")
this@MessageProtocolFacadeImpl.encoderPipeline.registerProcessor(
MessageEncoderProcessor(
encoder,
@ -212,27 +210,22 @@ internal class MessageProtocolFacadeImpl(
}
override fun add(decoder: MessageDecoder) {
println("add: $decoder")
this@MessageProtocolFacadeImpl.decoderPipeline.registerProcessor(MessageDecoderProcessor(decoder))
}
override fun add(preprocessor: OutgoingMessagePreprocessor) {
println("add: $preprocessor")
preprocessorPipeline.registerProcessor(OutgoingMessageProcessorAdapter(preprocessor))
}
override fun add(transformer: OutgoingMessageTransformer) {
println("add: $transformer")
outgoingPipeline.registerProcessor(OutgoingMessageProcessorAdapter(transformer))
}
override fun add(sender: OutgoingMessageSender) {
println("add: $sender")
outgoingPipeline.registerProcessor(OutgoingMessageProcessorAdapter(sender))
}
override fun add(postprocessor: OutgoingMessagePostprocessor) {
println("add: $postprocessor")
outgoingPipeline.registerProcessor(OutgoingMessageProcessorAdapter(postprocessor))
}

View File

@ -201,10 +201,8 @@ protected constructor(
override val processors: MutableDeque<ProcessorBox<P>> = ConcurrentLinkedDeque()
override fun registerProcessor(processor: P): ProcessorPipeline.DisposableRegistry {
println("registerProcessor: $processor")
val box = ProcessorBox(processor)
processors.add(box)
println("processors.add fin")
return ProcessorPipeline.DisposableRegistry {
processors.remove(box)
}

View File

@ -15,9 +15,7 @@ import net.mamoe.mirai.utils.hexToBytes
internal expect interface ECDHPrivateKey
internal expect interface ECDHPublicKey {
fun getEncoded(): ByteArray
}
internal expect interface ECDHPublicKey
internal expect class ECDHKeyPairImpl : ECDHKeyPair
@ -48,9 +46,6 @@ internal interface ECDHKeyPair {
}
}
/**
* 椭圆曲线密码, ECDH 加密
*/
internal expect class ECDH(keyPair: ECDHKeyPair) {
val keyPair: ECDHKeyPair
@ -62,8 +57,11 @@ internal expect class ECDH(keyPair: ECDHKeyPair) {
companion object {
val isECDHAvailable: Boolean
/**
* 由完整的 publicKey ByteArray 得到 [ECDHPublicKey]
* 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
@ -78,7 +76,7 @@ internal expect class ECDH(keyPair: ECDHKeyPair) {
fun generateKeyPair(initialPublicKey: ECDHPublicKey = defaultInitialPublicKey.key): ECDHKeyPair
/**
* 由一对密匙计算 shareKey
* 由一对密匙计算服务器需要的 shareKey
*/
fun calculateShareKey(privateKey: ECDHPrivateKey, publicKey: ECDHPublicKey): ByteArray
}
@ -119,19 +117,12 @@ internal data class ECDHWithPublicKey(private val initialPublicKey: ECDHInitialP
@Serializable
internal data class ECDHInitialPublicKey(val version: Int = 1, val keyStr: String, val expireTime: Long = 0) {
@Transient
internal val key: ECDHPublicKey = keyStr.adjustToPublicKey()
internal val key: ECDHPublicKey = keyStr.hexToBytes().adjustToPublicKey()
}
internal expect val publicKeyForVerify: ECDHPublicKey
internal val defaultInitialPublicKey: ECDHInitialPublicKey by lazy { ECDHInitialPublicKey(keyStr = "04EBCA94D733E399B2DB96EACDD3F69A8BB0F74224E2B44E3357812211D2E62EFBC91BB553098E25E33A799ADC7F76FEB208DA7C6522CDB0719A305180CC54A82E") }
private val signHead = "3059301306072a8648ce3d020106082a8648ce3d030107034200".hexToBytes()
internal fun String.adjustToPublicKey(): ECDHPublicKey {
return this.hexToBytes().adjustToPublicKey()
}
internal fun ByteArray.adjustToPublicKey(): ECDHPublicKey {
internal expect fun ByteArray.adjustToPublicKey(): ECDHPublicKey
return ECDH.constructPublicKey(signHead + this)
}
internal val ECDH.Companion.curveName get() = "prime256v1" // p-256

View File

@ -0,0 +1,70 @@
/*
* 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.internal.test.AbstractTest
import net.mamoe.mirai.utils.toUHexString
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
internal class ECDHTest : AbstractTest() {
@Test
fun `can generate key pair`() {
val alice = ECDH.generateKeyPair()
val bob = ECDH.generateKeyPair()
val aliceSecret = ECDH.calculateShareKey(alice.privateKey, bob.publicKey)
val bobSecret = ECDH.calculateShareKey(bob.privateKey, alice.publicKey)
println(aliceSecret.toUHexString())
assertContentEquals(aliceSecret, bobSecret)
}
@Test
fun `can get masked keys`() {
val alice = ECDH.generateKeyPair()
println(alice)
val maskedPublicKey = alice.maskedPublicKey
println(maskedPublicKey.toUHexString())
assertEquals(0x04, maskedPublicKey.first())
println(alice.maskedShareKey.toUHexString())
}
/*
EC_KEY *alice = create_key();
EC_KEY *bob = create_key();
assert(alice != NULL && bob != NULL);
const EC_POINT *alice_public = EC_KEY_get0_public_key(alice);
const EC_POINT *bob_public = EC_KEY_get0_public_key(bob);
size_t alice_secret_len;
size_t bob_secret_len;
unsigned char *alice_secret = get_secret(alice, bob_public, &alice_secret_len);
unsigned char *bob_secret = get_secret(bob, alice_public, &bob_secret_len);
assert(alice_secret != NULL && bob_secret != NULL
&& alice_secret_len == bob_secret_len);
for (int i = 0; i < alice_secret_len; i++)
assert(alice_secret[i] == bob_secret[i]);
EC_KEY_free(alice);
EC_KEY_free(bob);
OPENSSL_free(alice_secret);
OPENSSL_free(bob_secret);
return 0;
*/
}

View File

@ -12,6 +12,7 @@
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
@ -36,8 +37,13 @@ internal actual class ECDHKeyPairImpl(
}
internal actual val publicKeyForVerify: ECDHPublicKey by lazy {
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)
}

View File

@ -12,14 +12,16 @@ 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.*
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 {
private const val curveName = "prime256v1" // p-256
actual val isECDHAvailable: Boolean

View File

@ -0,0 +1,43 @@
headers = openssl/ec.h openssl/ecdh.h openssl/evp.h
linkerOpts.osx = -lcrypto \
-lssl \
-L/opt/openssl/lib \
-L/opt/homebrew/Cellar/openssl@3/3.0.3/lib \
-L/opt/homebrew/opt/openssl@3/lib \
compilerOpts.osx = -I/opt/openssl/include \
-I/usr/local/include/openssl@3 \
-I/opt/homebrew/Cellar/openssl@3/3.0.3/include \
-I/usr/include/openssl@3 \
-I/opt/homebrew/opt/openssl@3/include
linkerOpts.linux = -lcrypto \
-lssl \
-L/usr/lib64 \
-L/usr/lib/x86_64-linux-gnu \
-L/opt/local/lib \
-L/usr/local/opt/openssl@3/lib \
-L/opt/homebrew/opt/openssl@3/lib
compilerOpts.linux = -I/opt/local/include/openssl@3 \
-I/usr/bin/openssl@3 \
-I/usr/local/include/openssl@3 \
-I/usr/include/openssl@3 \
-I/opt/homebrew/opt/openssl@3/include
linkerOpts.mingw_x64 = -lcrypto \
-lssl \
-L/usr/lib64 \
-L/usr/lib/x86_64-linux-gnu \
-L/opt/local/lib \
-L/usr/local/opt/openssl@3/lib \
-L/opt/homebrew/opt/openssl@3/lib \
-LC:/Tools/msys64/mingw64/lib \
-LC:/Tools/msys2/mingw64/lib
compilerOpts.mingw_x64 = -I/opt/local/include/openssl@3 \
-I/usr/bin/openssl@3 \
-I/usr/local/include/openssl@3 \
-I/usr/include/openssl@3 \
-I/opt/homebrew/opt/openssl@3/include

View File

@ -0,0 +1,232 @@
/*
* 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
import platform.posix.free
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() }
internal actual interface ECDHPublicKey : OpenSSLKey {
val encoded: ByteArray
fun toPoint(): CPointer<EC_POINT>
}
internal actual interface ECDHPrivateKey : OpenSSLKey {
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 {
val bn = EC_KEY_get0_private_key(key) ?: error("Failed EC_KEY_get0_private_key")
val hex = try {
val ptr = BN_bn2hex(bn) ?: error("Failed EC_POINT_bn2point")
try {
ptr.toKString()
} finally {
free(ptr)
}
} finally {
BN_free(bn)
}
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,
private val 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 {
free(ptr)
}
}
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 {
memScoped {
key.usePinned { pin ->
val group = EC_GROUP_new_by_curve_name(curveId)
?: error("Failed to create EC_GROUP")
val p = EC_POINT_new(group) ?: error("Failed to create EC_POINT")
EC_POINT_hex2point(group, pin.get().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")
if (1 != EC_KEY_generate_key(key)) {
throw IllegalStateException("Failed to generate key, $errno")
}
try {
return ECDHKeyPairImpl.fromKey(key, initialPublicKey)
} finally {
free(key) // TODO: THIS MAY CAUSE MEMORY LEAK. But EC_KEY_free() will terminate the process for unknown reason.
}
}
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, privateKey.toBignum()).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)
}

View File

@ -1,86 +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
internal actual interface ECDHPrivateKey
internal actual interface ECDHPublicKey {
actual fun getEncoded(): ByteArray
}
internal actual class ECDHKeyPairImpl(
override val privateKey: ECDHPrivateKey,
override val publicKey: ECDHPublicKey,
override val maskedShareKey: ByteArray,
override val maskedPublicKey: ByteArray
) : ECDHKeyPair
/**
* 椭圆曲线密码, ECDH 加密
*/
internal actual class ECDH actual constructor(keyPair: ECDHKeyPair) {
actual val keyPair: ECDHKeyPair
get() = TODO("Not yet implemented")
/**
* [keyPair] 的私匙和 [peerPublicKey] 计算 shareKey
*/
actual fun calculateShareKeyByPeerPublicKey(peerPublicKey: ECDHPublicKey): ByteArray {
TODO("Not yet implemented")
}
actual companion object {
actual val isECDHAvailable: Boolean
get() = TODO("Not yet implemented")
/**
* 由完整的 publicKey ByteArray 得到 [ECDHPublicKey]
*/
actual fun constructPublicKey(key: ByteArray): ECDHPublicKey {
TODO("Not yet implemented")
}
/**
* 由完整的 rsaKey 校验 publicKey
*/
actual fun verifyPublicKey(
version: Int,
publicKey: String,
publicKeySign: String
): Boolean {
TODO("Not yet implemented")
}
/**
* 生成随机密匙对
*/
actual fun generateKeyPair(initialPublicKey: ECDHPublicKey): ECDHKeyPair {
TODO("Not yet implemented")
}
/**
* 由一对密匙计算 shareKey
*/
actual fun calculateShareKey(
privateKey: ECDHPrivateKey,
publicKey: ECDHPublicKey
): ByteArray {
TODO("Not yet implemented")
}
}
actual override fun toString(): String {
TODO("Not yet implemented")
}
}
internal actual val publicKeyForVerify: ECDHPublicKey
get() = TODO("Not yet implemented")