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