Embed AES/RSA/SHA-1 crypto implementation
This commit is contained in:
49 files changed
+931
-1107
No files matched your search
@@ -50,3 +50,5 @@ bin/
|
|||||||
/libmc-protocol/src/jvmTest/resources/accessToken.txt
|
/libmc-protocol/src/jvmTest/resources/accessToken.txt
|
||||||
/libmc-protocol-encrypt/src/commonTest/resources/accessToken.txt
|
/libmc-protocol-encrypt/src/commonTest/resources/accessToken.txt
|
||||||
/libmc-protocol-context/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
@@ -7,7 +7,7 @@ allprojects {
|
|||||||
group = "cn.rtast.libmc"
|
group = "cn.rtast.libmc"
|
||||||
val libVersion = getProperty("libVersion")
|
val libVersion = getProperty("libVersion")
|
||||||
version = when (name) {
|
version = when (name) {
|
||||||
"protocol" -> getProperty("protocolVersion")
|
"protocol" -> getProperty("protocolVersion") + "-" + libVersion
|
||||||
else -> libVersion
|
else -> libVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,7 +1,5 @@
|
|||||||
# libmc
|
# libmc
|
||||||
|
|
||||||
[中文](zh/README-zh.md)
|
|
||||||
|
|
||||||
A lightweight, modern Minecraft client protocol library designed for Kotlin Native & JVM. The core protocol library
|
A lightweight, modern Minecraft client protocol library designed for Kotlin Native & JVM. The core protocol library
|
||||||
module relies on the following dependencies:
|
module relies on the following dependencies:
|
||||||
|
|
||||||
@@ -14,9 +12,6 @@ module relies on the following dependencies:
|
|||||||
| Module Name | Required | Notes |
|
| Module Name | Required | Notes |
|
||||||
|:-------------|:------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|:-------------|:------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| TCP Socket | Yes | The `protocol` module does not have a built-in TCP Socket implementation. [Implement TCP Socket](Impl-TCP-Socket.md) |
|
| TCP Socket | Yes | The `protocol` module does not have a built-in TCP Socket implementation. [Implement TCP Socket](Impl-TCP-Socket.md) |
|
||||||
| 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) |
|
| 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
|
# 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
|
> `libmc-protocol` is current under development. It only supports the latest Minecraft version
|
||||||
> (Current supported Minecraft version: `26.2`, Protocol Version: `776`)
|
> (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
|
## NBT & SNBT
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -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实现
|
|
||||||
|
|
||||||
@@ -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")
|
|
||||||
)
|
|
||||||
```
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -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" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -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>
|
|
||||||
@@ -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)
|
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+3
-2
@@ -1,7 +1,8 @@
|
|||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
kotlin.native.ignoreDisabledTargets=true
|
kotlin.native.ignoreDisabledTargets=true
|
||||||
kotlin.native.enableKlibsCrossCompilation=true
|
kotlin.native.enableKlibsCrossCompilation=true
|
||||||
|
kotlin.mpp.enableCInteropCommonization=false
|
||||||
kotlin.daemon.jvmargs=-Xmx2048M
|
kotlin.daemon.jvmargs=-Xmx2048M
|
||||||
org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8
|
org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8
|
||||||
libVersion=0.1.1
|
libVersion=0.2.0
|
||||||
protocolVersion=26.2-0.1.1
|
protocolVersion=26.2
|
||||||
@@ -5,7 +5,6 @@ coroutines-test = "1.11.0"
|
|||||||
kotlinx-coroutines = "1.11.0"
|
kotlinx-coroutines = "1.11.0"
|
||||||
ktor-core = "3.5.2"
|
ktor-core = "3.5.2"
|
||||||
cryptography-core = "0.6.0"
|
cryptography-core = "0.6.0"
|
||||||
netty = "4.2.17.Final"
|
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
kotlinx-io = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io" }
|
kotlinx-io = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io" }
|
||||||
@@ -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-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-curl = { module = "io.ktor:ktor-client-curl", version.ref = "ktor-core" }
|
||||||
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor-core" }
|
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor-core" }
|
||||||
netty-handler = { module = "io.netty:netty-transport", version.ref = "netty" }
|
|
||||||
netty-buffer = { module = "io.netty:netty-buffer", version.ref = "netty" }
|
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
||||||
|
|||||||
+2
-1
@@ -7,7 +7,8 @@
|
|||||||
|
|
||||||
package cn.rtast.libmc.crypto
|
package cn.rtast.libmc.crypto
|
||||||
|
|
||||||
public interface NetworkCipher {
|
public interface NetworkChannelCipher {
|
||||||
public fun encrypt(buffer: ByteArray, offset: Int, length: Int)
|
public fun encrypt(buffer: ByteArray, offset: Int, length: Int)
|
||||||
public fun decrypt(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
|
import cn.rtast.libmc.network.SocketEngine
|
||||||
|
|
||||||
public data class ProtocolContext(
|
public data class ProtocolContext(
|
||||||
val rsaEncryptor: RSA1024Encryptor?,
|
|
||||||
val sha1Hasher: Sha1Hasher?,
|
|
||||||
val cipherFactory: ((sharedKey: ByteArray) -> NetworkCipher)?,
|
|
||||||
val authProvider: AuthenticationProvider?,
|
val authProvider: AuthenticationProvider?,
|
||||||
override val engine: SocketEngine,
|
override val engine: SocketEngine,
|
||||||
) : SocketContext()
|
) : SocketContext()
|
||||||
|
|
||||||
public class ProtocolContextBuilder(private val onlineMode: Boolean) {
|
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 authProvider: AuthenticationProvider
|
||||||
public lateinit var socketEngine: SocketEngine
|
public lateinit var socketEngine: SocketEngine
|
||||||
|
|
||||||
public fun build(): ProtocolContext =
|
public fun build(): ProtocolContext =
|
||||||
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) {
|
authProvider = if (onlineMode) {
|
||||||
if (::authProvider.isInitialized) authProvider else error("authProvider is required in online mode")
|
if (::authProvider.isInitialized) authProvider else error("authProvider is required in online mode")
|
||||||
} else if (::authProvider.isInitialized) authProvider else null,
|
} else if (::authProvider.isInitialized) authProvider else 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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-48
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-31
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-25
@@ -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) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,31 +16,23 @@ kotlin {
|
|||||||
api(project(":nbt"))
|
api(project(":nbt"))
|
||||||
}
|
}
|
||||||
|
|
||||||
jvmMain.dependencies {
|
jvmMain.dependencies {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
commonTest.dependencies {
|
commonTest.dependencies {
|
||||||
implementation(kotlin("test"))
|
implementation(kotlin("test"))
|
||||||
implementation(project(":protocol-context"))
|
|
||||||
implementation(libs.kotlinx.coroutines.test)
|
implementation(libs.kotlinx.coroutines.test)
|
||||||
implementation(libs.ktor.network)
|
implementation(libs.ktor.network)
|
||||||
|
implementation(libs.ktor.client.core)
|
||||||
}
|
}
|
||||||
|
|
||||||
jvmTest.dependencies {
|
jvmTest.dependencies {
|
||||||
implementation(libs.ktor.client.okhttp)
|
implementation(libs.ktor.client.okhttp)
|
||||||
}
|
}
|
||||||
|
|
||||||
linuxTest.dependencies {
|
nativeTest.dependencies {
|
||||||
implementation(libs.ktor.client.curl)
|
implementation(libs.ktor.client.curl)
|
||||||
}
|
}
|
||||||
|
|
||||||
mingwTest.dependencies {
|
|
||||||
implementation(libs.ktor.client.winhttp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
appleTest.dependencies {
|
compilerOptions.freeCompilerArgs.addAll("-Xexpect-actual-classes")
|
||||||
implementation(libs.ktor.client.darwin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+2
-1
@@ -32,7 +32,6 @@ public class MinecraftClient internal constructor(
|
|||||||
internal val protocolContext: ProtocolContext,
|
internal val protocolContext: ProtocolContext,
|
||||||
) : PacketEventDispatcher(), CoroutineScope {
|
) : PacketEventDispatcher(), CoroutineScope {
|
||||||
internal val stateMachine = ClientStateMachine()
|
internal val stateMachine = ClientStateMachine()
|
||||||
|
|
||||||
public val networkChannel: NetworkChannel = NetworkChannel(
|
public val networkChannel: NetworkChannel = NetworkChannel(
|
||||||
host, port, stateMachine,
|
host, port, stateMachine,
|
||||||
this, protocolContext
|
this, protocolContext
|
||||||
@@ -107,3 +106,5 @@ public fun createMinecraftClient(
|
|||||||
protocolContext = context
|
protocolContext = context
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal const val CURRENT_MINECRAFT_PROTOCOL_VERSION: Int = 776
|
||||||
-10
@@ -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
|
|
||||||
+16
@@ -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
|
||||||
+12
-17
@@ -1,26 +1,13 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright © 2026 RTAkland
|
* Copyright © 2026 RTAkland
|
||||||
* Author: 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
|
internal fun mcDigestToString(digest: ByteArray): String {
|
||||||
|
|
||||||
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 {
|
|
||||||
val isNegative = (digest[0].toInt() and 0x80) != 0
|
val isNegative = (digest[0].toInt() and 0x80) != 0
|
||||||
val bytes = if (isNegative) twosComplement(digest) else digest
|
val bytes = if (isNegative) twosComplement(digest) else digest
|
||||||
var hex = bytes.joinToString("") { (it.toInt() and 0xFF).toString(16).padStart(2, '0') }
|
var hex = bytes.joinToString("") { (it.toInt() and 0xFF).toString(16).padStart(2, '0') }
|
||||||
@@ -29,7 +16,7 @@ private fun mcDigestToString(digest: ByteArray): String {
|
|||||||
return if (isNegative) "-$hex" else hex
|
return if (isNegative) "-$hex" else hex
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun twosComplement(bytes: ByteArray): ByteArray {
|
internal fun twosComplement(bytes: ByteArray): ByteArray {
|
||||||
val result = ByteArray(bytes.size)
|
val result = ByteArray(bytes.size)
|
||||||
var carry = 1
|
var carry = 1
|
||||||
for (i in bytes.size - 1 downTo 0) {
|
for (i in bytes.size - 1 downTo 0) {
|
||||||
@@ -39,3 +26,11 @@ private fun twosComplement(bytes: ByteArray): ByteArray {
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal expect fun sha1Digest(data: ByteArray): ByteArray
|
||||||
|
|
||||||
|
internal fun minecraftServerIdHash(serverId: String, secretKey: ByteArray, publicKey: ByteArray): String {
|
||||||
|
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))
|
||||||
|
}
|
||||||
+5
-5
@@ -9,6 +9,8 @@ package cn.rtast.libmc.protocol.event
|
|||||||
|
|
||||||
import cn.rtast.libmc.packet.MinecraftPacket
|
import cn.rtast.libmc.packet.MinecraftPacket
|
||||||
import cn.rtast.libmc.protocol.client.MinecraftClient
|
import cn.rtast.libmc.protocol.client.MinecraftClient
|
||||||
|
import cn.rtast.libmc.protocol.crypto.minecraftServerIdHash
|
||||||
|
import cn.rtast.libmc.protocol.crypto.rsaEncrypt
|
||||||
import cn.rtast.libmc.protocol.packet.configuration.clientbound.*
|
import cn.rtast.libmc.protocol.packet.configuration.clientbound.*
|
||||||
import cn.rtast.libmc.protocol.packet.configuration.serverbound.*
|
import cn.rtast.libmc.protocol.packet.configuration.serverbound.*
|
||||||
import cn.rtast.libmc.protocol.packet.login.clientbound.ClientboundDisconnectLoginPacket
|
import cn.rtast.libmc.protocol.packet.login.clientbound.ClientboundDisconnectLoginPacket
|
||||||
@@ -46,8 +48,7 @@ public class InternalPacketDispatcher(private val client: MinecraftClient) {
|
|||||||
is ClientboundHelloPacket -> {
|
is ClientboundHelloPacket -> {
|
||||||
val sharedSecret = generateRandom16Bytes()
|
val sharedSecret = generateRandom16Bytes()
|
||||||
if (client.isOnlineMode) {
|
if (client.isOnlineMode) {
|
||||||
val serverHash = client.protocolContext.sha1Hasher!!
|
val serverHash = minecraftServerIdHash(packet.serverId, sharedSecret, packet.publicKey)
|
||||||
.hash(packet.serverId, sharedSecret, packet.publicKey)
|
|
||||||
client.protocolContext.authProvider!!.joinServer(
|
client.protocolContext.authProvider!!.joinServer(
|
||||||
"https://sessionserver.mojang.com/session/minecraft/join",
|
"https://sessionserver.mojang.com/session/minecraft/join",
|
||||||
client.accessToken!!,
|
client.accessToken!!,
|
||||||
@@ -55,9 +56,8 @@ public class InternalPacketDispatcher(private val client: MinecraftClient) {
|
|||||||
serverHash
|
serverHash
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val encryptedSecret = client.protocolContext.rsaEncryptor!!.encrypt(packet.publicKey, sharedSecret)
|
val encryptedSecret = rsaEncrypt(packet.publicKey, sharedSecret)
|
||||||
val encryptedVerifyToken =
|
val encryptedVerifyToken = rsaEncrypt(packet.publicKey, packet.verifyToken)
|
||||||
client.protocolContext.rsaEncryptor!!.encrypt(packet.publicKey, packet.verifyToken)
|
|
||||||
client.networkChannel.sendPacket(ServerboundKeyPacket(encryptedSecret, encryptedVerifyToken))
|
client.networkChannel.sendPacket(ServerboundKeyPacket(encryptedSecret, encryptedVerifyToken))
|
||||||
client.networkChannel.session.enableEncryption(sharedSecret)
|
client.networkChannel.session.enableEncryption(sharedSecret)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
package cn.rtast.libmc.protocol.network
|
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.ReadChannel
|
||||||
import cn.rtast.libmc.network.WriteChannel
|
import cn.rtast.libmc.network.WriteChannel
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ import cn.rtast.libmc.network.WriteChannel
|
|||||||
*/
|
*/
|
||||||
internal class CipherReadChannel(
|
internal class CipherReadChannel(
|
||||||
private val delegate: ReadChannel,
|
private val delegate: ReadChannel,
|
||||||
private val crypto: NetworkCipher,
|
private val crypto: NetworkChannelCipher,
|
||||||
) : ReadChannel {
|
) : ReadChannel {
|
||||||
override suspend fun readFully(out: ByteArray, start: Int, end: Int) {
|
override suspend fun readFully(out: ByteArray, start: Int, end: Int) {
|
||||||
delegate.readFully(out, start, end)
|
delegate.readFully(out, start, end)
|
||||||
@@ -42,7 +42,7 @@ internal class CipherReadChannel(
|
|||||||
*/
|
*/
|
||||||
internal class CipherWriteChannel(
|
internal class CipherWriteChannel(
|
||||||
private val delegate: WriteChannel,
|
private val delegate: WriteChannel,
|
||||||
private val crypto: NetworkCipher,
|
private val crypto: NetworkChannelCipher,
|
||||||
) : WriteChannel {
|
) : WriteChannel {
|
||||||
override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) {
|
override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) {
|
||||||
val length = endIndex - startIndex
|
val length = endIndex - startIndex
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
package cn.rtast.libmc.protocol.network
|
package cn.rtast.libmc.protocol.network
|
||||||
|
|
||||||
import cn.rtast.libmc.crypto.NetworkCipher
|
|
||||||
import cn.rtast.libmc.crypto.ProtocolContext
|
import cn.rtast.libmc.crypto.ProtocolContext
|
||||||
import cn.rtast.libmc.network.BytesBuffer
|
import cn.rtast.libmc.network.BytesBuffer
|
||||||
import cn.rtast.libmc.network.wrap
|
import cn.rtast.libmc.network.wrap
|
||||||
|
|||||||
+2
-2
@@ -11,6 +11,7 @@ import cn.rtast.libmc.network.RawSocket
|
|||||||
import cn.rtast.libmc.network.ReadChannel
|
import cn.rtast.libmc.network.ReadChannel
|
||||||
import cn.rtast.libmc.network.WriteChannel
|
import cn.rtast.libmc.network.WriteChannel
|
||||||
import cn.rtast.libmc.primitives.readVarInt
|
import cn.rtast.libmc.primitives.readVarInt
|
||||||
|
import cn.rtast.libmc.protocol.crypto.Aes128Cfb8ChannelCipher
|
||||||
|
|
||||||
public class NetworkSession internal constructor(
|
public class NetworkSession internal constructor(
|
||||||
private val host: String,
|
private val host: String,
|
||||||
@@ -18,7 +19,6 @@ public class NetworkSession internal constructor(
|
|||||||
private val context: ProtocolContext,
|
private val context: ProtocolContext,
|
||||||
) {
|
) {
|
||||||
private var socket: RawSocket? = null
|
private var socket: RawSocket? = null
|
||||||
|
|
||||||
public var readChannel: ReadChannel? = null
|
public var readChannel: ReadChannel? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ public class NetworkSession internal constructor(
|
|||||||
public fun enableEncryption(sharedKey: ByteArray) {
|
public fun enableEncryption(sharedKey: ByteArray) {
|
||||||
val currentRead = requireNotNull(readChannel)
|
val currentRead = requireNotNull(readChannel)
|
||||||
val currentWrite = requireNotNull(writeChannel)
|
val currentWrite = requireNotNull(writeChannel)
|
||||||
val cipher = context.cipherFactory!!.invoke(sharedKey)
|
val cipher = Aes128Cfb8ChannelCipher(sharedKey)
|
||||||
this.readChannel = CipherReadChannel(currentRead, cipher)
|
this.readChannel = CipherReadChannel(currentRead, cipher)
|
||||||
this.writeChannel = CipherWriteChannel(currentWrite, cipher)
|
this.writeChannel = CipherWriteChannel(currentWrite, cipher)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
package test
|
package client
|
||||||
|
|
||||||
import cn.rtast.libmc.primitives.FixedBitSet20
|
import cn.rtast.libmc.primitives.FixedBitSet20
|
||||||
import cn.rtast.libmc.primitives.createFixedBitSet20
|
import cn.rtast.libmc.primitives.createFixedBitSet20
|
||||||
+27
-18
@@ -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.packet.ClientboundUnknownPacket
|
||||||
import cn.rtast.libmc.protocol.client.createMinecraftClient
|
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.clientbound.ClientboundPlayerChatMessagePacket
|
||||||
import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundChatMessagePacket
|
import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundChatMessagePacket
|
||||||
import cn.rtast.libmc.protocol.util.generateOfflineUuid
|
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 kotlinx.coroutines.launch
|
||||||
import org.junit.Test
|
import kotlinx.io.buffered
|
||||||
import java.io.File
|
import kotlinx.io.files.Path
|
||||||
|
import kotlinx.io.files.SystemFileSystem
|
||||||
|
import test.KtorNetworkEngine
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
import kotlin.test.Test
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
|
||||||
class TestClientTestInJvm {
|
class TestClient {
|
||||||
val accessToken = File("src/jvmTest/resources/accessToken.txt").readText()
|
val accessToken = SystemFileSystem.source(Path("src/commonTest/resources/accessToken.txt"))
|
||||||
|
.buffered().use { it.readText() }
|
||||||
private val chatTracker = ClientChatTracker()
|
private val chatTracker = ClientChatTracker()
|
||||||
|
private val httpClient = HttpClient()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test client`() {
|
fun `test client`() {
|
||||||
@@ -32,7 +41,16 @@ class TestClientTestInJvm {
|
|||||||
"127.0.0.1", 25565, "RTAkland",
|
"127.0.0.1", 25565, "RTAkland",
|
||||||
Uuid.parse("bb033844-e68e-4909-a636-1a5d1821ddc4"),
|
Uuid.parse("bb033844-e68e-4909-a636-1a5d1821ddc4"),
|
||||||
accessToken,
|
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.on { packet, direction -> println("$direction -> $packet") }
|
||||||
cli.launch { cli.connect() }
|
cli.launch { cli.connect() }
|
||||||
@@ -45,15 +63,10 @@ class TestClientTestInJvm {
|
|||||||
val cli = createMinecraftClient(
|
val cli = createMinecraftClient(
|
||||||
"127.0.0.1", 25566, "11",
|
"127.0.0.1", 25566, "11",
|
||||||
generateOfflineUuid("11"), null,
|
generateOfflineUuid("11"), null,
|
||||||
context = DefaultProtocolContext.withCustom {
|
context = {
|
||||||
socketEngine = KtorNetworkEngine()
|
socketEngine = KtorNetworkEngine()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// cli.on { packet, direction ->
|
|
||||||
// if (packet !is ClientboundWaypointPacket)
|
|
||||||
// println("$direction -> $packet")
|
|
||||||
// }
|
|
||||||
cli.onPacket<ClientboundUnknownPacket> {
|
cli.onPacket<ClientboundUnknownPacket> {
|
||||||
println(it)
|
println(it)
|
||||||
val snapshot = chatTracker.prepareForOutgoingMessage()
|
val snapshot = chatTracker.prepareForOutgoingMessage()
|
||||||
@@ -69,10 +82,6 @@ class TestClientTestInJvm {
|
|||||||
cli.onPacket<ClientboundPlayerChatMessagePacket> {
|
cli.onPacket<ClientboundPlayerChatMessagePacket> {
|
||||||
chatTracker.onReceivePlayerChat(it.messageSignature)
|
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() }
|
cli.launch { cli.connect() }
|
||||||
while (true) {
|
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) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+8
-6
@@ -1,18 +1,18 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright © 2026 RTAkland
|
* Copyright © 2026 RTAkland
|
||||||
* Author: 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.Cipher
|
||||||
import javax.crypto.spec.IvParameterSpec
|
import javax.crypto.spec.IvParameterSpec
|
||||||
import javax.crypto.spec.SecretKeySpec
|
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 {
|
private val encryptCipher = Cipher.getInstance("AES/CFB8/NoPadding").apply {
|
||||||
init(Cipher.ENCRYPT_MODE, SecretKeySpec(sharedKey, "AES"), IvParameterSpec(sharedKey))
|
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))
|
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)
|
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)
|
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+396
@@ -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)
|
||||||
|
}
|
||||||
+299
@@ -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
@@ -1,3 +1,11 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
gradlePluginPortal()
|
||||||
|
mavenCentral()
|
||||||
|
maven("https://repo.rtast.cn/packages/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
|
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
|
||||||
}
|
}
|
||||||
@@ -6,12 +14,7 @@ rootProject.name = "libmc"
|
|||||||
|
|
||||||
includeSubModule("common")
|
includeSubModule("common")
|
||||||
includeSubModule("protocol")
|
includeSubModule("protocol")
|
||||||
includeSubModule("protocol-context")
|
|
||||||
includeSubModule("nbt")
|
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) {
|
fun includeSubModule(name: String, path: String? = null) {
|
||||||
include(":$name")
|
include(":$name")
|
||||||
|
|||||||
Reference in New Issue
Block a user