Update all codec to suspend; added a default ProtocolCryptoContext implementation

This commit is contained in:
2026-09-07 07:40:08 +08:00
parent 687039f69a
commit 641df674e4
296 files changed
+2199 -1818

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
# libmc-protocol-encrypt
This module implemented `ProtocolCryptoContext` and provided a `DefaultProtocolCryptoContext`.
## 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 = DefaultProtocolCryptoContext
)
}
```
+43
View File
@@ -0,0 +1,43 @@
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)
}
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)
}
}
}
@@ -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.common.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 suspend 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 suspend 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)
}
}
}
@@ -0,0 +1,31 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/7
*/
package cn.rtast.libmc.protocol.crypto
import cn.rtast.libmc.common.crypto.AuthenticationProvider
import cn.rtast.libmc.common.crypto.ProtocolContextBuilder
import cn.rtast.libmc.common.crypto.RSA1024Encryptor
import cn.rtast.libmc.common.crypto.Sha1Hasher
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.http.*
private val httpClient = HttpClient()
public val DefaultProtocolCryptoContext: 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)
}
}
@@ -0,0 +1,24 @@
/*
* 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 suspend fun rsaEncrypt(publicKeyBytes: ByteArray, data: ByteArray): ByteArray {
val provider = CryptographyProvider.Default
val rsa = provider.get(RSA.PKCS1)
val publicKey = rsa.publicKeyDecoder(SHA1).decodeFromByteArray(RSA.PublicKey.Format.DER, publicKeyBytes)
return publicKey.encryptor().encrypt(data)
}
@@ -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 suspend 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().hash(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.protocol.client.createMinecraftClient
import cn.rtast.libmc.protocol.crypto.DefaultProtocolCryptoContext
import cn.rtast.libmc.protocol.packet.login.clientbound.ClientboundLoginSuccessPacket
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundSystemChatMessagePacket
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,
crypto = DefaultProtocolCryptoContext
)
cli.on<ClientboundSystemChatMessagePacket> { println(it) }
cli.on<ClientboundLoginSuccessPacket> { println(it) }
cli.launch { cli.connect() }
while (true) {
}
}
}