mirror of
https://github.com/mamoe/mirai.git
synced 2025-02-26 20:20:14 +08:00
[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:
parent
d32a8a566c
commit
397d824d33
@ -58,7 +58,6 @@ kotlin {
|
|||||||
|
|
||||||
findByName("jvmBaseMain")?.apply {
|
findByName("jvmBaseMain")?.apply {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(bouncycastle)
|
|
||||||
implementation(`log4j-api`)
|
implementation(`log4j-api`)
|
||||||
implementation(`netty-all`)
|
implementation(`netty-all`)
|
||||||
implementation(`ktor-client-okhttp`)
|
implementation(`ktor-client-okhttp`)
|
||||||
@ -84,13 +83,13 @@ kotlin {
|
|||||||
implementation(kotlin("test-junit5", Versions.kotlinCompiler))
|
implementation(kotlin("test-junit5", Versions.kotlinCompiler))
|
||||||
implementation(kotlin("test-annotations-common"))
|
implementation(kotlin("test-annotations-common"))
|
||||||
implementation(kotlin("test-common"))
|
implementation(kotlin("test-common"))
|
||||||
//implementation("org.bouncycastle:bcprov-jdk15on:1.64")
|
implementation(bouncycastle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findByName("jvmMain")?.apply {
|
findByName("jvmMain")?.apply {
|
||||||
dependencies {
|
dependencies {
|
||||||
//implementation("org.bouncycastle:bcprov-jdk15on:1.64")
|
implementation(bouncycastle)
|
||||||
// api(kotlinx("coroutines-debug", Versions.coroutines))
|
// api(kotlinx("coroutines-debug", Versions.coroutines))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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"))
|
||||||
|
}
|
@ -35,4 +35,4 @@ internal actual class PlatformInitializationTest : AbstractTest() {
|
|||||||
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||||
assertIs<net.mamoe.mirai.internal.utils.StdoutLogger>(MiraiLogger.Factory.create(this::class, "1"))
|
assertIs<net.mamoe.mirai.internal.utils.StdoutLogger>(MiraiLogger.Factory.create(this::class, "1"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.getRandomByteArray
|
||||||
import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.get_mpasswd
|
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.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.TEA
|
||||||
import net.mamoe.mirai.internal.utils.crypto.defaultInitialPublicKey
|
|
||||||
import net.mamoe.mirai.internal.utils.io.ProtoBuf
|
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.loadAs
|
||||||
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
||||||
@ -73,7 +72,7 @@ internal interface AccountSecrets {
|
|||||||
|
|
||||||
var tgtgtKey: ByteArray
|
var tgtgtKey: ByteArray
|
||||||
val randomKey: ByteArray
|
val randomKey: ByteArray
|
||||||
var ecdhInitialPublicKey: ECDHInitialPublicKey
|
var ecdhInitialPublicKey: QQEcdhInitialPublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -87,7 +86,7 @@ internal data class AccountSecretsImpl(
|
|||||||
override var ksid: ByteArray,
|
override var ksid: ByteArray,
|
||||||
override var tgtgtKey: ByteArray,
|
override var tgtgtKey: ByteArray,
|
||||||
override val randomKey: ByteArray,
|
override val randomKey: ByteArray,
|
||||||
override var ecdhInitialPublicKey: ECDHInitialPublicKey,
|
override var ecdhInitialPublicKey: QQEcdhInitialPublicKey,
|
||||||
) : AccountSecrets, ProtoBuf {
|
) : AccountSecrets, ProtoBuf {
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
@ -141,7 +140,7 @@ internal fun AccountSecretsImpl(
|
|||||||
ksid = EMPTY_BYTE_ARRAY,
|
ksid = EMPTY_BYTE_ARRAY,
|
||||||
tgtgtKey = (account.passwordMd5 + ByteArray(4) + account.id.toInt().toByteArray()).md5(),
|
tgtgtKey = (account.passwordMd5 + ByteArray(4) + account.id.toInt().toByteArray()).md5(),
|
||||||
randomKey = getRandomByteArray(16),
|
randomKey = getRandomByteArray(16),
|
||||||
ecdhInitialPublicKey = defaultInitialPublicKey
|
ecdhInitialPublicKey = QQEcdhInitialPublicKey.default
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,25 +17,24 @@ import kotlinx.serialization.Serializable
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import net.mamoe.mirai.internal.QQAndroidBot
|
import net.mamoe.mirai.internal.QQAndroidBot
|
||||||
import net.mamoe.mirai.internal.network.component.ComponentKey
|
import net.mamoe.mirai.internal.network.component.ComponentKey
|
||||||
import net.mamoe.mirai.internal.utils.crypto.ECDH
|
import net.mamoe.mirai.internal.utils.crypto.QQEcdh
|
||||||
import net.mamoe.mirai.internal.utils.crypto.ECDHInitialPublicKey
|
import net.mamoe.mirai.internal.utils.crypto.QQEcdhInitialPublicKey
|
||||||
import net.mamoe.mirai.internal.utils.crypto.ECDHWithPublicKey
|
import net.mamoe.mirai.internal.utils.crypto.verify
|
||||||
import net.mamoe.mirai.internal.utils.crypto.defaultInitialPublicKey
|
|
||||||
import net.mamoe.mirai.utils.MiraiLogger
|
import net.mamoe.mirai.utils.MiraiLogger
|
||||||
import net.mamoe.mirai.utils.currentTimeSeconds
|
import net.mamoe.mirai.utils.currentTimeSeconds
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updater for updating [ECDHInitialPublicKey].
|
* Updater for updating [QQEcdhInitialPublicKey].
|
||||||
*/
|
*/
|
||||||
internal interface EcdhInitialPublicKeyUpdater {
|
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>
|
companion object : ComponentKey<EcdhInitialPublicKeyUpdater>
|
||||||
}
|
}
|
||||||
@ -67,19 +66,15 @@ internal class EcdhInitialPublicKeyUpdaterImpl(
|
|||||||
val keyVer: Int
|
val keyVer: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
companion object {
|
var qqEcdh: QQEcdh? = null
|
||||||
val json = Json {}
|
override fun getQQEcdh(): QQEcdh {
|
||||||
}
|
if (qqEcdh == null) {
|
||||||
|
error("Calling getQQEcdh without calling refreshInitialPublicKeyAndApplyEcdh")
|
||||||
var ecdhWithPublicKey: ECDHWithPublicKey? = null
|
|
||||||
override fun getECDHWithPublicKey(): ECDHWithPublicKey {
|
|
||||||
if (ecdhWithPublicKey == null) {
|
|
||||||
error("Calling getECDHWithPublicKey without calling refreshInitialPublicKeyAndApplyECDH")
|
|
||||||
}
|
}
|
||||||
return ecdhWithPublicKey!!
|
return qqEcdh!!
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun refreshInitialPublicKeyAndApplyECDH() {
|
override suspend fun refreshInitialPublicKeyAndApplyEcdh() {
|
||||||
|
|
||||||
val initialPublicKey = kotlin.runCatching {
|
val initialPublicKey = kotlin.runCatching {
|
||||||
val currentPublicKey = bot.client.ecdhInitialPublicKey
|
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}")
|
.get("https://keyrotate.qq.com/rotate_key?cipher_suite_ver=305&uin=${bot.client.uin}")
|
||||||
.bodyAsText()
|
.bodyAsText()
|
||||||
}
|
}
|
||||||
val resp = json.decodeFromString(ServerRespPOJO.serializer(), respStr)
|
val resp = Json.decodeFromString(ServerRespPOJO.serializer(), respStr)
|
||||||
resp.pubKeyMeta.let { meta ->
|
resp.pubKeyMeta.let { meta ->
|
||||||
val isValid = ECDH.verifyPublicKey(
|
val key = QQEcdhInitialPublicKey(meta.keyVer, meta.pubKey, currentTimeSeconds() + resp.querySpan)
|
||||||
version = meta.keyVer,
|
check(key.verify(meta.pubKeySign)) { "Ecdh public key which from server is invalid" }
|
||||||
publicKey = meta.pubKey,
|
|
||||||
publicKeySign = meta.pubKeySign
|
|
||||||
)
|
|
||||||
check(isValid) { "Ecdh public key which from server is invalid" }
|
|
||||||
logger.info("Successfully fetched ecdh public key from server.")
|
logger.info("Successfully fetched ecdh public key from server.")
|
||||||
ECDHInitialPublicKey(meta.keyVer, meta.pubKey, currentTimeSeconds() + resp.querySpan)
|
key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
logger.error("Failed to fetch ECDH public key from server, using default key instead", it)
|
logger.error("Failed to fetch ECDH public key from server, using default key instead", it)
|
||||||
defaultInitialPublicKey
|
QQEcdhInitialPublicKey.default
|
||||||
}
|
}
|
||||||
bot.client.ecdhInitialPublicKey = initialPublicKey
|
bot.client.ecdhInitialPublicKey = initialPublicKey
|
||||||
ecdhWithPublicKey = ECDHWithPublicKey(initialPublicKey)
|
qqEcdh = QQEcdh(initialPublicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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.components.PacketCodecException.Kind.*
|
||||||
import net.mamoe.mirai.internal.network.handler.selector.NetworkException
|
import net.mamoe.mirai.internal.network.handler.selector.NetworkException
|
||||||
import net.mamoe.mirai.internal.network.protocol.packet.*
|
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.TEA
|
||||||
import net.mamoe.mirai.internal.utils.crypto.adjustToPublicKey
|
|
||||||
import net.mamoe.mirai.utils.*
|
import net.mamoe.mirai.utils.*
|
||||||
|
|
||||||
|
|
||||||
@ -254,20 +254,20 @@ internal class PacketCodecImpl : PacketCodec {
|
|||||||
val encryptionMethod = this.readUShort().toInt()
|
val encryptionMethod = this.readUShort().toInt()
|
||||||
|
|
||||||
this.discardExact(1)
|
this.discardExact(1)
|
||||||
val ecdhWithPublicKey =
|
val qqEcdh =
|
||||||
(client as QQAndroidClient).bot.components[EcdhInitialPublicKeyUpdater].getECDHWithPublicKey()
|
(client as QQAndroidClient).bot.components[EcdhInitialPublicKeyUpdater].getQQEcdh()
|
||||||
return when (encryptionMethod) {
|
return when (encryptionMethod) {
|
||||||
4 -> {
|
4 -> {
|
||||||
val size = (this.remaining - 1).toInt()
|
val size = (this.remaining - 1).toInt()
|
||||||
val data =
|
val data =
|
||||||
TEA.decrypt(
|
TEA.decrypt(
|
||||||
this.readBytes(),
|
this.readBytes(),
|
||||||
ecdhWithPublicKey.keyPair.maskedShareKey,
|
qqEcdh.initialQQShareKey,
|
||||||
length = size
|
length = size
|
||||||
)
|
)
|
||||||
|
|
||||||
val peerShareKey =
|
val peerShareKey =
|
||||||
ecdhWithPublicKey.calculateShareKeyByPeerPublicKey(readUShortLVByteArray().adjustToPublicKey())
|
qqEcdh.calculateQQShareKey(Ecdh.Instance.importPublicKey(readUShortLVByteArray()))
|
||||||
TEA.decrypt(data, peerShareKey)
|
TEA.decrypt(data, peerShareKey)
|
||||||
}
|
}
|
||||||
3 -> {
|
3 -> {
|
||||||
@ -285,7 +285,7 @@ internal class PacketCodecImpl : PacketCodec {
|
|||||||
val byteArrayBuffer = this.readBytes(size)
|
val byteArrayBuffer = this.readBytes(size)
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
TEA.decrypt(byteArrayBuffer, ecdhWithPublicKey.keyPair.maskedShareKey, length = size)
|
TEA.decrypt(byteArrayBuffer, qqEcdh.initialQQShareKey, length = size)
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
TEA.decrypt(byteArrayBuffer, client.randomKey, length = size)
|
TEA.decrypt(byteArrayBuffer, client.randomKey, length = size)
|
||||||
}
|
}
|
||||||
|
@ -135,7 +135,7 @@ internal class SsoProcessorImpl(
|
|||||||
components[BdhSessionSyncer].loadServerListFromCache()
|
components[BdhSessionSyncer].loadServerListFromCache()
|
||||||
try {
|
try {
|
||||||
if (client.wLoginSigInfoInitialized) {
|
if (client.wLoginSigInfoInitialized) {
|
||||||
ssoContext.bot.components[EcdhInitialPublicKeyUpdater].refreshInitialPublicKeyAndApplyECDH()
|
ssoContext.bot.components[EcdhInitialPublicKeyUpdater].refreshInitialPublicKeyAndApplyEcdh()
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
FastLoginImpl(handler).doLogin()
|
FastLoginImpl(handler).doLogin()
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
@ -144,7 +144,7 @@ internal class SsoProcessorImpl(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
client = createClient(ssoContext.bot)
|
client = createClient(ssoContext.bot)
|
||||||
ssoContext.bot.components[EcdhInitialPublicKeyUpdater].refreshInitialPublicKeyAndApplyECDH()
|
ssoContext.bot.components[EcdhInitialPublicKeyUpdater].refreshInitialPublicKeyAndApplyEcdh()
|
||||||
SlowLoginImpl(handler).doLogin()
|
SlowLoginImpl(handler).doLogin()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -11,8 +11,7 @@ package net.mamoe.mirai.internal.network.protocol.packet
|
|||||||
|
|
||||||
import io.ktor.utils.io.core.*
|
import io.ktor.utils.io.core.*
|
||||||
import net.mamoe.mirai.internal.network.QQAndroidClient
|
import net.mamoe.mirai.internal.network.QQAndroidClient
|
||||||
import net.mamoe.mirai.internal.utils.crypto.ECDHKeyPair
|
import net.mamoe.mirai.internal.utils.crypto.QQEcdh
|
||||||
import net.mamoe.mirai.internal.utils.crypto.ECDHWithPublicKey
|
|
||||||
import net.mamoe.mirai.internal.utils.io.encryptAndWrite
|
import net.mamoe.mirai.internal.utils.io.encryptAndWrite
|
||||||
import net.mamoe.mirai.internal.utils.io.writeShortLVByteArray
|
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
|
override val currentLoginState: Int get() = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class EncryptMethodECDH135(override val ecdh: ECDHWithPublicKey) :
|
internal class EncryptMethodEcdh135(override val ecdh: QQEcdh) :
|
||||||
EncryptMethodECDH {
|
EncryptMethodEcdh {
|
||||||
override val id: Int get() = 135
|
override val id: Int get() = 135
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class EncryptMethodECDH7(override val ecdh: ECDHWithPublicKey) :
|
internal class EncryptMethodEcdh7(override val ecdh: QQEcdh) :
|
||||||
EncryptMethodECDH {
|
EncryptMethodEcdh {
|
||||||
override val id: Int get() = 7 // 135
|
override val id: Int get() = 7 // 135
|
||||||
}
|
}
|
||||||
|
|
||||||
internal interface EncryptMethodECDH : EncryptMethod {
|
internal interface EncryptMethodEcdh : EncryptMethod {
|
||||||
companion object {
|
companion object {
|
||||||
operator fun invoke(ecdh: ECDHWithPublicKey): EncryptMethodECDH {
|
operator fun invoke(ecdh: QQEcdh): EncryptMethodEcdh {
|
||||||
return if (ecdh.keyPair === ECDHKeyPair.DefaultStub) {
|
return if (ecdh.fallbackMode) {
|
||||||
EncryptMethodECDH135(ecdh)
|
EncryptMethodEcdh135(ecdh)
|
||||||
} else EncryptMethodECDH7(ecdh)
|
} else EncryptMethodEcdh7(ecdh)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val ecdh: ECDHWithPublicKey
|
val ecdh: QQEcdh
|
||||||
|
|
||||||
override fun makeBody(client: QQAndroidClient, body: BytePacketBuilder.() -> Unit): ByteReadPacket = buildPacket {
|
override fun makeBody(client: QQAndroidClient, body: BytePacketBuilder.() -> Unit): ByteReadPacket = buildPacket {
|
||||||
/* //new curve p-256
|
/* //new curve p-256
|
||||||
@ -97,14 +96,8 @@ internal interface EncryptMethodECDH : EncryptMethod {
|
|||||||
writeFully(client.randomKey)
|
writeFully(client.randomKey)
|
||||||
writeShort(0x0131)
|
writeShort(0x0131)
|
||||||
writeShort(ecdh.version.toShort())// public key version
|
writeShort(ecdh.version.toShort())// public key version
|
||||||
if (ecdh.keyPair === ECDHKeyPair.DefaultStub) {
|
// for p-256, drop(26). // but not really sure.
|
||||||
writeShortLVByteArray(ECDHKeyPair.DefaultStub.defaultPublicKey)
|
writeShortLVByteArray(ecdh.publicKey)
|
||||||
encryptAndWrite(ECDHKeyPair.DefaultStub.defaultShareKey, body)
|
encryptAndWrite(ecdh.initialQQShareKey, body)
|
||||||
} else {
|
|
||||||
// for p-256, drop(26). // but not really sure.
|
|
||||||
writeShortLVByteArray(ecdh.keyPair.maskedPublicKey)
|
|
||||||
|
|
||||||
encryptAndWrite(ecdh.keyPair.maskedShareKey, body)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -274,7 +274,7 @@ internal inline fun BytePacketBuilder.writeSsoPacket(
|
|||||||
|
|
||||||
internal fun BytePacketBuilder.writeOicqRequestPacket(
|
internal fun BytePacketBuilder.writeOicqRequestPacket(
|
||||||
client: QQAndroidClient,
|
client: QQAndroidClient,
|
||||||
encryptMethod: EncryptMethod = EncryptMethodECDH(client.bot.components[EcdhInitialPublicKeyUpdater].getECDHWithPublicKey()),
|
encryptMethod: EncryptMethod = EncryptMethodEcdh(client.bot.components[EcdhInitialPublicKeyUpdater].getQQEcdh()),
|
||||||
commandId: Int,
|
commandId: Int,
|
||||||
bodyBlock: BytePacketBuilder.() -> Unit
|
bodyBlock: BytePacketBuilder.() -> Unit
|
||||||
) {
|
) {
|
||||||
|
@ -1,128 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019-2022 Mamoe Technologies and contributors.
|
|
||||||
*
|
|
||||||
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
|
||||||
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
|
|
||||||
*
|
|
||||||
* https://github.com/mamoe/mirai/blob/dev/LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.mamoe.mirai.internal.utils.crypto
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.Transient
|
|
||||||
import net.mamoe.mirai.utils.hexToBytes
|
|
||||||
|
|
||||||
internal expect interface ECDHPrivateKey
|
|
||||||
|
|
||||||
internal expect interface ECDHPublicKey
|
|
||||||
|
|
||||||
internal expect class ECDHKeyPairImpl : ECDHKeyPair
|
|
||||||
|
|
||||||
internal interface ECDHKeyPair {
|
|
||||||
val privateKey: ECDHPrivateKey
|
|
||||||
val publicKey: ECDHPublicKey
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 私匙和动态公匙([ECDHInitialPublicKey]) 计算得到的 shareKey
|
|
||||||
*/
|
|
||||||
val maskedShareKey: ByteArray
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 私匙和动态公匙([ECDHInitialPublicKey]) 计算得到的 publicKey
|
|
||||||
*/
|
|
||||||
val maskedPublicKey: ByteArray
|
|
||||||
|
|
||||||
object DefaultStub : ECDHKeyPair {
|
|
||||||
val defaultPublicKey =
|
|
||||||
"04edb8906046f5bfbe9abbc5a88b37d70a6006bfbabc1f0cd49dfb33505e63efc5d78ee4e0a4595033b93d02096dcd3190279211f7b4f6785079e19004aa0e03bc".hexToBytes()
|
|
||||||
val defaultShareKey = "c129edba736f4909ecc4ab8e010f46a3".hexToBytes()
|
|
||||||
|
|
||||||
override val privateKey: Nothing get() = error("stub!")
|
|
||||||
override val publicKey: Nothing get() = error("stub!")
|
|
||||||
override val maskedShareKey: ByteArray get() = defaultShareKey
|
|
||||||
override val maskedPublicKey: ByteArray
|
|
||||||
get() = defaultPublicKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal expect class ECDH(keyPair: ECDHKeyPair) {
|
|
||||||
val keyPair: ECDHKeyPair
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 由 [keyPair] 的私匙和 [peerPublicKey] 计算 shareKey
|
|
||||||
*/
|
|
||||||
fun calculateShareKeyByPeerPublicKey(peerPublicKey: ECDHPublicKey): ByteArray
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val isECDHAvailable: Boolean
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This API is platform dependent.
|
|
||||||
* On JVM you need to add `signHead`,
|
|
||||||
* but on Native you need to provide a key with initial byte value 0x04 and of 65 bytes' length.
|
|
||||||
*/
|
|
||||||
fun constructPublicKey(key: ByteArray): ECDHPublicKey
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 由完整的 rsaKey 校验 publicKey
|
|
||||||
*/
|
|
||||||
fun verifyPublicKey(version: Int, publicKey: String, publicKeySign: String): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成随机密匙对
|
|
||||||
*/
|
|
||||||
fun generateKeyPair(initialPublicKey: ECDHPublicKey = defaultInitialPublicKey.key): ECDHKeyPair
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 由一对密匙计算服务器需要的 shareKey
|
|
||||||
*/
|
|
||||||
fun calculateShareKey(privateKey: ECDHPrivateKey, publicKey: ECDHPublicKey): ByteArray
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("FunctionName")
|
|
||||||
internal fun ecdhWithPublicKey(initialPublicKey: ECDHInitialPublicKey = defaultInitialPublicKey): ECDHWithPublicKey =
|
|
||||||
ECDHWithPublicKey(initialPublicKey)
|
|
||||||
|
|
||||||
internal data class ECDHWithPublicKey(private val initialPublicKey: ECDHInitialPublicKey = defaultInitialPublicKey) {
|
|
||||||
private val ecdhInstance: ECDH = ECDH(ECDH.generateKeyPair(initialPublicKey.key))
|
|
||||||
val version: Int = initialPublicKey.version
|
|
||||||
val keyPair: ECDHKeyPair = ecdhInstance.keyPair
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 由 [keyPair] 的私匙和 [peerPublicKey] 计算 shareKey
|
|
||||||
*/
|
|
||||||
fun calculateShareKeyByPeerPublicKey(peerPublicKey: ECDHPublicKey): ByteArray =
|
|
||||||
ecdhInstance.calculateShareKeyByPeerPublicKey(peerPublicKey)
|
|
||||||
|
|
||||||
}
|
|
||||||
// gen by p-256
|
|
||||||
//3059301306072A8648CE3D020106082A8648CE3D03010703420004FA540CB3F755D0A6572338777A4D0BEAFA86664D53040B27331CBF1B7F3C226CE8A1C05EFA9028F85510B103D8175172895C9F9FE4C80A47894BCA2BE569BFCB
|
|
||||||
//3059301306072A8648CE3D020106082A8648CE3D03010703420004949D41D7C14B92F0CB94B6232FB87BA51B0D5AB661FBAF95599A97472FFC4F50BC8CEC5865E79DB3782459A6E9A2298954CD198A25274CEEA8F925342D763D62
|
|
||||||
|
|
||||||
/*
|
|
||||||
// p-256
|
|
||||||
get() = ECDH.constructPublicKey(
|
|
||||||
("3059301306072A8648CE3D020106082A8648CE3D03010703420004" +
|
|
||||||
"EBCA94D733E399B2DB96EACDD3F69A8BB0F74224E2B44E3357812211D2E62EFB" +
|
|
||||||
"C91BB553098E25E33A799ADC7F76FEB208DA7C6522CDB0719A305180CC54A82E"
|
|
||||||
).chunkedHexToBytes()
|
|
||||||
)
|
|
||||||
* */
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
internal data class ECDHInitialPublicKey(val version: Int = 1, val keyStr: String, val expireTime: Long = 0) {
|
|
||||||
@Transient
|
|
||||||
internal val key: ECDHPublicKey = keyStr.hexToBytes().adjustToPublicKey()
|
|
||||||
}
|
|
||||||
|
|
||||||
internal val defaultInitialPublicKey: ECDHInitialPublicKey by lazy { ECDHInitialPublicKey(keyStr = "04EBCA94D733E399B2DB96EACDD3F69A8BB0F74224E2B44E3357812211D2E62EFBC91BB553098E25E33A799ADC7F76FEB208DA7C6522CDB0719A305180CC54A82E") }
|
|
||||||
|
|
||||||
|
|
||||||
internal expect fun ByteArray.adjustToPublicKey(): ECDHPublicKey
|
|
||||||
|
|
||||||
internal val ECDH.Companion.curveName get() = "prime256v1" // p-256
|
|
44
mirai-core/src/commonMain/kotlin/utils/crypto/Ecdh.kt
Normal file
44
mirai-core/src/commonMain/kotlin/utils/crypto/Ecdh.kt
Normal 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<*, *>
|
60
mirai-core/src/commonMain/kotlin/utils/crypto/QQEcdh.kt
Normal file
60
mirai-core/src/commonMain/kotlin/utils/crypto/QQEcdh.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
@ -15,29 +15,31 @@ import kotlin.test.Test
|
|||||||
import kotlin.test.assertContentEquals
|
import kotlin.test.assertContentEquals
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
internal class ECDHTest : AbstractTest() {
|
internal class EcdhTest : AbstractTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `can generate key pair`() {
|
fun `can generate key pair`() {
|
||||||
val alice = ECDH.generateKeyPair()
|
val alice = Ecdh.Instance.generateKeyPair()
|
||||||
val bob = ECDH.generateKeyPair()
|
val bob = Ecdh.Instance.generateKeyPair()
|
||||||
|
|
||||||
val aliceSecret = ECDH.calculateShareKey(alice.privateKey, bob.publicKey)
|
val aliceSecret = Ecdh.Instance.calculateShareKey(bob.public, alice.private)
|
||||||
val bobSecret = ECDH.calculateShareKey(bob.privateKey, alice.publicKey)
|
val bobSecret = Ecdh.Instance.calculateShareKey(alice.public, bob.private)
|
||||||
|
|
||||||
println(aliceSecret.toUHexString())
|
println(aliceSecret.toUHexString())
|
||||||
assertContentEquals(aliceSecret, bobSecret)
|
assertContentEquals(aliceSecret, bobSecret)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `can get masked keys`() {
|
fun `can export and import public keys`() {
|
||||||
val alice = ECDH.generateKeyPair()
|
val alice = Ecdh.Instance.generateKeyPair()
|
||||||
|
|
||||||
println(alice)
|
println(alice)
|
||||||
val maskedPublicKey = alice.maskedPublicKey
|
val publicKey = Ecdh.Instance.exportPublicKey(alice.public)
|
||||||
println(maskedPublicKey.toUHexString())
|
println(publicKey.toUHexString())
|
||||||
assertEquals(0x04, maskedPublicKey.first())
|
assertEquals(0x04, publicKey.first())
|
||||||
println(alice.maskedShareKey.toUHexString())
|
|
||||||
|
val importedAlicePubKey = Ecdh.Instance.importPublicKey(publicKey)
|
||||||
|
assertEquals(alice.public, importedAlicePubKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
@ -1,49 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019-2022 Mamoe Technologies and contributors.
|
|
||||||
*
|
|
||||||
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
|
||||||
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
|
|
||||||
*
|
|
||||||
* https://github.com/mamoe/mirai/blob/dev/LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
@file:JvmName("ECDHKt_jvmBase")
|
|
||||||
|
|
||||||
package net.mamoe.mirai.internal.utils.crypto
|
|
||||||
|
|
||||||
import net.mamoe.mirai.utils.decodeBase64
|
|
||||||
import net.mamoe.mirai.utils.hexToBytes
|
|
||||||
import java.security.KeyFactory
|
|
||||||
import java.security.KeyPair
|
|
||||||
import java.security.PrivateKey
|
|
||||||
import java.security.PublicKey
|
|
||||||
import java.security.spec.X509EncodedKeySpec
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Suppress("ACTUAL_WITHOUT_EXPECT")
|
|
||||||
internal actual typealias ECDHPrivateKey = PrivateKey
|
|
||||||
@Suppress("ACTUAL_WITHOUT_EXPECT")
|
|
||||||
internal actual typealias ECDHPublicKey = PublicKey
|
|
||||||
|
|
||||||
internal actual class ECDHKeyPairImpl(
|
|
||||||
private val delegate: KeyPair,
|
|
||||||
initialPublicKey: ECDHPublicKey = defaultInitialPublicKey.key
|
|
||||||
) : ECDHKeyPair {
|
|
||||||
override val privateKey: ECDHPrivateKey get() = delegate.private
|
|
||||||
override val publicKey: ECDHPublicKey get() = delegate.public
|
|
||||||
override val maskedPublicKey: ByteArray by lazy { publicKey.encoded.copyOfRange(26, 91) }
|
|
||||||
override val maskedShareKey: ByteArray by lazy { ECDH.calculateShareKey(privateKey, initialPublicKey) }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
internal val publicKeyForVerify: ECDHPublicKey by lazy {
|
|
||||||
KeyFactory.getInstance("RSA")
|
|
||||||
.generatePublic(X509EncodedKeySpec("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuJTW4abQJXeVdAODw1CamZH4QJZChyT08ribet1Gp0wpSabIgyKFZAOxeArcCbknKyBrRY3FFI9HgY1AyItH8DOUe6ajDEb6c+vrgjgeCiOiCVyum4lI5Fmp38iHKH14xap6xGaXcBccdOZNzGT82sPDM2Oc6QYSZpfs8EO7TYT7KSB2gaHz99RQ4A/Lel1Vw0krk+DescN6TgRCaXjSGn268jD7lOO23x5JS1mavsUJtOZpXkK9GqCGSTCTbCwZhI33CpwdQ2EHLhiP5RaXZCio6lksu+d8sKTWU1eEiEb3cQ7nuZXLYH7leeYFoPtbFV4RicIWp0/YG+RP7rLPCwIDAQAB".decodeBase64()))
|
|
||||||
}
|
|
||||||
|
|
||||||
private val signHead = "3059301306072a8648ce3d020106082a8648ce3d030107034200".hexToBytes()
|
|
||||||
|
|
||||||
internal actual fun ByteArray.adjustToPublicKey(): ECDHPublicKey {
|
|
||||||
return ECDH.constructPublicKey(signHead + this)
|
|
||||||
}
|
|
91
mirai-core/src/jvmBaseMain/kotlin/utils/crypto/JceEcdh.kt
Normal file
91
mirai-core/src/jvmBaseMain/kotlin/utils/crypto/JceEcdh.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019-2022 Mamoe Technologies and contributors.
|
||||||
|
*
|
||||||
|
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
||||||
|
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
|
||||||
|
*
|
||||||
|
* https://github.com/mamoe/mirai/blob/dev/LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.mamoe.mirai.internal.utils.crypto
|
||||||
|
|
||||||
|
import java.security.AlgorithmParameters
|
||||||
|
import java.security.KeyFactory
|
||||||
|
import java.security.KeyPairGenerator
|
||||||
|
import java.security.Provider
|
||||||
|
import javax.crypto.KeyAgreement
|
||||||
|
|
||||||
|
internal class JceEcdhWithProvider(val provider: Provider): JceEcdh() {
|
||||||
|
override fun newECKeyPairGenerator() = KeyPairGenerator.getInstance("EC", provider)
|
||||||
|
override fun newECKeyFactory() = KeyFactory.getInstance("EC", provider)
|
||||||
|
override fun newECAlgorithmParameters() = AlgorithmParameters.getInstance("EC", provider)
|
||||||
|
override fun newECDHKeyAgreement() = KeyAgreement.getInstance("ECDH", provider)
|
||||||
|
}
|
28
mirai-core/src/jvmBaseMain/kotlin/utils/crypto/QQEcdhJvm.kt
Normal file
28
mirai-core/src/jvmBaseMain/kotlin/utils/crypto/QQEcdhJvm.kt
Normal 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())
|
||||||
|
}
|
@ -1,93 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019-2022 Mamoe Technologies and contributors.
|
|
||||||
*
|
|
||||||
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
|
||||||
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
|
|
||||||
*
|
|
||||||
* https://github.com/mamoe/mirai/blob/dev/LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.mamoe.mirai.internal.utils.crypto
|
|
||||||
|
|
||||||
import net.mamoe.mirai.utils.decodeBase64
|
|
||||||
import net.mamoe.mirai.utils.md5
|
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
|
||||||
import java.security.KeyFactory
|
|
||||||
import java.security.KeyPairGenerator
|
|
||||||
import java.security.Security
|
|
||||||
import java.security.Signature
|
|
||||||
import java.security.spec.ECGenParameterSpec
|
|
||||||
import java.security.spec.X509EncodedKeySpec
|
|
||||||
import javax.crypto.KeyAgreement
|
|
||||||
|
|
||||||
internal actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
|
|
||||||
actual companion object {
|
|
||||||
|
|
||||||
actual val isECDHAvailable: Boolean
|
|
||||||
|
|
||||||
init {
|
|
||||||
isECDHAvailable = kotlin.runCatching {
|
|
||||||
fun testECDH() {
|
|
||||||
ECDHKeyPairImpl(
|
|
||||||
KeyPairGenerator.getInstance("ECDH")
|
|
||||||
.also { it.initialize(ECGenParameterSpec(curveName)) }
|
|
||||||
.genKeyPair()).let {
|
|
||||||
calculateShareKey(it.privateKey, it.publicKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (kotlin.runCatching { testECDH() }.isSuccess) {
|
|
||||||
return@runCatching
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) != null) {
|
|
||||||
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
|
|
||||||
}
|
|
||||||
Security.addProvider(BouncyCastleProvider())
|
|
||||||
testECDH()
|
|
||||||
}.onFailure {
|
|
||||||
it.printStackTrace()
|
|
||||||
}.isSuccess
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun generateKeyPair(initialPublicKey: ECDHPublicKey): ECDHKeyPair {
|
|
||||||
if (!isECDHAvailable) {
|
|
||||||
return ECDHKeyPair.DefaultStub
|
|
||||||
}
|
|
||||||
return ECDHKeyPairImpl(
|
|
||||||
KeyPairGenerator.getInstance("ECDH")
|
|
||||||
.also { it.initialize(ECGenParameterSpec(curveName)) }
|
|
||||||
.genKeyPair(), initialPublicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun verifyPublicKey(version: Int, publicKey: String, publicKeySign: String): Boolean {
|
|
||||||
val arrayForVerify = "305$version$publicKey".toByteArray()
|
|
||||||
val signInstance = Signature.getInstance("SHA256WithRSA")
|
|
||||||
signInstance.initVerify(publicKeyForVerify)
|
|
||||||
signInstance.update(arrayForVerify)
|
|
||||||
return signInstance.verify(publicKeySign.decodeBase64())
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun calculateShareKey(
|
|
||||||
privateKey: ECDHPrivateKey,
|
|
||||||
publicKey: ECDHPublicKey,
|
|
||||||
): ByteArray {
|
|
||||||
val instance = KeyAgreement.getInstance("ECDH", "BC")
|
|
||||||
instance.init(privateKey)
|
|
||||||
instance.doPhase(publicKey, true)
|
|
||||||
return instance.generateSecret().copyOf(16).md5()
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun constructPublicKey(key: ByteArray): ECDHPublicKey {
|
|
||||||
return KeyFactory.getInstance("EC", "BC").generatePublic(X509EncodedKeySpec(key))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun calculateShareKeyByPeerPublicKey(peerPublicKey: ECDHPublicKey): ByteArray {
|
|
||||||
return calculateShareKey(keyPair.privateKey, peerPublicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual override fun toString(): String {
|
|
||||||
return "ECDH(keyPair=$keyPair)"
|
|
||||||
}
|
|
||||||
}
|
|
27
mirai-core/src/jvmMain/kotlin/utils/crypto/EcdhJvmDesktop.kt
Normal file
27
mirai-core/src/jvmMain/kotlin/utils/crypto/EcdhJvmDesktop.kt
Normal 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())
|
||||||
|
}
|
@ -1,233 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019-2022 Mamoe Technologies and contributors.
|
|
||||||
*
|
|
||||||
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
|
||||||
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
|
|
||||||
*
|
|
||||||
* https://github.com/mamoe/mirai/blob/dev/LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.mamoe.mirai.internal.utils.crypto
|
|
||||||
|
|
||||||
import kotlinx.cinterop.*
|
|
||||||
import net.mamoe.mirai.utils.hexToBytes
|
|
||||||
import net.mamoe.mirai.utils.md5
|
|
||||||
import net.mamoe.mirai.utils.toUHexString
|
|
||||||
import openssl.*
|
|
||||||
import platform.posix.errno
|
|
||||||
|
|
||||||
private const val curveId = NID_X9_62_prime256v1
|
|
||||||
|
|
||||||
// shared, not freed!
|
|
||||||
private val group by lazy { EC_GROUP_new_by_curve_name(curveId) ?: error("Failed to get EC_GROUP") }
|
|
||||||
|
|
||||||
private val convForm by lazy { EC_GROUP_get_point_conversion_form(group) }
|
|
||||||
|
|
||||||
// shared, not freed!
|
|
||||||
private val bnCtx by lazy { BN_CTX_new() }
|
|
||||||
|
|
||||||
// ====ATTENTION====
|
|
||||||
// Do not use [platform.posix.free] easily
|
|
||||||
// For anything allocated by OpenSSL, <type>_free or CRYPTO_free
|
|
||||||
// (the underlying of OPENSSL_free macro) should be called.
|
|
||||||
// It's more than dangerous to assume OpenSSL uses the same memory manager as general posix functions,
|
|
||||||
// easily causing memory leaking (usually on *nix) or crash (usually on Windows)
|
|
||||||
|
|
||||||
internal actual interface ECDHPublicKey : OpenSSLKey {
|
|
||||||
val encoded: ByteArray
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return It is the caller's responsibility to free this memory with a subsequent call to [EC_POINT_free]
|
|
||||||
*/
|
|
||||||
fun toPoint(): CPointer<EC_POINT>
|
|
||||||
}
|
|
||||||
|
|
||||||
internal actual interface ECDHPrivateKey : OpenSSLKey {
|
|
||||||
/**
|
|
||||||
* @return It is the caller's responsibility to free this memory with a subsequent call to [BN_free]
|
|
||||||
*/
|
|
||||||
fun toBignum(): CPointer<BIGNUM>
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class OpenSslPrivateKey(
|
|
||||||
override val hex: String, // use Kotlin's memory
|
|
||||||
) : ECDHPrivateKey {
|
|
||||||
|
|
||||||
override fun toBignum(): CPointer<BIGNUM> {
|
|
||||||
val bn = BN_new() ?: error("Failed BN_new")
|
|
||||||
val values = cValuesOf(bn)
|
|
||||||
BN_hex2bn(values, hex).let { r ->
|
|
||||||
if (r <= 0) error("Failed BN_hex2bn: $r")
|
|
||||||
}
|
|
||||||
return bn
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromKey(key: CPointer<EC_KEY>): OpenSslPrivateKey {
|
|
||||||
// Note that the private key (bignum) is associated with the key
|
|
||||||
// We can't free it, or it'll crash when EC_KEY_free
|
|
||||||
val bn = EC_KEY_get0_private_key(key) ?: error("Failed EC_KEY_get0_private_key")
|
|
||||||
val ptr = BN_bn2hex(bn) ?: error("Failed EC_POINT_bn2point")
|
|
||||||
val hex = try {
|
|
||||||
ptr.toKString()
|
|
||||||
} finally {
|
|
||||||
CRYPTO_free(ptr, "OpenSslPrivateKey.Companion.fromKey(key: CPointer<EC_KEY>)", -1)
|
|
||||||
}
|
|
||||||
return OpenSslPrivateKey(hex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal interface OpenSSLKey {
|
|
||||||
val hex: String
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class OpenSslPublicKey(override val hex: String) : ECDHPublicKey {
|
|
||||||
override val encoded: ByteArray = hex.hexToBytes()
|
|
||||||
|
|
||||||
override fun toPoint(): CPointer<EC_POINT> {
|
|
||||||
val point = EC_POINT_new(group)
|
|
||||||
EC_POINT_hex2point(group, hex, point, bnCtx) ?: error("Failed EC_POINT_hex2point")
|
|
||||||
return point!!
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromKey(key: CPointer<EC_KEY>): OpenSslPublicKey =
|
|
||||||
fromPoint(EC_KEY_get0_public_key(key) ?: error("Failed to get private key"))
|
|
||||||
|
|
||||||
fun fromPoint(point: CPointer<EC_POINT>): OpenSslPublicKey {
|
|
||||||
return OpenSslPublicKey(point.toKtHex())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal actual class ECDHKeyPairImpl(
|
|
||||||
override val privateKey: OpenSslPrivateKey,
|
|
||||||
override val publicKey: OpenSslPublicKey,
|
|
||||||
initialPublicKey: ECDHPublicKey
|
|
||||||
) : ECDHKeyPair {
|
|
||||||
|
|
||||||
override val maskedPublicKey: ByteArray by lazy { publicKey.encoded }
|
|
||||||
override val maskedShareKey: ByteArray by lazy { ECDH.calculateShareKey(privateKey, initialPublicKey) }
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromKey(
|
|
||||||
key: CPointer<EC_KEY>,
|
|
||||||
initialPublicKey: ECDHPublicKey = defaultInitialPublicKey.key
|
|
||||||
): ECDHKeyPairImpl {
|
|
||||||
return ECDHKeyPairImpl(OpenSslPrivateKey.fromKey(key), OpenSslPublicKey.fromKey(key), initialPublicKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun CPointer<EC_POINT>.toKtHex(): String {
|
|
||||||
val ptr = EC_POINT_point2hex(group, this, convForm, bnCtx) ?: error("Failed EC_POINT_point2hex")
|
|
||||||
return try {
|
|
||||||
ptr.toKString()
|
|
||||||
} finally {
|
|
||||||
CRYPTO_free(ptr, "CPointer<EC_POINT>.toKtHex()", -1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
internal actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 由 [keyPair] 的私匙和 [peerPublicKey] 计算 shareKey
|
|
||||||
*/
|
|
||||||
actual fun calculateShareKeyByPeerPublicKey(peerPublicKey: ECDHPublicKey): ByteArray {
|
|
||||||
return calculateShareKey(keyPair.privateKey, peerPublicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual companion object {
|
|
||||||
actual val isECDHAvailable: Boolean get() = true
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 由完整的 publicKey ByteArray 得到 [ECDHPublicKey]
|
|
||||||
*/
|
|
||||||
actual fun constructPublicKey(key: ByteArray): ECDHPublicKey {
|
|
||||||
val p = EC_POINT_new(group) ?: error("Failed to create EC_POINT")
|
|
||||||
|
|
||||||
// TODO: 2022/6/1 native: check memory
|
|
||||||
EC_POINT_hex2point(group, key.toUHexString("").lowercase(), p, bnCtx)
|
|
||||||
|
|
||||||
return OpenSslPublicKey.fromPoint(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 由完整的 rsaKey 校验 publicKey
|
|
||||||
*/
|
|
||||||
actual fun verifyPublicKey(
|
|
||||||
version: Int,
|
|
||||||
publicKey: String,
|
|
||||||
publicKeySign: String
|
|
||||||
): Boolean = true
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成随机密匙对
|
|
||||||
*/
|
|
||||||
actual fun generateKeyPair(initialPublicKey: ECDHPublicKey): ECDHKeyPair {
|
|
||||||
val key: CPointer<EC_KEY> = EC_KEY_new_by_curve_name(curveId)
|
|
||||||
?: throw IllegalStateException("Failed to create key curve, $errno")
|
|
||||||
try {
|
|
||||||
if (1 != EC_KEY_generate_key(key)) {
|
|
||||||
throw IllegalStateException("Failed to generate key, $errno")
|
|
||||||
}
|
|
||||||
return ECDHKeyPairImpl.fromKey(key, initialPublicKey)
|
|
||||||
} finally {
|
|
||||||
EC_KEY_free(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun calculateCanonicalShareKey(privateKey: ECDHPrivateKey, publicKey: ECDHPublicKey): ByteArray {
|
|
||||||
check(publicKey is OpenSslPublicKey)
|
|
||||||
check(privateKey is OpenSslPrivateKey)
|
|
||||||
|
|
||||||
val k = EC_KEY_new_by_curve_name(curveId) ?: error("Failed to create EC key")
|
|
||||||
try {
|
|
||||||
val privateBignum = privateKey.toBignum()
|
|
||||||
try {
|
|
||||||
EC_KEY_set_private_key(k, privateBignum).let { r ->
|
|
||||||
if (r != 1) error("Failed EC_KEY_set_private_key: $r")
|
|
||||||
}
|
|
||||||
|
|
||||||
val fieldSize = EC_GROUP_get_degree(group)
|
|
||||||
if (fieldSize <= 0) {
|
|
||||||
error("Failed EC_GROUP_get_degree: $fieldSize")
|
|
||||||
}
|
|
||||||
|
|
||||||
var secretLen = (fieldSize + 7) / 8
|
|
||||||
|
|
||||||
val publicPoint = publicKey.toPoint()
|
|
||||||
try {
|
|
||||||
ByteArray(secretLen.convert()).usePinned { pin ->
|
|
||||||
secretLen = ECDH_compute_key(pin.addressOf(0), secretLen.convert(), publicPoint, k, null)
|
|
||||||
if (secretLen <= 0) {
|
|
||||||
error("Failed to compute secret")
|
|
||||||
}
|
|
||||||
|
|
||||||
return pin.get().copyOf(secretLen)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
EC_POINT_free(publicPoint)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
BN_free(privateBignum)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
EC_KEY_free(k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun calculateShareKey(
|
|
||||||
privateKey: ECDHPrivateKey,
|
|
||||||
publicKey: ECDHPublicKey
|
|
||||||
): ByteArray = calculateCanonicalShareKey(privateKey, publicKey).copyOf(16).md5()
|
|
||||||
}
|
|
||||||
|
|
||||||
actual override fun toString(): String = "ECDH($keyPair)"
|
|
||||||
}
|
|
||||||
|
|
||||||
internal actual fun ByteArray.adjustToPublicKey(): ECDHPublicKey {
|
|
||||||
return ECDH.constructPublicKey(this)
|
|
||||||
}
|
|
13
mirai-core/src/nativeMain/kotlin/utils/crypto/EcdhNative.kt
Normal file
13
mirai-core/src/nativeMain/kotlin/utils/crypto/EcdhNative.kt
Normal 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()
|
147
mirai-core/src/nativeMain/kotlin/utils/crypto/OpenSslEcdh.kt
Normal file
147
mirai-core/src/nativeMain/kotlin/utils/crypto/OpenSslEcdh.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user