Split Socket implementation, improve performance
This commit is contained in:
318 files changed
+1109
-2722
No files matched your search
@@ -0,0 +1,21 @@
|
||||
# libmc-protocol-encrypt
|
||||
|
||||
This module implemented `ProtocolContext` and provided a `DefaultProtocolContext`.
|
||||
|
||||
## Get started
|
||||
|
||||
> `Sha1`, `RSA1024`, `AES-128-CFB8` from `cryptography-kotlin`(and its based provider).
|
||||
> `HTTP Client` from `ktor-client`
|
||||
|
||||
> Before start, you need to add a `ktor client engine` for your platform. For JVM, `ktor-client-okhttp`(JVM 1.8+)
|
||||
> or `ktor-client-java`(JVM 11+), for Linux, use `ktor-client-curl`, for Windows, use `ktor-client-winhttp`,
|
||||
> for Apple, use `ktor-client-darwin`
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
val cli = createMinecraftClient(
|
||||
// ... other paramater
|
||||
crypto = DefaultProtocolContext
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
kotlin {
|
||||
explicitApi()
|
||||
withSourcesJar()
|
||||
|
||||
linuxX64()
|
||||
linuxArm64()
|
||||
macosArm64()
|
||||
mingwX64()
|
||||
jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 }
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
api(project(":common"))
|
||||
implementation(libs.cryptography.core)
|
||||
implementation(libs.cryptography.provider.optimal)
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.network)
|
||||
}
|
||||
|
||||
jvmTest.dependencies {
|
||||
implementation(libs.ktor.client.okhttp)
|
||||
}
|
||||
|
||||
linuxTest.dependencies {
|
||||
implementation(libs.ktor.client.curl)
|
||||
}
|
||||
|
||||
mingwTest.dependencies {
|
||||
implementation(libs.ktor.client.winhttp)
|
||||
}
|
||||
|
||||
appleTest.dependencies {
|
||||
implementation(libs.ktor.client.darwin)
|
||||
}
|
||||
|
||||
commonTest.dependencies {
|
||||
implementation(kotlin("test"))
|
||||
implementation(project(":protocol"))
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright © 2026 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2026/9/7
|
||||
*/
|
||||
|
||||
|
||||
@file:OptIn(DelicateCryptographyApi::class)
|
||||
|
||||
package cn.rtast.libmc.protocol.crypto
|
||||
|
||||
import cn.rtast.libmc.crypto.NetworkCipher
|
||||
import dev.whyoleg.cryptography.DelicateCryptographyApi
|
||||
import dev.whyoleg.cryptography.algorithms.AES
|
||||
|
||||
public class AesCFB8Cipher(sharedKey: ByteArray) : NetworkCipher {
|
||||
private val encryptIv = sharedKey.copyOf()
|
||||
private val decryptIv = sharedKey.copyOf()
|
||||
|
||||
private val cipher = provider.get(AES.CFB8)
|
||||
.keyDecoder()
|
||||
.decodeFromByteArrayBlocking(AES.Key.Format.RAW, sharedKey)
|
||||
.cipher()
|
||||
|
||||
override fun encrypt(buffer: ByteArray, offset: Int, length: Int) {
|
||||
val plaintext = buffer.copyOfRange(offset, offset + length)
|
||||
val ciphertext = cipher.encryptWithIvBlocking(encryptIv, plaintext)
|
||||
ciphertext.copyInto(buffer, destinationOffset = offset)
|
||||
updateIv(encryptIv, ciphertext)
|
||||
}
|
||||
|
||||
override fun decrypt(buffer: ByteArray, offset: Int, length: Int) {
|
||||
val ciphertext = buffer.copyOfRange(offset, offset + length)
|
||||
val plaintext = cipher.decryptWithIvBlocking(decryptIv, ciphertext)
|
||||
plaintext.copyInto(buffer, destinationOffset = offset)
|
||||
updateIv(decryptIv, ciphertext)
|
||||
}
|
||||
|
||||
private fun updateIv(iv: ByteArray, ciphertext: ByteArray) {
|
||||
val len = ciphertext.size
|
||||
if (len >= iv.size) {
|
||||
ciphertext.copyInto(iv, destinationOffset = 0, startIndex = len - iv.size, endIndex = len)
|
||||
} else {
|
||||
iv.copyInto(iv, destinationOffset = 0, startIndex = len, endIndex = iv.size)
|
||||
ciphertext.copyInto(iv, destinationOffset = iv.size - len)
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright © 2026 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2026/9/7
|
||||
*/
|
||||
|
||||
|
||||
package cn.rtast.libmc.protocol.crypto
|
||||
|
||||
import cn.rtast.libmc.crypto.AuthenticationProvider
|
||||
import cn.rtast.libmc.crypto.ProtocolContextBuilder
|
||||
import cn.rtast.libmc.crypto.RSA1024Encryptor
|
||||
import cn.rtast.libmc.crypto.Sha1Hasher
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
|
||||
private val httpClient = HttpClient()
|
||||
|
||||
public val DefaultProtocolContext: ProtocolContextBuilder.() -> Unit = {
|
||||
rsaEncryptor = RSA1024Encryptor { key, data -> rsaEncrypt(key, data) }
|
||||
sha1Hasher = Sha1Hasher { serverId, secretKey, publicKey -> minecraftServerIdHash(serverId, secretKey, publicKey) }
|
||||
cipherFactory = { key -> AesCFB8Cipher(key) }
|
||||
authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash ->
|
||||
val status = httpClient.post(url) {
|
||||
headers { header("Content-Type", "application/json") }
|
||||
setBody("{\"accessToken\":\"$accessToken\", \"selectedProfile\":\"$uuid\", \"serverId\":\"$serverIdHash\"}")
|
||||
}.status
|
||||
require(status == HttpStatusCode.NoContent)
|
||||
}
|
||||
socketEngine = KtorNetworkEngine()
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright © 2026 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2026/9/8
|
||||
*/
|
||||
|
||||
|
||||
package cn.rtast.libmc.protocol.crypto
|
||||
|
||||
import cn.rtast.libmc.network.RawSocket
|
||||
import cn.rtast.libmc.network.ReadChannel
|
||||
import cn.rtast.libmc.network.SocketEngine
|
||||
import cn.rtast.libmc.network.WriteChannel
|
||||
import io.ktor.network.selector.*
|
||||
import io.ktor.network.sockets.*
|
||||
import io.ktor.utils.io.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
|
||||
public class KtorNetworkEngine : SocketEngine {
|
||||
override fun create(host: String, port: Int): RawSocket = KtorNetworkSocket(host, port)
|
||||
}
|
||||
|
||||
public class KtorNetworkSocket(private val host: String, private val port: Int) : RawSocket {
|
||||
private val sm = SelectorManager(Dispatchers.IO)
|
||||
private lateinit var socket: Socket
|
||||
|
||||
override suspend fun connect(): Unit = run { socket = aSocket(sm).tcp().connect(host, port) }
|
||||
override fun openReadChannel(): ReadChannel = KtorReadChannel(socket.openReadChannel())
|
||||
override fun openWriteChannel(): WriteChannel = KtorWriteChannel(socket.openWriteChannel())
|
||||
override fun close() {
|
||||
socket.close()
|
||||
sm.close()
|
||||
}
|
||||
}
|
||||
|
||||
public class KtorReadChannel(private val readChannel: ByteReadChannel) : ReadChannel {
|
||||
override suspend fun readByte(): Byte = readChannel.readByte()
|
||||
override suspend fun readBytes(length: Int): ByteArray = readChannel.readByteArray(length)
|
||||
override suspend fun readFully(out: ByteArray, start: Int, end: Int): Unit =
|
||||
readChannel.readFully(out, start, end)
|
||||
}
|
||||
|
||||
public class KtorWriteChannel(private val writeChannel: ByteWriteChannel) : WriteChannel {
|
||||
override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) {
|
||||
writeChannel.writeFully(value, startIndex, endIndex)
|
||||
}
|
||||
|
||||
override suspend fun flush(): Unit = writeChannel.flush()
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright © 2026 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2026/9/7
|
||||
*/
|
||||
|
||||
|
||||
@file:OptIn(DelicateCryptographyApi::class)
|
||||
|
||||
package cn.rtast.libmc.protocol.crypto
|
||||
|
||||
import dev.whyoleg.cryptography.CryptographyProvider
|
||||
import dev.whyoleg.cryptography.DelicateCryptographyApi
|
||||
import dev.whyoleg.cryptography.algorithms.RSA
|
||||
import dev.whyoleg.cryptography.algorithms.SHA1
|
||||
|
||||
internal val provider = CryptographyProvider.Default
|
||||
|
||||
public fun rsaEncrypt(publicKeyBytes: ByteArray, data: ByteArray): ByteArray {
|
||||
val provider = CryptographyProvider.Default
|
||||
val rsa = provider.get(RSA.PKCS1)
|
||||
val publicKey = rsa.publicKeyDecoder(SHA1)
|
||||
.decodeFromByteArrayBlocking(RSA.PublicKey.Format.DER, publicKeyBytes)
|
||||
return publicKey.encryptor().encryptBlocking(data)
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright © 2026 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2026/9/7
|
||||
*/
|
||||
|
||||
|
||||
@file:OptIn(DelicateCryptographyApi::class)
|
||||
|
||||
package cn.rtast.libmc.protocol.crypto
|
||||
|
||||
import dev.whyoleg.cryptography.DelicateCryptographyApi
|
||||
import dev.whyoleg.cryptography.algorithms.SHA1
|
||||
|
||||
public fun minecraftServerIdHash(serverId: String, secretKey: ByteArray, publicKey: ByteArray): String {
|
||||
val serverIdBytes = serverId.encodeToByteArray()
|
||||
for (b in serverIdBytes) require((b.toInt() and 0xFF) <= 0x7F) { "serverId contains non-US-ASCII character" }
|
||||
val data = serverIdBytes + secretKey + publicKey
|
||||
val hash = provider.get(SHA1).hasher().hashBlocking(data)
|
||||
return mcDigestToString(hash)
|
||||
}
|
||||
|
||||
private fun mcDigestToString(digest: ByteArray): String {
|
||||
val isNegative = (digest[0].toInt() and 0x80) != 0
|
||||
val bytes = if (isNegative) twosComplement(digest) else digest
|
||||
var hex = bytes.joinToString("") { (it.toInt() and 0xFF).toString(16).padStart(2, '0') }
|
||||
hex = hex.trimStart('0')
|
||||
if (hex.isEmpty()) hex = "0"
|
||||
return if (isNegative) "-$hex" else hex
|
||||
}
|
||||
|
||||
private fun twosComplement(bytes: ByteArray): ByteArray {
|
||||
val result = ByteArray(bytes.size)
|
||||
var carry = 1
|
||||
for (i in bytes.size - 1 downTo 0) {
|
||||
val inverted = (bytes[i].toInt().inv() and 0xFF) + carry
|
||||
result[i] = (inverted and 0xFF).toByte()
|
||||
carry = inverted ushr 8
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright © 2026 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2026/9/7
|
||||
*/
|
||||
|
||||
package test
|
||||
|
||||
import cn.rtast.libmc.packet.MinecraftPacket
|
||||
import cn.rtast.libmc.protocol.client.createMinecraftClient
|
||||
import cn.rtast.libmc.protocol.crypto.DefaultProtocolContext
|
||||
import kotlinx.coroutines.launch
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
|
||||
class TestJvmClient {
|
||||
|
||||
val accessToken = File("src/commonTest/resources/accessToken.txt").readText()
|
||||
|
||||
@Test
|
||||
fun `test default protocol context`() {
|
||||
val cli = createMinecraftClient(
|
||||
"127.0.0.1",
|
||||
25565,
|
||||
"RTAkland",
|
||||
// generateOfflineUuid("RTAkland"),
|
||||
Uuid.parse("bb033844-e68e-4909-a636-1a5d1821ddc4"),
|
||||
// null,
|
||||
accessToken,
|
||||
contextBuilder = DefaultProtocolContext
|
||||
)
|
||||
// cli.on<ClientboundSystemChatMessagePacket> { println(it) }
|
||||
// cli.on<ClientboundLoginSuccessPacket> { println(it) }
|
||||
cli.onPacket<MinecraftPacket> { println(it) }
|
||||
cli.launch { cli.connect() }
|
||||
while (true) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user