1.19: wip signed connection

This commit is contained in:
Bixilon 2022-06-17 15:20:21 +02:00
parent 844ad8ccda
commit d2549fb0a5
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
13 changed files with 125 additions and 28 deletions

View File

@ -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)
}

View File

@ -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(),
)
}
}

View 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)
}
}

View File

@ -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)

View File

@ -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),
)

View File

@ -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)

View File

@ -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)

View File

@ -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)
}

View File

@ -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 {

View File

@ -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)
}

View File

@ -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) {

View File

@ -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")
}
}

View File

@ -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)
}
}