Embed AES/RSA/SHA-1 crypto implementation

This commit is contained in:
2026-09-09 01:23:47 +08:00
parent 74f9e6f529
commit c038082897
49 files changed
+931 -1107

No files matched your search

+2
View File
@@ -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/
+1 -1
View File
@@ -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
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
-10
View File
@@ -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
-31
View File
@@ -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
-67
View File
@@ -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)
}
}
```
-31
View File
@@ -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实现
-79
View File
@@ -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<ClientboundLoginSuccessPacket> {
println(it)
}
// 监听所有接收到的数据包
client.on { packet, direction ->
println("$direction -> $packet")
}
// 监听被发送出去的数据包, 必须以Serverbound开头
cli.onSent<ServerboundPongConfigurationPacket> {
println(it)
}
```
# 发送数据包
```kotlin
// 必须以Serverbound开头的数据包才可以被发送, 否则将会抛出UnsupportedOperationException异常
client.networkChannel.sendPacket(
ServerboundChatCommandPacket(command = "say Hello from libmc")
)
```
-67
View File
@@ -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)
}
}
```
-45
View File
@@ -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" }
}
}
```
-327
View File
@@ -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")
}
```
<details>
<summary>点击展开代码</summary>
```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<ByteArray>(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<SocketChannel>() {
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<ByteArray>) : 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部分的代码
</details>
# 基于ktor-network的TCP Socket实现
```kotlin
dependencies {
implementation("io.ktor:ktor-network:3.5.2")
}
```
<details>
<summary>点击展开代码</summary>
```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()
}
```
</details>
# 基于Java内置Socket的TCP Socket实现
<details>
<summary>点击展开代码</summary>
```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()
}
}
```
</details>
-32
View File
@@ -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)
+3 -2
View File
@@ -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
libVersion=0.2.0
protocolVersion=26.2
-3
View File
@@ -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" }
@@ -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()
}
@@ -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,
-21
View File
@@ -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
)
}
```
-21
View File
@@ -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)
}
}
}
@@ -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)
}
}
}
@@ -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)
}
}
@@ -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)
}
@@ -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<ClientboundSystemChatMessagePacket> { println(it) }
// cli.on<ClientboundLoginSuccessPacket> { println(it) }
cli.onPacket<MinecraftPacket> { println(it) }
cli.launch { cli.connect() }
while (true) {
}
}
}
+4 -12
View File
@@ -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")
}
@@ -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
@@ -107,3 +106,5 @@ public fun createMinecraftClient(
protocolContext = context
)
}
internal const val CURRENT_MINECRAFT_PROTOCOL_VERSION: Int = 776
@@ -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
@@ -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()
}
@@ -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
@@ -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) {
@@ -39,3 +26,11 @@ private fun twosComplement(bytes: ByteArray): ByteArray {
}
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))
}
@@ -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)
}
@@ -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
@@ -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
@@ -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)
}
@@ -5,7 +5,7 @@
*/
package test
package client
import cn.rtast.libmc.primitives.FixedBitSet20
import cn.rtast.libmc.primitives.createFixedBitSet20
@@ -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<ClientboundUnknownPacket> {
println(it)
val snapshot = chatTracker.prepareForOutgoingMessage()
@@ -69,10 +82,6 @@ class TestClientTestInJvm {
cli.onPacket<ClientboundPlayerChatMessagePacket> {
chatTracker.onReceivePlayerChat(it.messageSignature)
}
// cli.onPacket<ClientboundServerDataPacket> { println(it) }
// cli.onPacket<ClientboundServerLinksPacket> { println(it) }
// cli.onPacket<ClientboundCodeOfConductPacket> { println(it) }
// cli.onPacket<ClientboundPlayerInfoUpdatePacket> { println(it) }
cli.launch { cli.connect() }
while (true) {
}
@@ -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) {
}
}
}
@@ -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() {}
}
@@ -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)
}
@@ -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)
@@ -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()
}
}
@@ -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)
}
@@ -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<ByteArray, ByteArray> {
var offset = 0
fun readTag(): Int = der[offset++].toInt() and 0xFF
fun readLength(): Int {
var len = der[offset++].toInt() and 0xFF
if (len and 0x80 != 0) {
val numBytes = len and 0x7F; len = 0
repeat(numBytes) { len = (len shl 8) or (der[offset++].toInt() and 0xFF) }
}
return len
}
if (readTag() != 0x30) error("Invalid DER Format")
readLength()
val tag = readTag()
if (tag == 0x30) {
val algLen = readLength()
offset += algLen
} else offset--
if (readTag() == 0x03) {
readLength(); offset++
if (readTag() != 0x30) error("Invalid RSA Public Key")
readLength()
}
if (readTag() != 0x02) error("Modulus Required")
val modulusLen = readLength()
val modulus = der.copyOfRange(offset, offset + modulusLen)
offset += modulusLen
if (readTag() != 0x02) error("Exponent Required")
val exponentLen = readLength()
val exponent = der.copyOfRange(offset, offset + exponentLen)
return Pair(modulus, exponent)
}
private fun pkcs1Pad(data: ByteArray, keySizeBytes: Int): ByteArray {
val maxDataLen = keySizeBytes - 11
require(data.size <= maxDataLen)
val padded = ByteArray(keySizeBytes)
padded[0] = 0x00
padded[1] = 0x02
val psLen = keySizeBytes - data.size - 3
var i = 2
while (i < 2 + psLen) {
val randomByte = Random.nextInt(1, 256).toByte(); padded[i] = randomByte; i++
}
padded[i] = 0x00; i++
for (k in data.indices) padded[i + k] = data[k]
return padded
}
private fun rsa1024(input: ByteArray, exponent: ByteArray, modulus: ByteArray): ByteArray {
val dataLongs = bytesToLongArray16(input)
val expoLongs = bytesToLongArray16(exponent)
val modLongs = bytesToLongArray16(modulus)
val resLongs = LongArray(18)
rsa1024(resLongs, dataLongs, expoLongs, modLongs)
return longArray16ToBytes(resLongs)
}
private fun rsa1024(res: LongArray, data: LongArray, expo: LongArray, key: LongArray): Boolean {
val modData = LongArray(18)
val result = LongArray(18)
var tempExpo: Long
modBigNumber(modData, data, key, 16)
result[0] = 1L
val expoLen = bitLength(expo, 16) / 64
for (i in 0..expoLen) {
tempExpo = expo[i]
repeat(64) {
if ((tempExpo and 1L) != 0L) modMultiply1024(result, result, modData, key)
modMultiply1024(modData, modData, modData, key)
tempExpo = tempExpo ushr 1
}
}
for (i in 0 until 16) res[i] = result[i]
return true
}
private fun addBigNumber(res: LongArray, op1: LongArray, op2: LongArray, n: Int): Boolean {
var carry = 0L
val mask32 = 0xFFFFFFFFL
var i = 0
while (i < n) {
val j = (op1[i] and mask32) + (op2[i] and mask32) + carry
val k = (op1[i] ushr 32) + (op2[i] ushr 32) + (j ushr 32)
carry = k ushr 32
res[i] = ((k and mask32) shl 32) or (j and mask32)
i++
}
if (i < res.size) res[i] = carry
return false
}
private fun multBigNumber(res: LongArray, op1: LongArray, op2: Int, n: Int): Boolean {
var carry1: Long
var carry2 = 0L
val op2UL = op2.toLong() and 0xFFFFFFFFL
val mask32 = 0xFFFFFFFFL
var i = 0
while (i < n) {
var j = (op1[i] and mask32) * op2UL
var k = (op1[i] ushr 32) * op2UL
carry1 = k ushr 32
k = (k and mask32) + (j ushr 32)
j = (j and mask32) + carry2
k += (j ushr 32)
carry2 = carry1 + (k ushr 32)
res[i] = ((k and mask32) shl 32) or (j and mask32)
i++
}
if (i < res.size) res[i] = carry2
return false
}
private fun modMultiply1024(res: LongArray, op1: LongArray, op2: LongArray, mod: LongArray): Boolean {
val mult1 = LongArray(33)
val mult2 = LongArray(33)
val result = LongArray(33)
val xmod = LongArray(33)
for (i in 0 until 16) xmod[i] = mod[i]
for (i in 0 until 16) {
mult1.fill(0L)
mult2.fill(0L)
val op2Low = (op2[i] and 0xFFFFFFFFL).toInt()
val op2High = ((op2[i] ushr 32) and 0xFFFFFFFFL).toInt()
multBigNumber(mult1, op1, op2Low, 16)
multBigNumber(mult2, op1, op2High, 16)
slnBigNumber(mult2, mult2, 33, 32)
addBigNumber(mult2, mult2, mult1, 32)
slnBigNumber(mult2, mult2, 33, 64 * i)
addBigNumber(result, result, mult2, 32)
}
modBigNumber(result, result, xmod, 33)
for (i in 0 until 16) res[i] = result[i]
return false
}
private fun modBigNumber(res: LongArray, op1: LongArray, op2: LongArray, n: Int): Boolean {
val lenOp1 = bitLength(op1, n)
val lenOp2 = bitLength(op2, n)
val lenDif = lenOp1 - lenOp2
for (i in 0 until n) res[i] = op1[i]
if (lenDif < 0) return true
if (lenDif == 0) {
while (compare(res, op2, n) >= 0) subBigNumber(res, res, op2, n)
return true
}
val op2Work = op2.copyOf()
slnBigNumber(op2Work, op2Work, n, lenDif)
repeat(lenDif) {
srnBigNumber(op2Work, op2Work, n, 1)
while (compare(res, op2Work, n) >= 0) subBigNumber(res, res, op2Work, n)
}
return true
}
private fun compare(op1: LongArray, op2: LongArray, n: Int): Int {
for (i in n - 1 downTo 0) {
val a = op1[i]
val b = op2[i]
if (a != b) {
val aUnsigned = a xor Long.MIN_VALUE
val bUnsigned = b xor Long.MIN_VALUE
return if (aUnsigned > bUnsigned) 1 else -1
}
}
return 0
}
private fun subBigNumber(res: LongArray, op1: LongArray, op2: LongArray, n: Int): Boolean {
var carry = false
val op1Copy = op1.copyOf()
for (i in 0 until n) {
var v1 = op1Copy[i]
if (carry) {
if (v1 != 0L) carry = false
v1 -= 1L
op1Copy[i] = v1
}
if ((v1 xor Long.MIN_VALUE) < (op2[i] xor Long.MIN_VALUE)) carry = true
res[i] = v1 - op2[i]
}
return carry
}
private fun slnBigNumber(res: LongArray, op: LongArray, len: Int, n: Int): Boolean {
val xShift = n / 64
val yShift = n % 64
var i = len
while (i - xShift > 0) {
res[i - 1] = op[i - 1 - xShift]
i--
}
while (i > 0) {
res[i - 1] = 0L
i--
}
if (yShift == 0) return true
var carry = 0L
for (idx in 0 until len) {
val j = res[idx]
val nextCarry = j ushr (64 - yShift)
res[idx] = (j shl yShift) or carry
carry = nextCarry
}
return true
}
private fun srnBigNumber(res: LongArray, op: LongArray, len: Int, n: Int): Boolean {
val xShift = n / 64
val yShift = n % 64
var i = 0
while (i + xShift < len) {
res[i] = op[i + xShift]; i++
}
while (i < len) {
res[i] = 0L; i++
}
if (yShift == 0) return true
var carry = 0L
for (idx in len downTo 1) {
val j = res[idx - 1]
val nextCarry = j shl (64 - yShift)
res[idx - 1] = (j ushr yShift) or carry
carry = nextCarry
}
return true
}
private fun bitLength(op: LongArray, n: Int): Int {
var len = 0
val unit = 1L
for (idx in n downTo 1) {
if (op[idx - 1] == 0L) continue
for (i in 64 downTo 1) {
if ((op[idx - 1] and (unit shl (i - 1))) != 0L) {
len = (64 * (idx - 1)) + i
break
}
}
if (len != 0) break
}
return len
}
private fun bytesToLongArray16(bytes: ByteArray): LongArray {
val cleanBytes = if (bytes.size > 128 && bytes[0] == 0.toByte()) bytes.copyOfRange(1, bytes.size) else bytes
val padded = ByteArray(128)
val startIdx = 128 - cleanBytes.size
for (k in cleanBytes.indices) padded[startIdx + k] = cleanBytes[k]
for (k in 0 until 64) {
val tmp = padded[k]
padded[k] = padded[127 - k]
padded[127 - k] = tmp
}
val result = LongArray(16)
for (i in 0 until 16) {
var value = 0L
for (j in 0 until 8) {
val byteVal = padded[i * 8 + j].toLong() and 0xFFL
value = value or (byteVal shl (j * 8))
}
result[i] = value
}
return result
}
private fun longArray16ToBytes(array: LongArray): ByteArray {
val bytes = ByteArray(128)
for (i in 0 until 16) {
val value = array[i]
for (j in 0 until 8) bytes[i * 8 + j] = ((value ushr (j * 8)) and 0xFFL).toByte()
}
for (k in 0 until 64) {
val tmp = bytes[k]
bytes[k] = bytes[127 - k]
bytes[127 - k] = tmp
}
return bytes
}
@@ -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
}
}
+8 -5
View File
@@ -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")