mirror of
https://gitlab.bixilon.de/bixilon/minosoft.git
synced 2025-09-19 12:25:12 -04:00
1.19: wip signed connection
This commit is contained in:
parent
844ad8ccda
commit
d2549fb0a5
@ -13,11 +13,16 @@
|
||||
|
||||
package de.bixilon.minosoft.data.entities.entities.player.local
|
||||
|
||||
import de.bixilon.minosoft.protocol.PlayerPublicKey
|
||||
import java.security.PrivateKey
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
|
||||
class PlayerPrivateKey(
|
||||
val expiresAt: Instant,
|
||||
val signature: ByteArray,
|
||||
val private: PrivateKey,
|
||||
val public: PublicKey,
|
||||
)
|
||||
) {
|
||||
val playerKey: PlayerPublicKey = PlayerPublicKey(expiresAt, public, signature)
|
||||
}
|
||||
|
@ -13,18 +13,27 @@
|
||||
|
||||
package de.bixilon.minosoft.protocol
|
||||
|
||||
import de.bixilon.kutil.base64.Base64Util.toBase64
|
||||
import de.bixilon.kutil.json.JsonObject
|
||||
import de.bixilon.kutil.primitive.LongUtil.toLong
|
||||
import de.bixilon.minosoft.protocol.protocol.encryption.CryptManager
|
||||
import de.bixilon.minosoft.util.KUtil.fromBase64
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
|
||||
class PlayerPublicKey(
|
||||
val expiresAt: Long,
|
||||
val keyString: String,
|
||||
val signature: String,
|
||||
val expiresAt: Instant,
|
||||
val publicKey: PublicKey,
|
||||
val signature: ByteArray,
|
||||
) {
|
||||
|
||||
constructor(nbt: JsonObject) : this(nbt["expires_at"].toLong(), nbt["key"].toString(), nbt["signature"].toString())
|
||||
constructor(nbt: JsonObject) : this(Instant.ofEpochMilli(nbt["expires_at"].toLong()), CryptManager.getPlayerPublicKey(nbt["key"].toString()), nbt["signature"].toString().fromBase64())
|
||||
|
||||
fun toNbt(): JsonObject {
|
||||
TODO()
|
||||
return mapOf(
|
||||
"expires_at" to expiresAt.epochSecond,
|
||||
"key" to publicKey.encoded.toBase64(),
|
||||
"signature" to signature.toBase64(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
23
src/main/java/de/bixilon/minosoft/protocol/ProtocolUtil.kt
Normal file
23
src/main/java/de/bixilon/minosoft/protocol/ProtocolUtil.kt
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Minosoft
|
||||
* Copyright (C) 2020-2022 Moritz Zwerger
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
|
||||
*/
|
||||
|
||||
package de.bixilon.minosoft.protocol
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
object ProtocolUtil {
|
||||
|
||||
fun String.encodeNetwork(): ByteArray {
|
||||
return this.toByteArray(StandardCharsets.UTF_8)
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@
|
||||
|
||||
package de.bixilon.minosoft.protocol.network.connection.play
|
||||
|
||||
import com.google.common.primitives.Longs
|
||||
import de.bixilon.kotlinglm.vec3.Vec3d
|
||||
import de.bixilon.kutil.string.WhitespaceUtil.removeTrailingWhitespaces
|
||||
import de.bixilon.minosoft.commands.stack.CommandStack
|
||||
@ -24,16 +25,23 @@ import de.bixilon.minosoft.gui.rendering.util.vec.vec3.Vec3dUtil.EMPTY
|
||||
import de.bixilon.minosoft.modding.event.events.ChatMessageSendEvent
|
||||
import de.bixilon.minosoft.modding.event.events.InternalMessageReceiveEvent
|
||||
import de.bixilon.minosoft.modding.event.events.container.ContainerCloseEvent
|
||||
import de.bixilon.minosoft.protocol.ProtocolUtil.encodeNetwork
|
||||
import de.bixilon.minosoft.protocol.packets.c2s.play.chat.ChatMessageC2SP
|
||||
import de.bixilon.minosoft.protocol.packets.c2s.play.chat.SignedChatMessageC2SP
|
||||
import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition
|
||||
import de.bixilon.minosoft.protocol.protocol.encryption.CryptManager
|
||||
import de.bixilon.minosoft.protocol.protocol.encryption.SignatureData
|
||||
import de.bixilon.minosoft.terminal.cli.CLI.removeDuplicatedWhitespaces
|
||||
import de.bixilon.minosoft.util.logging.Log
|
||||
import de.bixilon.minosoft.util.logging.LogLevels
|
||||
import de.bixilon.minosoft.util.logging.LogMessageType
|
||||
import java.security.SecureRandom
|
||||
import java.time.Instant
|
||||
|
||||
class ConnectionUtil(
|
||||
private val connection: PlayConnection,
|
||||
) {
|
||||
private val random = SecureRandom()
|
||||
|
||||
fun sendDebugMessage(message: Any) {
|
||||
val component = BaseComponent(RenderConstants.DEBUG_MESSAGES_PREFIX, ChatComponent.of(message).apply { this.setFallbackColor(ChatColors.BLUE) })
|
||||
@ -60,13 +68,29 @@ class ConnectionUtil(
|
||||
return
|
||||
}
|
||||
Log.log(LogMessageType.CHAT_OUT) { message }
|
||||
if (!connection.version.requiresSignedChat) {
|
||||
val privateKey = connection.player.privateKey?.private
|
||||
if (privateKey == null || !connection.version.requiresSignedChat) {
|
||||
return connection.sendPacket(ChatMessageC2SP(message))
|
||||
}
|
||||
TODO("Can not send signed chat!")
|
||||
|
||||
val signature = CryptManager.createSignature(connection.version)
|
||||
|
||||
val messageBytes = message.encodeNetwork()
|
||||
val salt = random.nextLong()
|
||||
val time = Instant.now()
|
||||
val uuid = connection.player.uuid
|
||||
|
||||
signature.initSign(privateKey)
|
||||
signature.update(Longs.toByteArray(salt))
|
||||
signature.update(Longs.toByteArray(uuid.leastSignificantBits))
|
||||
signature.update(Longs.toByteArray(uuid.mostSignificantBits))
|
||||
signature.update(Longs.toByteArray(time.epochSecond))
|
||||
signature.update(messageBytes)
|
||||
|
||||
connection.sendPacket(SignedChatMessageC2SP(messageBytes, time = time, signature = SignatureData(salt, signature.sign()), false))
|
||||
}
|
||||
|
||||
@Deprecated("message will re removed asa brigadier is fully implemented")
|
||||
@Deprecated("message will re removed as soon as brigadier is fully implemented")
|
||||
fun sendCommand(message: String, stack: CommandStack) {
|
||||
if (!connection.version.requiresSignedChat) {
|
||||
return sendChatMessage(message)
|
||||
|
@ -58,7 +58,6 @@ import de.bixilon.minosoft.protocol.protocol.ProtocolStates
|
||||
import de.bixilon.minosoft.protocol.protocol.encryption.CryptManager
|
||||
import de.bixilon.minosoft.terminal.RunConfiguration
|
||||
import de.bixilon.minosoft.terminal.cli.CLI
|
||||
import de.bixilon.minosoft.util.KUtil.fromBase64
|
||||
import de.bixilon.minosoft.util.ServerAddress
|
||||
import de.bixilon.minosoft.util.logging.Log
|
||||
import de.bixilon.minosoft.util.logging.LogLevels
|
||||
@ -185,8 +184,12 @@ class PlayConnection(
|
||||
if (version.requiresSignedChat) {
|
||||
taskWorker += Task(optional = true) {
|
||||
val minecraftKey = account.fetchKey(latch) ?: return@Task
|
||||
if (!minecraftKey.isSignatureCorrect()) {
|
||||
throw IllegalArgumentException("Yggdrasil signature mismatch!")
|
||||
}
|
||||
privateKey = PlayerPrivateKey(
|
||||
signature = minecraftKey.signature.fromBase64(),
|
||||
expiresAt = minecraftKey.expiresAt,
|
||||
signature = minecraftKey.signatureBytes,
|
||||
private = CryptManager.getPlayerPrivateKey(minecraftKey.pair.private),
|
||||
public = CryptManager.getPlayerPublicKey(minecraftKey.pair.public),
|
||||
)
|
||||
|
@ -19,13 +19,10 @@ import de.bixilon.minosoft.protocol.packets.factory.LoadPacket
|
||||
import de.bixilon.minosoft.protocol.protocol.PlayOutByteBuffer
|
||||
import de.bixilon.minosoft.protocol.protocol.ProtocolStates
|
||||
import de.bixilon.minosoft.protocol.protocol.ProtocolVersions
|
||||
import de.bixilon.minosoft.protocol.protocol.encryption.CryptManager
|
||||
import de.bixilon.minosoft.protocol.protocol.encryption.SignatureData
|
||||
import de.bixilon.minosoft.util.logging.Log
|
||||
import de.bixilon.minosoft.util.logging.LogLevels
|
||||
import de.bixilon.minosoft.util.logging.LogMessageType
|
||||
import java.security.PublicKey
|
||||
import javax.crypto.SecretKey
|
||||
|
||||
@LoadPacket(state = ProtocolStates.LOGIN)
|
||||
class EncryptionC2SP private constructor(
|
||||
@ -33,8 +30,6 @@ class EncryptionC2SP private constructor(
|
||||
val nonce: Any,
|
||||
) : PlayC2SPacket {
|
||||
|
||||
constructor(secretKey: SecretKey, nonce: ByteArray, key: PublicKey) : this(CryptManager.encryptData(key, secretKey.encoded), CryptManager.encryptData(key, nonce))
|
||||
|
||||
constructor(secret: ByteArray, nonce: ByteArray) : this(secret, nonce as Any)
|
||||
constructor(secret: ByteArray, nonce: SignatureData) : this(secret, nonce as Any)
|
||||
|
||||
|
@ -26,10 +26,10 @@ import de.bixilon.minosoft.util.logging.LogMessageType
|
||||
@LoadPacket(state = ProtocolStates.LOGIN)
|
||||
class StartC2SP(
|
||||
val username: String,
|
||||
val publicKey: PlayerPublicKey? = null,
|
||||
val publicKey: PlayerPublicKey?,
|
||||
) : PlayC2SPacket {
|
||||
|
||||
constructor(player: LocalPlayerEntity) : this(player.name)
|
||||
constructor(player: LocalPlayerEntity) : this(player.name, player.privateKey?.playerKey)
|
||||
|
||||
override fun write(buffer: PlayOutByteBuffer) {
|
||||
buffer.writeString(username)
|
||||
|
@ -24,7 +24,7 @@ import java.time.Instant
|
||||
|
||||
@LoadPacket(threadSafe = false)
|
||||
class SignedChatMessageC2SP(
|
||||
val message: String,
|
||||
val message: ByteArray,
|
||||
val time: Instant = Instant.now(),
|
||||
val signature: SignatureData? = null,
|
||||
val previewed: Boolean = false,
|
||||
@ -34,13 +34,11 @@ class SignedChatMessageC2SP(
|
||||
if (buffer.versionId == ProtocolVersions.V_22W17A) {
|
||||
buffer.writeInstant(time)
|
||||
}
|
||||
buffer.writeString(message)
|
||||
buffer.writeByteArray(message)
|
||||
if (buffer.versionId >= ProtocolVersions.V_22W18A) {
|
||||
buffer.writeInstant(time)
|
||||
}
|
||||
if (buffer.versionId >= ProtocolVersions.V_22W17A) {
|
||||
buffer.writeSignatureData(signature ?: SignatureData.EMPTY)
|
||||
}
|
||||
buffer.writeSignatureData(signature ?: SignatureData.EMPTY)
|
||||
if (buffer.versionId >= ProtocolVersions.V_22W19A) {
|
||||
buffer.writeBoolean(previewed)
|
||||
}
|
||||
|
@ -12,6 +12,7 @@
|
||||
*/
|
||||
package de.bixilon.minosoft.protocol.packets.s2c.login
|
||||
|
||||
import com.google.common.primitives.Longs
|
||||
import de.bixilon.kutil.base64.Base64Util.toBase64
|
||||
import de.bixilon.minosoft.protocol.PacketErrorHandler
|
||||
import de.bixilon.minosoft.protocol.network.connection.Connection
|
||||
@ -22,17 +23,19 @@ import de.bixilon.minosoft.protocol.packets.s2c.PlayS2CPacket
|
||||
import de.bixilon.minosoft.protocol.protocol.PlayInByteBuffer
|
||||
import de.bixilon.minosoft.protocol.protocol.ProtocolStates
|
||||
import de.bixilon.minosoft.protocol.protocol.encryption.CryptManager
|
||||
import de.bixilon.minosoft.protocol.protocol.encryption.SignatureData
|
||||
import de.bixilon.minosoft.util.logging.Log
|
||||
import de.bixilon.minosoft.util.logging.LogLevels
|
||||
import de.bixilon.minosoft.util.logging.LogMessageType
|
||||
import java.math.BigInteger
|
||||
import java.security.SecureRandom
|
||||
import javax.crypto.Cipher
|
||||
|
||||
@LoadPacket(state = ProtocolStates.LOGIN, threadSafe = false)
|
||||
class EncryptionS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket {
|
||||
val serverId: String = buffer.readString()
|
||||
val publicKey: ByteArray = buffer.readByteArray()
|
||||
val verifyToken: ByteArray = buffer.readByteArray()
|
||||
val nonce: ByteArray = buffer.readByteArray()
|
||||
|
||||
override fun handle(connection: PlayConnection) {
|
||||
val secretKey = CryptManager.createNewSharedKey()
|
||||
@ -43,14 +46,27 @@ class EncryptionS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket {
|
||||
val encryptCipher = CryptManager.createNetCipherInstance(Cipher.ENCRYPT_MODE, secretKey)
|
||||
val decryptCipher = CryptManager.createNetCipherInstance(Cipher.DECRYPT_MODE, secretKey)
|
||||
|
||||
val encryptedSecretKey = CryptManager.encryptData(publicKey, secretKey.encoded)
|
||||
val privateKey = connection.player.privateKey
|
||||
if (connection.version.requiresSignedChat && privateKey != null) {
|
||||
val salt = SecureRandom().nextLong()
|
||||
|
||||
connection.sendPacket(EncryptionC2SP(secretKey, verifyToken, publicKey))
|
||||
val signature = CryptManager.createSignature(connection.version)
|
||||
signature.initSign(privateKey.private)
|
||||
signature.update(nonce)
|
||||
signature.update(Longs.toByteArray(salt))
|
||||
val signed = signature.sign()
|
||||
|
||||
connection.sendPacket(EncryptionC2SP(encryptedSecretKey, SignatureData(salt, signed)))
|
||||
} else {
|
||||
connection.sendPacket(EncryptionC2SP(encryptedSecretKey, CryptManager.encryptData(secretKey, nonce)))
|
||||
}
|
||||
|
||||
connection.network.setupEncryption(encryptCipher, decryptCipher)
|
||||
}
|
||||
|
||||
override fun log(reducedLog: Boolean) {
|
||||
Log.log(LogMessageType.NETWORK_PACKETS_IN, level = LogLevels.VERBOSE) { "Encryption request (serverId=$serverId, publicKey=${publicKey.toBase64()}, verifyToken=${verifyToken.toBase64()})" }
|
||||
Log.log(LogMessageType.NETWORK_PACKETS_IN, level = LogLevels.VERBOSE) { "Encryption request (serverId=$serverId, publicKey=${publicKey.toBase64()}, nonce=${nonce.toBase64()})" }
|
||||
}
|
||||
|
||||
companion object : PacketErrorHandler {
|
||||
|
@ -18,6 +18,7 @@ import de.bixilon.kotlinglm.vec3.Vec3d
|
||||
import de.bixilon.kotlinglm.vec3.Vec3i
|
||||
import de.bixilon.minosoft.data.registries.ResourceLocation
|
||||
import de.bixilon.minosoft.data.text.ChatComponent
|
||||
import de.bixilon.minosoft.protocol.ProtocolUtil.encodeNetwork
|
||||
import de.bixilon.minosoft.util.collections.bytes.HeapArrayByteList
|
||||
import de.bixilon.minosoft.util.nbt.tag.NBTTagTypes
|
||||
import de.bixilon.minosoft.util.nbt.tag.NBTUtil.nbtType
|
||||
@ -66,7 +67,7 @@ open class OutByteBuffer() {
|
||||
|
||||
fun writeString(string: String) {
|
||||
check(string.length <= ProtocolDefinition.STRING_MAX_LENGTH) { "String max string length exceeded ${string.length} > ${ProtocolDefinition.STRING_MAX_LENGTH}" }
|
||||
val bytes = string.toByteArray(StandardCharsets.UTF_8)
|
||||
val bytes = string.encodeNetwork()
|
||||
writeVarInt(bytes.size)
|
||||
writeUnprefixedByteArray(bytes)
|
||||
}
|
||||
|
@ -77,7 +77,13 @@ class PlayOutByteBuffer(val connection: PlayConnection) : OutByteBuffer() {
|
||||
}
|
||||
|
||||
fun writePublicKey(key: PlayerPublicKey) {
|
||||
writeNBT(key.toNbt())
|
||||
if (versionId <= ProtocolVersions.V_22W18A) { // ToDo: find version
|
||||
writeNBT(key.toNbt())
|
||||
} else {
|
||||
writeInstant(key.expiresAt)
|
||||
writeByteArray(key.publicKey.encoded)
|
||||
writeByteArray(key.signature)
|
||||
}
|
||||
}
|
||||
|
||||
fun writeSignatureData(signature: SignatureData) {
|
||||
|
@ -12,6 +12,8 @@
|
||||
*/
|
||||
package de.bixilon.minosoft.protocol.protocol.encryption
|
||||
|
||||
import de.bixilon.minosoft.data.registries.versions.Version
|
||||
import de.bixilon.minosoft.protocol.protocol.ProtocolVersions
|
||||
import de.bixilon.minosoft.util.KUtil.fromBase64
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.*
|
||||
@ -86,4 +88,8 @@ object CryptManager {
|
||||
val rsa = KeyFactory.getInstance("RSA")
|
||||
return rsa.generatePublic(X509EncodedKeySpec(key))
|
||||
}
|
||||
|
||||
fun createSignature(version: Version): Signature {
|
||||
return Signature.getInstance(if (version.versionId == ProtocolVersions.V_22W17A) "SHA1withRSA" else "SHA256withRSA")
|
||||
}
|
||||
}
|
||||
|
@ -13,8 +13,12 @@
|
||||
|
||||
package de.bixilon.minosoft.util.account.minecraft
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import de.bixilon.minosoft.util.KUtil.fromBase64
|
||||
import de.bixilon.minosoft.util.YggdrasilUtil
|
||||
import de.bixilon.minosoft.util.account.minecraft.key.MinecraftKeyPair
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.time.Instant
|
||||
|
||||
data class MinecraftPrivateKey(
|
||||
@ -23,9 +27,16 @@ data class MinecraftPrivateKey(
|
||||
@JsonProperty("expiresAt") val expiresAt: Instant,
|
||||
@JsonProperty("refreshedAfter") val refreshedAfter: Instant,
|
||||
) {
|
||||
@get:JsonIgnore val signatureBytes: ByteArray by lazy { signature.fromBase64() }
|
||||
|
||||
fun isExpired(): Boolean {
|
||||
val now = Instant.now()
|
||||
return now.isAfter(expiresAt) || now.isAfter(refreshedAfter)
|
||||
}
|
||||
|
||||
fun isSignatureCorrect(): Boolean {
|
||||
val bytes = (expiresAt.toEpochMilli().toString() + pair.public).toByteArray(StandardCharsets.US_ASCII)
|
||||
|
||||
return YggdrasilUtil.verify(bytes, signatureBytes)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user