[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>
This commit is contained in:
AdoptOSS 2022-09-11 20:14:39 +08:00 committed by GitHub
parent d32a8a566c
commit 397d824d33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 538 additions and 699 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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 qqEcdh: QQEcdh? = null
override fun getQQEcdh(): QQEcdh {
if (qqEcdh == null) {
error("Calling getQQEcdh without calling refreshInitialPublicKeyAndApplyEcdh")
}
return qqEcdh!!
}
var ecdhWithPublicKey: ECDHWithPublicKey? = null
override fun getECDHWithPublicKey(): ECDHWithPublicKey {
if (ecdhWithPublicKey == null) {
error("Calling getECDHWithPublicKey without calling refreshInitialPublicKeyAndApplyECDH")
}
return ecdhWithPublicKey!!
}
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)
}

View File

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

View File

@ -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) {

View File

@ -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)
}
writeShortLVByteArray(ecdh.publicKey)
encryptAndWrite(ecdh.initialQQShareKey, body)
}
}

View File

@ -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
) {

View File

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

View File

@ -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<*, *>

View File

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

View File

@ -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)
}
/*

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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