Fix some clientbound message bugs

send signed message{wip}
This commit is contained in:
2026-09-12 01:03:05 +08:00
parent b2ea685a9a
commit 480d0089e6
37 files changed
+689 -1206

No files matched your search

-32
View File
@@ -1,32 +0,0 @@
Starting with version `0.2.0` (`26.2-0.2.0`), `libmc` includes a built-in cryptography implementation.
It uses the JDK's built-in cryptographic APIs on the JVM platform and a pure-Kotlin implementation
on `kotlin-native` platforms. However, due to a lack of low-level optimizations, performance results
in encoding and decoding tests are significantly slower than JDK's built-in implementations
A simplified table below shows the benchmark results:
| Algorithm | Data Size (Bytes) | libmc's Built-in (ops/s) | JDK's Built-in (ops/s) | Gap |
|:-------------------------|:------------------|:-------------------------|:-----------------------|:-------------------------------------------|
| **AES-128-CFB8 Encrypt** | 32 | 112,554.27 | 1,527,235.11 | **~13.6 times slower than JDK's built-in** |
| | 1,024 | 2,976.95 | 49,842.94 | **~16.7 times slower than JDK's built-in** |
| | 65,536 | 45.04 | 773.46 | **~17.2 times slower than JDK's built-in** |
| **AES-128-CFB8 Decrypt** | 32 | 115,031.69 | 1,515,218.15 | **~13.2 times slower than JDK's built-in** |
| | 1,024 | 2,925.74 | 49,868.55 | **~17.0 times slower than JDK's built-in** |
| | 65,536 | 48.47 | 777.11 | **~16.0 times slower than JDK's built-in** |
| **RSA-1024 Encrypt** | 16 | 163.87 | 91,600.51 | **~559 times slower than JDK's built-in** |
| | 64 | 158.27 | 93,042.81 | **~587 times slower than JDK's built-in** |
| | 117 | 160.95 | 94,826.94 | **~589 times slower than JDK's built-in** |
| **SHA-1 Hashing** | 64 | 2,134,974.72 | 11,609,497.92 | **~5.4 times slower than JDK's built-in** |
| | 1,024 | 259,804.80 | 1,856,575.14 | **~7.1 times slower than JDK's built-in** |
| | 65,536 | 4,485.95 | 32,132.67 | **~7.2 times slower than JDK's built-in** |
> **Benchmark Environment:**
> - **CPU:** AMD Ryzen 5 5500U (6 Cores / 12 Threads @ 2.10GHz)
> - **RAM:** 16GB DDR4 2667MHz
> - **OS:** Windows 11 64-bit
> - **Runtime:** Microsoft Build of OpenJDK 17.0.8, Kotlin 2.4.10 (MingwX64 & JVM)
**Fortunately**, except for `AES-128-CFB8` (which requires continuous stream encryption/decryption during networking),
the other operations (RSA & SHA-1) are only executed once during the initial server authentication phase
If you have optimized native algorithm implementations (via `cinterop` or other approaches), PRs are welcome
-41
View File
@@ -1,41 +0,0 @@
# Ktor based http client
```kotlin
private val httpClient = HttpClient()
public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = {
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)
}
}
```
# HttpURLConnection based http client
```kotlin
private fun sendJoinRequest(url: String, accessToken: String, uuid: String, serverIdHash: String): Boolean {
val connection = (URL(url).openConnection() as HttpURLConnection).apply {
requestMethod = "POST"
setRequestProperty("Content-Type", "application/json")
doOutput = true
connectTimeout = 5000
readTimeout = 5000
}
val jsonPayload = """{"accessToken":"$accessToken","selectedProfile":"$uuid","serverId":"$serverIdHash"}"""
connection.outputStream.use { os -> os.write(jsonPayload.toByteArray(Charsets.UTF_8)) }
val responseCode = connection.responseCode
connection.disconnect()
return responseCode == HttpURLConnection.HTTP_NO_CONTENT
}
public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = {
authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash ->
val success = sendJoinRequest(url, accessToken, uuid, serverIdHash)
require(success) { "Failed to authenticate with Mojang session server" }
}
}
```
+4 -4
View File
@@ -9,10 +9,10 @@ module relies on the following dependencies:
## Required APIs ## Required APIs
| Module Name | Required | Notes | | Module Name | Required | Notes |
|:------------|:------------|:------------------------------------------------------------------------------------------------------------------------------------------------------| |:------------|:------------|:-----------------------------------------------------------------------------------------------------------------------------|
| TCP Socket | Yes | The `protocol` module does not have a built-in TCP Socket implementation. [Implement TCP Socket](Impl-tcp-socket.md) | | TCP Socket | Yes | The `protocol` module does not have a built-in TCP Socket implementation. [Implement TCP Socket](Impl-tcp-socket.md) |
| HTTP Client | Conditional | Required only when logging into an `online-mode` server to send join request to mojang's session server. [Implement HTTP Client](Impl-http-client.md) | | HTTP Client | Conditional | Required only when logging into an `online-mode` server to send join request to mojang's session server. **No Document yet** |
# Get started # Get started
+6
View File
@@ -4,6 +4,8 @@ kotlinx-io = "0.9.1"
coroutines-test = "1.11.0" coroutines-test = "1.11.0"
kotlinx-coroutines = "1.11.0" kotlinx-coroutines = "1.11.0"
ktor-core = "3.5.2" ktor-core = "3.5.2"
cryptography = "0.6.0"
kotlinx-serialization = "1.12.0"
[libraries] [libraries]
kotlinx-io = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io" } kotlinx-io = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io" }
@@ -13,7 +15,11 @@ kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core",
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor-core" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor-core" }
ktor-client-curl = { module = "io.ktor:ktor-client-curl", version.ref = "ktor-core" } ktor-client-curl = { module = "io.ktor:ktor-client-curl", version.ref = "ktor-core" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor-core" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor-core" }
cryptography-core = { module = "dev.whyoleg.cryptography:cryptography-core", version.ref = "cryptography" }
cryptography-provider-optimal = { module = "dev.whyoleg.cryptography:cryptography-provider-optimal", version.ref = "cryptography" }
kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
[plugins] [plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
maven-publish = { id = "maven-publish" } maven-publish = { id = "maven-publish" }
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
@@ -0,0 +1,12 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/7
*/
package cn.rtast.libmc.context
public interface HttpClientProvider {
public suspend fun post(url: String, headers: Map<String, String>?, body: String?): String
}
@@ -5,7 +5,7 @@
*/ */
package cn.rtast.libmc.crypto package cn.rtast.libmc.context
public interface NetworkChannelCipher { public interface NetworkChannelCipher {
public fun encrypt(buffer: ByteArray, offset: Int, length: Int) public fun encrypt(buffer: ByteArray, offset: Int, length: Int)
@@ -0,0 +1,29 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/7
*/
package cn.rtast.libmc.context
import cn.rtast.libmc.network.SocketContext
import cn.rtast.libmc.network.SocketEngine
public data class ProtocolContext(
val httpClientProvider: HttpClientProvider?,
override val socketEngine: SocketEngine,
) : SocketContext()
public class ProtocolContextBuilder(private val onlineMode: Boolean) {
public lateinit var httpClientProvider: HttpClientProvider
public lateinit var socketEngine: SocketEngine
public fun build(): ProtocolContext =
ProtocolContext(
httpClientProvider = if (onlineMode) {
if (::httpClientProvider.isInitialized) httpClientProvider else error("authProvider is required in online mode")
} else if (::httpClientProvider.isInitialized) httpClientProvider else null,
socketEngine = if (::socketEngine.isInitialized) socketEngine else error("SocketEngine is not configured"),
)
}
@@ -1,12 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/7
*/
package cn.rtast.libmc.crypto
public fun interface AuthenticationProvider {
public suspend fun joinServer(url: String, accessToken: String, uuid: String, serverIdHash: String)
}
@@ -1,38 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/7
*/
package cn.rtast.libmc.crypto
import cn.rtast.libmc.network.SocketContext
import cn.rtast.libmc.network.SocketEngine
public data class ProtocolContext(
val authProvider: AuthenticationProvider?,
override val engine: SocketEngine,
) : SocketContext()
public class ProtocolContextBuilder(private val onlineMode: Boolean) {
public lateinit var authProvider: AuthenticationProvider
public lateinit var socketEngine: SocketEngine
public fun build(): ProtocolContext =
ProtocolContext(
authProvider = if (onlineMode) {
if (::authProvider.isInitialized) authProvider else error("authProvider is required in online mode")
} else if (::authProvider.isInitialized) authProvider else null,
engine = if (::socketEngine.isInitialized) socketEngine else error("SocketEngine is not configured"),
)
}
public fun interface RSA1024Encryptor {
public fun encrypt(key: ByteArray, data: ByteArray): ByteArray
}
public fun interface Sha1Hasher {
public fun hash(serverId: String, secretKey: ByteArray, publicKey: ByteArray): String
}
@@ -7,12 +7,12 @@
package cn.rtast.libmc.network package cn.rtast.libmc.network
import cn.rtast.libmc.crypto.ProtocolContextBuilder import cn.rtast.libmc.context.ProtocolContextBuilder
public abstract class SocketContext { public abstract class SocketContext {
public abstract val engine: SocketEngine public abstract val socketEngine: SocketEngine
public fun createSocket(host: String, port: Int): RawSocket = engine.create(host, port) public fun createSocket(host: String, port: Int): RawSocket = socketEngine.create(host, port)
} }
public fun (ProtocolContextBuilder.() -> Unit).withCustom( public fun (ProtocolContextBuilder.() -> Unit).withCustom(
@@ -0,0 +1,114 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/12
*/
package cn.rtast.libmc.serialization
public object LiteralJsonParser {
public fun extractString(json: String, keyPath: List<String>): String? {
var currentScope = json
for (i in keyPath.indices) {
val key = keyPath[i]
val value = extractRawValue(currentScope, key) ?: return null
if (i == keyPath.lastIndex) return parseJsonString(value)
currentScope = value
}
return null
}
private fun extractRawValue(json: String, targetKey: String): String? {
val keyPattern = "\"$targetKey\""
val keyIndex = json.indexOf(keyPattern)
if (keyIndex == -1) return null
val colonIndex = json.indexOf(':', keyIndex + keyPattern.length)
if (colonIndex == -1) return null
var startIndex = colonIndex + 1
while (startIndex < json.length && json[startIndex].isWhitespace()) startIndex++
if (startIndex >= json.length) return null
val firstChar = json[startIndex]
return when (firstChar) {
'"' -> extractStringLiteral(json, startIndex)
'{' -> extractObjectLiteral(json, startIndex)
else -> null
}
}
private fun extractStringLiteral(json: String, startIndex: Int): String? {
var inEscape = false
for (i in startIndex + 1 until json.length) {
val c = json[i]
if (inEscape) {
inEscape = false
} else if (c == '\\') {
inEscape = true
} else if (c == '"') return json.substring(startIndex, i + 1)
}
return null
}
private fun extractObjectLiteral(json: String, startIndex: Int): String? {
var depth = 0
var inString = false
var inEscape = false
for (i in startIndex until json.length) {
val c = json[i]
if (inString) {
if (inEscape) {
inEscape = false
} else if (c == '\\') {
inEscape = true
} else if (c == '"') {
inString = false
}
} else {
when (c) {
'"' -> inString = true
'{' -> depth++
'}' -> {
depth--
if (depth == 0) return json.substring(startIndex, i + 1)
}
}
}
}
return null
}
private fun parseJsonString(rawJsonString: String): String? {
if (rawJsonString.length < 2 || !rawJsonString.startsWith('"') || !rawJsonString.endsWith('"')) return null
val content = rawJsonString.substring(1, rawJsonString.length - 1)
val sb = StringBuilder()
var i = 0
while (i < content.length) {
val c = content[i]
if (c == '\\' && i + 1 < content.length) {
when (val next = content[i + 1]) {
'n' -> sb.append('\n')
'r' -> sb.append('\r')
't' -> sb.append('\t')
'"' -> sb.append('"')
'\\' -> sb.append('\\')
'/' -> sb.append('/')
'u' -> {
if (i + 5 < content.length) {
val hex = content.substring(i + 2, i + 6)
sb.append(hex.toInt(16).toChar())
i += 5
} else {
sb.append(c)
}
}
else -> sb.append(next)
}
i += 2
} else {
sb.append(c)
i++
}
}
return sb.toString()
}
}
@@ -0,0 +1,39 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/12
*/
package test
import cn.rtast.libmc.serialization.LiteralJsonParser
import kotlin.test.Test
class TestJsonParser {
@Test
fun testJsonParser() {
val input = """
{
"keyPair": {
"privateKey": "-----BEGIN RSA PRIVATE KEY-----",
"publicKey": "-----BEGIN RSA PUBLIC KEY-----"
},
"publicKeySignature": "",
"publicKeySignatureV2": "",
"expiresAt": "2026-09-13T03:52:50.501218Z",
"refreshedAfter": "2026-09-12T19:52:50.501218Z"
}
""".trimIndent()
println(LiteralJsonParser.extractString(input, listOf("keyPair", "privateKey")))
}
}
+2
View File
@@ -14,6 +14,8 @@ kotlin {
commonMain.dependencies { commonMain.dependencies {
api(project(":common")) api(project(":common"))
api(project(":nbt")) api(project(":nbt"))
implementation(libs.cryptography.core)
implementation(libs.cryptography.provider.optimal)
} }
jvmMain.dependencies {} jvmMain.dependencies {}
@@ -6,8 +6,8 @@
package cn.rtast.libmc.protocol.client package cn.rtast.libmc.protocol.client
import cn.rtast.libmc.crypto.ProtocolContext import cn.rtast.libmc.context.ProtocolContext
import cn.rtast.libmc.crypto.ProtocolContextBuilder import cn.rtast.libmc.context.ProtocolContextBuilder
import cn.rtast.libmc.protocol.network.NetworkChannel import cn.rtast.libmc.protocol.network.NetworkChannel
import cn.rtast.libmc.protocol.protocol.event.PacketEventDispatcher import cn.rtast.libmc.protocol.protocol.event.PacketEventDispatcher
import cn.rtast.libmc.protocol.protocol.session.Session import cn.rtast.libmc.protocol.protocol.session.Session
@@ -4,13 +4,60 @@
* Date: 2026/9/8 * Date: 2026/9/8
*/ */
@file:OptIn(DelicateCryptographyApi::class)
package cn.rtast.libmc.protocol.crypto package cn.rtast.libmc.protocol.crypto
import cn.rtast.libmc.crypto.NetworkChannelCipher import cn.rtast.libmc.context.NetworkChannelCipher
import dev.whyoleg.cryptography.DelicateCryptographyApi
import dev.whyoleg.cryptography.algorithms.AES
import kotlinx.io.Buffer
import kotlinx.io.RawSink
import kotlinx.io.readTo
internal expect class Aes128Cfb8ChannelCipher internal constructor(sharedKey: ByteArray) : NetworkChannelCipher { internal class Aes128Cfb8ChannelCipher(sharedKey: ByteArray) : NetworkChannelCipher {
override fun encrypt(buffer: ByteArray, offset: Int, length: Int) private val aes = cryptoProvider.get(AES.CFB8)
override fun decrypt(buffer: ByteArray, offset: Int, length: Int) private val key = aes.keyDecoder().decodeFromByteArrayBlocking(AES.Key.Format.RAW, sharedKey)
override fun close() private val cipher = key.cipher()
private val encryptInput = Buffer()
private val decryptInput = Buffer()
private val encryptOutput = Buffer()
private val decryptOutput = Buffer()
private val encryptSink = cipher.encryptingSinkWithIv(sharedKey, object : RawSink {
override fun write(source: Buffer, byteCount: Long) = source.readTo(encryptOutput, byteCount)
override fun flush() = Unit
override fun close() = Unit
})
private val decryptSink = cipher.decryptingSinkWithIv(sharedKey, object : RawSink {
override fun write(source: Buffer, byteCount: Long) = source.readTo(decryptOutput, byteCount)
override fun flush() = Unit
override fun close() = Unit
})
private fun process(buffer: ByteArray, offset: Int, length: Int, input: Buffer, sink: RawSink, output: Buffer) {
if (length == 0) return
input.write(buffer, offset, offset + length)
sink.write(input, length.toLong())
sink.flush()
output.readTo(buffer, offset, offset + length)
}
override fun encrypt(buffer: ByteArray, offset: Int, length: Int) {
process(buffer, offset, length, encryptInput, encryptSink, encryptOutput)
}
override fun decrypt(buffer: ByteArray, offset: Int, length: Int) {
process(buffer, offset, length, decryptInput, decryptSink, decryptOutput)
}
override fun close() {
encryptSink.close()
decryptSink.close()
encryptInput.clear()
decryptInput.clear()
encryptOutput.clear()
decryptOutput.clear()
}
} }
@@ -0,0 +1,12 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
package cn.rtast.libmc.protocol.crypto
import dev.whyoleg.cryptography.CryptographyProvider
internal val cryptoProvider = CryptographyProvider.Default
@@ -0,0 +1,35 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
package cn.rtast.libmc.protocol.crypto
import dev.whyoleg.cryptography.algorithms.RSA
import dev.whyoleg.cryptography.algorithms.SHA256
private val rsaProvider = cryptoProvider.get(RSA.PKCS1)
internal suspend fun signMessageData(privateKeyDerBytes: ByteArray, dataToSign: ByteArray): ByteArray {
val privateKey = rsaProvider.privateKeyDecoder(SHA256)
.decodeFromByteArray(RSA.PrivateKey.Format.DER, privateKeyDerBytes)
val signer = privateKey.signatureGenerator()
return signer.generateSignature(dataToSign)
}
internal suspend fun verifyMessageSignature(
publicKeyDerBytes: ByteArray,
signature: ByteArray,
originalData: ByteArray,
): Boolean {
return try {
val publicKey = rsaProvider.publicKeyDecoder(SHA256)
.decodeFromByteArray(RSA.PublicKey.Format.DER, publicKeyDerBytes)
val verifier = publicKey.signatureVerifier()
verifier.tryVerifySignature(originalData, signature)
} catch (_: Exception) {
false
}
}
@@ -5,6 +5,16 @@
*/ */
@file:OptIn(DelicateCryptographyApi::class)
package cn.rtast.libmc.protocol.crypto package cn.rtast.libmc.protocol.crypto
internal expect fun rsaEncrypt(publicKeyBytes: ByteArray, data: ByteArray): ByteArray import dev.whyoleg.cryptography.DelicateCryptographyApi
import dev.whyoleg.cryptography.algorithms.RSA
import dev.whyoleg.cryptography.algorithms.SHA1
internal suspend fun rsaEncrypt(publicKeyBytes: ByteArray, data: ByteArray): ByteArray {
val rsa = cryptoProvider.get(RSA.PKCS1)
val pk = rsa.publicKeyDecoder(SHA1).decodeFromByteArray(RSA.PublicKey.Format.DER, publicKeyBytes)
return pk.encryptor().encrypt(data)
}
@@ -5,9 +5,15 @@
*/ */
@file:OptIn(DelicateCryptographyApi::class)
package cn.rtast.libmc.protocol.crypto package cn.rtast.libmc.protocol.crypto
internal fun mcDigestToString(digest: ByteArray): String { import dev.whyoleg.cryptography.DelicateCryptographyApi
import dev.whyoleg.cryptography.algorithms.SHA1
import dev.whyoleg.cryptography.algorithms.SHA256
private fun mcDigestToString(digest: ByteArray): String {
val isNegative = (digest[0].toInt() and 0x80) != 0 val isNegative = (digest[0].toInt() and 0x80) != 0
val bytes = if (isNegative) twosComplement(digest) else digest val bytes = if (isNegative) twosComplement(digest) else digest
var hex = bytes.joinToString("") { (it.toInt() and 0xFF).toString(16).padStart(2, '0') } var hex = bytes.joinToString("") { (it.toInt() and 0xFF).toString(16).padStart(2, '0') }
@@ -16,7 +22,7 @@ internal fun mcDigestToString(digest: ByteArray): String {
return if (isNegative) "-$hex" else hex return if (isNegative) "-$hex" else hex
} }
internal fun twosComplement(bytes: ByteArray): ByteArray { private fun twosComplement(bytes: ByteArray): ByteArray {
val result = ByteArray(bytes.size) val result = ByteArray(bytes.size)
var carry = 1 var carry = 1
for (i in bytes.size - 1 downTo 0) { for (i in bytes.size - 1 downTo 0) {
@@ -27,10 +33,15 @@ internal fun twosComplement(bytes: ByteArray): ByteArray {
return result return result
} }
internal expect fun sha1Digest(data: ByteArray): ByteArray
internal fun minecraftServerIdHash(serverId: String, secretKey: ByteArray, publicKey: ByteArray): String { internal suspend fun minecraftServerIdHash(serverId: String, secretKey: ByteArray, publicKey: ByteArray): String {
val serverIdBytes = serverId.encodeToByteArray() val serverIdBytes = serverId.encodeToByteArray()
for (b in serverIdBytes) require((b.toInt() and 0xFF) <= 0x7F) { "serverId contains non-US-ASCII character" } for (b in serverIdBytes) require((b.toInt() and 0xFF) <= 0x7F) { "serverId contains non-US-ASCII character" }
return mcDigestToString(sha1Digest(serverIdBytes + secretKey + publicKey)) return mcDigestToString(sha1Digest(serverIdBytes + secretKey + publicKey))
} }
private val sha1Hasher = cryptoProvider.get(SHA1).hasher()
private suspend fun sha1Digest(data: ByteArray) = sha1Hasher.hash(data)
private val sha256Hasher = cryptoProvider.get(SHA256).hasher()
internal suspend fun sha256Digest(data: ByteArray) = sha256Hasher.hash(data)
@@ -7,7 +7,7 @@
package cn.rtast.libmc.protocol.network package cn.rtast.libmc.protocol.network
import cn.rtast.libmc.crypto.NetworkChannelCipher import cn.rtast.libmc.context.NetworkChannelCipher
import cn.rtast.libmc.network.ReadChannel import cn.rtast.libmc.network.ReadChannel
import cn.rtast.libmc.network.WriteChannel import cn.rtast.libmc.network.WriteChannel
@@ -7,6 +7,7 @@
package cn.rtast.libmc.protocol.packet.play.clientbound package cn.rtast.libmc.protocol.packet.play.clientbound
import cn.rtast.libmc.network.BytesBuffer
import cn.rtast.libmc.packet.MinecraftPacket import cn.rtast.libmc.packet.MinecraftPacket
import cn.rtast.libmc.packet.PacketCodec import cn.rtast.libmc.packet.PacketCodec
import cn.rtast.libmc.primitives.readMcString import cn.rtast.libmc.primitives.readMcString
@@ -14,7 +15,6 @@ import cn.rtast.libmc.primitives.readUuid
import cn.rtast.libmc.primitives.readVarInt import cn.rtast.libmc.primitives.readVarInt
import cn.rtast.libmc.protocol.protocol.game.chat.TextComponent import cn.rtast.libmc.protocol.protocol.game.chat.TextComponent
import cn.rtast.libmc.protocol.protocol.game.chat.readTextComponent import cn.rtast.libmc.protocol.protocol.game.chat.readTextComponent
import cn.rtast.libmc.network.BytesBuffer
import kotlin.uuid.Uuid import kotlin.uuid.Uuid
/** /**
@@ -1,89 +1,142 @@
/* /*
* Copyright © 2026 RTAkland * Copyright © 2026 RTAkland
* Author: RTAkland * Author: RTAkland
* Date: 2026/9/7 * Date: 2026/9/11
*/ */
package cn.rtast.libmc.protocol.packet.play.clientbound package cn.rtast.libmc.protocol.packet.play.clientbound
import cn.rtast.libmc.network.BytesBuffer
import cn.rtast.libmc.packet.MinecraftPacket import cn.rtast.libmc.packet.MinecraftPacket
import cn.rtast.libmc.packet.PacketCodec import cn.rtast.libmc.packet.PacketCodec
import cn.rtast.libmc.primitives.* import cn.rtast.libmc.primitives.*
import cn.rtast.libmc.protocol.protocol.game.chat.TextComponent
import cn.rtast.libmc.protocol.protocol.game.chat.readTextComponent import cn.rtast.libmc.protocol.protocol.game.chat.readTextComponent
import cn.rtast.libmc.protocol.protocol.game.player.action.PlayerInfoUpdateEntry
import cn.rtast.libmc.protocol.protocol.game.player.action.PlayerUpdateInfoAction
import cn.rtast.libmc.protocol.protocol.game.player.action.SinglePlayerAction
import cn.rtast.libmc.protocol.protocol.game.session.GameProfile import cn.rtast.libmc.protocol.protocol.game.session.GameProfile
import cn.rtast.libmc.network.BytesBuffer import kotlin.uuid.Uuid
public data class ClientboundPlayerInfoUpdatePacket( public data class ClientboundPlayerInfoUpdatePacket(
val actions: Set<PlayerUpdateInfoAction>, val actions: Set<UpdateAction>,
val entries: List<PlayerInfoUpdateEntry>, val entries: List<Entry>,
) : MinecraftPacket { ) : MinecraftPacket {
public enum class UpdateAction(public val mask: Int) {
ADD_PLAYER(0x01),
INITIALIZE_CHAT(0x02),
UPDATE_GAME_MODE(0x04),
UPDATE_LISTED(0x08),
UPDATE_LATENCY(0x10),
UPDATE_DISPLAY_NAME(0x20),
UPDATE_LIST_PRIORITY(0x40),
UPDATE_HAT(0x80);
public companion object {
public fun fromMask(mask: Int): Set<UpdateAction> {
val set = mutableSetOf<UpdateAction>()
entries.forEach { action -> if ((mask and action.mask) != 0) set.add(action) }
return set
}
public fun toMask(actions: Set<UpdateAction>): Int {
var mask = 0
for (action in actions) mask = mask or action.mask
return mask
}
}
}
public data class Entry(val profileId: Uuid, val actions: Map<UpdateAction, ActionValue>)
public sealed interface ActionValue {
public data class AddPlayer(val name: String, val properties: List<GameProfile.Property>) : ActionValue
public data class InitializeChat(val chatSession: ChatSessionData?) : ActionValue
public data class UpdateGameMode(val gameMode: Int) : ActionValue
public data class UpdateListed(val listed: Boolean) : ActionValue
public data class UpdateLatency(val latency: Int) : ActionValue
public data class UpdateDisplayName(val displayName: TextComponent?) : ActionValue
public data class UpdateListPriority(val priority: Int) : ActionValue
public data class UpdateHat(val hatVisible: Boolean) : ActionValue
}
public data class ChatSessionData(
val sessionId: Uuid,
val publicKeyExpiryTime: Long,
val encodedPublicKey: ByteArray,
val publicKeySignature: ByteArray,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ChatSessionData
if (publicKeyExpiryTime != other.publicKeyExpiryTime) return false
if (sessionId != other.sessionId) return false
if (!encodedPublicKey.contentEquals(other.encodedPublicKey)) return false
if (!publicKeySignature.contentEquals(other.publicKeySignature)) return false
return true
}
override fun hashCode(): Int {
var result = publicKeyExpiryTime.hashCode()
result = 31 * result + sessionId.hashCode()
result = 31 * result + encodedPublicKey.contentHashCode()
result = 31 * result + publicKeySignature.contentHashCode()
return result
}
}
internal companion object Codec : PacketCodec<ClientboundPlayerInfoUpdatePacket> { internal companion object Codec : PacketCodec<ClientboundPlayerInfoUpdatePacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundPlayerInfoUpdatePacket) {} override fun encode(buffer: BytesBuffer, value: ClientboundPlayerInfoUpdatePacket) {}
override fun decode(buffer: BytesBuffer): ClientboundPlayerInfoUpdatePacket { override fun decode(buffer: BytesBuffer): ClientboundPlayerInfoUpdatePacket {
val actionsMask = buffer.readUByte().toInt() val mask = buffer.readByte().toInt() and 0xff
val actionsSet = PlayerUpdateInfoAction.parseActions(actionsMask) val actions = UpdateAction.fromMask(mask)
val playerCount = buffer.readVarInt() val entries = buffer.readPrefixed {
val entries = ArrayList<PlayerInfoUpdateEntry>(playerCount) val profileId = buffer.readUuid()
repeat(playerCount) { val actionMap = mutableMapOf<UpdateAction, ActionValue>()
val playerUuid = buffer.readUuid() if (UpdateAction.ADD_PLAYER in actions) {
val playerActions = ArrayList<SinglePlayerAction>(actionsSet.size) val name = buffer.readMcString()
for (action in PlayerUpdateInfoAction.entries) { val properties = buffer.readPrefixed { GameProfile.Property.decode(this) }
if (action !in actionsSet) continue actionMap[UpdateAction.ADD_PLAYER] = ActionValue.AddPlayer(name, properties)
val parsedAction = when (action) {
PlayerUpdateInfoAction.ADD_PLAYER -> {
val name = buffer.readMcString()
val properties = buffer.readPrefixed {
GameProfile.Property.decode(buffer)
}
SinglePlayerAction.AddPlayer(name, properties)
}
PlayerUpdateInfoAction.INITIALIZE_CHAT -> {
buffer.readPrefixOptional {
SinglePlayerAction.InitializeChat(
readUuid(), readLong(),
readBytes(512), readBytes(4096)
)
} ?: SinglePlayerAction.InitializeChat(null, null, null, null)
}
PlayerUpdateInfoAction.UPDATE_GAME_MODE -> {
SinglePlayerAction.UpdateGameMode(buffer.readVarInt())
}
PlayerUpdateInfoAction.UPDATE_LISTED -> {
SinglePlayerAction.UpdateListed(buffer.readBoolean())
}
PlayerUpdateInfoAction.UPDATE_LATENCY -> {
SinglePlayerAction.UpdateLatency(buffer.readVarInt())
}
PlayerUpdateInfoAction.UPDATE_DISPLAY_NAME -> {
val hasDisplayName = buffer.readBoolean()
val displayName = if (hasDisplayName) buffer.readTextComponent() else null
SinglePlayerAction.UpdateDisplayName(displayName)
}
PlayerUpdateInfoAction.UPDATE_LIST_PRIORITY -> {
SinglePlayerAction.UpdateListPriority(buffer.readVarInt())
}
PlayerUpdateInfoAction.UPDATE_HAT -> {
SinglePlayerAction.UpdateHat(buffer.readBoolean())
}
}
playerActions.add(parsedAction)
} }
entries.add(PlayerInfoUpdateEntry(playerUuid, playerActions)) if (UpdateAction.INITIALIZE_CHAT in actions) {
val chatSession = buffer.readPrefixOptional {
val sessionId = buffer.readUuid()
val expiry = buffer.readLong()
val publicKeyLength = buffer.readVarInt()
val publicKey = buffer.readBytes(publicKeyLength)
val signatureLength = buffer.readVarInt()
val signature = buffer.readBytes(signatureLength)
ChatSessionData(sessionId, expiry, publicKey, signature)
}
actionMap[UpdateAction.INITIALIZE_CHAT] = ActionValue.InitializeChat(chatSession)
}
if (UpdateAction.UPDATE_GAME_MODE in actions) {
actionMap[UpdateAction.UPDATE_GAME_MODE] = ActionValue.UpdateGameMode(buffer.readVarInt())
}
if (UpdateAction.UPDATE_LISTED in actions) {
actionMap[UpdateAction.UPDATE_LISTED] = ActionValue.UpdateListed(buffer.readBoolean())
}
if (UpdateAction.UPDATE_LATENCY in actions) {
actionMap[UpdateAction.UPDATE_LATENCY] = ActionValue.UpdateLatency(buffer.readVarInt())
}
if (UpdateAction.UPDATE_DISPLAY_NAME in actions) {
val displayName = buffer.readPrefixOptional { readTextComponent() }
actionMap[UpdateAction.UPDATE_DISPLAY_NAME] = ActionValue.UpdateDisplayName(displayName)
}
if (UpdateAction.UPDATE_LIST_PRIORITY in actions) {
actionMap[UpdateAction.UPDATE_LIST_PRIORITY] = ActionValue.UpdateListPriority(buffer.readVarInt())
}
if (UpdateAction.UPDATE_HAT in actions) {
actionMap[UpdateAction.UPDATE_HAT] = ActionValue.UpdateHat(buffer.readBoolean())
}
Entry(profileId, actionMap)
} }
return ClientboundPlayerInfoUpdatePacket(actionsSet, entries) return ClientboundPlayerInfoUpdatePacket(actions, entries)
} }
} }
} }
@@ -0,0 +1,66 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
package cn.rtast.libmc.protocol.protocol.chat
import cn.rtast.libmc.network.BytesBuffer
import cn.rtast.libmc.primitives.FixedBitSet20
import cn.rtast.libmc.primitives.createFixedBitSet20
import cn.rtast.libmc.primitives.writeUuid
import kotlin.uuid.Uuid
internal class ClientChatTracker {
data class ChatStateSnapshot(val lastSeenSignatures: List<ByteArray>, val messageCount: Int)
private val lastSeenQueue = ArrayDeque<ByteArray>(20)
var pendingMessageCount: Int = 0
private set
fun onReceivePlayerChat(signature: ByteArray?) {
if (signature == null || signature.size != 256) return
if (lastSeenQueue.size >= 20) {
lastSeenQueue.removeFirst()
}
lastSeenQueue.addLast(signature)
pendingMessageCount++
}
fun prepareForOutgoingMessage(): ChatStateSnapshot {
val signatures = lastSeenQueue.toList()
val count = pendingMessageCount
pendingMessageCount = 0
return ChatStateSnapshot(signatures, count)
}
}
internal fun buildMessageSignData(
senderUuid: Uuid,
sessionId: Uuid,
messageIndex: Int,
salt: Long,
timestampEpochSec: Long,
messageHash: ByteArray,
lastSeenSignatures: List<ByteArray>,
): ByteArray {
val buffer = BytesBuffer().apply {
writeUuid(senderUuid)
writeUuid(sessionId)
writeInt(messageIndex)
writeLong(salt)
writeLong(timestampEpochSec)
writeBytes(messageHash)
writeInt(lastSeenSignatures.size)
for (sig in lastSeenSignatures) writeBytes(sig)
}
return buffer.toByteArray()
}
internal fun createAcknowledgedBitSet(lastSeenSignatures: List<ByteArray>): FixedBitSet20 {
val bitSet = createFixedBitSet20()
for (i in 0 until minOf(lastSeenSignatures.size, 20)) bitSet[i] = true
return bitSet
}
@@ -0,0 +1,34 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
package cn.rtast.libmc.protocol.protocol.chat
public class OutgoingMessageBuilder(public val text: String) {
public var signed: Boolean = true
internal fun build(): PreparedMessage = PreparedMessage(text, signed)
}
public data class PreparedMessage(val text: String, val isSigned: Boolean)
public fun String.signed(enabled: Boolean = true): PreparedMessage = PreparedMessage(this, enabled)
public fun String.unsigned(): PreparedMessage = PreparedMessage(this, false)
private fun computeByteArrayHashCode(bytes: ByteArray): Int {
var result = 1
for (element in bytes) result = 31 * result + element.toInt()
return result
}
internal fun computePacketChecksum(lastSeenSignatures: List<ByteArray>): Byte {
var combinedHash = 1
for (sig in lastSeenSignatures) {
val sigHash = computeByteArrayHashCode(sig)
combinedHash = 31 * combinedHash + sigHash
}
val resultByte = combinedHash.toByte()
return if (resultByte == 0.toByte()) 1.toByte() else resultByte
}
@@ -1,79 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/7
*/
package cn.rtast.libmc.protocol.protocol.game.player.action
import cn.rtast.libmc.protocol.protocol.game.chat.TextComponent
import cn.rtast.libmc.protocol.protocol.game.session.GameProfile
import kotlin.uuid.Uuid
public enum class PlayerUpdateInfoAction(public val mask: Int) {
ADD_PLAYER(0x01),
INITIALIZE_CHAT(0x02),
UPDATE_GAME_MODE(0x04),
UPDATE_LISTED(0x08),
UPDATE_LATENCY(0x10),
UPDATE_DISPLAY_NAME(0x20),
UPDATE_LIST_PRIORITY(0x40),
UPDATE_HAT(0x80);
public companion object {
public fun parseActions(mask: Int): Set<PlayerUpdateInfoAction> {
val set = HashSet<PlayerUpdateInfoAction>()
for (action in entries) if ((mask and action.mask) != 0) set.add(action)
return set
}
public fun toMask(actions: Set<PlayerUpdateInfoAction>): Int {
var mask = 0
for (action in actions) mask = mask or action.mask
return mask
}
}
}
public sealed class SinglePlayerAction {
public data class AddPlayer(val name: String, val properties: List<GameProfile.Property>) : SinglePlayerAction()
public data class InitializeChat(
val sessionId: Uuid?,
val keyExpiryTime: Long?,
val encodedPublicKey: ByteArray?,
val publicKeySignature: ByteArray?,
) : SinglePlayerAction() {
val hasSignatureData: Boolean get() = sessionId != null
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as InitializeChat
if (keyExpiryTime != other.keyExpiryTime) return false
if (sessionId != other.sessionId) return false
if (!encodedPublicKey.contentEquals(other.encodedPublicKey)) return false
if (!publicKeySignature.contentEquals(other.publicKeySignature)) return false
if (hasSignatureData != other.hasSignatureData) return false
return true
}
override fun hashCode(): Int {
var result = keyExpiryTime.hashCode()
result = 31 * result + sessionId.hashCode()
result = 31 * result + (encodedPublicKey?.contentHashCode() ?: 0)
result = 31 * result + (publicKeySignature?.contentHashCode() ?: 0)
result = 31 * result + hasSignatureData.hashCode()
return result
}
}
public data class UpdateGameMode(val gameMode: Int) : SinglePlayerAction()
public data class UpdateListed(val listed: Boolean) : SinglePlayerAction()
public data class UpdateLatency(val ping: Int) : SinglePlayerAction()
public data class UpdateDisplayName(val displayName: TextComponent?) : SinglePlayerAction()
public data class UpdateListPriority(val priority: Int) : SinglePlayerAction()
public data class UpdateHat(val visible: Boolean) : SinglePlayerAction()
}
public data class PlayerInfoUpdateEntry(val uuid: Uuid, val actions: List<SinglePlayerAction>)
@@ -9,6 +9,8 @@ package cn.rtast.libmc.protocol.protocol.session
import cn.rtast.libmc.packet.MinecraftPacket import cn.rtast.libmc.packet.MinecraftPacket
import cn.rtast.libmc.protocol.client.CURRENT_MINECRAFT_PROTOCOL_VERSION import cn.rtast.libmc.protocol.client.CURRENT_MINECRAFT_PROTOCOL_VERSION
import cn.rtast.libmc.protocol.protocol.chat.OutgoingMessageBuilder
import cn.rtast.libmc.protocol.protocol.chat.PreparedMessage
import cn.rtast.libmc.protocol.protocol.state.HandshakeIntent import cn.rtast.libmc.protocol.protocol.state.HandshakeIntent
import kotlin.reflect.KClass import kotlin.reflect.KClass
@@ -26,6 +28,24 @@ public interface Session {
public suspend fun disconnect() public suspend fun disconnect()
public suspend fun init() public suspend fun init()
public suspend fun sendPacket(packet: MinecraftPacket) public suspend fun sendPacket(packet: MinecraftPacket)
public suspend fun sendMessage(message: String)
public suspend fun sendMessage(message: PreparedMessage)
public suspend fun sendMessage(text: String, block: OutgoingMessageBuilder.() -> Unit) {
val builder = OutgoingMessageBuilder(text).apply(block)
sendMessage(builder.build())
}
public suspend fun validateSignature(
senderPublicKey: ByteArray,
signature: ByteArray,
messageText: ByteArray,
timestamp: Long,
salt: Long,
): Boolean
public suspend fun setProfileKey(key: ByteArray)
public suspend fun acquireProfileKey(): ByteArray
} }
public inline fun <reified T : SessionEvent> Session.onEvent(noinline block: suspend Session.(T) -> Unit) { public inline fun <reified T : SessionEvent> Session.onEvent(noinline block: suspend Session.(T) -> Unit) {
@@ -11,6 +11,7 @@ import cn.rtast.libmc.protocol.protocol.game.chat.TextComponent
import cn.rtast.libmc.protocol.protocol.state.ProtocolState import cn.rtast.libmc.protocol.protocol.state.ProtocolState
public sealed interface SessionEvent { public sealed interface SessionEvent {
public data object Initialized : SessionEvent
public data class DisconnectedEvent(val reason: TextComponent, val state: ProtocolState) : SessionEvent public data class DisconnectedEvent(val reason: TextComponent, val state: ProtocolState) : SessionEvent
public object ConnectedEvent : SessionEvent public object ConnectedEvent : SessionEvent
public sealed interface ChangedState : SessionEvent { public sealed interface ChangedState : SessionEvent {
@@ -2,8 +2,7 @@ package cn.rtast.libmc.protocol.protocol.session
import cn.rtast.libmc.packet.MinecraftPacket import cn.rtast.libmc.packet.MinecraftPacket
import cn.rtast.libmc.protocol.client.MinecraftClient import cn.rtast.libmc.protocol.client.MinecraftClient
import cn.rtast.libmc.protocol.crypto.minecraftServerIdHash import cn.rtast.libmc.protocol.crypto.*
import cn.rtast.libmc.protocol.crypto.rsaEncrypt
import cn.rtast.libmc.protocol.packet.configuration.clientbound.* import cn.rtast.libmc.protocol.packet.configuration.clientbound.*
import cn.rtast.libmc.protocol.packet.configuration.serverbound.ServerboundAckFinishConfigurationPacket import cn.rtast.libmc.protocol.packet.configuration.serverbound.ServerboundAckFinishConfigurationPacket
import cn.rtast.libmc.protocol.packet.configuration.serverbound.ServerboundKeepAliveConfigurationPacket import cn.rtast.libmc.protocol.packet.configuration.serverbound.ServerboundKeepAliveConfigurationPacket
@@ -20,25 +19,37 @@ import cn.rtast.libmc.protocol.packet.login.serverbound.ServerboundLoginStartPac
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundDisconnectPlayPacket import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundDisconnectPlayPacket
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundKeepAlivePlayPacket import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundKeepAlivePlayPacket
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundPingPacket import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundPingPacket
import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundChatMessagePacket
import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundKeepAlivePlayPacket import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundKeepAlivePlayPacket
import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundPongPlayPacket import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundPongPlayPacket
import cn.rtast.libmc.protocol.packet.status.clientbound.ClientboundStatusResponsePacket import cn.rtast.libmc.protocol.packet.status.clientbound.ClientboundStatusResponsePacket
import cn.rtast.libmc.protocol.packet.status.serverbound.ServerboundStatusRequestPacket import cn.rtast.libmc.protocol.packet.status.serverbound.ServerboundStatusRequestPacket
import cn.rtast.libmc.protocol.protocol.chat.*
import cn.rtast.libmc.protocol.protocol.event.ListenerRegistration import cn.rtast.libmc.protocol.protocol.event.ListenerRegistration
import cn.rtast.libmc.protocol.protocol.game.chat.TextComponent import cn.rtast.libmc.protocol.protocol.game.chat.TextComponent
import cn.rtast.libmc.protocol.protocol.state.HandshakeIntent import cn.rtast.libmc.protocol.protocol.state.HandshakeIntent
import cn.rtast.libmc.protocol.protocol.state.ProtocolState import cn.rtast.libmc.protocol.protocol.state.ProtocolState
import cn.rtast.libmc.protocol.util.generateRandom16Bytes import cn.rtast.libmc.protocol.util.generateRandom16Bytes
import cn.rtast.libmc.serialization.LiteralJsonParser
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
import kotlin.io.encoding.Base64
import kotlin.random.Random
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.time.Clock
import kotlin.uuid.Uuid
private typealias EventHandler = suspend Session.(SessionEvent) -> Unit private typealias EventHandler = suspend Session.(SessionEvent) -> Unit
public class SessionImpl internal constructor() : Session { public class SessionImpl internal constructor() : Session {
private lateinit var client: MinecraftClient private lateinit var client: MinecraftClient
private lateinit var playerProfilePrivateKeyBytes: ByteArray
private lateinit var sessionId: Uuid
private val chatTracker = ClientChatTracker()
internal fun attachClient(client: MinecraftClient) { internal fun attachClient(client: MinecraftClient) {
this.client = client this.client = client
} }
@@ -50,6 +61,12 @@ public class SessionImpl internal constructor() : Session {
internal val eventListener: HashMap<KClass<out SessionEvent>, MutableList<EventHandler>> = hashMapOf() internal val eventListener: HashMap<KClass<out SessionEvent>, MutableList<EventHandler>> = hashMapOf()
override suspend fun init() { override suspend fun init() {
client.onPacket<ClientboundHelloPacket> { acceptEncryption(it) }
client.onPacket<ClientboundSetCompressionPacket> { setCompression(it.threshold) }
client.onPacket<ClientboundLoginSuccessPacket> { acknowledgeLogin(it) }
client.onPacket<ClientboundFinishConfigurationPacket> { finishConfiguration() }
client.onPacket<ClientboundPingPacket> { networkChannel.sendPacket(ServerboundPongPlayPacket(it.id)) }
client.onPacket<ClientboundKeepAlivePlayPacket> { networkChannel.sendPacket(ServerboundKeepAlivePlayPacket(it.id)) }
client.onPacket<ClientboundDisconnectLoginPacket> { client.onPacket<ClientboundDisconnectLoginPacket> {
emitEvent(SessionEvent.DisconnectedEvent(it.reason, stateMachine.currentState)) emitEvent(SessionEvent.DisconnectedEvent(it.reason, stateMachine.currentState))
} }
@@ -59,13 +76,6 @@ public class SessionImpl internal constructor() : Session {
client.onPacket<ClientboundDisconnectConfigurationPacket> { client.onPacket<ClientboundDisconnectConfigurationPacket> {
emitEvent(SessionEvent.DisconnectedEvent(it.reason, stateMachine.currentState)) emitEvent(SessionEvent.DisconnectedEvent(it.reason, stateMachine.currentState))
} }
client.onPacket<ClientboundHelloPacket> { acceptEncryption(it) }
client.onPacket<ClientboundSetCompressionPacket> { setCompression(it.threshold) }
client.onPacket<ClientboundLoginSuccessPacket> { acknowledgeLogin() }
client.onPacket<ClientboundFinishConfigurationPacket> { finishConfiguration() }
client.onPacket<ClientboundPingPacket> { networkChannel.sendPacket(ServerboundPongPlayPacket(it.id)) }
client.onPacket<ClientboundKeepAlivePlayPacket> { networkChannel.sendPacket(ServerboundKeepAlivePlayPacket(it.id)) }
client.onPacket<ClientboundKeepAliveConfigurationPacket> { client.onPacket<ClientboundKeepAliveConfigurationPacket> {
networkChannel.sendPacket(ServerboundKeepAliveConfigurationPacket(it.id)) networkChannel.sendPacket(ServerboundKeepAliveConfigurationPacket(it.id))
} }
@@ -75,9 +85,68 @@ public class SessionImpl internal constructor() : Session {
client.onPacket<ClientboundSelectKnownPacksPacket> { client.onPacket<ClientboundSelectKnownPacksPacket> {
networkChannel.sendPacket(ServerboundSelectKnownPacksPacket(emptyList())) networkChannel.sendPacket(ServerboundSelectKnownPacksPacket(emptyList()))
} }
emitEvent(SessionEvent.Initialized)
} }
public override suspend fun sendPacket(packet: MinecraftPacket): Unit = client.networkChannel.sendPacket(packet) public override suspend fun sendPacket(packet: MinecraftPacket): Unit = client.networkChannel.sendPacket(packet)
override suspend fun sendMessage(message: String): Unit = sendMessage(PreparedMessage(message, isSigned = true))
override suspend fun sendMessage(message: PreparedMessage) {
ensureState(ProtocolState.PLAY)
val snapshot = chatTracker.prepareForOutgoingMessage()
val timestamp = Clock.System.now().toEpochMilliseconds()
val salt = Random.nextLong()
val signature = if (message.isSigned && ::playerProfilePrivateKeyBytes.isInitialized) {
val messageHash = sha256Digest(message.text.encodeToByteArray())
val signData = buildMessageSignData(
client.uuid, sessionId, snapshot.messageCount,
salt, timestamp / 1000, messageHash, snapshot.lastSeenSignatures
)
signMessageData(playerProfilePrivateKeyBytes, signData)
} else null // TODO FIX ME
val packet = ServerboundChatMessagePacket(
message.text, timestamp, salt, signature,
snapshot.messageCount, createAcknowledgedBitSet(snapshot.lastSeenSignatures).toByteArray(),
computePacketChecksum(snapshot.lastSeenSignatures)
)
sendPacket(packet)
}
override suspend fun validateSignature(
senderPublicKey: ByteArray, signature: ByteArray,
messageText: ByteArray, timestamp: Long, salt: Long,
): Boolean {
return !(signature.size != 256 || senderPublicKey.isEmpty()) && try {
val messageHash = sha256Digest(messageText)
val signData = buildMessageSignData(
client.uuid, sessionId, 0,
salt, timestamp / 1000, messageHash, emptyList()
)
verifyMessageSignature(
publicKeyDerBytes = senderPublicKey,
signature = signature,
originalData = signData
)
} catch (_: Exception) {
false
}
}
public override suspend fun setProfileKey(key: ByteArray) {
this.playerProfilePrivateKeyBytes = key
}
public override suspend fun acquireProfileKey(): ByteArray {
val json = client.protocolContext.httpClientProvider!!.post(
"https://api.minecraftservices.com/player/certificates",
mapOf("Authorization" to "Bearer ${client.accessToken}"), null
)
val privateKeyString = LiteralJsonParser.extractString(json, listOf("keyPair", "privateKey"))!!
val cleanBase64 = privateKeyString.replace("-----BEGIN RSA PRIVATE KEY-----", "")
.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END RSA PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "").replace("\r", "").replace("\n", "").replace(" ", "").trim()
return Base64.decode(cleanBase64)
}
override fun <T : SessionEvent> _onEvent(clazz: KClass<T>, block: suspend Session.(T) -> Unit) { override fun <T : SessionEvent> _onEvent(clazz: KClass<T>, block: suspend Session.(T) -> Unit) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@@ -129,17 +198,19 @@ public class SessionImpl internal constructor() : Session {
ensureState(ProtocolState.LOGIN) ensureState(ProtocolState.LOGIN)
val sharedSecret = generateRandom16Bytes() val sharedSecret = generateRandom16Bytes()
val serverHash = minecraftServerIdHash(context.serverId, sharedSecret, context.publicKey) val serverHash = minecraftServerIdHash(context.serverId, sharedSecret, context.publicKey)
client.protocolContext.authProvider!!.joinServer( val body = "{\"accessToken\":\"${client.accessToken}\", \"selectedProfile\":\"${
"https://sessionserver.mojang.com/session/minecraft/join", client.uuid.toString().replace("-", "")
client.accessToken!!, client.uuid.toString().replace("-", ""), serverHash }\", \"serverId\":\"$serverHash\"}"
) client.protocolContext.httpClientProvider!!
.post("https://sessionserver.mojang.com/session/minecraft/join", null, body)
val encryptedSecret = rsaEncrypt(context.publicKey, sharedSecret) val encryptedSecret = rsaEncrypt(context.publicKey, sharedSecret)
val encryptedVerifyToken = rsaEncrypt(context.publicKey, context.verifyToken) val encryptedVerifyToken = rsaEncrypt(context.publicKey, context.verifyToken)
networkChannel.sendPacket(ServerboundKeyPacket(encryptedSecret, encryptedVerifyToken)) networkChannel.sendPacket(ServerboundKeyPacket(encryptedSecret, encryptedVerifyToken))
networkChannel.networkSession.enableEncryption(sharedSecret) networkChannel.networkSession.enableEncryption(sharedSecret)
} }
internal suspend fun acknowledgeLogin() { internal suspend fun acknowledgeLogin(packet: ClientboundLoginSuccessPacket) {
this.sessionId = packet.sessionId
ensureState(ProtocolState.LOGIN) ensureState(ProtocolState.LOGIN)
networkChannel.sendPacket(ServerboundLoginAcknowledgedPacket) networkChannel.sendPacket(ServerboundLoginAcknowledgedPacket)
stateMachine.transitionTo(ProtocolState.CONFIGURATION) stateMachine.transitionTo(ProtocolState.CONFIGURATION)
@@ -39,22 +39,4 @@ fun createAcknowledgedBitSet(lastSeenSignatures: List<ByteArray>): FixedBitSet20
bitSet[i] = true bitSet[i] = true
} }
return bitSet return bitSet
}
object ChatPacketUtils {
fun computeByteArrayHashCode(bytes: ByteArray): Int {
var result = 1
for (element in bytes) result = 31 * result + element.toInt()
return result
}
fun computePacketChecksum(lastSeenSignatures: List<ByteArray>): Byte {
var combinedHash = 1
for (sig in lastSeenSignatures) {
val sigHash = computeByteArrayHashCode(sig)
combinedHash = 31 * combinedHash + sigHash
}
val resultByte = combinedHash.toByte()
return if (resultByte == 0.toByte()) 1.toByte() else resultByte
}
} }
@@ -7,15 +7,12 @@
package client package client
import cn.rtast.libmc.crypto.AuthenticationProvider import cn.rtast.libmc.context.HttpClientProvider
import cn.rtast.libmc.protocol.client.createMinecraftClient import cn.rtast.libmc.protocol.client.createMinecraftClient
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundAwardStatisticsPacket import cn.rtast.libmc.protocol.packet.play.clientbound.*
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundPlayerChatMessagePacket
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundSetCursorItemPacket
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundSetHealthPacket
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundStepTickPacket
import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundClientCommandPacket import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundClientCommandPacket
import cn.rtast.libmc.protocol.packet.status.clientbound.ClientboundStatusResponsePacket import cn.rtast.libmc.protocol.protocol.PacketDirection
import cn.rtast.libmc.protocol.protocol.chat.signed
import cn.rtast.libmc.protocol.protocol.game.registry.ClientAction import cn.rtast.libmc.protocol.protocol.game.registry.ClientAction
import cn.rtast.libmc.protocol.protocol.session.SessionEvent import cn.rtast.libmc.protocol.protocol.session.SessionEvent
import cn.rtast.libmc.protocol.protocol.session.onEvent import cn.rtast.libmc.protocol.protocol.session.onEvent
@@ -23,7 +20,6 @@ import cn.rtast.libmc.protocol.util.generateOfflineUuid
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.statement.* import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.utils.io.* import io.ktor.utils.io.*
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.io.buffered import kotlinx.io.buffered
@@ -48,26 +44,32 @@ class TestClient {
accessToken, accessToken,
context = { context = {
socketEngine = KtorNetworkEngine() socketEngine = KtorNetworkEngine()
authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash -> httpClientProvider = object : HttpClientProvider {
val status = httpClient.post(url) { override suspend fun post(url: String, headers: Map<String, String>?, body: String?): String {
headers { header("Content-Type", "application/json") } return httpClient.post(url) {
setBody("{\"accessToken\":\"$accessToken\", \"selectedProfile\":\"$uuid\", \"serverId\":\"$serverIdHash\"}") headers {
header("Content-Type", "application/json")
headers?.forEach { header(it.key, it.value) }
}
body?.let { setBody(it) }
}.bodyAsText()
} }
require(status.status == HttpStatusCode.NoContent) { status.bodyAsText() }
} }
} }
) )
cli.onEvent<SessionEvent.Initialized> { setProfileKey(acquireProfileKey()) }
cli.onEvent<SessionEvent.ConnectedEvent> { cli.onEvent<SessionEvent.ConnectedEvent> {
// println(status()) // println(status())
login() login()
// disconnect() // disconnect()
} }
cli.onEvent<SessionEvent.DisconnectedEvent> { cli.onPacket<ClientboundSystemChatMessagePacket> { println(it) }
println(it.reason.toJsonString()) cli.onPacket<ClientboundDisguisedChatMessagePacket> { println(it) }
cli.onPacket<ClientboundPlayerChatMessagePacket> {
sendMessage(it.message)
chatTracker.onReceivePlayerChat(it.messageSignature)
} }
cli.onPacket<ClientboundPlayerChatMessagePacket> { chatTracker.onReceivePlayerChat(it.messageSignature) } cli.on { packet, direction -> if (direction == PacketDirection.CLIENTBOUND) println(packet) }
cli.onPacket<ClientboundStepTickPacket> { println(it) }
cli.on { packet, direction -> println("$direction -> $packet") }
cli.connect() cli.connect()
// awaitCancellation() // awaitCancellation()
while (true) { while (true) {
@@ -96,22 +98,14 @@ class TestClient {
// ) // )
// } // }
cli.onEvent<SessionEvent.ConnectedEvent> { cli.onEvent<SessionEvent.ConnectedEvent> { login() }
// println(status())
login()
// disconnect()
}
cli.onEvent<SessionEvent.ChangedState> {
println(it)
}
cli.onPacket<ClientboundPlayerChatMessagePacket> { chatTracker.onReceivePlayerChat(it.messageSignature) } cli.onPacket<ClientboundPlayerChatMessagePacket> { chatTracker.onReceivePlayerChat(it.messageSignature) }
cli.onPacket<ClientboundSetHealthPacket> { cli.onPacket<ClientboundSetHealthPacket> {
if (it.health <= 0) sendPacket(ServerboundClientCommandPacket(ClientAction.PerformRespawn)) if (it.health <= 0) sendPacket(ServerboundClientCommandPacket(ClientAction.PerformRespawn))
} }
cli.onPacket<ClientboundSetCursorItemPacket> { println(it) } cli.onPacket<ClientboundPlayerInfoUpdatePacket> { println(it) }
// cli.on { packet, direction -> println("$direction -> $packet") } // cli.on { packet, direction -> println("$direction -> $packet") }
cli.connect() cli.connect()
// awaitCancellation() // awaitCancellation()
while (true) { while (true) {
} }
@@ -0,0 +1,3 @@
POST https://api.minecraftservices.com/player/certificates
Authorization: Bearer
###
@@ -1,33 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/8
*/
package cn.rtast.libmc.protocol.crypto
import cn.rtast.libmc.crypto.NetworkChannelCipher
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
internal actual class Aes128Cfb8ChannelCipher internal actual constructor(sharedKey: ByteArray) : NetworkChannelCipher {
private val encryptCipher = Cipher.getInstance("AES/CFB8/NoPadding").apply {
init(Cipher.ENCRYPT_MODE, SecretKeySpec(sharedKey, "AES"), IvParameterSpec(sharedKey))
}
private val decryptCipher = Cipher.getInstance("AES/CFB8/NoPadding").apply {
init(Cipher.DECRYPT_MODE, SecretKeySpec(sharedKey, "AES"), IvParameterSpec(sharedKey))
}
actual override fun encrypt(buffer: ByteArray, offset: Int, length: Int) {
encryptCipher.update(buffer, offset, length, buffer, offset)
}
actual override fun decrypt(buffer: ByteArray, offset: Int, length: Int) {
decryptCipher.update(buffer, offset, length, buffer, offset)
}
actual override fun close() {}
}
@@ -1,20 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/8
*/
package cn.rtast.libmc.protocol.crypto
import java.security.KeyFactory
import java.security.spec.X509EncodedKeySpec
import javax.crypto.Cipher
internal actual fun rsaEncrypt(publicKeyBytes: ByteArray, data: ByteArray): ByteArray {
val keySpec = X509EncodedKeySpec(publicKeyBytes)
val keyFactory = KeyFactory.getInstance("RSA")
val publicKey = keyFactory.generatePublic(keySpec)
val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
return cipher.doFinal(data)
}
@@ -1,12 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/8
*/
package cn.rtast.libmc.protocol.crypto
import java.security.MessageDigest
internal actual fun sha1Digest(data: ByteArray): ByteArray =
MessageDigest.getInstance("SHA-1").digest(data)
@@ -1,396 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/8
*/
package cn.rtast.libmc.protocol.crypto
import cn.rtast.libmc.crypto.NetworkChannelCipher
internal actual class Aes128Cfb8ChannelCipher internal actual constructor(sharedKey: ByteArray) : NetworkChannelCipher {
private val encryptor = Aes128Cfb8(sharedKey, sharedKey)
private val decryptor = Aes128Cfb8(sharedKey, sharedKey)
actual override fun encrypt(buffer: ByteArray, offset: Int, length: Int) {
val stream = ByteArray(Aes128Cfb8.BLOCK_SIZE)
for (i in offset until (offset + length)) {
encryptor.getIv(stream)
encryptor.encryptBlock(stream, stream)
val ciphertext = (buffer[i].toInt() and 0xff xor stream[0].toInt() and 0xff).toByte()
encryptor.shiftFeedback(ciphertext)
buffer[i] = ciphertext
}
}
actual override fun decrypt(buffer: ByteArray, offset: Int, length: Int) {
val stream = ByteArray(Aes128Cfb8.BLOCK_SIZE)
for (i in offset until (offset + length)) {
val ciphertext = buffer[i]
decryptor.getIv(stream)
decryptor.encryptBlock(stream, stream)
buffer[i] = (ciphertext.toInt() and 0xff xor stream[0].toInt() and 0xff).toByte()
decryptor.shiftFeedback(ciphertext)
}
}
actual override fun close() {}
}
/**
* PERFORMANCE IMPROVEMENT REQUIRED.
*/
private class Aes128Cfb8(key: ByteArray, iv: ByteArray) {
companion object {
const val BLOCK_SIZE = 16
private val S_BOX = byteArrayOf(
0x63, 0x7c, 0x77, 0x7b, 0xf2.toByte(), 0x6b, 0x6f, 0xc5.toByte(),
0x30, 0x01, 0x67, 0x2b, 0xfe.toByte(), 0xd7.toByte(), 0xab.toByte(), 0x76,
0xca.toByte(), 0x82.toByte(), 0xc9.toByte(), 0x7d, 0xfa.toByte(), 0x59,
0x47, 0xf0.toByte(), 0xad.toByte(), 0xd4.toByte(), 0xa2.toByte(),
0xaf.toByte(), 0x9c.toByte(), 0xa4.toByte(), 0x72,
0xc0.toByte(), 0xb7.toByte(), 0xfd.toByte(), 0x93.toByte(), 0x26,
0x36, 0x3f, 0xf7.toByte(), 0xcc.toByte(), 0x34, 0xa5.toByte(),
0xe5.toByte(), 0xf1.toByte(), 0x71, 0xd8.toByte(), 0x31, 0x15,
0x04, 0xc7.toByte(), 0x23, 0xc3.toByte(), 0x18, 0x96.toByte(),
0x05, 0x9a.toByte(), 0x07, 0x12, 0x80.toByte(), 0xe2.toByte(),
0xeb.toByte(), 0x27, 0xb2.toByte(), 0x75,
0x09, 0x83.toByte(), 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0.toByte(),
0x52, 0x3b, 0xd6.toByte(), 0xb3.toByte(), 0x29, 0xe3.toByte(),
0x2f, 0x84.toByte(), 0x53, 0xd1.toByte(), 0x00, 0xed.toByte(),
0x20, 0xfc.toByte(), 0xb1.toByte(), 0x5b, 0x6a, 0xcb.toByte(),
0xbe.toByte(), 0x39, 0x4a, 0x4c, 0x58, 0xcf.toByte(),
0xd0.toByte(), 0xef.toByte(), 0xaa.toByte(), 0xfb.toByte(),
0x43, 0x4d, 0x33, 0x85.toByte(), 0x45, 0xf9.toByte(),
0x02, 0x7f, 0x50, 0x3c, 0x9f.toByte(), 0xa8.toByte(),
0x51, 0xa3.toByte(), 0x40, 0x8f.toByte(), 0x92.toByte(),
0x9d.toByte(), 0x38, 0xf5.toByte(), 0xbc.toByte(),
0xb6.toByte(), 0xda.toByte(), 0x21, 0x10, 0xff.toByte(),
0xf3.toByte(), 0xd2.toByte(), 0xcd.toByte(), 0x0c,
0x13, 0xec.toByte(), 0x5f, 0x97.toByte(), 0x44, 0x17,
0xc4.toByte(), 0xa7.toByte(), 0x7e, 0x3d, 0x64, 0x5d,
0x19, 0x73, 0x60, 0x81.toByte(), 0x4f, 0xdc.toByte(),
0x22, 0x2a, 0x90.toByte(), 0x88.toByte(), 0x46, 0xee.toByte(),
0xb8.toByte(), 0x14, 0xde.toByte(), 0x5e, 0x0b,
0xdb.toByte(), 0xe0.toByte(), 0x32, 0x3a, 0x0a, 0x49,
0x06, 0x24, 0x5c, 0xc2.toByte(), 0xd3.toByte(),
0xac.toByte(), 0x62, 0x91.toByte(), 0x95.toByte(), 0xe4.toByte(),
0x79, 0xe7.toByte(), 0xc8.toByte(), 0x37, 0x6d,
0x8d.toByte(), 0xd5.toByte(), 0x4e, 0xa9.toByte(), 0x6c,
0x56, 0xf4.toByte(), 0xea.toByte(), 0x65, 0x7a,
0xae.toByte(), 0x08, 0xba.toByte(), 0x78, 0x25,
0x2e, 0x1c, 0xa6.toByte(), 0xb4.toByte(), 0xc6.toByte(),
0xe8.toByte(), 0xdd.toByte(), 0x74, 0x1f, 0x4b,
0xbd.toByte(), 0x8b.toByte(), 0x8a.toByte(), 0x70,
0x3e, 0xb5.toByte(), 0x66, 0x48, 0x03, 0xf6.toByte(),
0x0e, 0x61, 0x35, 0x57, 0xb9.toByte(), 0x86.toByte(),
0xc1.toByte(), 0x1d, 0x9e.toByte(), 0xe1.toByte(),
0xf8.toByte(), 0x98.toByte(), 0x11, 0x69, 0xd9.toByte(),
0x8e.toByte(), 0x94.toByte(), 0x9b.toByte(), 0x1e,
0x87.toByte(), 0xe9.toByte(), 0xce.toByte(), 0x55,
0x28, 0xdf.toByte(), 0x8c.toByte(), 0xa1.toByte(),
0x89.toByte(), 0x0d, 0xbf.toByte(), 0xe6.toByte(),
0x42, 0x68, 0x41, 0x99.toByte(), 0x2d, 0x0f,
0xb0.toByte(), 0x54, 0xbb.toByte(), 0x16
)
private val INV_S_BOX = ByteArray(256).also { inverse ->
for (i in 0 until 256) inverse[S_BOX[i].toInt() and 0xff] = i.toByte()
}
private val RCON = byteArrayOf(
0x00, 0x01, 0x02, 0x04, 0x08, 0x10,
0x20, 0x40, 0x80.toByte(), 0x1b, 0x36, 0x6c, 0xd8.toByte(),
0xab.toByte(), 0x4d, 0x9a.toByte()
)
private fun Byte.u(): Int = toInt() and 0xff
private fun sBox(value: Byte): Byte = S_BOX[value.u()]
private fun invSBox(value: Byte): Byte = INV_S_BOX[value.u()]
private fun xtime(value: Byte): Byte {
val x = value.u()
return (((x shl 1) xor (((x ushr 7) and 1) * 0x1b)) and 0xff).toByte()
}
private fun multiply(x: Byte, y: Int): Byte {
var a = x.u()
var b = y
var result = 0
while (b != 0) {
if ((b and 1) != 0) result = result xor a
a = if ((a and 0x80) != 0) ((a shl 1) xor 0x1b) and 0xff else (a shl 1) and 0xff
b = b ushr 1
}
return result.toByte()
}
}
private val rounds: Int
private val roundKey: ByteArray
private val iv = ByteArray(BLOCK_SIZE)
private val ctrBuffer = ByteArray(BLOCK_SIZE)
private var ctrPosition = BLOCK_SIZE
init {
iv.copyInto(this.iv)
val nk = key.size / 4
rounds = nk + 6
roundKey = ByteArray(BLOCK_SIZE * (rounds + 1))
expandKey(key, nk, rounds, roundKey)
}
fun setIv(newIv: ByteArray) {
newIv.copyInto(iv)
ctrPosition = BLOCK_SIZE
}
fun encryptBlock(input: ByteArray, output: ByteArray = ByteArray(BLOCK_SIZE)) {
input.copyInto(output, 0, 0, BLOCK_SIZE)
cipher(output)
}
fun decryptBlock(input: ByteArray, output: ByteArray = ByteArray(BLOCK_SIZE)) {
input.copyInto(output, 0, 0, BLOCK_SIZE)
invCipher(output)
}
private fun cipher(state: ByteArray) {
addRoundKey(state, 0)
for (round in 1 until rounds) {
subBytes(state)
shiftRows(state)
mixColumns(state)
addRoundKey(state, round)
}
subBytes(state)
shiftRows(state)
addRoundKey(state, rounds)
}
private fun invCipher(state: ByteArray) {
addRoundKey(state, rounds)
for (round in rounds - 1 downTo 1) {
invShiftRows(state)
invSubBytes(state)
addRoundKey(state, round)
invMixColumns(state)
}
invShiftRows(state)
invSubBytes(state)
addRoundKey(state, 0)
}
private fun addRoundKey(state: ByteArray, round: Int) {
val offset = round * BLOCK_SIZE
for (i in 0 until BLOCK_SIZE) state[i] = (state[i].u() xor roundKey[offset + i].u()).toByte()
}
private fun subBytes(state: ByteArray) = run { for (i in 0 until BLOCK_SIZE) state[i] = sBox(state[i]) }
private fun invSubBytes(state: ByteArray) = run { for (i in 0 until BLOCK_SIZE) state[i] = invSBox(state[i]) }
private fun shiftRows(state: ByteArray) {
var tmp = state[1]
state[1] = state[5]
state[5] = state[9]
state[9] = state[13]
state[13] = tmp
tmp = state[2]
state[2] = state[10]
state[10] = tmp
tmp = state[6]
state[6] = state[14]
state[14] = tmp
tmp = state[3]
state[3] = state[15]
state[15] = state[11]
state[11] = state[7]
state[7] = tmp
}
private fun invShiftRows(state: ByteArray) {
var tmp = state[13]
state[13] = state[9]
state[9] = state[5]
state[5] = state[1]
state[1] = tmp
tmp = state[2]
state[2] = state[10]
state[10] = tmp
tmp = state[6]
state[6] = state[14]
state[14] = tmp
tmp = state[3]
state[3] = state[7]
state[7] = state[11]
state[11] = state[15]
state[15] = tmp
}
private fun mixColumns(state: ByteArray) {
for (column in 0 until 4) {
val i = column * 4
val a0 = state[i]
val a1 = state[i + 1]
val a2 = state[i + 2]
val a3 = state[i + 3]
val t = a0.u() xor a1.u() xor a2.u() xor a3.u()
state[i] = (a0.u() xor (xtime((a0.u() xor a1.u()).toByte()).u()) xor t).toByte()
state[i + 1] = (a1.u() xor (xtime((a1.u() xor a2.u()).toByte()).u()) xor t).toByte()
state[i + 2] = (a2.u() xor (xtime((a2.u() xor a3.u()).toByte()).u()) xor t).toByte()
state[i + 3] = (a3.u() xor (xtime((a3.u() xor a0.u()).toByte()).u()) xor t).toByte()
}
}
private fun invMixColumns(state: ByteArray) {
for (column in 0 until 4) {
val i = column * 4
val a = state[i]
val b = state[i + 1]
val c = state[i + 2]
val d = state[i + 3]
state[i] = (multiply(a, 0x0e).u() xor multiply(b, 0x0b).u()
xor multiply(c, 0x0d).u() xor multiply(d, 0x09).u()).toByte()
state[i + 1] = (multiply(a, 0x09).u() xor multiply(b, 0x0e).u()
xor multiply(c, 0x0b).u() xor multiply(d, 0x0d).u()).toByte()
state[i + 2] = (multiply(a, 0x0d).u() xor multiply(b, 0x09).u()
xor multiply(c, 0x0e).u() xor multiply(d, 0x0b).u()).toByte()
state[i + 3] = (multiply(a, 0x0b).u() xor multiply(b, 0x0d).u()
xor multiply(c, 0x09).u() xor multiply(d, 0x0e).u()).toByte()
}
}
fun ecbEncrypt(data: ByteArray): ByteArray {
val output = data.copyOf()
for (offset in output.indices step BLOCK_SIZE) cipherAt(output, offset)
return output
}
fun ecbDecrypt(data: ByteArray): ByteArray {
val output = data.copyOf()
for (offset in output.indices step BLOCK_SIZE) invCipherAt(output, offset)
return output
}
private fun cipherAt(data: ByteArray, offset: Int) {
val block = ByteArray(BLOCK_SIZE)
data.copyInto(block, 0, offset, offset + BLOCK_SIZE)
cipher(block)
block.copyInto(data, offset)
}
private fun invCipherAt(data: ByteArray, offset: Int) {
val block = ByteArray(BLOCK_SIZE)
data.copyInto(block, 0, offset, offset + BLOCK_SIZE)
invCipher(block)
block.copyInto(data, offset)
}
fun cbcEncrypt(data: ByteArray): ByteArray {
val output = data.copyOf()
val block = ByteArray(BLOCK_SIZE)
for (offset in output.indices step BLOCK_SIZE) {
for (i in 0 until BLOCK_SIZE) block[i] = (output[offset + i].u() xor iv[i].u()).toByte()
cipher(block)
block.copyInto(output, offset)
block.copyInto(iv)
}
return output
}
fun cbcDecrypt(data: ByteArray): ByteArray {
val output = data.copyOf()
val block = ByteArray(BLOCK_SIZE)
val nextIv = ByteArray(BLOCK_SIZE)
for (offset in output.indices step BLOCK_SIZE) {
output.copyInto(nextIv, 0, offset, offset + BLOCK_SIZE)
output.copyInto(block, 0, offset, offset + BLOCK_SIZE)
invCipher(block)
for (i in 0 until BLOCK_SIZE) block[i] = (block[i].u() xor iv[i].u()).toByte()
block.copyInto(output, offset)
nextIv.copyInto(iv)
}
return output
}
fun ctrXcrypt(data: ByteArray): ByteArray {
val output = data.copyOf()
for (i in output.indices) {
if (ctrPosition == BLOCK_SIZE) {
iv.copyInto(ctrBuffer)
cipher(ctrBuffer)
incrementCounter()
ctrPosition = 0
}
output[i] = (output[i].u() xor ctrBuffer[ctrPosition].u()).toByte()
ctrPosition++
}
return output
}
private fun incrementCounter() {
for (i in BLOCK_SIZE - 1 downTo 0) {
if (iv[i].u() == 0xff) iv[i] = 0 else {
iv[i] = (iv[i].u() + 1).toByte()
break
}
}
}
fun cfb8Encrypt(data: ByteArray): ByteArray {
val output = data.copyOf()
val stream = ByteArray(BLOCK_SIZE)
for (i in output.indices) {
iv.copyInto(stream)
cipher(stream)
val ciphertext = (output[i].u() xor stream[0].u()).toByte()
shiftFeedback(ciphertext)
output[i] = ciphertext
}
return output
}
fun cfb8Decrypt(data: ByteArray): ByteArray {
val output = data.copyOf()
val stream = ByteArray(BLOCK_SIZE)
for (i in output.indices) {
val ciphertext = output[i]
iv.copyInto(stream)
cipher(stream)
output[i] = (ciphertext.u() xor stream[0].u()).toByte()
shiftFeedback(ciphertext)
}
return output
}
fun shiftFeedback(value: Byte) {
for (i in 0 until BLOCK_SIZE - 1) iv[i] = iv[i + 1]
iv[BLOCK_SIZE - 1] = value
}
private fun expandKey(key: ByteArray, nk: Int, rounds: Int, output: ByteArray) {
key.copyInto(output)
var generated = key.size
var rconIndex = 1
val total = BLOCK_SIZE * (rounds + 1)
val temp = ByteArray(4)
while (generated < total) {
for (i in 0 until 4) temp[i] = output[generated - 4 + i]
if (generated % key.size == 0) {
val t = temp[0]
temp[0] = temp[1]
temp[1] = temp[2]
temp[2] = temp[3]
temp[3] = t
for (i in 0 until 4) temp[i] = sBox(temp[i])
temp[0] = (temp[0].u() xor RCON[rconIndex].u()).toByte()
rconIndex++
} else if (nk == 8 && generated % key.size == 16) for (i in 0 until 4) temp[i] = sBox(temp[i])
for (i in 0 until 4) {
output[generated] = (output[generated - key.size].u() xor temp[i].u()).toByte()
generated++
}
}
}
fun getIv(output: ByteArray): ByteArray = iv.copyInto(output)
}
@@ -1,299 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/8
*/
package cn.rtast.libmc.protocol.crypto
import kotlin.random.Random
internal actual fun rsaEncrypt(publicKeyBytes: ByteArray, data: ByteArray): ByteArray {
val (modulus, exponent) = parseRsaPublicKeyDer(publicKeyBytes)
val paddedData = pkcs1Pad(data, keySizeBytes = 128)
return rsa1024(paddedData, exponent, modulus)
}
private fun parseRsaPublicKeyDer(der: ByteArray): Pair<ByteArray, ByteArray> {
var offset = 0
fun readTag(): Int = der[offset++].toInt() and 0xFF
fun readLength(): Int {
var len = der[offset++].toInt() and 0xFF
if (len and 0x80 != 0) {
val numBytes = len and 0x7F; len = 0
repeat(numBytes) { len = (len shl 8) or (der[offset++].toInt() and 0xFF) }
}
return len
}
if (readTag() != 0x30) error("Invalid DER Format")
readLength()
val tag = readTag()
if (tag == 0x30) {
val algLen = readLength()
offset += algLen
} else offset--
if (readTag() == 0x03) {
readLength(); offset++
if (readTag() != 0x30) error("Invalid RSA Public Key")
readLength()
}
if (readTag() != 0x02) error("Modulus Required")
val modulusLen = readLength()
val modulus = der.copyOfRange(offset, offset + modulusLen)
offset += modulusLen
if (readTag() != 0x02) error("Exponent Required")
val exponentLen = readLength()
val exponent = der.copyOfRange(offset, offset + exponentLen)
return Pair(modulus, exponent)
}
private fun pkcs1Pad(data: ByteArray, keySizeBytes: Int): ByteArray {
val maxDataLen = keySizeBytes - 11
require(data.size <= maxDataLen)
val padded = ByteArray(keySizeBytes)
padded[0] = 0x00
padded[1] = 0x02
val psLen = keySizeBytes - data.size - 3
var i = 2
while (i < 2 + psLen) {
val randomByte = Random.nextInt(1, 256).toByte(); padded[i] = randomByte; i++
}
padded[i] = 0x00; i++
for (k in data.indices) padded[i + k] = data[k]
return padded
}
private fun rsa1024(input: ByteArray, exponent: ByteArray, modulus: ByteArray): ByteArray {
val dataLongs = bytesToLongArray16(input)
val expoLongs = bytesToLongArray16(exponent)
val modLongs = bytesToLongArray16(modulus)
val resLongs = LongArray(18)
rsa1024(resLongs, dataLongs, expoLongs, modLongs)
return longArray16ToBytes(resLongs)
}
private fun rsa1024(res: LongArray, data: LongArray, expo: LongArray, key: LongArray): Boolean {
val modData = LongArray(18)
val result = LongArray(18)
var tempExpo: Long
modBigNumber(modData, data, key, 16)
result[0] = 1L
val expoLen = bitLength(expo, 16) / 64
for (i in 0..expoLen) {
tempExpo = expo[i]
repeat(64) {
if ((tempExpo and 1L) != 0L) modMultiply1024(result, result, modData, key)
modMultiply1024(modData, modData, modData, key)
tempExpo = tempExpo ushr 1
}
}
for (i in 0 until 16) res[i] = result[i]
return true
}
private fun addBigNumber(res: LongArray, op1: LongArray, op2: LongArray, n: Int): Boolean {
var carry = 0L
val mask32 = 0xFFFFFFFFL
var i = 0
while (i < n) {
val j = (op1[i] and mask32) + (op2[i] and mask32) + carry
val k = (op1[i] ushr 32) + (op2[i] ushr 32) + (j ushr 32)
carry = k ushr 32
res[i] = ((k and mask32) shl 32) or (j and mask32)
i++
}
if (i < res.size) res[i] = carry
return false
}
private fun multBigNumber(res: LongArray, op1: LongArray, op2: Int, n: Int): Boolean {
var carry1: Long
var carry2 = 0L
val op2UL = op2.toLong() and 0xFFFFFFFFL
val mask32 = 0xFFFFFFFFL
var i = 0
while (i < n) {
var j = (op1[i] and mask32) * op2UL
var k = (op1[i] ushr 32) * op2UL
carry1 = k ushr 32
k = (k and mask32) + (j ushr 32)
j = (j and mask32) + carry2
k += (j ushr 32)
carry2 = carry1 + (k ushr 32)
res[i] = ((k and mask32) shl 32) or (j and mask32)
i++
}
if (i < res.size) res[i] = carry2
return false
}
private fun modMultiply1024(res: LongArray, op1: LongArray, op2: LongArray, mod: LongArray): Boolean {
val mult1 = LongArray(33)
val mult2 = LongArray(33)
val result = LongArray(33)
val xmod = LongArray(33)
for (i in 0 until 16) xmod[i] = mod[i]
for (i in 0 until 16) {
mult1.fill(0L)
mult2.fill(0L)
val op2Low = (op2[i] and 0xFFFFFFFFL).toInt()
val op2High = ((op2[i] ushr 32) and 0xFFFFFFFFL).toInt()
multBigNumber(mult1, op1, op2Low, 16)
multBigNumber(mult2, op1, op2High, 16)
slnBigNumber(mult2, mult2, 33, 32)
addBigNumber(mult2, mult2, mult1, 32)
slnBigNumber(mult2, mult2, 33, 64 * i)
addBigNumber(result, result, mult2, 32)
}
modBigNumber(result, result, xmod, 33)
for (i in 0 until 16) res[i] = result[i]
return false
}
private fun modBigNumber(res: LongArray, op1: LongArray, op2: LongArray, n: Int): Boolean {
val lenOp1 = bitLength(op1, n)
val lenOp2 = bitLength(op2, n)
val lenDif = lenOp1 - lenOp2
for (i in 0 until n) res[i] = op1[i]
if (lenDif < 0) return true
if (lenDif == 0) {
while (compare(res, op2, n) >= 0) subBigNumber(res, res, op2, n)
return true
}
val op2Work = op2.copyOf()
slnBigNumber(op2Work, op2Work, n, lenDif)
repeat(lenDif) {
srnBigNumber(op2Work, op2Work, n, 1)
while (compare(res, op2Work, n) >= 0) subBigNumber(res, res, op2Work, n)
}
return true
}
private fun compare(op1: LongArray, op2: LongArray, n: Int): Int {
for (i in n - 1 downTo 0) {
val a = op1[i]
val b = op2[i]
if (a != b) {
val aUnsigned = a xor Long.MIN_VALUE
val bUnsigned = b xor Long.MIN_VALUE
return if (aUnsigned > bUnsigned) 1 else -1
}
}
return 0
}
private fun subBigNumber(res: LongArray, op1: LongArray, op2: LongArray, n: Int): Boolean {
var carry = false
val op1Copy = op1.copyOf()
for (i in 0 until n) {
var v1 = op1Copy[i]
if (carry) {
if (v1 != 0L) carry = false
v1 -= 1L
op1Copy[i] = v1
}
if ((v1 xor Long.MIN_VALUE) < (op2[i] xor Long.MIN_VALUE)) carry = true
res[i] = v1 - op2[i]
}
return carry
}
private fun slnBigNumber(res: LongArray, op: LongArray, len: Int, n: Int): Boolean {
val xShift = n / 64
val yShift = n % 64
var i = len
while (i - xShift > 0) {
res[i - 1] = op[i - 1 - xShift]
i--
}
while (i > 0) {
res[i - 1] = 0L
i--
}
if (yShift == 0) return true
var carry = 0L
for (idx in 0 until len) {
val j = res[idx]
val nextCarry = j ushr (64 - yShift)
res[idx] = (j shl yShift) or carry
carry = nextCarry
}
return true
}
private fun srnBigNumber(res: LongArray, op: LongArray, len: Int, n: Int): Boolean {
val xShift = n / 64
val yShift = n % 64
var i = 0
while (i + xShift < len) {
res[i] = op[i + xShift]; i++
}
while (i < len) {
res[i] = 0L; i++
}
if (yShift == 0) return true
var carry = 0L
for (idx in len downTo 1) {
val j = res[idx - 1]
val nextCarry = j shl (64 - yShift)
res[idx - 1] = (j ushr yShift) or carry
carry = nextCarry
}
return true
}
private fun bitLength(op: LongArray, n: Int): Int {
var len = 0
val unit = 1L
for (idx in n downTo 1) {
if (op[idx - 1] == 0L) continue
for (i in 64 downTo 1) {
if ((op[idx - 1] and (unit shl (i - 1))) != 0L) {
len = (64 * (idx - 1)) + i
break
}
}
if (len != 0) break
}
return len
}
private fun bytesToLongArray16(bytes: ByteArray): LongArray {
val cleanBytes = if (bytes.size > 128 && bytes[0] == 0.toByte()) bytes.copyOfRange(1, bytes.size) else bytes
val padded = ByteArray(128)
val startIdx = 128 - cleanBytes.size
for (k in cleanBytes.indices) padded[startIdx + k] = cleanBytes[k]
for (k in 0 until 64) {
val tmp = padded[k]
padded[k] = padded[127 - k]
padded[127 - k] = tmp
}
val result = LongArray(16)
for (i in 0 until 16) {
var value = 0L
for (j in 0 until 8) {
val byteVal = padded[i * 8 + j].toLong() and 0xFFL
value = value or (byteVal shl (j * 8))
}
result[i] = value
}
return result
}
private fun longArray16ToBytes(array: LongArray): ByteArray {
val bytes = ByteArray(128)
for (i in 0 until 16) {
val value = array[i]
for (j in 0 until 8) bytes[i * 8 + j] = ((value ushr (j * 8)) and 0xFFL).toByte()
}
for (k in 0 until 64) {
val tmp = bytes[k]
bytes[k] = bytes[127 - k]
bytes[127 - k] = tmp
}
return bytes
}
@@ -1,96 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/8
*/
package cn.rtast.libmc.protocol.crypto
internal actual fun sha1Digest(data: ByteArray): ByteArray = Sha1.digest(data)
/**
* PERFORMANCE IMPROVEMENT REQUIRED.
*/
private object Sha1 {
private fun rotateLeft(value: Int, bits: Int): Int {
return (value shl bits) or (value ushr (32 - bits))
}
private fun align(address: Int, alignment: Int): Int {
val tmp = alignment - 1
return (address + tmp) and tmp.inv()
}
fun digest(input: ByteArray): ByteArray {
val bitLength = input.size.toLong() * 8L
val bufferSize = align(input.size + 9, 64)
val buffer = ByteArray(bufferSize)
input.copyInto(buffer)
buffer[input.size] = 0x80.toByte()
for (i in 0 until 8) buffer[bufferSize - 8 + i] = ((bitLength ushr ((7 - i) * 8)) and 0xFFL).toByte()
var h0 = 0x67452301
var h1 = -0x10325477
var h2 = -0x67452302
var h3 = 0x10325476
var h4 = -0x3C2D1E10
val w = IntArray(80)
for (offset in buffer.indices step 64) {
for (i in 0 until 16) {
val idx = offset + (i * 4)
w[i] = ((buffer[idx].toInt() and 0xFF) shl 24) or
((buffer[idx + 1].toInt() and 0xFF) shl 16) or
((buffer[idx + 2].toInt() and 0xFF) shl 8) or
(buffer[idx + 3].toInt() and 0xFF)
}
for (i in 16 until 80) w[i] = rotateLeft(w[i - 3] xor w[i - 8] xor w[i - 14] xor w[i - 16], 1)
var a = h0
var b = h1
var c = h2
var d = h3
var e = h4
for (i in 0 until 80) {
val f: Int
val k: Int
when (i) {
in 0..19 -> {
f = (b and c) or (b.inv() and d)
k = 0x5A827999
}
in 20..39 -> {
f = b xor c xor d
k = 0x6ED9EBA1.toInt()
}
in 40..59 -> {
f = (b and c) or (b and d) or (c and d)
k = -0x70E44324
}
else -> {
f = b xor c xor d
k = -0x359D3E2A
}
}
val temp = rotateLeft(a, 5) + f + e + k + w[i]
e = d; d = c
c = rotateLeft(b, 30)
b = a; a = temp
}
h0 += a; h1 += b
h2 += c; h3 += d
h4 += e
}
val result = ByteArray(20)
val state = intArrayOf(h0, h1, h2, h3, h4)
for (i in 0 until 5) {
val v = state[i]
result[i * 4] = (v ushr 24).toByte()
result[i * 4 + 1] = (v ushr 16).toByte()
result[i * 4 + 2] = (v ushr 8).toByte()
result[i * 4 + 3] = v.toByte()
}
return result
}
}