network: 1.19.1-pre4

This commit is contained in:
Bixilon 2022-10-16 18:04:58 +02:00
parent 1fc08c0662
commit 80027e3754
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
26 changed files with 324 additions and 92 deletions

View File

@ -15,7 +15,7 @@ package de.bixilon.minosoft.data.accounts.types.microsoft
import com.fasterxml.jackson.annotation.JsonProperty
import de.bixilon.kutil.latch.CountUpAndDownLatch
import de.bixilon.kutil.time.TimeUtil
import de.bixilon.kutil.time.TimeUtil.millis
import de.bixilon.minosoft.config.profile.profiles.account.AccountProfileManager
import de.bixilon.minosoft.data.accounts.Account
import de.bixilon.minosoft.data.accounts.AccountStates
@ -80,7 +80,7 @@ class MicrosoftAccount(
// already checking
return
}
if (minecraft.expires >= TimeUtil.millis / 1000) {
if (minecraft.expires >= millis() / 1000) {
return check(latch, "null")
}
if (state == AccountStates.WORKING) {
@ -99,7 +99,7 @@ class MicrosoftAccount(
private fun refreshMinecraftToken(latch: CountUpAndDownLatch?) {
state = AccountStates.REFRESHING
val time = TimeUtil.millis / 1000
val time = millis() / 1000
if (time >= msa.expires) {
// token expired
refreshMicrosoftToken(latch)
@ -123,7 +123,7 @@ class MicrosoftAccount(
private fun checkMinecraftToken(latch: CountUpAndDownLatch?) {
state = AccountStates.CHECKING
val time = TimeUtil.millis / 1000
val time = millis() / 1000
if (time >= minecraft.expires) {
// token expired
refreshMinecraftToken(latch)
@ -143,7 +143,7 @@ class MicrosoftAccount(
override fun fetchKey(latch: CountUpAndDownLatch?): MinecraftPrivateKey {
var key = key
if (key == null || key.isExpired()) {
if (key == null || key.isExpired() || key.signatureV2 == null) {
key = AccountUtil.fetchPrivateKey(minecraft)
this.key = key
save()

View File

@ -13,12 +13,12 @@
package de.bixilon.minosoft.data.chat.message
import de.bixilon.minosoft.data.chat.ChatMessageTypes
import de.bixilon.minosoft.data.chat.type.DefaultMessageTypes
import de.bixilon.minosoft.data.text.ChatComponent
import java.util.*
open class PlayerChatMessage(
text: ChatComponent,
type: ChatMessageTypes,
type: DefaultMessageTypes,
val sender: UUID,
) : SimpleChatMessage(text, type)

View File

@ -13,10 +13,10 @@
package de.bixilon.minosoft.data.chat.message
import de.bixilon.minosoft.data.chat.ChatMessageTypes
import de.bixilon.minosoft.data.chat.type.DefaultMessageTypes
import de.bixilon.minosoft.data.text.ChatComponent
open class SimpleChatMessage(
override val text: ChatComponent,
val type: ChatMessageTypes,
val type: DefaultMessageTypes,
) : ChatMessage

View File

@ -0,0 +1,65 @@
/*
* Minosoft
* Copyright (C) 2020-2022 Moritz Zwerger and contributors
*
* 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.data.chat.signature
import com.fasterxml.jackson.core.io.JsonStringEncoder
import com.google.common.hash.Hashing
import com.google.common.primitives.Longs
import de.bixilon.minosoft.data.registries.versions.Version
import de.bixilon.minosoft.data.text.ChatComponent
import de.bixilon.minosoft.protocol.ProtocolUtil.encodeNetwork
import de.bixilon.minosoft.protocol.protocol.OutByteBuffer
import de.bixilon.minosoft.protocol.protocol.ProtocolVersions
import de.bixilon.minosoft.protocol.protocol.encryption.CryptManager
import java.security.PrivateKey
import java.time.Instant
import java.util.*
class MessageChain {
private var previous: ByteArray? = null
fun signMessage(version: Version, privateKey: PrivateKey, message: String, preview: ChatComponent?, salt: Long, sender: UUID, time: Instant): ByteArray {
val signature = CryptManager.createSignature(version)
signature.initSign(privateKey)
if (version < ProtocolVersions.V_1_19_1_PRE4) {
signature.update(Longs.toByteArray(salt))
signature.update(Longs.toByteArray(sender.mostSignificantBits))
signature.update(Longs.toByteArray(sender.leastSignificantBits))
signature.update(Longs.toByteArray(time.epochSecond))
signature.update(message.getSignatureBytes())
} else {
val buffer = OutByteBuffer()
buffer.writeLong(salt)
buffer.writeLong(time.epochSecond)
buffer.writeBareByteArray(message.getSignatureBytes())
// TODo: lastseen
val hash = Hashing.sha256().hashBytes(buffer.toArray()).asBytes()
previous?.let { signature.update(it) }
signature.update(Longs.toByteArray(sender.mostSignificantBits))
signature.update(Longs.toByteArray(sender.leastSignificantBits))
signature.update(hash)
}
val singed = signature.sign()
this.previous = singed
return singed
}
private fun String.getSignatureBytes(): ByteArray {
return """{"text":"${String(JsonStringEncoder.getInstance().quoteAsString(this))}"}""".encodeNetwork()
}
}

View File

@ -13,6 +13,7 @@
package de.bixilon.minosoft.data.chat.signature
import de.bixilon.minosoft.data.chat.type.MessageType
import de.bixilon.minosoft.data.text.ChatComponent
data class SignedMessage(
@ -20,4 +21,5 @@ data class SignedMessage(
val signature: ByteArray,
val body: MessageBody,
val unsigned: ChatComponent? = null,
val type: MessageType? = null,
)

View File

@ -1,6 +1,6 @@
/*
* Minosoft
* Copyright (C) 2020-2022 Moritz Zwerger
* Copyright (C) 2020-2022 Moritz Zwerger and contributors
*
* 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.
*
@ -11,12 +11,13 @@
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.data.chat
package de.bixilon.minosoft.data.chat.type
import de.bixilon.kutil.enums.EnumUtil
import de.bixilon.kutil.enums.ValuesEnum
import de.bixilon.minosoft.data.chat.ChatTextPositions
enum class ChatMessageTypes(val position: ChatTextPositions = ChatTextPositions.CHAT) {
enum class DefaultMessageTypes(val position: ChatTextPositions = ChatTextPositions.CHAT) {
CHAT_MESSAGE,
SYSTEM_MESSAGE,
GAME_MESSAGE,
@ -28,8 +29,8 @@ enum class ChatMessageTypes(val position: ChatTextPositions = ChatTextPositions.
COMMAND_TELLRAW,
;
companion object : ValuesEnum<ChatMessageTypes> {
override val VALUES: Array<ChatMessageTypes> = values()
override val NAME_MAP: Map<String, ChatMessageTypes> = EnumUtil.getEnumValues(VALUES)
companion object : ValuesEnum<DefaultMessageTypes> {
override val VALUES: Array<DefaultMessageTypes> = values()
override val NAME_MAP: Map<String, DefaultMessageTypes> = EnumUtil.getEnumValues(VALUES)
}
}

View File

@ -0,0 +1,23 @@
/*
* Minosoft
* Copyright (C) 2020-2022 Moritz Zwerger and contributors
*
* 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.data.chat.type
import de.bixilon.minosoft.data.text.ChatComponent
@Deprecated("Refactor")
class MessageType(
val id: Int,
val text: ChatComponent,
val targetName: ChatComponent?,
)

View File

@ -12,7 +12,7 @@
*/
package de.bixilon.minosoft.modding.event.events
import de.bixilon.minosoft.data.chat.ChatMessageTypes
import de.bixilon.minosoft.data.chat.type.DefaultMessageTypes
import de.bixilon.minosoft.data.text.ChatComponent
import de.bixilon.minosoft.modding.event.EventInitiators
import de.bixilon.minosoft.modding.event.events.connection.play.PlayConnectionEvent
@ -26,14 +26,14 @@ class ChatMessageReceiveEvent(
connection: PlayConnection,
initiator: EventInitiators,
val message: ChatComponent,
val type: ChatMessageTypes,
val type: DefaultMessageTypes,
val sender: UUID?,
) : PlayConnectionEvent(connection, initiator), CancelableEvent {
constructor(connection: PlayConnection, packet: ChatMessageS2CP) : this(connection, EventInitiators.SERVER, packet.message, packet.type, packet.sender)
constructor(connection: PlayConnection, packet: HotbarTextS2CP) : this(connection, EventInitiators.SERVER, packet.text, ChatMessageTypes.GAME_MESSAGE, null)
constructor(connection: PlayConnection, packet: HotbarTextS2CP) : this(connection, EventInitiators.SERVER, packet.text, DefaultMessageTypes.GAME_MESSAGE, null)
constructor(connection: PlayConnection, packet: SignedChatMessageS2CP) : this(connection, EventInitiators.SERVER, packet.message, packet.type, packet.sender.uuid)
constructor(connection: PlayConnection, packet: SignedChatMessageS2CP) : this(connection, EventInitiators.SERVER, packet.message.body.text, DefaultMessageTypes.CHAT_MESSAGE, packet.message.header.sender)
}

View File

@ -17,12 +17,16 @@ import de.bixilon.kutil.base64.Base64Util.fromBase64
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.ProtocolVersions
import de.bixilon.minosoft.protocol.protocol.encryption.CryptManager
import de.bixilon.minosoft.protocol.protocol.encryption.CryptManager.encodeNetwork
import de.bixilon.minosoft.util.account.minecraft.key.MinecraftKeyPair
import de.bixilon.minosoft.util.yggdrasil.YggdrasilException
import de.bixilon.minosoft.util.yggdrasil.YggdrasilUtil
import java.nio.charset.StandardCharsets
import java.security.PublicKey
import java.time.Instant
import java.util.*
class PlayerPublicKey(
val expiresAt: Instant,
@ -30,10 +34,6 @@ class PlayerPublicKey(
val signature: ByteArray,
) {
init {
check(isSignatureCorrect()) { "Yggdrasil signature invalid" }
}
constructor(nbt: JsonObject) : this(Instant.ofEpochMilli(nbt["expires_at"].toLong()), CryptManager.getPlayerPublicKey(nbt["key"].toString()), nbt["signature"].toString().fromBase64())
fun toNbt(): JsonObject {
@ -44,9 +44,21 @@ class PlayerPublicKey(
)
}
private fun isSignatureCorrect(): Boolean {
fun isLegacySignatureCorrect(): Boolean {
val bytes = (expiresAt.toEpochMilli().toString() + publicKey.encodeNetwork()).toByteArray(StandardCharsets.US_ASCII)
return YggdrasilUtil.verify(bytes, signature)
}
fun isSignatureCorrect(uuid: UUID): Boolean {
return MinecraftKeyPair.isSignatureCorrect(uuid, expiresAt, publicKey, signature)
}
fun requireSignature(versionId: Int, uuid: UUID) {
if (versionId < ProtocolVersions.V_1_19_1_PRE4) {
if (!isLegacySignatureCorrect()) throw YggdrasilException()
} else {
if (!isSignatureCorrect(uuid)) throw YggdrasilException()
}
}
}

View File

@ -13,11 +13,10 @@
package de.bixilon.minosoft.protocol.network.connection.play
import com.fasterxml.jackson.core.io.JsonStringEncoder
import com.google.common.primitives.Longs
import de.bixilon.kotlinglm.vec3.Vec3d
import de.bixilon.kutil.string.WhitespaceUtil.trimWhitespaces
import de.bixilon.minosoft.commands.stack.CommandStack
import de.bixilon.minosoft.data.chat.signature.MessageChain
import de.bixilon.minosoft.data.text.BaseComponent
import de.bixilon.minosoft.data.text.ChatComponent
import de.bixilon.minosoft.data.text.formatting.color.ChatColors
@ -30,17 +29,18 @@ 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.util.logging.Log
import de.bixilon.minosoft.util.logging.LogLevels
import de.bixilon.minosoft.util.logging.LogMessageType
import java.security.PrivateKey
import java.security.SecureRandom
import java.time.Instant
class ConnectionUtil(
private val connection: PlayConnection,
) {
private val chain = MessageChain()
private val random = SecureRandom()
fun sendDebugMessage(message: Any) {
@ -75,22 +75,17 @@ class ConnectionUtil(
if (privateKey == null || !connection.version.requiresSignedChat) {
return connection.sendPacket(ChatMessageC2SP(message))
}
sendSignedMessage(privateKey, message)
}
val signature = CryptManager.createSignature(connection.version)
fun sendSignedMessage(privateKey: PrivateKey = connection.player.privateKey?.private!!, message: String) {
val salt = random.nextLong()
val time = Instant.now()
val uuid = connection.player.uuid
signature.initSign(privateKey)
val signature = chain.signMessage(connection.version, privateKey, message, null, salt, uuid, time)
signature.update(Longs.toByteArray(salt))
signature.update(Longs.toByteArray(uuid.mostSignificantBits))
signature.update(Longs.toByteArray(uuid.leastSignificantBits))
signature.update(Longs.toByteArray(time.epochSecond))
signature.update("""{"text":"${String(JsonStringEncoder.getInstance().quoteAsString(message))}"}""".encodeNetwork())
connection.sendPacket(SignedChatMessageC2SP(message.encodeNetwork(), time = time, signature = SignatureData(salt, signature.sign()), false))
connection.sendPacket(SignedChatMessageC2SP(message.encodeNetwork(), time = time, salt = salt, signature = SignatureData(signature), false))
}
@Deprecated("message will re removed as soon as brigadier is fully implemented")

View File

@ -56,7 +56,6 @@ import de.bixilon.minosoft.protocol.network.connection.play.tick.ConnectionTicke
import de.bixilon.minosoft.protocol.packets.c2s.handshaking.HandshakeC2SP
import de.bixilon.minosoft.protocol.packets.c2s.login.StartC2SP
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.ServerAddress
@ -189,12 +188,12 @@ class PlayConnection(
if (version.requiresSignedChat) {
taskWorker += WorkerTask(optional = true) {
val minecraftKey = account.fetchKey(latch) ?: return@WorkerTask
minecraftKey.requireSignature()
minecraftKey.requireSignature(account.uuid)
privateKey = PlayerPrivateKey(
expiresAt = minecraftKey.expiresAt,
signature = minecraftKey.signatureBytes,
private = CryptManager.getPlayerPrivateKey(minecraftKey.pair.private),
public = CryptManager.getPlayerPublicKey(minecraftKey.pair.public),
signature = minecraftKey.getSignature(version.versionId),
private = minecraftKey.pair.private,
public = minecraftKey.pair.public,
)
}
}

View File

@ -19,7 +19,7 @@ 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.SignatureData
import de.bixilon.minosoft.protocol.protocol.encryption.EncryptionSignatureData
import de.bixilon.minosoft.util.logging.Log
import de.bixilon.minosoft.util.logging.LogLevels
import de.bixilon.minosoft.util.logging.LogMessageType
@ -31,7 +31,7 @@ class EncryptionC2SP private constructor(
) : PlayC2SPacket {
constructor(secret: ByteArray, nonce: ByteArray) : this(secret, nonce as Any)
constructor(secret: ByteArray, nonce: SignatureData) : this(secret, nonce as Any)
constructor(secret: ByteArray, nonce: EncryptionSignatureData) : this(secret, nonce as Any)
override fun write(buffer: PlayOutByteBuffer) {
buffer.writeByteArray(secret)
@ -43,7 +43,9 @@ class EncryptionC2SP private constructor(
buffer.writeByteArray(nonce)
} else {
buffer.writeBoolean(false)
buffer.writeSignatureData(nonce as SignatureData)
val nonce = nonce as EncryptionSignatureData
buffer.writeLong(nonce.salt)
buffer.writeSignatureData(nonce.signatureData)
}
}
}

View File

@ -25,6 +25,7 @@ import java.time.Instant
class CommandC2SP(
val command: String,
val time: Instant = Instant.now(),
val salt: Long,
val signature: CommandArgumentSignature,
val signedPreview: Boolean,
) : PlayC2SPacket {
@ -32,7 +33,7 @@ class CommandC2SP(
override fun write(buffer: PlayOutByteBuffer) {
buffer.writeString(command)
buffer.writeInstant(time)
buffer.writeLong(signature.salt)
buffer.writeLong(salt)
buffer.writeVarInt(signature.signatures.size)
for ((argument, signature) in signature.signatures) {
buffer.writeString(argument)
@ -44,12 +45,11 @@ class CommandC2SP(
}
override fun log(reducedLog: Boolean) {
Log.log(LogMessageType.NETWORK_PACKETS_OUT, LogLevels.VERBOSE) { "Chat message (message=$command, time=$time, signature=$signature)" }
Log.log(LogMessageType.NETWORK_PACKETS_OUT, LogLevels.VERBOSE) { "Command (message=$command, time=$time, signature=$signature)" }
}
data class CommandArgumentSignature(
val salt: Long,
val signatures: Map<String, ByteArray>,
)
}

View File

@ -26,6 +26,7 @@ import java.time.Instant
class SignedChatMessageC2SP(
val message: ByteArray,
val time: Instant = Instant.now(),
val salt: Long,
val signature: SignatureData? = null,
val previewed: Boolean = false,
) : PlayC2SPacket {
@ -38,6 +39,7 @@ class SignedChatMessageC2SP(
if (buffer.versionId >= ProtocolVersions.V_22W18A) {
buffer.writeInstant(time)
}
buffer.writeLong(salt)
buffer.writeSignatureData(signature ?: SignatureData.EMPTY)
if (buffer.versionId >= ProtocolVersions.V_22W19A) {
buffer.writeBoolean(previewed)

View File

@ -23,6 +23,7 @@ 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.EncryptionSignatureData
import de.bixilon.minosoft.protocol.protocol.encryption.SignatureData
import de.bixilon.minosoft.util.logging.Log
import de.bixilon.minosoft.util.logging.LogLevels
@ -57,7 +58,7 @@ class EncryptionS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket {
signature.update(Longs.toByteArray(salt))
val signed = signature.sign()
connection.sendPacket(EncryptionC2SP(encryptedSecretKey, SignatureData(salt, signed)))
connection.sendPacket(EncryptionC2SP(encryptedSecretKey, EncryptionSignatureData(salt, SignatureData(signed))))
} else {
connection.sendPacket(EncryptionC2SP(encryptedSecretKey, CryptManager.encryptData(publicKey, nonce)))
}

View File

@ -12,7 +12,7 @@
*/
package de.bixilon.minosoft.protocol.packets.s2c.play.chat
import de.bixilon.minosoft.data.chat.ChatMessageTypes
import de.bixilon.minosoft.data.chat.type.DefaultMessageTypes
import de.bixilon.minosoft.data.text.ChatComponent
import de.bixilon.minosoft.modding.event.events.ChatMessageReceiveEvent
import de.bixilon.minosoft.protocol.network.connection.play.PlayConnection
@ -29,7 +29,7 @@ import java.util.*
@LoadPacket(threadSafe = false)
class ChatMessageS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket {
val message: ChatComponent = buffer.readChatComponent()
var type: ChatMessageTypes = ChatMessageTypes.CHAT_MESSAGE
var type: DefaultMessageTypes = DefaultMessageTypes.CHAT_MESSAGE
private set
var sender: UUID? = null
private set
@ -42,7 +42,7 @@ class ChatMessageS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket {
if (buffer.versionId >= ProtocolVersions.V_1_19_1_PRE2) {
overlay = buffer.readBoolean()
} else {
type = ChatMessageTypes[buffer.readVarInt()]
type = DefaultMessageTypes[buffer.readVarInt()]
if (buffer.versionId >= ProtocolVersions.V_20W21A && buffer.versionId < ProtocolVersions.V_22W17A) {
sender = buffer.readUUID()
}

View File

@ -0,0 +1,29 @@
/*
* 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.packets.s2c.play.chat
import de.bixilon.minosoft.protocol.packets.factory.LoadPacket
import de.bixilon.minosoft.protocol.packets.s2c.PlayS2CPacket
import de.bixilon.minosoft.protocol.protocol.PlayInByteBuffer
import de.bixilon.minosoft.util.logging.Log
import de.bixilon.minosoft.util.logging.LogLevels
import de.bixilon.minosoft.util.logging.LogMessageType
@LoadPacket(threadSafe = false)
class HideMessageS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket {
val signature = buffer.readByteArray()
override fun log(reducedLog: Boolean) {
Log.log(LogMessageType.NETWORK_PACKETS_IN, level = LogLevels.VERBOSE) { "Hide message (signature=$signature)" }
}
}

View File

@ -0,0 +1,31 @@
/*
* 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.packets.s2c.play.chat
import de.bixilon.minosoft.protocol.packets.factory.LoadPacket
import de.bixilon.minosoft.protocol.packets.s2c.PlayS2CPacket
import de.bixilon.minosoft.protocol.protocol.PlayInByteBuffer
import de.bixilon.minosoft.util.logging.Log
import de.bixilon.minosoft.util.logging.LogLevels
import de.bixilon.minosoft.util.logging.LogMessageType
@LoadPacket(threadSafe = false)
class MessageHeaderS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket {
val header = buffer.readMessageHeader()
val signature = buffer.readByteArray()
val bodyDigest = buffer.readByteArray()
override fun log(reducedLog: Boolean) {
Log.log(LogMessageType.NETWORK_PACKETS_IN, level = LogLevels.VERBOSE) { "Message header (header=$header, signature=$signature, bodyDigest=$bodyDigest)" }
}
}

View File

@ -12,25 +12,18 @@
*/
package de.bixilon.minosoft.protocol.packets.s2c.play.chat
import de.bixilon.minosoft.data.chat.ChatMessageTypes
import de.bixilon.minosoft.modding.event.events.ChatMessageReceiveEvent
import de.bixilon.minosoft.protocol.network.connection.play.PlayConnection
import de.bixilon.minosoft.protocol.packets.factory.LoadPacket
import de.bixilon.minosoft.protocol.packets.s2c.PlayS2CPacket
import de.bixilon.minosoft.protocol.protocol.PlayInByteBuffer
import de.bixilon.minosoft.protocol.protocol.ProtocolVersions
import de.bixilon.minosoft.util.logging.Log
import de.bixilon.minosoft.util.logging.LogLevels
import de.bixilon.minosoft.util.logging.LogMessageType
@LoadPacket(threadSafe = false)
class SignedChatMessageS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket {
val message = buffer.readChatComponent()
val unsignedContent = if (buffer.versionId >= ProtocolVersions.V_22W19A) buffer.readOptional { readChatComponent() } else null
var type = ChatMessageTypes[buffer.readVarInt()]
val sender = buffer.readChatMessageSender()
val sendingTime = buffer.readInstant()
val signatureData = buffer.readSignatureData()
val message = buffer.readSignedMessage()
override fun handle(connection: PlayConnection) {
val event = ChatMessageReceiveEvent(connection, this)
@ -40,6 +33,6 @@ class SignedChatMessageS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket {
}
override fun log(reducedLog: Boolean) {
Log.log(LogMessageType.NETWORK_PACKETS_IN, level = LogLevels.VERBOSE) { "Chat message (message=\"$message\", type=$type, sender=$sender, sendingTime=$sendingTime, signateDate=$signatureData)" }
Log.log(LogMessageType.NETWORK_PACKETS_IN, level = LogLevels.VERBOSE) { "Chat message (message=$message)" }
}
}

View File

@ -53,6 +53,7 @@ class TabListS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket {
null
}
val publicKey = if (buffer.versionId >= ProtocolVersions.V_22W18A) buffer.readOptional { buffer.readPlayerPublicKey() } else null
publicKey?.requireSignature(buffer.versionId, uuid)
data = TabListItemData(
name = name,
properties = properties,
@ -62,12 +63,15 @@ class TabListS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket {
publicKey = publicKey,
)
}
TabListItemActions.UPDATE_GAMEMODE -> {
data = TabListItemData(gamemode = Gamemodes[buffer.readVarInt()])
}
TabListItemActions.UPDATE_LATENCY -> {
data = TabListItemData(ping = buffer.readVarInt())
}
TabListItemActions.UPDATE_DISPLAY_NAME -> {
val hasDisplayName = buffer.readBoolean()
val displayName = if (hasDisplayName) {
@ -80,6 +84,7 @@ class TabListS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket {
displayName = displayName,
)
}
TabListItemActions.REMOVE_PLAYER -> {
data = null
}

View File

@ -26,6 +26,8 @@ import de.bixilon.minosoft.data.chat.signature.LastSeenMessage
import de.bixilon.minosoft.data.chat.signature.MessageBody
import de.bixilon.minosoft.data.chat.signature.MessageHeader
import de.bixilon.minosoft.data.chat.signature.SignedMessage
import de.bixilon.minosoft.data.chat.type.DefaultMessageTypes
import de.bixilon.minosoft.data.chat.type.MessageType
import de.bixilon.minosoft.data.container.ItemStackUtil
import de.bixilon.minosoft.data.container.stack.ItemStack
import de.bixilon.minosoft.data.entities.entities.player.properties.PlayerProperties
@ -282,21 +284,6 @@ class PlayInByteBuffer : InByteBuffer {
}
}
fun readChatMessageSender(): ChatMessageSender {
return ChatMessageSender(readUUID(), readChatComponent(), if (versionId >= ProtocolVersions.V_22W18A) readOptional { readChatComponent() } else null)
}
fun readSignatureData(): SignatureData {
return SignatureData(readLong(), readByteArray())
}
fun readPlayerPublicKey(): PlayerPublicKey? {
if (versionId <= ProtocolVersions.V_22W18A) { // ToDo: find version
return readNBT()?.let { PlayerPublicKey(it.asJsonObject()) }
}
return PlayerPublicKey(readInstant(), CryptManager.getPlayerPublicKey(readByteArray()), readByteArray())
}
fun readInstant(): Instant {
val time = readLong()
if (versionId >= ProtocolVersions.V_22W19A) {
@ -360,6 +347,22 @@ class PlayInByteBuffer : InByteBuffer {
return registry[readVarInt()]
}
fun readChatMessageSender(): ChatMessageSender {
return ChatMessageSender(readUUID(), readChatComponent(), if (versionId >= ProtocolVersions.V_22W18A) readOptional { readChatComponent() } else null)
}
fun readSignatureData(): SignatureData {
return SignatureData(readByteArray())
}
fun readPlayerPublicKey(): PlayerPublicKey? {
if (versionId <= ProtocolVersions.V_22W18A) { // ToDo: find version
return readNBT()?.let { PlayerPublicKey(it.asJsonObject()) }
}
return PlayerPublicKey(readInstant(), CryptManager.getPlayerPublicKey(readByteArray()), readByteArray())
}
fun readMessageHeader(): MessageHeader {
return MessageHeader(readOptional { readByteArray() }, readUUID())
}
@ -377,7 +380,22 @@ class PlayInByteBuffer : InByteBuffer {
)
}
fun readMessageType(): MessageType {
return MessageType(readVarInt(), readChatComponent(), readOptional { readChatComponent() })
}
fun readSignedMessage(): SignedMessage {
return SignedMessage(readMessageHeader(), readByteArray(), readMessageBody(), readOptional { readChatComponent() })
if (versionId < ProtocolVersions.V_1_19_1_PRE4) {
val message = readChatComponent()
val unsignedContent = if (versionId >= ProtocolVersions.V_22W19A) readOptional { readChatComponent() } else null
var type = DefaultMessageTypes[readVarInt()]
val sender = readChatMessageSender()
val sendingTime = readInstant()
val salt = readLong()
val signatureData = readSignatureData()
TODO("return message, refactor")
}
return SignedMessage(readMessageHeader(), readByteArray(), readMessageBody(), readOptional { readChatComponent() }, readMessageType())
}
}

View File

@ -87,7 +87,6 @@ class PlayOutByteBuffer(val connection: PlayConnection) : OutByteBuffer() {
}
fun writeSignatureData(signature: SignatureData) {
writeLong(signature.salt)
writeByteArray(signature.signature)
}

View File

@ -0,0 +1,19 @@
/*
* Minosoft
* Copyright (C) 2020-2022 Moritz Zwerger and contributors
*
* 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.protocol.encryption
class EncryptionSignatureData(
val salt: Long,
val signatureData: SignatureData,
)

View File

@ -14,10 +14,9 @@
package de.bixilon.minosoft.protocol.protocol.encryption
data class SignatureData(
val salt: Long,
val signature: ByteArray,
) {
companion object {
val EMPTY = SignatureData(0L, byteArrayOf())
val EMPTY = SignatureData(byteArrayOf())
}
}

View File

@ -16,32 +16,39 @@ package de.bixilon.minosoft.util.account.minecraft
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
import de.bixilon.kutil.base64.Base64Util.fromBase64
import de.bixilon.minosoft.protocol.protocol.ProtocolVersions
import de.bixilon.minosoft.util.account.minecraft.key.MinecraftKeyPair
import de.bixilon.minosoft.util.yggdrasil.YggdrasilUtil
import java.nio.charset.StandardCharsets
import java.time.Instant
import java.util.*
data class MinecraftPrivateKey(
@JsonProperty("keyPair") val pair: MinecraftKeyPair,
@JsonProperty("publicKeySignature") val signature: String,
@JsonProperty("publicKeySignature") val signature: String?,
@JsonProperty("publicKeySignatureV2") val signatureV2: String?,
@JsonProperty("expiresAt") val expiresAt: Instant,
@JsonProperty("refreshedAfter") val refreshedAfter: Instant,
) {
@get:JsonIgnore val signatureBytes: ByteArray by lazy { signature.fromBase64() }
@get:JsonIgnore val signatureBytes: ByteArray? by lazy { signature?.fromBase64() }
@get:JsonIgnore val signatureBytesV2: ByteArray by lazy { signatureV2!!.fromBase64() }
@JsonIgnore
fun isExpired(): Boolean {
val now = Instant.now()
return now.isAfter(expiresAt) || now.isAfter(refreshedAfter)
}
private val getSignedBytes: ByteArray
get() = (expiresAt.toEpochMilli().toString() + pair.public).toByteArray(StandardCharsets.US_ASCII)
fun isSignatureCorrect(): Boolean {
return YggdrasilUtil.verify(getSignedBytes, signatureBytes)
@JsonIgnore
fun requireSignature(uuid: UUID) {
MinecraftKeyPair.requireSignature(uuid, expiresAt, pair.public, signatureBytesV2)
signatureBytes?.let { YggdrasilUtil.requireSignature((expiresAt.toEpochMilli().toString() + pair.publicString).toByteArray(StandardCharsets.US_ASCII), it) }
}
fun requireSignature() {
YggdrasilUtil.requireSignature(getSignedBytes, signatureBytes)
fun getSignature(versionId: Int): ByteArray {
if (versionId < ProtocolVersions.V_1_19_1_PRE4) {
return signatureBytes ?: throw IllegalStateException("v1 signature required")
}
return signatureBytesV2
}
}

View File

@ -13,9 +13,39 @@
package de.bixilon.minosoft.util.account.minecraft.key
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
import de.bixilon.minosoft.protocol.protocol.OutByteBuffer
import de.bixilon.minosoft.protocol.protocol.encryption.CryptManager
import de.bixilon.minosoft.util.yggdrasil.YggdrasilException
import de.bixilon.minosoft.util.yggdrasil.YggdrasilUtil
import java.security.PublicKey
import java.time.Instant
import java.util.*
data class MinecraftKeyPair(
@JsonProperty("privateKey") val private: String,
@JsonProperty("publicKey") val public: String,
)
@JsonProperty("privateKey") val privateString: String,
@JsonProperty("publicKey") val publicString: String,
) {
@get:JsonIgnore val private by lazy { CryptManager.getPlayerPrivateKey(privateString) }
@get:JsonIgnore val public by lazy { CryptManager.getPlayerPublicKey(publicString) }
companion object {
fun isSignatureCorrect(uuid: UUID, expiresAt: Instant, publicKey: PublicKey, signature: ByteArray): Boolean {
val signed = OutByteBuffer()
signed.writeUUID(uuid)
signed.writeLong(expiresAt.toEpochMilli())
signed.writeBareByteArray(publicKey.encoded)
return YggdrasilUtil.verify(signed.toArray(), signature)
}
fun requireSignature(uuid: UUID, expiresAt: Instant, publicKey: PublicKey, signature: ByteArray) {
if (!isSignatureCorrect(uuid, expiresAt, publicKey, signature)) {
throw YggdrasilException()
}
}
}
}