From c038082897161159b8dc0d18c5e28cc4117dd08e Mon Sep 17 00:00:00 2001 From: RTAkland Date: Wed, 9 Sep 2026 01:23:47 +0800 Subject: [PATCH] Embed AES/RSA/SHA-1 crypto implementation --- .gitignore | 2 + build.gradle.kts | 2 +- docs/{en => }/Get-started.md | 0 docs/{en => }/Impl-HTTP-Client.md | 0 docs/{en => }/Impl-TCP-Socket.md | 0 docs/{en => }/NBT-SNBT.md | 0 docs/README.md | 10 - docs/en/Assemble-context.md | 31 -- docs/en/Impl-Crypto.md | 67 --- docs/zh/Assemble-context-zh.md | 31 -- docs/zh/Get-started-zh.md | 79 ---- docs/zh/Impl-Crypto-zh.md | 67 --- docs/zh/Impl-HTTP-Client-zh.md | 45 -- docs/zh/Impl-TCP-Socket-zh.md | 327 --------------- docs/zh/README-zh.md | 32 -- .../JavaSocketNetworkEngine.kt | 0 .../KtorNetworkSocketEngine.kt | 0 .../NettyNetworkSocketEngine.kt | 0 gradle.properties | 5 +- gradle/libs.versions.toml | 3 - ...tworkCipher.kt => NetworkChannelCipher.kt} | 3 +- .../cn/rtast/libmc/crypto/ProtocolContext.kt | 18 - libmc-protocol-context/README.md | 21 - libmc-protocol-context/build.gradle.kts | 21 - .../libmc/protocol/context/AesCFB8Cipher.kt | 48 --- .../context/DefaultProtocolCryptoContext.kt | 31 -- .../libmc/protocol/context/RSAEncryotor.kt | 25 -- .../src/jvmTest/kotlin/test/TestJvmClient.kt | 41 -- libmc-protocol/build.gradle.kts | 18 +- .../libmc/protocol/client/MinecraftClient.kt | 5 +- .../client/MinecraftProtocolVersion.kt | 10 - .../crypto/Aes128Cfb8ChannelCipher.kt | 16 + .../cn/rtast/libmc/protocol/crypto/RSA1024.kt | 10 + .../cn/rtast/libmc/protocol/crypto/SHA1.kt | 29 +- .../event/InternalPacketDispatcher.kt | 10 +- .../libmc/protocol/network/CipherChannel.kt | 6 +- .../libmc/protocol/network/NetworkChannel.kt | 1 - .../libmc/protocol/network/NetworkSession.kt | 4 +- .../kotlin/client}/ClientChatTracker.kt | 2 +- .../kotlin/client/TestClient.kt} | 45 +- .../src/commonTest/kotlin/test/TestClient.kt | 34 -- .../protocol/crypto/Aes128Cfb8Cipher.jvm.kt} | 14 +- .../libmc/protocol/crypto/RSA1024.jvm.kt | 20 + .../rtast/libmc/protocol/crypto/SHA1.jvm.kt | 12 + .../jvmTest/kotlin/test/JavaSocketEngine.kt | 89 ---- .../crypto/Aes128Cfb8ChannelCipher.native.kt | 396 ++++++++++++++++++ .../libmc/protocol/crypto/RSA1024.native.kt | 299 +++++++++++++ .../libmc/protocol/crypto/SHA1.native.kt | 96 +++++ settings.gradle.kts | 13 +- 49 files changed, 931 insertions(+), 1107 deletions(-) rename docs/{en => }/Get-started.md (100%) rename docs/{en => }/Impl-HTTP-Client.md (100%) rename docs/{en => }/Impl-TCP-Socket.md (100%) rename docs/{en => }/NBT-SNBT.md (100%) delete mode 100644 docs/en/Assemble-context.md delete mode 100644 docs/en/Impl-Crypto.md delete mode 100644 docs/zh/Assemble-context-zh.md delete mode 100644 docs/zh/Get-started-zh.md delete mode 100644 docs/zh/Impl-Crypto-zh.md delete mode 100644 docs/zh/Impl-HTTP-Client-zh.md delete mode 100644 docs/zh/Impl-TCP-Socket-zh.md delete mode 100644 docs/zh/README-zh.md rename example/{network-engines => }/JavaSocketNetworkEngine.kt (100%) rename example/{network-engines => }/KtorNetworkSocketEngine.kt (100%) rename example/{network-engines => }/NettyNetworkSocketEngine.kt (100%) rename libmc-common/src/commonMain/kotlin/cn/rtast/libmc/crypto/{NetworkCipher.kt => NetworkChannelCipher.kt} (79%) delete mode 100644 libmc-protocol-context/README.md delete mode 100644 libmc-protocol-context/build.gradle.kts delete mode 100644 libmc-protocol-context/src/commonMain/kotlin/cn/rtast/libmc/protocol/context/AesCFB8Cipher.kt delete mode 100644 libmc-protocol-context/src/commonMain/kotlin/cn/rtast/libmc/protocol/context/DefaultProtocolCryptoContext.kt delete mode 100644 libmc-protocol-context/src/commonMain/kotlin/cn/rtast/libmc/protocol/context/RSAEncryotor.kt delete mode 100644 libmc-protocol-context/src/jvmTest/kotlin/test/TestJvmClient.kt delete mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/client/MinecraftProtocolVersion.kt create mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/Aes128Cfb8ChannelCipher.kt create mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/RSA1024.kt rename libmc-protocol-context/src/commonMain/kotlin/cn/rtast/libmc/protocol/context/Sha1Hasher.kt => libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/SHA1.kt (58%) rename libmc-protocol/src/{jvmTest/kotlin/test => commonTest/kotlin/client}/ClientChatTracker.kt (99%) rename libmc-protocol/src/{jvmTest/kotlin/test/TestClientTestInJvm.kt => commonTest/kotlin/client/TestClient.kt} (63%) delete mode 100644 libmc-protocol/src/commonTest/kotlin/test/TestClient.kt rename libmc-protocol/src/{jvmTest/kotlin/test/JvmAesCipher.kt => jvmMain/kotlin/cn/rtast/libmc/protocol/crypto/Aes128Cfb8Cipher.jvm.kt} (61%) create mode 100644 libmc-protocol/src/jvmMain/kotlin/cn/rtast/libmc/protocol/crypto/RSA1024.jvm.kt create mode 100644 libmc-protocol/src/jvmMain/kotlin/cn/rtast/libmc/protocol/crypto/SHA1.jvm.kt delete mode 100644 libmc-protocol/src/jvmTest/kotlin/test/JavaSocketEngine.kt create mode 100644 libmc-protocol/src/nativeMain/kotlin/cn/rtast/libmc/protocol/crypto/Aes128Cfb8ChannelCipher.native.kt create mode 100644 libmc-protocol/src/nativeMain/kotlin/cn/rtast/libmc/protocol/crypto/RSA1024.native.kt create mode 100644 libmc-protocol/src/nativeMain/kotlin/cn/rtast/libmc/protocol/crypto/SHA1.native.kt diff --git a/.gitignore b/.gitignore index 1d2fae1..d969598 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,5 @@ bin/ /libmc-protocol/src/jvmTest/resources/accessToken.txt /libmc-protocol-encrypt/src/commonTest/resources/accessToken.txt /libmc-protocol-context/src/commonTest/resources/accessToken.txt +/libmc-protocol/src/commonTest/resources/accessToken.txt +/libmc-protocol/src/cinterop/generated_defs/ diff --git a/build.gradle.kts b/build.gradle.kts index d1e8cab..3c990d5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ allprojects { group = "cn.rtast.libmc" val libVersion = getProperty("libVersion") version = when (name) { - "protocol" -> getProperty("protocolVersion") + "protocol" -> getProperty("protocolVersion") + "-" + libVersion else -> libVersion } diff --git a/docs/en/Get-started.md b/docs/Get-started.md similarity index 100% rename from docs/en/Get-started.md rename to docs/Get-started.md diff --git a/docs/en/Impl-HTTP-Client.md b/docs/Impl-HTTP-Client.md similarity index 100% rename from docs/en/Impl-HTTP-Client.md rename to docs/Impl-HTTP-Client.md diff --git a/docs/en/Impl-TCP-Socket.md b/docs/Impl-TCP-Socket.md similarity index 100% rename from docs/en/Impl-TCP-Socket.md rename to docs/Impl-TCP-Socket.md diff --git a/docs/en/NBT-SNBT.md b/docs/NBT-SNBT.md similarity index 100% rename from docs/en/NBT-SNBT.md rename to docs/NBT-SNBT.md diff --git a/docs/README.md b/docs/README.md index 59d7f81..540c31b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,7 +1,5 @@ # libmc -[中文](zh/README-zh.md) - A lightweight, modern Minecraft client protocol library designed for Kotlin Native & JVM. The core protocol library module relies on the following dependencies: @@ -14,9 +12,6 @@ module relies on the following dependencies: | 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) | -| AES-128-CFB8 | Conditional | Required only when logging into an `online-mode` server to encrypt/decrypt traffic. [Implement AES-128-CFB8](Impl-Crypto.md#AES-128-CFB8) | -| RSA 1024 | Conditional | Required only when logging into an `online-mode` server to encrypt the shared secret with the server's public key. [Implement RSA 1024](Impl-Crypto.md#RSA1024) | -| SHA1 | Conditional | Required only when logging into an `online-mode` server to compute the Server ID hash. [Implement SHA1](Impl-Crypto.md#SHA1) | | 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) | # Get started @@ -26,11 +21,6 @@ module relies on the following dependencies: > `libmc-protocol` is current under development. It only supports the latest Minecraft version > (Current supported Minecraft version: `26.2`, Protocol Version: `776`) -[Start using libmc-protocol](en/Get-started.md) - -# Assemble all context APIs - -[Assemble context APIs](en/Assemble-context.md) ## NBT & SNBT diff --git a/docs/en/Assemble-context.md b/docs/en/Assemble-context.md deleted file mode 100644 index ee46059..0000000 --- a/docs/en/Assemble-context.md +++ /dev/null @@ -1,31 +0,0 @@ -# Assemble context API - -```kotlin -public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = { - rsaEncryptor = RSA1024Encryptor { key, data -> ... } - sha1Hasher = Sha1Hasher { serverId, secretKey, publicKey -> ... } - cipherFactory = { key -> ... } - authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash -> ... } - socketEngine = MyCustomTCPSocketImpl() -} -``` - -> If using `DefaultProtocolContext` from `libmc-protocol-context` as the default context implementation, -> please refer to [Implementing TCPSocket](Impl-TCP-Socket.md) - -# Modify exists context API implementation - -```kotlin -fun main() { - val myNewProtocolContext = DefaultProtocolContext.withCustom { - rsaEncryptor = RSA1024Encryptor { key, data -> ... } - sha1Hasher = Sha1Hasher { serverId, secretKey, publicKey -> ... } - cipherFactory = { key -> ... } - authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash -> ... } - socketEngine = MyCustomTCPSocketImpl() - } -} -``` - -> Call `.withCustom` on a `ProtocolContextBuilder` to modify exists context implementation - diff --git a/docs/en/Impl-Crypto.md b/docs/en/Impl-Crypto.md deleted file mode 100644 index 44c3104..0000000 --- a/docs/en/Impl-Crypto.md +++ /dev/null @@ -1,67 +0,0 @@ -# Before start - -`AES-128-CFB8`, `RSA1024` and `SHA1` has been implemented in `libmc-protocol-context` module, it uses -`cryptography-kotlin`(and its platform based provider) - -> All impl example below are based on Java's built-in security & cryptography API - -# AES-128-CFB8 - -```kotlin -class JavaAesCipher(sharedKey: ByteArray) : NetworkCipher { - 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)) - } - - override fun encrypt(buffer: ByteArray, offset: Int, length: Int) { - encryptCipher.update(buffer, offset, length, buffer, offset) - } - - override fun decrypt(buffer: ByteArray, offset: Int, length: Int) { - decryptCipher.update(buffer, offset, length, buffer, offset) - } -} -``` - -# RSA1024 - -```kotlin -private fun encrypt(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) -} - -public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = { - rsaEncryptor = RSA1024Encryptor { key: ByteArray, data: ByteArray -> encrypt(key, data) } -} -``` - -# SHA1 - -```kotlin -private fun minecraftServerIdHash(serverId: String, secretKey: ByteArray, publicKey: ByteArray): String { - val serverIdBytes: ByteArray = serverId.encodeToByteArray() - for (b in serverIdBytes) require((b.toInt() and 0xFF) <= 0x7F) { "serverId contains non-US-ASCII character" } - val data = ByteArray(serverIdBytes.size + secretKey.size + publicKey.size) - System.arraycopy(serverIdBytes, 0, data, 0, serverIdBytes.size) - System.arraycopy(secretKey, 0, data, serverIdBytes.size, secretKey.size) - System.arraycopy(publicKey, 0, data, serverIdBytes.size + secretKey.size, publicKey.size) - val digest = MessageDigest.getInstance("SHA-1") - val hash = digest.digest(data) - return BigInteger(hash).toString(16) -} - -public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = { - sha1Hasher = Sha1Hasher { serverId: String, secretKey: ByteArray, publicKey: ByteArray -> - minecraftServerIdHash(serverId, secretKey, publicKey) - } -} -``` \ No newline at end of file diff --git a/docs/zh/Assemble-context-zh.md b/docs/zh/Assemble-context-zh.md deleted file mode 100644 index 1b4c709..0000000 --- a/docs/zh/Assemble-context-zh.md +++ /dev/null @@ -1,31 +0,0 @@ -# 组合上下文API - -```kotlin -public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = { - rsaEncryptor = RSA1024Encryptor { key, data -> ... } - sha1Hasher = Sha1Hasher { serverId, secretKey, publicKey -> ... } - cipherFactory = { key -> ... } - authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash -> ... } - socketEngine = MyCustomTCPSocketImpl() -} -``` - -> 如果使用`libmc-protocol-context`中的`DefaultProtocolContext`作为默认上下文实现, -> 请参阅[实现 TCPSocket](Impl-TCP-Socket-zh.md) - -# 修改默认实现 - -```kotlin -fun main() { - val myNewProtocolContext = DefaultProtocolContext.withCustom { - rsaEncryptor = RSA1024Encryptor { key, data -> ... } - sha1Hasher = Sha1Hasher { serverId, secretKey, publicKey -> ... } - cipherFactory = { key -> ... } - authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash -> ... } - socketEngine = MyCustomTCPSocketImpl() - } -} -``` - -> 使用`.withCustom`来修改已有的上下文API实现 - diff --git a/docs/zh/Get-started-zh.md b/docs/zh/Get-started-zh.md deleted file mode 100644 index ae8b135..0000000 --- a/docs/zh/Get-started-zh.md +++ /dev/null @@ -1,79 +0,0 @@ -# 创建客户端 - -```kotlin -public fun main() = runBlocking { - val client = createMinecraftClient( - "127.0.0.1", 25566, "MyBot", - generateOfflineUuid("MyBot"), - accessToken = null, - context = DefaultProtocolContext.withCustom { - socketEngine = KtorNetworkEngine() - } - ) - client.launch { client.connect() } - while (true) { - delay(5.seconds) - } -} -``` - -> 在上面的示例代码中, 创建了一个`MinecraftClient`, 这个客户端将会使用`MyBot`作为玩家名称连接到`127.0.0.1:25565`的`离线` -> 服务器, 并且将底层TCP Socket实现替换为了基于`ktor-network`实现的TCP Socket. -> (有关如何创建`SocketEngine`请参阅[实现TCP Socket](Impl-TCP-Socket-zh.md), -> 有关如何创建Context请参阅[需要实现的API](README-zh.md#需要实现的api)) -> `MinecraftClient`实现了`CoroutineContext`, 使用`client.connect()`后将会切换到后台线程执行, 添加阻塞线程代码以免程序直接退出. - -## 连接到在线服务器 - -```kotlin -private val accessToken = "eyJraWQiOiIw..." - -public fun main() = runBlocking { - val client = createMinecraftClient( - // 其他参数 - username = "RTAkland", - uuid = Uuid.parse("bb033844-e68e-4909-a636-1a5d1821ddc4"), - accessToken = accessToken, - // 其他参数 - ) -} -``` - -> 上面的示例代码中, 将会使用`RTAkland`作为玩家名称连接到服务器 - -### 快速获取AccessToken - -打开[minecraft.net](https://minecraft.net)并登录后按下F12, 在Console内输入 - -```javascript -console.log(`; ${document.cookie}`.split('; bearer_token=').pop().split(';').shift()) -``` - -> AccessToken的有效期为24小时 - -# 监听数据包 - -```kotlin -// 监听接收到的指定类型的数据包, 必须以Clientbound开头 -client.onPacket { - println(it) -} -// 监听所有接收到的数据包 -client.on { packet, direction -> - println("$direction -> $packet") -} - -// 监听被发送出去的数据包, 必须以Serverbound开头 -cli.onSent { - println(it) -} -``` - -# 发送数据包 - -```kotlin -// 必须以Serverbound开头的数据包才可以被发送, 否则将会抛出UnsupportedOperationException异常 -client.networkChannel.sendPacket( - ServerboundChatCommandPacket(command = "say Hello from libmc") -) -``` \ No newline at end of file diff --git a/docs/zh/Impl-Crypto-zh.md b/docs/zh/Impl-Crypto-zh.md deleted file mode 100644 index 3ed495f..0000000 --- a/docs/zh/Impl-Crypto-zh.md +++ /dev/null @@ -1,67 +0,0 @@ -# Before start - -`libmc-protocol-context` 模块中默认实现了 `AES-128-CFB8`, `RSA1024` 以及 `SHA1`, -三种加密/签名算法都基于`cryptography-kotlin`及其对应平台的provider - -> 下面给出的示例代码均基于Java内置的密码学API - -# AES-128-CFB8 - -```kotlin -class JavaAesCipher(sharedKey: ByteArray) : NetworkCipher { - 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)) - } - - override fun encrypt(buffer: ByteArray, offset: Int, length: Int) { - encryptCipher.update(buffer, offset, length, buffer, offset) - } - - override fun decrypt(buffer: ByteArray, offset: Int, length: Int) { - decryptCipher.update(buffer, offset, length, buffer, offset) - } -} -``` - -# RSA1024 - -```kotlin -private fun encrypt(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) -} - -public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = { - rsaEncryptor = RSA1024Encryptor { key: ByteArray, data: ByteArray -> encrypt(key, data) } -} -``` - -# SHA1 - -```kotlin -private fun minecraftServerIdHash(serverId: String, secretKey: ByteArray, publicKey: ByteArray): String { - val serverIdBytes: ByteArray = serverId.encodeToByteArray() - for (b in serverIdBytes) require((b.toInt() and 0xFF) <= 0x7F) { "serverId contains non-US-ASCII character" } - val data = ByteArray(serverIdBytes.size + secretKey.size + publicKey.size) - System.arraycopy(serverIdBytes, 0, data, 0, serverIdBytes.size) - System.arraycopy(secretKey, 0, data, serverIdBytes.size, secretKey.size) - System.arraycopy(publicKey, 0, data, serverIdBytes.size + secretKey.size, publicKey.size) - val digest = MessageDigest.getInstance("SHA-1") - val hash = digest.digest(data) - return BigInteger(hash).toString(16) -} - -public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = { - sha1Hasher = Sha1Hasher { serverId: String, secretKey: ByteArray, publicKey: ByteArray -> - minecraftServerIdHash(serverId, secretKey, publicKey) - } -} -``` \ No newline at end of file diff --git a/docs/zh/Impl-HTTP-Client-zh.md b/docs/zh/Impl-HTTP-Client-zh.md deleted file mode 100644 index 8cd37f9..0000000 --- a/docs/zh/Impl-HTTP-Client-zh.md +++ /dev/null @@ -1,45 +0,0 @@ -# 前言 - -`libmc-protocol-context` 中提供了默认HTTP客户端实现, 如果使用了此模块则不需要手动配置HTTP客户端认证部分 - -# 基于Ktor的HTTP客户端 - -```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) - } -} -``` - -# 基于Java8 HttpURLConnection的HTTP客户端 - -```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/zh/Impl-TCP-Socket-zh.md b/docs/zh/Impl-TCP-Socket-zh.md deleted file mode 100644 index 8f1f918..0000000 --- a/docs/zh/Impl-TCP-Socket-zh.md +++ /dev/null @@ -1,327 +0,0 @@ -# 前言 - -如果底层TCP Socket库是非挂起的, 那么**不要**将IO读写操作使用`withContext`包裹, -这会导致在网络IO中频繁的切换线程导致程序性能**下降**. - -# 基于Netty的TCP Socket实现 - -```kotlin -dependencies { - implementation("io.netty:netty-transport:4.2.17.Final") - implementation("io.netty:netty-buffer:4.2.17.Final") -} -``` - -
-点击展开代码 - -```kotlin -import cn.rtast.libmc.network.RawSocket -import cn.rtast.libmc.network.ReadChannel -import cn.rtast.libmc.network.SocketEngine -import cn.rtast.libmc.network.WriteChannel -import io.netty.bootstrap.Bootstrap -import io.netty.buffer.ByteBuf -import io.netty.buffer.Unpooled -import io.netty.channel.* -import io.netty.channel.nio.NioIoHandler -import io.netty.channel.socket.SocketChannel -import io.netty.channel.socket.nio.NioSocketChannel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import io.netty.channel.Channel as NettyChannel -import kotlinx.coroutines.channels.Channel as KotlinChannel - -public class NettyNetworkSocketEngine : SocketEngine { - override fun create(host: String, port: Int): RawSocket = NettyNetworkSocket(host, port) - - private class NettyNetworkSocket(private val host: String, private val port: Int) : RawSocket { - private val workerGroup: EventLoopGroup = MultiThreadIoEventLoopGroup(0, NioIoHandler.newFactory()) - private lateinit var channel: NettyChannel - private val inboundQueue = KotlinChannel(KotlinChannel.UNLIMITED) - private lateinit var readChannel: NettyReadChannel - private lateinit var writeChannel: NettyWriteChannel - - override suspend fun connect() { - val bootstrap = Bootstrap() - .group(workerGroup) - .channel(NioSocketChannel::class.java) - .option(ChannelOption.TCP_NODELAY, true) - .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) - .handler(object : ChannelInitializer() { - override fun initChannel(ch: SocketChannel) { - ch.pipeline().addLast(object : ChannelInboundHandlerAdapter() { - override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { - if (msg is ByteBuf) { - try { - val bytes = ByteArray(msg.readableBytes()) - msg.readBytes(bytes) - inboundQueue.trySend(bytes) - } finally { - msg.release() - } - } - } - - override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { - inboundQueue.close(cause) - ctx.close() - } - - override fun channelInactive(ctx: ChannelHandlerContext) { - inboundQueue.close() - super.channelInactive(ctx) - } - }) - } - }) - - channel = withContext(Dispatchers.IO) { - bootstrap.connect(host, port).suspendAwait() - } - - readChannel = NettyReadChannel(inboundQueue) - writeChannel = NettyWriteChannel(channel) - } - - override fun openReadChannel(): ReadChannel = readChannel - override fun openWriteChannel(): WriteChannel = writeChannel - - override fun close() { - if (::channel.isInitialized && channel.isOpen) { - channel.close() - } - inboundQueue.close() - workerGroup.shutdownGracefully() - } - } - - private class NettyReadChannel(private val inboundQueue: KotlinChannel) : ReadChannel { - private var currentChunk: ByteArray? = null - private var chunkOffset = 0 - - private suspend fun fetchNextChunk() { - val next = inboundQueue.receiveCatching().getOrNull() - ?: throw IllegalStateException("Socket/Channel closed while reading") - currentChunk = next - chunkOffset = 0 - } - - override suspend fun readByte(): Byte { - while (currentChunk == null || chunkOffset >= currentChunk!!.size) { - fetchNextChunk() - } - val chunk = currentChunk!! - return chunk[chunkOffset++] - } - - override suspend fun readBytes(length: Int): ByteArray { - val result = ByteArray(length) - readFully(result, 0, length) - return result - } - - override suspend fun readFully(out: ByteArray, start: Int, end: Int) { - var written = start - - while (written < end) { - while (currentChunk == null || chunkOffset >= currentChunk!!.size) { - fetchNextChunk() - } - val chunk = currentChunk!! - val available = chunk.size - chunkOffset - val toCopy = minOf(available, end - written) - - chunk.copyInto( - out, - destinationOffset = written, - startIndex = chunkOffset, - endIndex = chunkOffset + toCopy - ) - chunkOffset += toCopy - written += toCopy - } - } - } - - private class NettyWriteChannel(private val channel: NettyChannel) : WriteChannel { - private val writeMutex = Mutex() - - override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) { - val length = endIndex - startIndex - if (length <= 0) return - - val nettyBuf = Unpooled.copiedBuffer(value, startIndex, length) - - writeMutex.withLock { - withContext(Dispatchers.IO) { - channel.writeAndFlush(nettyBuf).suspendAwait() - } - } - } - - override suspend fun flush() { - writeMutex.withLock { - withContext(Dispatchers.IO) { - channel.flush() - } - } - } - } - -} - -private suspend inline fun ChannelFuture.suspendAwait(): NettyChannel = suspendCancellableCoroutine { cont -> - if (isDone) { - if (isSuccess) cont.resume(channel()) - else cont.resumeWithException(cause() ?: RuntimeException("Netty operation failed")) - return@suspendCancellableCoroutine - } - - addListener { future -> - if (future.isSuccess) { - cont.resume(channel()) - } else { - cont.resumeWithException(future.cause() ?: RuntimeException("Netty operation failed")) - } - } - cont.invokeOnCancellation { cancel(false) } - -} -``` - -> 请一并复制import部分的代码 - -
- -# 基于ktor-network的TCP Socket实现 - -```kotlin -dependencies { - implementation("io.ktor:ktor-network:3.5.2") -} -``` - -
-点击展开代码 - -```kotlin -public class KtorNetworkEngine : SocketEngine { - override fun create(host: String, port: Int): RawSocket = KtorNetworkSocket(host, port) -} - -public class KtorNetworkSocket(private val host: String, private val port: Int) : RawSocket { - private val sm = SelectorManager(Dispatchers.IO) - private lateinit var socket: Socket - - override suspend fun connect(): Unit = run { socket = aSocket(sm).tcp().connect(host, port) } - override fun openReadChannel(): ReadChannel = KtorReadChannel(socket.openReadChannel()) - override fun openWriteChannel(): WriteChannel = KtorWriteChannel(socket.openWriteChannel()) - override fun close() { - socket.close() - sm.close() - } -} - -public class KtorReadChannel(private val readChannel: ByteReadChannel) : ReadChannel { - override suspend fun readByte(): Byte = readChannel.readByte() - override suspend fun readBytes(length: Int): ByteArray = readChannel.readByteArray(length) - override suspend fun readFully(out: ByteArray, start: Int, end: Int): Unit = - readChannel.readFully(out, start, end) -} - -public class KtorWriteChannel(private val writeChannel: ByteWriteChannel) : WriteChannel { - override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) { - writeChannel.writeFully(value, startIndex, endIndex) - } - - override suspend fun flush(): Unit = writeChannel.flush() -} -``` - -
- -# 基于Java内置Socket的TCP Socket实现 - -
-点击展开代码 - -```kotlin -class JavaSocketEngine : SocketEngine { - override fun create(host: String, port: Int): RawSocket = JavaNetworkSocket(host, port) -} - -class JavaNetworkSocket( - private val host: String, - private val port: Int, - private val connectTimeoutMs: Int = 10000, -) : RawSocket { - private lateinit var socket: Socket - private lateinit var readChannel: JavaReadChannel - private lateinit var writeChannel: JavaWriteChannel - - override suspend fun connect() { - val s = Socket() - s.tcpNoDelay = true - s.connect(InetSocketAddress(host, port), connectTimeoutMs) - - socket = s - readChannel = JavaReadChannel(s.getInputStream()) - writeChannel = JavaWriteChannel(s.getOutputStream()) - } - - override fun openReadChannel(): ReadChannel = readChannel - override fun openWriteChannel(): WriteChannel = writeChannel - - override fun close() { - if (::socket.isInitialized && !socket.isClosed) { - runCatching { socket.close() } - } - } -} - -class JavaReadChannel(private val inputStream: InputStream) : ReadChannel { - override suspend fun readByte(): Byte { - val b = inputStream.read() - if (b == -1) throw IllegalStateException("Socket stream reached EOF while reading byte") - return b.toByte() - } - - override suspend fun readBytes(length: Int): ByteArray { - val buffer = ByteArray(length) - readFullyInternal(buffer, 0, length) - return buffer - } - - override suspend fun readFully(out: ByteArray, start: Int, end: Int) { - readFullyInternal(out, start, end - start) - } - - private fun readFullyInternal(out: ByteArray, offset: Int, length: Int) { - var bytesRead = 0 - while (bytesRead < length) { - val count = inputStream.read(out, offset + bytesRead, length - bytesRead) - if (count == -1) { - throw IllegalStateException("Socket stream closed unexpectedly (read $bytesRead of $length bytes)") - } - bytesRead += count - } - } -} - -class JavaWriteChannel(private val outputStream: OutputStream) : WriteChannel { - override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) { - outputStream.write(value, startIndex, endIndex - startIndex) - } - - override suspend fun flush() { - outputStream.flush() - } -} -``` -
\ No newline at end of file diff --git a/docs/zh/README-zh.md b/docs/zh/README-zh.md deleted file mode 100644 index c733b87..0000000 --- a/docs/zh/README-zh.md +++ /dev/null @@ -1,32 +0,0 @@ -# libmc - -一个轻量级适用于Kotlin Native & Jvm的现代Minecraft客户端协议库, 核心协议库模块使用了以下依赖 - -- 标准库 (`kotlin-stdlib`) -- IO (`kotlinx-io`) - 高效的将每个数据包封装为一个Buffer -- 协程库 (`kotlinx-coroutines`) - 实现高性能异步操作 - -## 需要实现的API - -| 名称 | 是否必须实现 | 额外说明 | -|:-------------|:-------------|:-----------------------------------------------------------------------------------------------------------------------------------------------| -| TCP Socket | 是 | `protocol` 模块没有内置TCP Socket实现. [实现 TCP Socket](Impl-TCP-Socket-zh.md) | -| AES-128-CFB8 | 视情况而定 | 仅在登录开启了正版验证(`online-mode`)的服务器时需要, 用于加密/解密流量. [实现AES-128-CFB8](Impl-Crypto-zh.md#AES-128-CFB8) | -| RSA 1024 | 视情况而定 | 仅在登录开启了正版验证(`online-mode`)的服务器时需要, 用于对服务器下发的公钥进行签名. [实现RSA 1024](Impl-Crypto-zh.md#RSA1024) | -| SHA1 | 视情况而定 | 仅在登录开启了正版验证(`online-mode`)的服务器时需要, 用于计算服务器的ServerId. [实现SHA1](Impl-Crypto-zh.md#SHA1) | -| HTTP 客户端 | 视情况而定 | 仅在登录开启了正版验证(`online-mode`)的服务器时需要, 用于向Mojang的Session服务器发送加入服务器的请求 [实现HTTP 客户端](Impl-HTTP-Client-zh.md) | - -# 开始使用 - -> `libmc-protocol`目前正在开发中, 也许存在着某些未被发现的问题. -> 它仅支持最新的Minecraft版本 (目前支持的游戏版本: `26.2`, 协议版本: `776`) - -[开始使用libmc-protocol](Get-started-zh.md) - -# 将实现的API组合起来 - -[将所有上下文API组合](Assemble-context-zh.md) - -# NBT & SNBT - -[NBT & SNBT](../en/NBT-SNBT.md) \ No newline at end of file diff --git a/example/network-engines/JavaSocketNetworkEngine.kt b/example/JavaSocketNetworkEngine.kt similarity index 100% rename from example/network-engines/JavaSocketNetworkEngine.kt rename to example/JavaSocketNetworkEngine.kt diff --git a/example/network-engines/KtorNetworkSocketEngine.kt b/example/KtorNetworkSocketEngine.kt similarity index 100% rename from example/network-engines/KtorNetworkSocketEngine.kt rename to example/KtorNetworkSocketEngine.kt diff --git a/example/network-engines/NettyNetworkSocketEngine.kt b/example/NettyNetworkSocketEngine.kt similarity index 100% rename from example/network-engines/NettyNetworkSocketEngine.kt rename to example/NettyNetworkSocketEngine.kt diff --git a/gradle.properties b/gradle.properties index 39800ea..c9fc770 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,8 @@ kotlin.code.style=official kotlin.native.ignoreDisabledTargets=true kotlin.native.enableKlibsCrossCompilation=true +kotlin.mpp.enableCInteropCommonization=false kotlin.daemon.jvmargs=-Xmx2048M org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8 -libVersion=0.1.1 -protocolVersion=26.2-0.1.1 \ No newline at end of file +libVersion=0.2.0 +protocolVersion=26.2 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aad749b..543e4d7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,6 @@ coroutines-test = "1.11.0" kotlinx-coroutines = "1.11.0" ktor-core = "3.5.2" cryptography-core = "0.6.0" -netty = "4.2.17.Final" [libraries] kotlinx-io = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io" } @@ -19,8 +18,6 @@ ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "kto ktor-client-winhttp = { module = "io.ktor:ktor-client-winhttp", 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" } -netty-handler = { module = "io.netty:netty-transport", version.ref = "netty" } -netty-buffer = { module = "io.netty:netty-buffer", version.ref = "netty" } [plugins] kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } diff --git a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/crypto/NetworkCipher.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/crypto/NetworkChannelCipher.kt similarity index 79% rename from libmc-common/src/commonMain/kotlin/cn/rtast/libmc/crypto/NetworkCipher.kt rename to libmc-common/src/commonMain/kotlin/cn/rtast/libmc/crypto/NetworkChannelCipher.kt index 70d13d0..59fb846 100644 --- a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/crypto/NetworkCipher.kt +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/crypto/NetworkChannelCipher.kt @@ -7,7 +7,8 @@ package cn.rtast.libmc.crypto -public interface NetworkCipher { +public interface NetworkChannelCipher { public fun encrypt(buffer: ByteArray, offset: Int, length: Int) public fun decrypt(buffer: ByteArray, offset: Int, length: Int) + public fun close() } \ 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 index 2881c57..f4d5ebd 100644 --- a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/crypto/ProtocolContext.kt +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/crypto/ProtocolContext.kt @@ -11,34 +11,16 @@ import cn.rtast.libmc.network.SocketContext import cn.rtast.libmc.network.SocketEngine public data class ProtocolContext( - val rsaEncryptor: RSA1024Encryptor?, - val sha1Hasher: Sha1Hasher?, - val cipherFactory: ((sharedKey: ByteArray) -> NetworkCipher)?, val authProvider: AuthenticationProvider?, override val engine: SocketEngine, ) : SocketContext() public class ProtocolContextBuilder(private val onlineMode: Boolean) { - public lateinit var rsaEncryptor: RSA1024Encryptor - public lateinit var sha1Hasher: Sha1Hasher - public lateinit var cipherFactory: (sharedKey: ByteArray) -> NetworkCipher public lateinit var authProvider: AuthenticationProvider public lateinit var socketEngine: SocketEngine public fun build(): ProtocolContext = ProtocolContext( - rsaEncryptor = if (onlineMode) { - if (::rsaEncryptor.isInitialized) rsaEncryptor else error("rsaEncryptor is required in online mode") - } else if (::rsaEncryptor.isInitialized) rsaEncryptor else null, - - sha1Hasher = if (onlineMode) { - if (::sha1Hasher.isInitialized) sha1Hasher else error("sha1Hasher is required in online mode") - } else if (::sha1Hasher.isInitialized) sha1Hasher else null, - - cipherFactory = if (onlineMode) { - if (::cipherFactory.isInitialized) cipherFactory else error("cipherFactory is required in online mode") - } else if (::cipherFactory.isInitialized) cipherFactory else null, - authProvider = if (onlineMode) { if (::authProvider.isInitialized) authProvider else error("authProvider is required in online mode") } else if (::authProvider.isInitialized) authProvider else null, diff --git a/libmc-protocol-context/README.md b/libmc-protocol-context/README.md deleted file mode 100644 index bbd084a..0000000 --- a/libmc-protocol-context/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# libmc-protocol-encrypt - -This module implemented `ProtocolContext` and provided a `DefaultProtocolContext`. - -## Get started - -> `Sha1`, `RSA1024`, `AES-128-CFB8` from `cryptography-kotlin`(and its based provider). -> `HTTP Client` from `ktor-client` - -> Before start, you need to add a `ktor client engine` for your platform. For JVM, `ktor-client-okhttp`(JVM 1.8+) -> or `ktor-client-java`(JVM 11+), for Linux, use `ktor-client-curl`, for Windows, use `ktor-client-winhttp`, -> for Apple, use `ktor-client-darwin` - -```kotlin -fun main() { - val cli = createMinecraftClient( - // ... other paramater - crypto = DefaultProtocolContext - ) -} -``` \ No newline at end of file diff --git a/libmc-protocol-context/build.gradle.kts b/libmc-protocol-context/build.gradle.kts deleted file mode 100644 index ff03d6d..0000000 --- a/libmc-protocol-context/build.gradle.kts +++ /dev/null @@ -1,21 +0,0 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -kotlin { - explicitApi() - withSourcesJar() - - linuxX64() - linuxArm64() - macosArm64() - mingwX64() - jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } - - sourceSets { - commonMain.dependencies { - api(project(":common")) - implementation(libs.cryptography.core) - implementation(libs.cryptography.provider.optimal) - implementation(libs.ktor.client.core) - } - } -} \ No newline at end of file diff --git a/libmc-protocol-context/src/commonMain/kotlin/cn/rtast/libmc/protocol/context/AesCFB8Cipher.kt b/libmc-protocol-context/src/commonMain/kotlin/cn/rtast/libmc/protocol/context/AesCFB8Cipher.kt deleted file mode 100644 index e276568..0000000 --- a/libmc-protocol-context/src/commonMain/kotlin/cn/rtast/libmc/protocol/context/AesCFB8Cipher.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright © 2026 RTAkland - * Author: RTAkland - * Date: 2026/9/7 - */ - - -@file:OptIn(DelicateCryptographyApi::class) - -package cn.rtast.libmc.protocol.context - -import cn.rtast.libmc.crypto.NetworkCipher -import dev.whyoleg.cryptography.DelicateCryptographyApi -import dev.whyoleg.cryptography.algorithms.AES - -public class AesCFB8Cipher(sharedKey: ByteArray) : NetworkCipher { - private val encryptIv = sharedKey.copyOf() - private val decryptIv = sharedKey.copyOf() - - private val cipher = provider.get(AES.CFB8) - .keyDecoder() - .decodeFromByteArrayBlocking(AES.Key.Format.RAW, sharedKey) - .cipher() - - override fun encrypt(buffer: ByteArray, offset: Int, length: Int) { - val plaintext = buffer.copyOfRange(offset, offset + length) - val ciphertext = cipher.encryptWithIvBlocking(encryptIv, plaintext) - ciphertext.copyInto(buffer, destinationOffset = offset) - updateIv(encryptIv, ciphertext) - } - - override fun decrypt(buffer: ByteArray, offset: Int, length: Int) { - val ciphertext = buffer.copyOfRange(offset, offset + length) - val plaintext = cipher.decryptWithIvBlocking(decryptIv, ciphertext) - plaintext.copyInto(buffer, destinationOffset = offset) - updateIv(decryptIv, ciphertext) - } - - private fun updateIv(iv: ByteArray, ciphertext: ByteArray) { - val len = ciphertext.size - if (len >= iv.size) { - ciphertext.copyInto(iv, destinationOffset = 0, startIndex = len - iv.size, endIndex = len) - } else { - iv.copyInto(iv, destinationOffset = 0, startIndex = len, endIndex = iv.size) - ciphertext.copyInto(iv, destinationOffset = iv.size - len) - } - } -} \ No newline at end of file diff --git a/libmc-protocol-context/src/commonMain/kotlin/cn/rtast/libmc/protocol/context/DefaultProtocolCryptoContext.kt b/libmc-protocol-context/src/commonMain/kotlin/cn/rtast/libmc/protocol/context/DefaultProtocolCryptoContext.kt deleted file mode 100644 index a959c39..0000000 --- a/libmc-protocol-context/src/commonMain/kotlin/cn/rtast/libmc/protocol/context/DefaultProtocolCryptoContext.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright © 2026 RTAkland - * Author: RTAkland - * Date: 2026/9/7 - */ - - -package cn.rtast.libmc.protocol.context - -import cn.rtast.libmc.crypto.AuthenticationProvider -import cn.rtast.libmc.crypto.ProtocolContextBuilder -import cn.rtast.libmc.crypto.RSA1024Encryptor -import cn.rtast.libmc.crypto.Sha1Hasher -import io.ktor.client.* -import io.ktor.client.request.* -import io.ktor.http.* - -private val httpClient: HttpClient = HttpClient() - -public val DefaultProtocolContext: ProtocolContextBuilder.() -> Unit = { - rsaEncryptor = RSA1024Encryptor { key, data -> rsaEncrypt(key, data) } - sha1Hasher = Sha1Hasher { serverId, secretKey, publicKey -> minecraftServerIdHash(serverId, secretKey, publicKey) } - cipherFactory = { key -> AesCFB8Cipher(key) } - authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash -> - val status = httpClient.post(url) { - headers { header("Content-Type", "application/json") } - setBody("{\"accessToken\":\"$accessToken\", \"selectedProfile\":\"$uuid\", \"serverId\":\"$serverIdHash\"}") - }.status - require(status == HttpStatusCode.NoContent) - } -} \ No newline at end of file diff --git a/libmc-protocol-context/src/commonMain/kotlin/cn/rtast/libmc/protocol/context/RSAEncryotor.kt b/libmc-protocol-context/src/commonMain/kotlin/cn/rtast/libmc/protocol/context/RSAEncryotor.kt deleted file mode 100644 index b1d7d6e..0000000 --- a/libmc-protocol-context/src/commonMain/kotlin/cn/rtast/libmc/protocol/context/RSAEncryotor.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright © 2026 RTAkland - * Author: RTAkland - * Date: 2026/9/7 - */ - - -@file:OptIn(DelicateCryptographyApi::class) - -package cn.rtast.libmc.protocol.context - -import dev.whyoleg.cryptography.CryptographyProvider -import dev.whyoleg.cryptography.DelicateCryptographyApi -import dev.whyoleg.cryptography.algorithms.RSA -import dev.whyoleg.cryptography.algorithms.SHA1 - -internal val provider = CryptographyProvider.Default - -public fun rsaEncrypt(publicKeyBytes: ByteArray, data: ByteArray): ByteArray { - val provider = CryptographyProvider.Default - val rsa = provider.get(RSA.PKCS1) - val publicKey = rsa.publicKeyDecoder(SHA1) - .decodeFromByteArrayBlocking(RSA.PublicKey.Format.DER, publicKeyBytes) - return publicKey.encryptor().encryptBlocking(data) -} \ No newline at end of file diff --git a/libmc-protocol-context/src/jvmTest/kotlin/test/TestJvmClient.kt b/libmc-protocol-context/src/jvmTest/kotlin/test/TestJvmClient.kt deleted file mode 100644 index 144d188..0000000 --- a/libmc-protocol-context/src/jvmTest/kotlin/test/TestJvmClient.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright © 2026 RTAkland - * Author: RTAkland - * Date: 2026/9/7 - */ - -package test - -import cn.rtast.libmc.packet.MinecraftPacket -import cn.rtast.libmc.protocol.client.createMinecraftClient -import cn.rtast.libmc.protocol.context.DefaultProtocolContext -import kotlinx.coroutines.launch -import org.junit.Test -import java.io.File -import kotlin.uuid.Uuid - - -class TestJvmClient { - - val accessToken = File("src/commonTest/resources/accessToken.txt").readText() - - @Test - fun `test default protocol context`() { - val cli = createMinecraftClient( - "127.0.0.1", - 25565, - "RTAkland", -// generateOfflineUuid("RTAkland"), - Uuid.parse("bb033844-e68e-4909-a636-1a5d1821ddc4"), -// null, - accessToken, - contextBuilder = DefaultProtocolContext - ) -// cli.on { println(it) } -// cli.on { println(it) } - cli.onPacket { println(it) } - cli.launch { cli.connect() } - while (true) { - } - } -} \ No newline at end of file diff --git a/libmc-protocol/build.gradle.kts b/libmc-protocol/build.gradle.kts index ba86c42..3c066fe 100644 --- a/libmc-protocol/build.gradle.kts +++ b/libmc-protocol/build.gradle.kts @@ -16,31 +16,23 @@ kotlin { api(project(":nbt")) } - jvmMain.dependencies { - - } + jvmMain.dependencies {} commonTest.dependencies { implementation(kotlin("test")) - implementation(project(":protocol-context")) implementation(libs.kotlinx.coroutines.test) implementation(libs.ktor.network) + implementation(libs.ktor.client.core) } jvmTest.dependencies { implementation(libs.ktor.client.okhttp) } - linuxTest.dependencies { + nativeTest.dependencies { implementation(libs.ktor.client.curl) } - - mingwTest.dependencies { - implementation(libs.ktor.client.winhttp) - } - - appleTest.dependencies { - implementation(libs.ktor.client.darwin) - } } + + compilerOptions.freeCompilerArgs.addAll("-Xexpect-actual-classes") } \ No newline at end of file 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 10ef5ab..063d9df 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 @@ -32,7 +32,6 @@ public class MinecraftClient internal constructor( internal val protocolContext: ProtocolContext, ) : PacketEventDispatcher(), CoroutineScope { internal val stateMachine = ClientStateMachine() - public val networkChannel: NetworkChannel = NetworkChannel( host, port, stateMachine, this, protocolContext @@ -106,4 +105,6 @@ public fun createMinecraftClient( ioDispatcher = ioDispatcher, protocolContext = context ) -} \ No newline at end of file +} + +internal const val CURRENT_MINECRAFT_PROTOCOL_VERSION: Int = 776 \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/client/MinecraftProtocolVersion.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/client/MinecraftProtocolVersion.kt deleted file mode 100644 index 9893251..0000000 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/client/MinecraftProtocolVersion.kt +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright © 2026 RTAkland - * Author: RTAkland - * Date: 2026/9/8 - */ - - -package cn.rtast.libmc.protocol.client - -internal const val CURRENT_MINECRAFT_PROTOCOL_VERSION: Int = 776 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 new file mode 100644 index 0000000..21a9a00 --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/Aes128Cfb8ChannelCipher.kt @@ -0,0 +1,16 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/8 + */ + + +package cn.rtast.libmc.protocol.crypto + +import cn.rtast.libmc.crypto.NetworkChannelCipher + +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() +} \ No newline at end of file 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 new file mode 100644 index 0000000..6a13fce --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/RSA1024.kt @@ -0,0 +1,10 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/8 + */ + + +package cn.rtast.libmc.protocol.crypto + +internal expect fun rsaEncrypt(publicKeyBytes: ByteArray, data: ByteArray): ByteArray \ No newline at end of file diff --git a/libmc-protocol-context/src/commonMain/kotlin/cn/rtast/libmc/protocol/context/Sha1Hasher.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/SHA1.kt similarity index 58% rename from libmc-protocol-context/src/commonMain/kotlin/cn/rtast/libmc/protocol/context/Sha1Hasher.kt rename to libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/SHA1.kt index b655a9c..e74637c 100644 --- a/libmc-protocol-context/src/commonMain/kotlin/cn/rtast/libmc/protocol/context/Sha1Hasher.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/SHA1.kt @@ -1,26 +1,13 @@ /* * Copyright © 2026 RTAkland * Author: RTAkland - * Date: 2026/9/7 + * Date: 2026/9/8 */ -@file:OptIn(DelicateCryptographyApi::class) +package cn.rtast.libmc.protocol.crypto -package cn.rtast.libmc.protocol.context - -import dev.whyoleg.cryptography.DelicateCryptographyApi -import dev.whyoleg.cryptography.algorithms.SHA1 - -public fun minecraftServerIdHash(serverId: String, secretKey: ByteArray, publicKey: ByteArray): String { - val serverIdBytes = serverId.encodeToByteArray() - for (b in serverIdBytes) require((b.toInt() and 0xFF) <= 0x7F) { "serverId contains non-US-ASCII character" } - val data = serverIdBytes + secretKey + publicKey - val hash = provider.get(SHA1).hasher().hashBlocking(data) - return mcDigestToString(hash) -} - -private fun mcDigestToString(digest: ByteArray): String { +internal 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') } @@ -29,7 +16,7 @@ private fun mcDigestToString(digest: ByteArray): String { return if (isNegative) "-$hex" else hex } -private fun twosComplement(bytes: ByteArray): ByteArray { +internal fun twosComplement(bytes: ByteArray): ByteArray { val result = ByteArray(bytes.size) var carry = 1 for (i in bytes.size - 1 downTo 0) { @@ -38,4 +25,12 @@ private fun twosComplement(bytes: ByteArray): ByteArray { carry = inverted ushr 8 } return result +} + +internal expect fun sha1Digest(data: ByteArray): ByteArray + +internal 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 diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/event/InternalPacketDispatcher.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/event/InternalPacketDispatcher.kt index 7bd8807..b7253f2 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/event/InternalPacketDispatcher.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/event/InternalPacketDispatcher.kt @@ -9,6 +9,8 @@ package cn.rtast.libmc.protocol.event 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.packet.configuration.clientbound.* import cn.rtast.libmc.protocol.packet.configuration.serverbound.* import cn.rtast.libmc.protocol.packet.login.clientbound.ClientboundDisconnectLoginPacket @@ -46,8 +48,7 @@ public class InternalPacketDispatcher(private val client: MinecraftClient) { is ClientboundHelloPacket -> { val sharedSecret = generateRandom16Bytes() if (client.isOnlineMode) { - val serverHash = client.protocolContext.sha1Hasher!! - .hash(packet.serverId, sharedSecret, packet.publicKey) + val serverHash = minecraftServerIdHash(packet.serverId, sharedSecret, packet.publicKey) client.protocolContext.authProvider!!.joinServer( "https://sessionserver.mojang.com/session/minecraft/join", client.accessToken!!, @@ -55,9 +56,8 @@ public class InternalPacketDispatcher(private val client: MinecraftClient) { serverHash ) } - val encryptedSecret = client.protocolContext.rsaEncryptor!!.encrypt(packet.publicKey, sharedSecret) - val encryptedVerifyToken = - client.protocolContext.rsaEncryptor!!.encrypt(packet.publicKey, packet.verifyToken) + val encryptedSecret = rsaEncrypt(packet.publicKey, sharedSecret) + val encryptedVerifyToken = rsaEncrypt(packet.publicKey, packet.verifyToken) client.networkChannel.sendPacket(ServerboundKeyPacket(encryptedSecret, encryptedVerifyToken)) client.networkChannel.session.enableEncryption(sharedSecret) } 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 0781619..d233428 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.NetworkCipher +import cn.rtast.libmc.crypto.NetworkChannelCipher import cn.rtast.libmc.network.ReadChannel import cn.rtast.libmc.network.WriteChannel @@ -16,7 +16,7 @@ import cn.rtast.libmc.network.WriteChannel */ internal class CipherReadChannel( private val delegate: ReadChannel, - private val crypto: NetworkCipher, + private val crypto: NetworkChannelCipher, ) : ReadChannel { override suspend fun readFully(out: ByteArray, start: Int, end: Int) { delegate.readFully(out, start, end) @@ -42,7 +42,7 @@ internal class CipherReadChannel( */ internal class CipherWriteChannel( private val delegate: WriteChannel, - private val crypto: NetworkCipher, + private val crypto: NetworkChannelCipher, ) : WriteChannel { override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) { val length = endIndex - startIndex diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/NetworkChannel.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/NetworkChannel.kt index 1b1ebb0..6f593a0 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/NetworkChannel.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/NetworkChannel.kt @@ -6,7 +6,6 @@ package cn.rtast.libmc.protocol.network -import cn.rtast.libmc.crypto.NetworkCipher import cn.rtast.libmc.crypto.ProtocolContext import cn.rtast.libmc.network.BytesBuffer import cn.rtast.libmc.network.wrap diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/NetworkSession.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/NetworkSession.kt index 94b0e86..f77531d 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/NetworkSession.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/NetworkSession.kt @@ -11,6 +11,7 @@ import cn.rtast.libmc.network.RawSocket import cn.rtast.libmc.network.ReadChannel import cn.rtast.libmc.network.WriteChannel import cn.rtast.libmc.primitives.readVarInt +import cn.rtast.libmc.protocol.crypto.Aes128Cfb8ChannelCipher public class NetworkSession internal constructor( private val host: String, @@ -18,7 +19,6 @@ public class NetworkSession internal constructor( private val context: ProtocolContext, ) { private var socket: RawSocket? = null - public var readChannel: ReadChannel? = null private set @@ -36,7 +36,7 @@ public class NetworkSession internal constructor( public fun enableEncryption(sharedKey: ByteArray) { val currentRead = requireNotNull(readChannel) val currentWrite = requireNotNull(writeChannel) - val cipher = context.cipherFactory!!.invoke(sharedKey) + val cipher = Aes128Cfb8ChannelCipher(sharedKey) this.readChannel = CipherReadChannel(currentRead, cipher) this.writeChannel = CipherWriteChannel(currentWrite, cipher) } diff --git a/libmc-protocol/src/jvmTest/kotlin/test/ClientChatTracker.kt b/libmc-protocol/src/commonTest/kotlin/client/ClientChatTracker.kt similarity index 99% rename from libmc-protocol/src/jvmTest/kotlin/test/ClientChatTracker.kt rename to libmc-protocol/src/commonTest/kotlin/client/ClientChatTracker.kt index 4ade903..c9157f1 100644 --- a/libmc-protocol/src/jvmTest/kotlin/test/ClientChatTracker.kt +++ b/libmc-protocol/src/commonTest/kotlin/client/ClientChatTracker.kt @@ -5,7 +5,7 @@ */ -package test +package client import cn.rtast.libmc.primitives.FixedBitSet20 import cn.rtast.libmc.primitives.createFixedBitSet20 diff --git a/libmc-protocol/src/jvmTest/kotlin/test/TestClientTestInJvm.kt b/libmc-protocol/src/commonTest/kotlin/client/TestClient.kt similarity index 63% rename from libmc-protocol/src/jvmTest/kotlin/test/TestClientTestInJvm.kt rename to libmc-protocol/src/commonTest/kotlin/client/TestClient.kt index c48a396..f9e85fc 100644 --- a/libmc-protocol/src/jvmTest/kotlin/test/TestClientTestInJvm.kt +++ b/libmc-protocol/src/commonTest/kotlin/client/TestClient.kt @@ -5,26 +5,35 @@ */ -package test +package client -import cn.rtast.libmc.network.withCustom +import cn.rtast.libmc.crypto.AuthenticationProvider import cn.rtast.libmc.packet.ClientboundUnknownPacket import cn.rtast.libmc.protocol.client.createMinecraftClient -import cn.rtast.libmc.protocol.context.DefaultProtocolContext import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundPlayerChatMessagePacket import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundChatMessagePacket 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.launch -import org.junit.Test -import java.io.File +import kotlinx.io.buffered +import kotlinx.io.files.Path +import kotlinx.io.files.SystemFileSystem +import test.KtorNetworkEngine import kotlin.random.Random +import kotlin.test.Test import kotlin.time.Clock import kotlin.uuid.Uuid -class TestClientTestInJvm { - val accessToken = File("src/jvmTest/resources/accessToken.txt").readText() +class TestClient { + val accessToken = SystemFileSystem.source(Path("src/commonTest/resources/accessToken.txt")) + .buffered().use { it.readText() } private val chatTracker = ClientChatTracker() + private val httpClient = HttpClient() @Test fun `test client`() { @@ -32,7 +41,16 @@ class TestClientTestInJvm { "127.0.0.1", 25565, "RTAkland", Uuid.parse("bb033844-e68e-4909-a636-1a5d1821ddc4"), accessToken, - context = DefaultProtocolContext + 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\"}") + } + require(status.status == HttpStatusCode.NoContent) { status.bodyAsText() } + } + } ) cli.on { packet, direction -> println("$direction -> $packet") } cli.launch { cli.connect() } @@ -45,15 +63,10 @@ class TestClientTestInJvm { val cli = createMinecraftClient( "127.0.0.1", 25566, "11", generateOfflineUuid("11"), null, - context = DefaultProtocolContext.withCustom { + context = { socketEngine = KtorNetworkEngine() } ) - -// cli.on { packet, direction -> -// if (packet !is ClientboundWaypointPacket) -// println("$direction -> $packet") -// } cli.onPacket { println(it) val snapshot = chatTracker.prepareForOutgoingMessage() @@ -69,10 +82,6 @@ class TestClientTestInJvm { cli.onPacket { chatTracker.onReceivePlayerChat(it.messageSignature) } -// cli.onPacket { println(it) } -// cli.onPacket { println(it) } -// cli.onPacket { println(it) } -// cli.onPacket { println(it) } cli.launch { cli.connect() } while (true) { } diff --git a/libmc-protocol/src/commonTest/kotlin/test/TestClient.kt b/libmc-protocol/src/commonTest/kotlin/test/TestClient.kt deleted file mode 100644 index 38e1a34..0000000 --- a/libmc-protocol/src/commonTest/kotlin/test/TestClient.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright © 2026 RTAkland - * Author: RTAkland - * Date: 2026/9/4 - */ - - -package test - -import cn.rtast.libmc.protocol.client.createMinecraftClient -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.uuid.Uuid - -class TestClient { - - @Test - fun `test client`() = runTest { - val cli = createMinecraftClient( - "127.0.0.1", - 25565, - "RTAkland", - Uuid.parse("bb033844-e68e-4909-a636-1a5d1821ddc4"), - null - ) { -// rsaEncryptor = RSA1024Encryptor { data, sharedKey -> } - } - cli.launch { cli.connect() } - cli.on { packet, direction -> println("${direction} -> $packet") } - while (true) { - } - } -} \ No newline at end of file diff --git a/libmc-protocol/src/jvmTest/kotlin/test/JvmAesCipher.kt b/libmc-protocol/src/jvmMain/kotlin/cn/rtast/libmc/protocol/crypto/Aes128Cfb8Cipher.jvm.kt similarity index 61% rename from libmc-protocol/src/jvmTest/kotlin/test/JvmAesCipher.kt rename to libmc-protocol/src/jvmMain/kotlin/cn/rtast/libmc/protocol/crypto/Aes128Cfb8Cipher.jvm.kt index 82172f8..493b015 100644 --- a/libmc-protocol/src/jvmTest/kotlin/test/JvmAesCipher.kt +++ b/libmc-protocol/src/jvmMain/kotlin/cn/rtast/libmc/protocol/crypto/Aes128Cfb8Cipher.jvm.kt @@ -1,18 +1,18 @@ /* * Copyright © 2026 RTAkland * Author: RTAkland - * Date: 2026/9/7 + * Date: 2026/9/8 */ -package test +package cn.rtast.libmc.protocol.crypto -import cn.rtast.libmc.crypto.NetworkCipher +import cn.rtast.libmc.crypto.NetworkChannelCipher import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec -class JvmAesCipher(sharedKey: ByteArray) : NetworkCipher { +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)) } @@ -21,11 +21,13 @@ class JvmAesCipher(sharedKey: ByteArray) : NetworkCipher { init(Cipher.DECRYPT_MODE, SecretKeySpec(sharedKey, "AES"), IvParameterSpec(sharedKey)) } - override fun encrypt(buffer: ByteArray, offset: Int, length: Int) { + actual override fun encrypt(buffer: ByteArray, offset: Int, length: Int) { encryptCipher.update(buffer, offset, length, buffer, offset) } - override fun decrypt(buffer: ByteArray, offset: Int, length: Int) { + 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 new file mode 100644 index 0000000..935947b --- /dev/null +++ b/libmc-protocol/src/jvmMain/kotlin/cn/rtast/libmc/protocol/crypto/RSA1024.jvm.kt @@ -0,0 +1,20 @@ +/* + * 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 new file mode 100644 index 0000000..9b72f71 --- /dev/null +++ b/libmc-protocol/src/jvmMain/kotlin/cn/rtast/libmc/protocol/crypto/SHA1.jvm.kt @@ -0,0 +1,12 @@ +/* + * 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/jvmTest/kotlin/test/JavaSocketEngine.kt b/libmc-protocol/src/jvmTest/kotlin/test/JavaSocketEngine.kt deleted file mode 100644 index 8983850..0000000 --- a/libmc-protocol/src/jvmTest/kotlin/test/JavaSocketEngine.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright © 2026 RTAkland - * Author: RTAkland - * Date: 2026/9/7 - */ - - -package test - -import cn.rtast.libmc.network.RawSocket -import cn.rtast.libmc.network.ReadChannel -import cn.rtast.libmc.network.SocketEngine -import cn.rtast.libmc.network.WriteChannel -import java.io.InputStream -import java.io.OutputStream -import java.net.InetSocketAddress -import java.net.Socket - -class JavaSocketEngine : SocketEngine { - override fun create(host: String, port: Int): RawSocket = JavaNetworkSocket(host, port) -} - -class JavaNetworkSocket( - private val host: String, - private val port: Int, - private val connectTimeoutMs: Int = 10000, -) : RawSocket { - private lateinit var socket: Socket - private lateinit var readChannel: JavaReadChannel - private lateinit var writeChannel: JavaWriteChannel - - override suspend fun connect() { - val s = Socket() - s.tcpNoDelay = true - s.connect(InetSocketAddress(host, port), connectTimeoutMs) - - socket = s - readChannel = JavaReadChannel(s.getInputStream()) - writeChannel = JavaWriteChannel(s.getOutputStream()) - } - - override fun openReadChannel(): ReadChannel = readChannel - override fun openWriteChannel(): WriteChannel = writeChannel - - override fun close() { - if (::socket.isInitialized && !socket.isClosed) { - runCatching { socket.close() } - } - } -} - -class JavaReadChannel(private val inputStream: InputStream) : ReadChannel { - override suspend fun readByte(): Byte { - val b = inputStream.read() - if (b == -1) throw IllegalStateException("Socket stream reached EOF while reading byte") - return b.toByte() - } - - override suspend fun readBytes(length: Int): ByteArray { - val buffer = ByteArray(length) - readFullyInternal(buffer, 0, length) - return buffer - } - - override suspend fun readFully(out: ByteArray, start: Int, end: Int) { - readFullyInternal(out, start, end - start) - } - - private fun readFullyInternal(out: ByteArray, offset: Int, length: Int) { - var bytesRead = 0 - while (bytesRead < length) { - val count = inputStream.read(out, offset + bytesRead, length - bytesRead) - if (count == -1) { - throw IllegalStateException("Socket stream closed unexpectedly (read $bytesRead of $length bytes)") - } - bytesRead += count - } - } -} - -class JavaWriteChannel(private val outputStream: OutputStream) : WriteChannel { - override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) { - outputStream.write(value, startIndex, endIndex - startIndex) - } - - override suspend fun flush() { - outputStream.flush() - } -} \ 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 new file mode 100644 index 0000000..61c765e --- /dev/null +++ b/libmc-protocol/src/nativeMain/kotlin/cn/rtast/libmc/protocol/crypto/Aes128Cfb8ChannelCipher.native.kt @@ -0,0 +1,396 @@ +/* + * 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 new file mode 100644 index 0000000..55027d6 --- /dev/null +++ b/libmc-protocol/src/nativeMain/kotlin/cn/rtast/libmc/protocol/crypto/RSA1024.native.kt @@ -0,0 +1,299 @@ +/* + * 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 new file mode 100644 index 0000000..69228c3 --- /dev/null +++ b/libmc-protocol/src/nativeMain/kotlin/cn/rtast/libmc/protocol/crypto/SHA1.native.kt @@ -0,0 +1,96 @@ +/* + * 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 diff --git a/settings.gradle.kts b/settings.gradle.kts index df0f981..96438ec 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,11 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + maven("https://repo.rtast.cn/packages/") + } +} + plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" } @@ -6,12 +14,7 @@ rootProject.name = "libmc" includeSubModule("common") includeSubModule("protocol") -includeSubModule("protocol-context") includeSubModule("nbt") -//includeSubModule("snbt") - -//includeSubModule("protocol-engine-netty", path = "libmc-network-engines/netty") -//includeSubModule("protocol-engine-ktor-network", path = "libmc-network-engines/ktor-network") fun includeSubModule(name: String, path: String? = null) { include(":$name")