From 480d0089e6fdf894d2bca65c5259a9ae425a2ee6 Mon Sep 17 00:00:00 2001 From: RTAkland Date: Sat, 12 Sep 2026 01:03:05 +0800 Subject: [PATCH] Fix some clientbound message bugs send signed message{wip} --- docs/Embedded-cryptography.md | 32 -- docs/Impl-HTTP-Client.md | 41 -- docs/README.md | 8 +- gradle/libs.versions.toml | 6 + .../rtast/libmc/context/HttpClientProvider.kt | 12 + .../NetworkChannelCipher.kt | 2 +- .../cn/rtast/libmc/context/ProtocolContext.kt | 29 ++ .../libmc/crypto/AuthenticationProvider.kt | 12 - .../cn/rtast/libmc/crypto/ProtocolContext.kt | 38 -- .../cn/rtast/libmc/network/SocketContext.kt | 6 +- .../cn/rtast/libmc/serialization/Json.kt | 114 +++++ .../commonTest/kotlin/test/TestJsonParser.kt | 39 ++ libmc-protocol/build.gradle.kts | 2 + .../libmc/protocol/client/MinecraftClient.kt | 4 +- .../crypto/Aes128Cfb8ChannelCipher.kt | 57 ++- .../libmc/protocol/crypto/CryptoProvider.kt | 12 + .../libmc/protocol/crypto/MessageSigner.kt | 35 ++ .../cn/rtast/libmc/protocol/crypto/RSA1024.kt | 12 +- .../libmc/protocol/crypto/{SHA1.kt => SHA.kt} | 21 +- .../libmc/protocol/network/CipherChannel.kt | 2 +- .../ClientboundPlayerChatMessagePacket.kt | 2 +- .../ClientboundPlayerInfoUpdatePacket.kt | 183 +++++--- .../protocol/chat/ClientChatTracker.kt | 66 +++ .../protocol/chat/OutgoingMessageBuilder.kt | 34 ++ .../player/action/PlayerUpdateInfoAction.kt | 79 ---- .../protocol/protocol/session/Session.kt | 20 + .../protocol/protocol/session/SessionEvent.kt | 1 + .../protocol/protocol/session/SessionImpl.kt | 99 ++++- .../kotlin/client/ClientChatTracker.kt | 18 - .../commonTest/kotlin/client/TestClient.kt | 50 +-- .../commonTest/kotlin/client/private-key.http | 3 + .../protocol/crypto/Aes128Cfb8Cipher.jvm.kt | 33 -- .../libmc/protocol/crypto/RSA1024.jvm.kt | 20 - .../rtast/libmc/protocol/crypto/SHA1.jvm.kt | 12 - .../crypto/Aes128Cfb8ChannelCipher.native.kt | 396 ------------------ .../libmc/protocol/crypto/RSA1024.native.kt | 299 ------------- .../libmc/protocol/crypto/SHA1.native.kt | 96 ----- 37 files changed, 689 insertions(+), 1206 deletions(-) delete mode 100644 docs/Embedded-cryptography.md delete mode 100644 docs/Impl-HTTP-Client.md create mode 100644 libmc-common/src/commonMain/kotlin/cn/rtast/libmc/context/HttpClientProvider.kt rename libmc-common/src/commonMain/kotlin/cn/rtast/libmc/{crypto => context}/NetworkChannelCipher.kt (89%) create mode 100644 libmc-common/src/commonMain/kotlin/cn/rtast/libmc/context/ProtocolContext.kt delete mode 100644 libmc-common/src/commonMain/kotlin/cn/rtast/libmc/crypto/AuthenticationProvider.kt delete mode 100644 libmc-common/src/commonMain/kotlin/cn/rtast/libmc/crypto/ProtocolContext.kt create mode 100644 libmc-common/src/commonMain/kotlin/cn/rtast/libmc/serialization/Json.kt create mode 100644 libmc-common/src/commonTest/kotlin/test/TestJsonParser.kt create mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/CryptoProvider.kt create mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/MessageSigner.kt rename libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/{SHA1.kt => SHA.kt} (57%) create mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/chat/ClientChatTracker.kt create mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/chat/OutgoingMessageBuilder.kt delete mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/player/action/PlayerUpdateInfoAction.kt create mode 100644 libmc-protocol/src/commonTest/kotlin/client/private-key.http delete mode 100644 libmc-protocol/src/jvmMain/kotlin/cn/rtast/libmc/protocol/crypto/Aes128Cfb8Cipher.jvm.kt delete mode 100644 libmc-protocol/src/jvmMain/kotlin/cn/rtast/libmc/protocol/crypto/RSA1024.jvm.kt delete mode 100644 libmc-protocol/src/jvmMain/kotlin/cn/rtast/libmc/protocol/crypto/SHA1.jvm.kt delete mode 100644 libmc-protocol/src/nativeMain/kotlin/cn/rtast/libmc/protocol/crypto/Aes128Cfb8ChannelCipher.native.kt delete mode 100644 libmc-protocol/src/nativeMain/kotlin/cn/rtast/libmc/protocol/crypto/RSA1024.native.kt delete mode 100644 libmc-protocol/src/nativeMain/kotlin/cn/rtast/libmc/protocol/crypto/SHA1.native.kt diff --git a/docs/Embedded-cryptography.md b/docs/Embedded-cryptography.md deleted file mode 100644 index bc73306..0000000 --- a/docs/Embedded-cryptography.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/docs/Impl-HTTP-Client.md b/docs/Impl-HTTP-Client.md deleted file mode 100644 index 794ffc7..0000000 --- a/docs/Impl-HTTP-Client.md +++ /dev/null @@ -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" } - } -} -``` \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 00eaa40..f64de0d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,10 +9,10 @@ module relies on the following dependencies: ## Required APIs -| 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) | -| 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) | +| 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) | +| 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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7707dc5..996b2bf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,8 @@ kotlinx-io = "0.9.1" coroutines-test = "1.11.0" kotlinx-coroutines = "1.11.0" ktor-core = "3.5.2" +cryptography = "0.6.0" +kotlinx-serialization = "1.12.0" [libraries] 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-curl = { module = "io.ktor:ktor-client-curl", 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] kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } maven-publish = { id = "maven-publish" } +kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/context/HttpClientProvider.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/context/HttpClientProvider.kt new file mode 100644 index 0000000..d3d5c66 --- /dev/null +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/context/HttpClientProvider.kt @@ -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?, body: String?): String +} \ No newline at end of file diff --git a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/crypto/NetworkChannelCipher.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/context/NetworkChannelCipher.kt similarity index 89% rename from libmc-common/src/commonMain/kotlin/cn/rtast/libmc/crypto/NetworkChannelCipher.kt rename to libmc-common/src/commonMain/kotlin/cn/rtast/libmc/context/NetworkChannelCipher.kt index 59fb846..10c29be 100644 --- a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/crypto/NetworkChannelCipher.kt +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/context/NetworkChannelCipher.kt @@ -5,7 +5,7 @@ */ -package cn.rtast.libmc.crypto +package cn.rtast.libmc.context public interface NetworkChannelCipher { public fun encrypt(buffer: ByteArray, offset: Int, length: Int) diff --git a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/context/ProtocolContext.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/context/ProtocolContext.kt new file mode 100644 index 0000000..c7c324e --- /dev/null +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/context/ProtocolContext.kt @@ -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"), + ) +} \ No newline at end of file diff --git a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/crypto/AuthenticationProvider.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/crypto/AuthenticationProvider.kt deleted file mode 100644 index 4b0925c..0000000 --- a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/crypto/AuthenticationProvider.kt +++ /dev/null @@ -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) -} \ No newline at end of file diff --git a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/crypto/ProtocolContext.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/crypto/ProtocolContext.kt deleted file mode 100644 index b43598b..0000000 --- a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/crypto/ProtocolContext.kt +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/network/SocketContext.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/network/SocketContext.kt index ad9938a..f441199 100644 --- a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/network/SocketContext.kt +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/network/SocketContext.kt @@ -7,12 +7,12 @@ package cn.rtast.libmc.network -import cn.rtast.libmc.crypto.ProtocolContextBuilder +import cn.rtast.libmc.context.ProtocolContextBuilder 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( diff --git a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/serialization/Json.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/serialization/Json.kt new file mode 100644 index 0000000..28b95e9 --- /dev/null +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/serialization/Json.kt @@ -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? { + 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() + } +} \ No newline at end of file diff --git a/libmc-common/src/commonTest/kotlin/test/TestJsonParser.kt b/libmc-common/src/commonTest/kotlin/test/TestJsonParser.kt new file mode 100644 index 0000000..a93bf41 --- /dev/null +++ b/libmc-common/src/commonTest/kotlin/test/TestJsonParser.kt @@ -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"))) + } +} \ No newline at end of file diff --git a/libmc-protocol/build.gradle.kts b/libmc-protocol/build.gradle.kts index 3c066fe..dacae13 100644 --- a/libmc-protocol/build.gradle.kts +++ b/libmc-protocol/build.gradle.kts @@ -14,6 +14,8 @@ kotlin { commonMain.dependencies { api(project(":common")) api(project(":nbt")) + implementation(libs.cryptography.core) + implementation(libs.cryptography.provider.optimal) } jvmMain.dependencies {} diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/client/MinecraftClient.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/client/MinecraftClient.kt index 465e833..b9d428f 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/client/MinecraftClient.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/client/MinecraftClient.kt @@ -6,8 +6,8 @@ package cn.rtast.libmc.protocol.client -import cn.rtast.libmc.crypto.ProtocolContext -import cn.rtast.libmc.crypto.ProtocolContextBuilder +import cn.rtast.libmc.context.ProtocolContext +import cn.rtast.libmc.context.ProtocolContextBuilder import cn.rtast.libmc.protocol.network.NetworkChannel import cn.rtast.libmc.protocol.protocol.event.PacketEventDispatcher import cn.rtast.libmc.protocol.protocol.session.Session diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/Aes128Cfb8ChannelCipher.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/Aes128Cfb8ChannelCipher.kt index 21a9a00..f85e238 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/Aes128Cfb8ChannelCipher.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/Aes128Cfb8ChannelCipher.kt @@ -4,13 +4,60 @@ * Date: 2026/9/8 */ +@file:OptIn(DelicateCryptographyApi::class) 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 { - override fun encrypt(buffer: ByteArray, offset: Int, length: Int) - override fun decrypt(buffer: ByteArray, offset: Int, length: Int) - override fun close() +internal class Aes128Cfb8ChannelCipher(sharedKey: ByteArray) : NetworkChannelCipher { + private val aes = cryptoProvider.get(AES.CFB8) + private val key = aes.keyDecoder().decodeFromByteArrayBlocking(AES.Key.Format.RAW, sharedKey) + 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() + } } \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/CryptoProvider.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/CryptoProvider.kt new file mode 100644 index 0000000..dfe1fd4 --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/CryptoProvider.kt @@ -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 \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/MessageSigner.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/MessageSigner.kt new file mode 100644 index 0000000..052c711 --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/MessageSigner.kt @@ -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 + } +} diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/RSA1024.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/RSA1024.kt index 6a13fce..9ff1184 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/RSA1024.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/RSA1024.kt @@ -5,6 +5,16 @@ */ +@file:OptIn(DelicateCryptographyApi::class) + package cn.rtast.libmc.protocol.crypto -internal expect fun rsaEncrypt(publicKeyBytes: ByteArray, data: ByteArray): ByteArray \ No newline at end of file +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) +} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/SHA1.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/SHA.kt similarity index 57% rename from libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/SHA1.kt rename to libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/SHA.kt index e74637c..8441e90 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/SHA1.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/SHA.kt @@ -5,9 +5,15 @@ */ +@file:OptIn(DelicateCryptographyApi::class) + 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 bytes = if (isNegative) twosComplement(digest) else digest 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 } -internal fun twosComplement(bytes: ByteArray): ByteArray { +private fun twosComplement(bytes: ByteArray): ByteArray { val result = ByteArray(bytes.size) var carry = 1 for (i in bytes.size - 1 downTo 0) { @@ -27,10 +33,15 @@ internal fun twosComplement(bytes: ByteArray): ByteArray { 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() for (b in serverIdBytes) require((b.toInt() and 0xFF) <= 0x7F) { "serverId contains non-US-ASCII character" } return mcDigestToString(sha1Digest(serverIdBytes + secretKey + publicKey)) -} \ No newline at end of file +} + +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) \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/CipherChannel.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/CipherChannel.kt index d233428..61d9a55 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/CipherChannel.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/CipherChannel.kt @@ -7,7 +7,7 @@ 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.WriteChannel diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundPlayerChatMessagePacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundPlayerChatMessagePacket.kt index 5ca7b2f..0c68a96 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundPlayerChatMessagePacket.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundPlayerChatMessagePacket.kt @@ -7,6 +7,7 @@ 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.PacketCodec 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.protocol.protocol.game.chat.TextComponent import cn.rtast.libmc.protocol.protocol.game.chat.readTextComponent -import cn.rtast.libmc.network.BytesBuffer import kotlin.uuid.Uuid /** diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundPlayerInfoUpdatePacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundPlayerInfoUpdatePacket.kt index 54c14c7..d7f98df 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundPlayerInfoUpdatePacket.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundPlayerInfoUpdatePacket.kt @@ -1,89 +1,142 @@ /* * Copyright © 2026 RTAkland * Author: RTAkland - * Date: 2026/9/7 + * Date: 2026/9/11 */ - 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.PacketCodec 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.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.network.BytesBuffer +import kotlin.uuid.Uuid public data class ClientboundPlayerInfoUpdatePacket( - val actions: Set, - val entries: List, + val actions: Set, + val entries: List, ) : 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 { + val set = mutableSetOf() + entries.forEach { action -> if ((mask and action.mask) != 0) set.add(action) } + return set + } + + public fun toMask(actions: Set): 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) + public sealed interface ActionValue { + public data class AddPlayer(val name: String, val properties: List) : 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 { override fun encode(buffer: BytesBuffer, value: ClientboundPlayerInfoUpdatePacket) {} override fun decode(buffer: BytesBuffer): ClientboundPlayerInfoUpdatePacket { - val actionsMask = buffer.readUByte().toInt() - val actionsSet = PlayerUpdateInfoAction.parseActions(actionsMask) - val playerCount = buffer.readVarInt() - val entries = ArrayList(playerCount) - repeat(playerCount) { - val playerUuid = buffer.readUuid() - val playerActions = ArrayList(actionsSet.size) - for (action in PlayerUpdateInfoAction.entries) { - if (action !in actionsSet) continue - 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) + val mask = buffer.readByte().toInt() and 0xff + val actions = UpdateAction.fromMask(mask) + val entries = buffer.readPrefixed { + val profileId = buffer.readUuid() + val actionMap = mutableMapOf() + if (UpdateAction.ADD_PLAYER in actions) { + val name = buffer.readMcString() + val properties = buffer.readPrefixed { GameProfile.Property.decode(this) } + actionMap[UpdateAction.ADD_PLAYER] = ActionValue.AddPlayer(name, properties) } - 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) } } } \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/chat/ClientChatTracker.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/chat/ClientChatTracker.kt new file mode 100644 index 0000000..49c18f6 --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/chat/ClientChatTracker.kt @@ -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, val messageCount: Int) + + private val lastSeenQueue = ArrayDeque(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 { + 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): FixedBitSet20 { + val bitSet = createFixedBitSet20() + for (i in 0 until minOf(lastSeenSignatures.size, 20)) bitSet[i] = true + return bitSet +} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/chat/OutgoingMessageBuilder.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/chat/OutgoingMessageBuilder.kt new file mode 100644 index 0000000..945d62a --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/chat/OutgoingMessageBuilder.kt @@ -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): 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 +} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/player/action/PlayerUpdateInfoAction.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/player/action/PlayerUpdateInfoAction.kt deleted file mode 100644 index e6e443b..0000000 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/player/action/PlayerUpdateInfoAction.kt +++ /dev/null @@ -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 { - val set = HashSet() - for (action in entries) if ((mask and action.mask) != 0) set.add(action) - return set - } - - public fun toMask(actions: Set): 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) : 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) \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/session/Session.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/session/Session.kt index cd1cc7d..37ea815 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/session/Session.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/session/Session.kt @@ -9,6 +9,8 @@ package cn.rtast.libmc.protocol.protocol.session import cn.rtast.libmc.packet.MinecraftPacket 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 kotlin.reflect.KClass @@ -26,6 +28,24 @@ public interface Session { public suspend fun disconnect() public suspend fun init() 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 Session.onEvent(noinline block: suspend Session.(T) -> Unit) { diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/session/SessionEvent.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/session/SessionEvent.kt index 39a65c1..d62d68c 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/session/SessionEvent.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/session/SessionEvent.kt @@ -11,6 +11,7 @@ import cn.rtast.libmc.protocol.protocol.game.chat.TextComponent import cn.rtast.libmc.protocol.protocol.state.ProtocolState public sealed interface SessionEvent { + public data object Initialized : SessionEvent public data class DisconnectedEvent(val reason: TextComponent, val state: ProtocolState) : SessionEvent public object ConnectedEvent : SessionEvent public sealed interface ChangedState : SessionEvent { diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/session/SessionImpl.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/session/SessionImpl.kt index 822a0d4..fdae5b2 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/session/SessionImpl.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/session/SessionImpl.kt @@ -2,8 +2,7 @@ package cn.rtast.libmc.protocol.protocol.session import cn.rtast.libmc.packet.MinecraftPacket import cn.rtast.libmc.protocol.client.MinecraftClient -import cn.rtast.libmc.protocol.crypto.minecraftServerIdHash -import cn.rtast.libmc.protocol.crypto.rsaEncrypt +import cn.rtast.libmc.protocol.crypto.* 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.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.ClientboundKeepAlivePlayPacket 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.ServerboundPongPlayPacket import cn.rtast.libmc.protocol.packet.status.clientbound.ClientboundStatusResponsePacket 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.game.chat.TextComponent import cn.rtast.libmc.protocol.protocol.state.HandshakeIntent import cn.rtast.libmc.protocol.protocol.state.ProtocolState import cn.rtast.libmc.protocol.util.generateRandom16Bytes +import cn.rtast.libmc.serialization.LiteralJsonParser import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException +import kotlin.io.encoding.Base64 +import kotlin.random.Random import kotlin.reflect.KClass +import kotlin.time.Clock +import kotlin.uuid.Uuid private typealias EventHandler = suspend Session.(SessionEvent) -> Unit public class SessionImpl internal constructor() : Session { private lateinit var client: MinecraftClient + private lateinit var playerProfilePrivateKeyBytes: ByteArray + private lateinit var sessionId: Uuid + + private val chatTracker = ClientChatTracker() + internal fun attachClient(client: MinecraftClient) { this.client = client } @@ -50,6 +61,12 @@ public class SessionImpl internal constructor() : Session { internal val eventListener: HashMap, MutableList> = hashMapOf() override suspend fun init() { + client.onPacket { acceptEncryption(it) } + client.onPacket { setCompression(it.threshold) } + client.onPacket { acknowledgeLogin(it) } + client.onPacket { finishConfiguration() } + client.onPacket { networkChannel.sendPacket(ServerboundPongPlayPacket(it.id)) } + client.onPacket { networkChannel.sendPacket(ServerboundKeepAlivePlayPacket(it.id)) } client.onPacket { emitEvent(SessionEvent.DisconnectedEvent(it.reason, stateMachine.currentState)) } @@ -59,13 +76,6 @@ public class SessionImpl internal constructor() : Session { client.onPacket { emitEvent(SessionEvent.DisconnectedEvent(it.reason, stateMachine.currentState)) } - - client.onPacket { acceptEncryption(it) } - client.onPacket { setCompression(it.threshold) } - client.onPacket { acknowledgeLogin() } - client.onPacket { finishConfiguration() } - client.onPacket { networkChannel.sendPacket(ServerboundPongPlayPacket(it.id)) } - client.onPacket { networkChannel.sendPacket(ServerboundKeepAlivePlayPacket(it.id)) } client.onPacket { networkChannel.sendPacket(ServerboundKeepAliveConfigurationPacket(it.id)) } @@ -75,9 +85,68 @@ public class SessionImpl internal constructor() : Session { client.onPacket { networkChannel.sendPacket(ServerboundSelectKnownPacksPacket(emptyList())) } + + emitEvent(SessionEvent.Initialized) } 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 _onEvent(clazz: KClass, block: suspend Session.(T) -> Unit) { @Suppress("UNCHECKED_CAST") @@ -129,17 +198,19 @@ public class SessionImpl internal constructor() : Session { ensureState(ProtocolState.LOGIN) val sharedSecret = generateRandom16Bytes() val serverHash = minecraftServerIdHash(context.serverId, sharedSecret, context.publicKey) - client.protocolContext.authProvider!!.joinServer( - "https://sessionserver.mojang.com/session/minecraft/join", - client.accessToken!!, client.uuid.toString().replace("-", ""), serverHash - ) + val body = "{\"accessToken\":\"${client.accessToken}\", \"selectedProfile\":\"${ + client.uuid.toString().replace("-", "") + }\", \"serverId\":\"$serverHash\"}" + client.protocolContext.httpClientProvider!! + .post("https://sessionserver.mojang.com/session/minecraft/join", null, body) val encryptedSecret = rsaEncrypt(context.publicKey, sharedSecret) val encryptedVerifyToken = rsaEncrypt(context.publicKey, context.verifyToken) networkChannel.sendPacket(ServerboundKeyPacket(encryptedSecret, encryptedVerifyToken)) networkChannel.networkSession.enableEncryption(sharedSecret) } - internal suspend fun acknowledgeLogin() { + internal suspend fun acknowledgeLogin(packet: ClientboundLoginSuccessPacket) { + this.sessionId = packet.sessionId ensureState(ProtocolState.LOGIN) networkChannel.sendPacket(ServerboundLoginAcknowledgedPacket) stateMachine.transitionTo(ProtocolState.CONFIGURATION) diff --git a/libmc-protocol/src/commonTest/kotlin/client/ClientChatTracker.kt b/libmc-protocol/src/commonTest/kotlin/client/ClientChatTracker.kt index c9157f1..74b439e 100644 --- a/libmc-protocol/src/commonTest/kotlin/client/ClientChatTracker.kt +++ b/libmc-protocol/src/commonTest/kotlin/client/ClientChatTracker.kt @@ -39,22 +39,4 @@ fun createAcknowledgedBitSet(lastSeenSignatures: List): FixedBitSet20 bitSet[i] = true } 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): 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 - } } \ No newline at end of file diff --git a/libmc-protocol/src/commonTest/kotlin/client/TestClient.kt b/libmc-protocol/src/commonTest/kotlin/client/TestClient.kt index bbed044..41adbf0 100644 --- a/libmc-protocol/src/commonTest/kotlin/client/TestClient.kt +++ b/libmc-protocol/src/commonTest/kotlin/client/TestClient.kt @@ -7,15 +7,12 @@ 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.packet.play.clientbound.ClientboundAwardStatisticsPacket -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.clientbound.* 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.session.SessionEvent 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.request.* import io.ktor.client.statement.* -import io.ktor.http.* import io.ktor.utils.io.* import kotlinx.coroutines.test.runTest import kotlinx.io.buffered @@ -48,26 +44,32 @@ class TestClient { accessToken, context = { socketEngine = KtorNetworkEngine() - authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash -> - val status = httpClient.post(url) { - headers { header("Content-Type", "application/json") } - setBody("{\"accessToken\":\"$accessToken\", \"selectedProfile\":\"$uuid\", \"serverId\":\"$serverIdHash\"}") + httpClientProvider = object : HttpClientProvider { + override suspend fun post(url: String, headers: Map?, body: String?): String { + return httpClient.post(url) { + 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 { setProfileKey(acquireProfileKey()) } cli.onEvent { // println(status()) login() // disconnect() } - cli.onEvent { - println(it.reason.toJsonString()) + cli.onPacket { println(it) } + cli.onPacket { println(it) } + cli.onPacket { + sendMessage(it.message) + chatTracker.onReceivePlayerChat(it.messageSignature) } - cli.onPacket { chatTracker.onReceivePlayerChat(it.messageSignature) } - cli.onPacket { println(it) } - cli.on { packet, direction -> println("$direction -> $packet") } + cli.on { packet, direction -> if (direction == PacketDirection.CLIENTBOUND) println(packet) } cli.connect() // awaitCancellation() while (true) { @@ -96,22 +98,14 @@ class TestClient { // ) // } - cli.onEvent { -// println(status()) - login() -// disconnect() - } - cli.onEvent { - println(it) - } + cli.onEvent { login() } cli.onPacket { chatTracker.onReceivePlayerChat(it.messageSignature) } cli.onPacket { if (it.health <= 0) sendPacket(ServerboundClientCommandPacket(ClientAction.PerformRespawn)) } - cli.onPacket { println(it) } + cli.onPacket { println(it) } // cli.on { packet, direction -> println("$direction -> $packet") } cli.connect() - // awaitCancellation() while (true) { } diff --git a/libmc-protocol/src/commonTest/kotlin/client/private-key.http b/libmc-protocol/src/commonTest/kotlin/client/private-key.http new file mode 100644 index 0000000..ad28a48 --- /dev/null +++ b/libmc-protocol/src/commonTest/kotlin/client/private-key.http @@ -0,0 +1,3 @@ +POST https://api.minecraftservices.com/player/certificates +Authorization: Bearer +### \ No newline at end of file diff --git a/libmc-protocol/src/jvmMain/kotlin/cn/rtast/libmc/protocol/crypto/Aes128Cfb8Cipher.jvm.kt b/libmc-protocol/src/jvmMain/kotlin/cn/rtast/libmc/protocol/crypto/Aes128Cfb8Cipher.jvm.kt deleted file mode 100644 index 493b015..0000000 --- a/libmc-protocol/src/jvmMain/kotlin/cn/rtast/libmc/protocol/crypto/Aes128Cfb8Cipher.jvm.kt +++ /dev/null @@ -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() {} -} \ No newline at end of file diff --git a/libmc-protocol/src/jvmMain/kotlin/cn/rtast/libmc/protocol/crypto/RSA1024.jvm.kt b/libmc-protocol/src/jvmMain/kotlin/cn/rtast/libmc/protocol/crypto/RSA1024.jvm.kt deleted file mode 100644 index 935947b..0000000 --- a/libmc-protocol/src/jvmMain/kotlin/cn/rtast/libmc/protocol/crypto/RSA1024.jvm.kt +++ /dev/null @@ -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) -} \ No newline at end of file diff --git a/libmc-protocol/src/jvmMain/kotlin/cn/rtast/libmc/protocol/crypto/SHA1.jvm.kt b/libmc-protocol/src/jvmMain/kotlin/cn/rtast/libmc/protocol/crypto/SHA1.jvm.kt deleted file mode 100644 index 9b72f71..0000000 --- a/libmc-protocol/src/jvmMain/kotlin/cn/rtast/libmc/protocol/crypto/SHA1.jvm.kt +++ /dev/null @@ -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) \ No newline at end of file diff --git a/libmc-protocol/src/nativeMain/kotlin/cn/rtast/libmc/protocol/crypto/Aes128Cfb8ChannelCipher.native.kt b/libmc-protocol/src/nativeMain/kotlin/cn/rtast/libmc/protocol/crypto/Aes128Cfb8ChannelCipher.native.kt deleted file mode 100644 index 61c765e..0000000 --- a/libmc-protocol/src/nativeMain/kotlin/cn/rtast/libmc/protocol/crypto/Aes128Cfb8ChannelCipher.native.kt +++ /dev/null @@ -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) -} diff --git a/libmc-protocol/src/nativeMain/kotlin/cn/rtast/libmc/protocol/crypto/RSA1024.native.kt b/libmc-protocol/src/nativeMain/kotlin/cn/rtast/libmc/protocol/crypto/RSA1024.native.kt deleted file mode 100644 index 55027d6..0000000 --- a/libmc-protocol/src/nativeMain/kotlin/cn/rtast/libmc/protocol/crypto/RSA1024.native.kt +++ /dev/null @@ -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 { - 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 -} \ No newline at end of file diff --git a/libmc-protocol/src/nativeMain/kotlin/cn/rtast/libmc/protocol/crypto/SHA1.native.kt b/libmc-protocol/src/nativeMain/kotlin/cn/rtast/libmc/protocol/crypto/SHA1.native.kt deleted file mode 100644 index 69228c3..0000000 --- a/libmc-protocol/src/nativeMain/kotlin/cn/rtast/libmc/protocol/crypto/SHA1.native.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file