diff --git a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java index a6b756a6a..d363f83e6 100644 --- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java +++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java @@ -57,7 +57,8 @@ public enum ProtocolVersion { MINECRAFT_1_17(755, "1.17"), MINECRAFT_1_17_1(756, "1.17.1"), MINECRAFT_1_18(757, "1.18", "1.18.1"), - MINECRAFT_1_18_2(758, "1.18.2"); + MINECRAFT_1_18_2(758, "1.18.2"), + MINECRAFT_1_19(759, "1.19"); private static final int SNAPSHOT_BIT = 30; diff --git a/api/src/main/java/com/velocitypowered/api/proxy/LoginPhaseConnection.java b/api/src/main/java/com/velocitypowered/api/proxy/LoginPhaseConnection.java index d448a71cc..e0fb221e1 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/LoginPhaseConnection.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/LoginPhaseConnection.java @@ -7,6 +7,7 @@ package com.velocitypowered.api.proxy; +import com.velocitypowered.api.proxy.crypto.KeyIdentifiable; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import org.checkerframework.checker.nullness.qual.Nullable; @@ -14,7 +15,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; * Allows the server to communicate with a client logging into the proxy using login plugin * messages. */ -public interface LoginPhaseConnection extends InboundConnection { +public interface LoginPhaseConnection extends InboundConnection, KeyIdentifiable { void sendLoginPluginMessage(ChannelIdentifier identifier, byte[] contents, MessageConsumer consumer); diff --git a/api/src/main/java/com/velocitypowered/api/proxy/Player.java b/api/src/main/java/com/velocitypowered/api/proxy/Player.java index 94bbde7bd..60c4aace9 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/Player.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/Player.java @@ -9,6 +9,7 @@ package com.velocitypowered.api.proxy; import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; +import com.velocitypowered.api.proxy.crypto.KeyIdentifiable; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.ChannelMessageSink; import com.velocitypowered.api.proxy.messages.ChannelMessageSource; @@ -37,7 +38,7 @@ import org.jetbrains.annotations.NotNull; * Represents a player who is connected to the proxy. */ public interface Player extends CommandSource, Identified, InboundConnection, - ChannelMessageSource, ChannelMessageSink, HoverEventSource, Keyed { + ChannelMessageSource, ChannelMessageSink, HoverEventSource, Keyed, KeyIdentifiable { /** * Returns the player's current username. diff --git a/api/src/main/java/com/velocitypowered/api/proxy/crypto/IdentifiedKey.java b/api/src/main/java/com/velocitypowered/api/proxy/crypto/IdentifiedKey.java new file mode 100644 index 000000000..537afc5e5 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/proxy/crypto/IdentifiedKey.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.proxy.crypto; + +import java.security.PublicKey; + +/** + * Represents session-server cross-signed dated RSA public key. + */ +public interface IdentifiedKey extends KeySigned { + + /** + * Returns RSA public key. + * Note: this key is at least 2048 bits but may be larger. + * + * @return the RSA public key in question + */ + PublicKey getSignedPublicKey(); + + + /** + * Validates a signature against this public key. + * @param signature the signature data + * @param toVerify the signed data + * + * @return validity of the signature + */ + boolean verifyDataSignature(byte[] signature, byte[]... toVerify); + +} diff --git a/api/src/main/java/com/velocitypowered/api/proxy/crypto/KeyIdentifiable.java b/api/src/main/java/com/velocitypowered/api/proxy/crypto/KeyIdentifiable.java new file mode 100644 index 000000000..eb0c019f5 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/proxy/crypto/KeyIdentifiable.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.proxy.crypto; + +/** + * Identifies a type with a public RSA signature. + */ +public interface KeyIdentifiable { + + /** + * Returns the timed identified key of the object context. + *

Only available in 1.19 and newer

+ * @return the key or null if not available + */ + IdentifiedKey getIdentifiedKey(); +} diff --git a/api/src/main/java/com/velocitypowered/api/proxy/crypto/KeySigned.java b/api/src/main/java/com/velocitypowered/api/proxy/crypto/KeySigned.java new file mode 100644 index 000000000..ce8ba57aa --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/proxy/crypto/KeySigned.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.proxy.crypto; + +import com.google.common.annotations.Beta; + +import java.security.PublicKey; +import java.time.Instant; + +import org.checkerframework.checker.nullness.qual.Nullable; + +public interface KeySigned { + + /** + * Returns the key used to sign the object. + * + * @return the key + */ + PublicKey getSigner(); + + /** + * Returns the expiry time point of the key. + * Note: this limit is arbitrary. RSA keys don't expire, + * but the signature of this key as provided by the session + * server will expire. + * + * @return the expiry time point + */ + Instant getExpiryTemporal(); + + + /** + * Check if the signature has expired. + * + * @return true if proxy time is after expiry time + */ + default boolean hasExpired() { + return Instant.now().isAfter(getExpiryTemporal()); + } + + /** + * Retrieves the signature of the signed object. + * + * @return an RSA signature + */ + @Nullable + byte[] getSignature(); + + /** + * Validates the signature, expiry temporal and key against the + * signer public key. Note: This will **not** check for + * expiry. You can check for expiry with {@link KeySigned#hasExpired()}. + *

DOES NOT WORK YET FOR MESSAGES AND COMMANDS!

+ * + * @return validity of the signature + */ + @Beta + default boolean isSignatureValid() { + return false; + } + + /** + * Returns the signature salt or null if not salted. + * + * @return signature salt or null + */ + default byte[] getSalt() { + return null; + } + +} diff --git a/api/src/main/java/com/velocitypowered/api/proxy/crypto/SignedMessage.java b/api/src/main/java/com/velocitypowered/api/proxy/crypto/SignedMessage.java new file mode 100644 index 000000000..d8e42d7b2 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/proxy/crypto/SignedMessage.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.proxy.crypto; + +import java.util.UUID; + +public interface SignedMessage extends KeySigned { + + /** + * Returns the signed message. + * + * @return the message + */ + String getMessage(); + + /** + * Returns the signers UUID. + * + * @return the uuid + */ + UUID getSignerUuid(); + + /** + * If true the signature of this message applies to a stylized component instead. + * + * @return signature signs preview + */ + boolean isPreviewSigned(); + +} diff --git a/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java b/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java index 95e5640ec..7dfeb81f8 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java @@ -8,6 +8,7 @@ package com.velocitypowered.api.proxy.player; import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.api.util.GameProfile; import java.util.Collection; import java.util.Optional; @@ -80,4 +81,19 @@ public interface TabList { @Deprecated TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, int gameMode); + + /** + * Builds a tab list entry. + * + * @deprecated Internal usage. Use {@link TabListEntry.Builder} instead. + * @param profile profile + * @param displayName display name + * @param latency latency + * @param gameMode game mode + * @param key the player key + * @return entry + */ + @Deprecated + TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, + int gameMode, @Nullable IdentifiedKey key); } diff --git a/api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java b/api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java index 08199f4d9..c537d477d 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java @@ -7,6 +7,8 @@ package com.velocitypowered.api.proxy.player; +import com.velocitypowered.api.proxy.crypto.IdentifiedKey; +import com.velocitypowered.api.proxy.crypto.KeyIdentifiable; import com.velocitypowered.api.util.GameProfile; import java.util.Optional; import net.kyori.adventure.text.Component; @@ -15,7 +17,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; /** * Represents a single entry in a {@link TabList}. */ -public interface TabListEntry { +public interface TabListEntry extends KeyIdentifiable { /** * Returns the parent {@link TabList} of this {@code this} {@link TabListEntry}. @@ -125,6 +127,8 @@ public interface TabListEntry { private int latency = 0; private int gameMode = 0; + private @Nullable IdentifiedKey playerKey; + private Builder() { } @@ -152,6 +156,18 @@ public interface TabListEntry { return this; } + /** + * Sets the {@link IdentifiedKey} of the {@link TabListEntry}. + * + * @param playerKey key to set + * @return {@code this}, for chaining + * @see TabListEntry#getIdentifiedKey() + */ + public Builder playerKey(IdentifiedKey playerKey) { + this.playerKey = playerKey; + return this; + } + /** * Sets the displayed name of the {@link TabListEntry}. * @@ -200,7 +216,7 @@ public interface TabListEntry { if (profile == null) { throw new IllegalStateException("The GameProfile must be set when building a TabListEntry"); } - return tabList.buildEntry(profile, displayName, latency, gameMode); + return tabList.buildEntry(profile, displayName, latency, gameMode, playerKey); } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index f05d60a24..783d100e5 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -45,6 +45,7 @@ import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; import com.velocitypowered.proxy.console.VelocityConsole; +import com.velocitypowered.proxy.crypto.EncryptionUtils; import com.velocitypowered.proxy.event.VelocityEventManager; import com.velocitypowered.proxy.network.ConnectionManager; import com.velocitypowered.proxy.plugin.VelocityPluginManager; @@ -55,7 +56,6 @@ import com.velocitypowered.proxy.scheduler.VelocityScheduler; import com.velocitypowered.proxy.server.ServerMap; import com.velocitypowered.proxy.util.AddressUtil; import com.velocitypowered.proxy.util.ClosestLocaleMatcher; -import com.velocitypowered.proxy.util.EncryptionUtils; import com.velocitypowered.proxy.util.FileSystemUtils; import com.velocitypowered.proxy.util.VelocityChannelRegistrar; import com.velocitypowered.proxy.util.bossbar.AdventureBossBarManager; @@ -80,7 +80,6 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.ResourceBundle; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; @@ -97,7 +96,6 @@ import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.translation.GlobalTranslator; import net.kyori.adventure.translation.TranslationRegistry; -import net.kyori.adventure.util.UTF8ResourceBundleControl; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.asynchttpclient.AsyncHttpClient; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java index 26438a28a..f31be126b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java @@ -75,6 +75,7 @@ public class VelocityConfiguration implements ProxyConfig { @Expose private boolean enablePlayerAddressLogging = true; private net.kyori.adventure.text.@MonotonicNonNull Component motdAsComponent; private @Nullable Favicon favicon; + @Expose private boolean forceKeyAuthentication = true; // Added in 1.19 private VelocityConfiguration(Servers servers, ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics) { @@ -90,7 +91,7 @@ public class VelocityConfiguration implements ProxyConfig { PlayerInfoForwarding playerInfoForwardingMode, byte[] forwardingSecret, boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough, boolean enablePlayerAddressLogging, Servers servers,ForcedHosts forcedHosts, - Advanced advanced, Query query, Metrics metrics) { + Advanced advanced, Query query, Metrics metrics, boolean forceKeyAuthentication) { this.bind = bind; this.motd = motd; this.showMaxPlayers = showMaxPlayers; @@ -107,6 +108,7 @@ public class VelocityConfiguration implements ProxyConfig { this.advanced = advanced; this.query = query; this.metrics = metrics; + this.forceKeyAuthentication = forceKeyAuthentication; } /** @@ -381,6 +383,10 @@ public class VelocityConfiguration implements ProxyConfig { return advanced.isLogPlayerConnections(); } + public boolean isForceKeyAuthentication() { + return forceKeyAuthentication; + } + @Override public String toString() { return MoreObjects.toStringHelper(this) @@ -397,6 +403,7 @@ public class VelocityConfiguration implements ProxyConfig { .add("query", query) .add("favicon", favicon) .add("enablePlayerAddressLogging", enablePlayerAddressLogging) + .add("forceKeyAuthentication", forceKeyAuthentication) .toString(); } @@ -466,6 +473,7 @@ public class VelocityConfiguration implements ProxyConfig { String motd = config.getOrElse("motd", " add3A Velocity Server"); int maxPlayers = config.getIntOrElse("show-max-players", 500); Boolean onlineMode = config.getOrElse("online-mode", true); + Boolean forceKeyAuthentication = config.getOrElse("force-key-authentication", true); Boolean announceForge = config.getOrElse("announce-forge", true); Boolean preventClientProxyConnections = config.getOrElse("prevent-client-proxy-connections", true); @@ -488,7 +496,8 @@ public class VelocityConfiguration implements ProxyConfig { new ForcedHosts(forcedHostsConfig), new Advanced(advancedConfig), new Query(queryConfig), - new Metrics(metricsConfig) + new Metrics(metricsConfig), + forceKeyAuthentication ); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java index e72f42d64..bdd3b5a0b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java @@ -20,7 +20,6 @@ package com.velocitypowered.proxy.connection; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.packet.AvailableCommands; import com.velocitypowered.proxy.protocol.packet.BossBar; -import com.velocitypowered.proxy.protocol.packet.Chat; import com.velocitypowered.proxy.protocol.packet.ClientSettings; import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.EncryptionRequest; @@ -46,6 +45,13 @@ import com.velocitypowered.proxy.protocol.packet.StatusRequest; import com.velocitypowered.proxy.protocol.packet.StatusResponse; import com.velocitypowered.proxy.protocol.packet.TabCompleteRequest; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse; +import com.velocitypowered.proxy.protocol.packet.chat.LegacyChat; +import com.velocitypowered.proxy.protocol.packet.chat.PlayerChat; +import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatPreview; +import com.velocitypowered.proxy.protocol.packet.chat.PlayerCommand; +import com.velocitypowered.proxy.protocol.packet.chat.ServerChatPreview; +import com.velocitypowered.proxy.protocol.packet.chat.ServerPlayerChat; +import com.velocitypowered.proxy.protocol.packet.chat.SystemChat; import com.velocitypowered.proxy.protocol.packet.title.LegacyTitlePacket; import com.velocitypowered.proxy.protocol.packet.title.TitleActionbarPacket; import com.velocitypowered.proxy.protocol.packet.title.TitleClearPacket; @@ -104,7 +110,7 @@ public interface MinecraftSessionHandler { return false; } - default boolean handle(Chat packet) { + default boolean handle(LegacyChat packet) { return false; } @@ -231,4 +237,28 @@ public interface MinecraftSessionHandler { default boolean handle(ResourcePackResponse packet) { return false; } + + default boolean handle(PlayerChat packet) { + return false; + } + + default boolean handle(SystemChat packet) { + return false; + } + + default boolean handle(ServerPlayerChat packet) { + return false; + } + + default boolean handle(PlayerChatPreview packet) { + return false; + } + + default boolean handle(ServerChatPreview packet) { + return false; + } + + default boolean handle(PlayerCommand packet) { + return false; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/VelocityConstants.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/VelocityConstants.java index 79839e89a..039582cf1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/VelocityConstants.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/VelocityConstants.java @@ -24,7 +24,8 @@ public class VelocityConstants { } public static final String VELOCITY_IP_FORWARDING_CHANNEL = "velocity:player_info"; - public static final int FORWARDING_VERSION = 1; + public static final int MODERN_FORWARDING_DEFAULT = 1; + public static final int MODERN_FORWARDING_WITH_KEY = 2; public static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java index 5ed599c21..00ed17a69 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java @@ -18,6 +18,8 @@ package com.velocitypowered.proxy.connection.backend; import com.velocitypowered.api.event.player.ServerLoginPluginMessageEvent; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.proxy.VelocityServer; @@ -47,6 +49,7 @@ import javax.crypto.Mac; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.Nullable; public class LoginSessionHandler implements MinecraftSessionHandler { @@ -78,7 +81,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler { && packet.getChannel().equals(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL)) { ByteBuf forwardingData = createForwardingData(configuration.getForwardingSecret(), serverConn.getPlayerRemoteAddressAsString(), - serverConn.getPlayer().getGameProfile()); + serverConn.getPlayer().getGameProfile(), mc.getProtocolVersion(), serverConn.getPlayer().getIdentifiedKey()); LoginPluginResponse response = new LoginPluginResponse(packet.getId(), true, forwardingData); mc.write(response); informationForwarded = true; @@ -163,15 +166,23 @@ public class LoginSessionHandler implements MinecraftSessionHandler { } private static ByteBuf createForwardingData(byte[] hmacSecret, String address, - GameProfile profile) { + GameProfile profile, ProtocolVersion version, + @Nullable IdentifiedKey playerKey) { ByteBuf forwarded = Unpooled.buffer(2048); try { - ProtocolUtils.writeVarInt(forwarded, VelocityConstants.FORWARDING_VERSION); + int forwardingVersion = version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0 && playerKey != null + ? VelocityConstants.MODERN_FORWARDING_WITH_KEY : VelocityConstants.MODERN_FORWARDING_DEFAULT; + + ProtocolUtils.writeVarInt(forwarded, forwardingVersion); ProtocolUtils.writeString(forwarded, address); ProtocolUtils.writeUuid(forwarded, profile.getId()); ProtocolUtils.writeString(forwarded, profile.getName()); ProtocolUtils.writeProperties(forwarded, profile.getProperties()); + if (forwardingVersion >= VelocityConstants.MODERN_FORWARDING_WITH_KEY) { + ProtocolUtils.writePlayerKey(forwarded, playerKey); + } + SecretKey key = new SecretKeySpec(hmacSecret, "HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(key); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java index 709ea34a5..cde9abe23 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java @@ -190,7 +190,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, mc.setProtocolVersion(protocolVersion); mc.setState(StateRegistry.LOGIN); - mc.delayedWrite(new ServerLogin(proxyPlayer.getUsername())); + mc.delayedWrite(new ServerLogin(proxyPlayer.getUsername(), proxyPlayer.getIdentifiedKey())); mc.flush(); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java index 4ba401f96..e2c90a98a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java @@ -85,7 +85,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler { // Initiate a regular connection and move over to it. ConnectedPlayer player = new ConnectedPlayer(server, profileEvent.getGameProfile(), - mcConnection, inbound.getVirtualHost().orElse(null), onlineMode); + mcConnection, inbound.getVirtualHost().orElse(null), onlineMode, inbound.getIdentifiedKey()); this.connectedPlayer = player; if (!server.canRegisterConnection(player)) { player.disconnect0(Component.translatable("velocity.error.already-connected-proxy", @@ -133,6 +133,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler { } ServerLoginSuccess success = new ServerLoginSuccess(); success.setUsername(player.getUsername()); + success.setProperties(player.getGameProfileProperties()); success.setUuid(playerUniqueId); mcConnection.write(success); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index 1a107e350..b65d413de 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -42,10 +42,11 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.backend.BackendConnectionPhases; import com.velocitypowered.proxy.connection.backend.BungeeCordMessageResponder; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; +import com.velocitypowered.proxy.crypto.SignedChatCommand; +import com.velocitypowered.proxy.crypto.SignedChatMessage; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.BossBar; -import com.velocitypowered.proxy.protocol.packet.Chat; import com.velocitypowered.proxy.protocol.packet.ClientSettings; import com.velocitypowered.proxy.protocol.packet.JoinGame; import com.velocitypowered.proxy.protocol.packet.KeepAlive; @@ -55,6 +56,10 @@ import com.velocitypowered.proxy.protocol.packet.Respawn; import com.velocitypowered.proxy.protocol.packet.TabCompleteRequest; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse.Offer; +import com.velocitypowered.proxy.protocol.packet.chat.ChatBuilder; +import com.velocitypowered.proxy.protocol.packet.chat.LegacyChat; +import com.velocitypowered.proxy.protocol.packet.chat.PlayerChat; +import com.velocitypowered.proxy.protocol.packet.chat.PlayerCommand; import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.util.CharacterUtil; @@ -62,6 +67,8 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import io.netty.util.ReferenceCountUtil; + +import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -90,9 +97,11 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { private final Queue loginPluginMessages = new ConcurrentLinkedQueue<>(); private final VelocityServer server; private @Nullable TabCompleteRequest outstandingTabComplete; + private @Nullable Instant lastChatMessage; // Added in 1.19 /** * Constructs a client play session handler. + * * @param server the Velocity server instance * @param player the player */ @@ -101,6 +110,83 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { this.server = server; } + // I will not allow hacks to bypass this; + private boolean tickLastMessage(SignedChatMessage nextMessage) { + if (lastChatMessage != null && lastChatMessage.isAfter(nextMessage.getExpiryTemporal())) { + player.disconnect(Component.translatable("multiplayer.disconnect.out_of_order_chat")); + return false; + } + + lastChatMessage = nextMessage.getExpiryTemporal(); + return true; + } + + private boolean validateChat(String message) { + if (CharacterUtil.containsIllegalCharacters(message)) { + player.disconnect(Component.translatable("velocity.error.illegal-chat-characters", + NamedTextColor.RED)); + return false; + } + return true; + } + + private MinecraftConnection retrieveServerConnection() { + VelocityServerConnection serverConnection = player.getConnectedServer(); + if (serverConnection == null) { + return null; + } + return serverConnection.getConnection(); + } + + private void processCommandMessage(String message, @Nullable SignedChatCommand signedCommand, + MinecraftPacket original) { + server.getCommandManager().callCommandEvent(player, message) + .thenComposeAsync(event -> processCommandExecuteResult(message, + event.getResult(), signedCommand)) + .whenComplete((ignored, throwable) -> { + if (server.getConfiguration().isLogCommandExecutions()) { + logger.info("{} -> executed command /{}", player, message); + } + }) + .exceptionally(e -> { + logger.info("Exception occurred while running command for {}", + player.getUsername(), e); + player.sendMessage(Component.translatable("velocity.command.generic-error", + NamedTextColor.RED)); + return null; + }); + } + + private void processPlayerChat(String message, @Nullable SignedChatMessage signedMessage, + MinecraftPacket original) { + MinecraftConnection smc = retrieveServerConnection(); + if (smc == null) { + return; + } + PlayerChatEvent event = new PlayerChatEvent(player, message); + server.getEventManager().fire(event) + .thenAcceptAsync(pme -> { + PlayerChatEvent.ChatResult chatResult = pme.getResult(); + if (chatResult.isAllowed()) { + Optional eventMsg = pme.getResult().getMessage(); + if (eventMsg.isPresent()) { + if (player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0 + && player.getIdentifiedKey() != null) { + logger.warn("A plugin changed a signed chat message. The server may not accept it."); + } + smc.write(ChatBuilder.builder(player.getProtocolVersion()) + .message(event.getMessage()).toServer()); + } else { + smc.write(original); + } + } + }, smc.eventLoop()) + .exceptionally((ex) -> { + logger.error("Exception while handling player chat for {}", player, ex); + return null; + }); + } + @Override public void activated() { Collection channels = server.getChannelRegistrar().getChannelsForProtocol(player @@ -142,58 +228,57 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } @Override - public boolean handle(Chat packet) { - VelocityServerConnection serverConnection = player.getConnectedServer(); - if (serverConnection == null) { + public boolean handle(PlayerCommand packet) { + if (!validateChat(packet.getCommand())) { return true; } - MinecraftConnection smc = serverConnection.getConnection(); - if (smc == null) { + if (!packet.isUnsigned()) { + SignedChatCommand signedCommand = packet.signedContainer(player.getIdentifiedKey(), player.getUniqueId(), false); + if (signedCommand != null) { + processCommandMessage(packet.getCommand(), signedCommand, packet); + return true; + } + } + + processCommandMessage(packet.getCommand(), null, packet); + return true; + } + + @Override + public boolean handle(PlayerChat packet) { + if (!validateChat(packet.getMessage())) { return true; } + if (!packet.isUnsigned()) { + // Bad if spoofed + SignedChatMessage signedChat = packet.signedContainer(player.getIdentifiedKey(), player.getUniqueId(), false); + if (signedChat != null) { + // Server doesn't care for expiry as long as order is correct + if (!tickLastMessage(signedChat)) { + return true; + } + + processPlayerChat(packet.getMessage(), signedChat, packet); + return true; + } + } + + processPlayerChat(packet.getMessage(), null, packet); + return true; + } + + @Override + public boolean handle(LegacyChat packet) { String msg = packet.getMessage(); - if (CharacterUtil.containsIllegalCharacters(msg)) { - player.disconnect(Component.translatable("velocity.error.illegal-chat-characters", - NamedTextColor.RED)); + if (!validateChat(msg)) { return true; } if (msg.startsWith("/")) { - String originalCommand = msg.substring(1); - server.getCommandManager().callCommandEvent(player, msg.substring(1)) - .thenComposeAsync(event -> processCommandExecuteResult(originalCommand, - event.getResult())) - .whenComplete((ignored, throwable) -> { - if (server.getConfiguration().isLogCommandExecutions()) { - logger.info("{} -> executed command /{}", player, originalCommand); - } - }) - .exceptionally(e -> { - logger.info("Exception occurred while running command for {}", - player.getUsername(), e); - player.sendMessage(Component.translatable("velocity.command.generic-error", - NamedTextColor.RED)); - return null; - }); + processCommandMessage(msg.substring(1), null, packet); } else { - PlayerChatEvent event = new PlayerChatEvent(player, msg); - server.getEventManager().fire(event) - .thenAcceptAsync(pme -> { - PlayerChatEvent.ChatResult chatResult = pme.getResult(); - if (chatResult.isAllowed()) { - Optional eventMsg = pme.getResult().getMessage(); - if (eventMsg.isPresent()) { - smc.write(Chat.createServerbound(eventMsg.get())); - } else { - smc.write(packet); - } - } - }, smc.eventLoop()) - .exceptionally((ex) -> { - logger.error("Exception while handling player chat for {}", player, ex); - return null; - }); + processPlayerChat(msg, null, packet); } return true; } @@ -229,7 +314,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } } server.getEventManager().fireAndForget(new PlayerChannelRegisterEvent(player, - ImmutableList.copyOf(channelIdentifiers))); + ImmutableList.copyOf(channelIdentifiers))); backendConn.write(packet.retain()); } else if (PluginMessageUtil.isUnregister(packet)) { player.getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet)); @@ -368,7 +453,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { /** * Handles the {@code JoinGame} packet. This function is responsible for handling the client-side * switching servers in Velocity. - * @param joinGame the join game packet + * + * @param joinGame the join game packet * @param destination the new server we are connecting to */ public void handleBackendJoinGame(JoinGame joinGame, VelocityServerConnection destination) { @@ -452,7 +538,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { new Respawn(sentOldDim, joinGame.getPartialHashedSeed(), joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType(), false, joinGame.getDimensionInfo(), joinGame.getPreviousGamemode(), - joinGame.getCurrentDimensionData())); + joinGame.getCurrentDimensionData(), joinGame.getLastDeathPosition())); } private void doSafeClientServerSwitch(JoinGame joinGame) { @@ -469,14 +555,14 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { new Respawn(tempDim, joinGame.getPartialHashedSeed(), joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType(), false, joinGame.getDimensionInfo(), joinGame.getPreviousGamemode(), - joinGame.getCurrentDimensionData())); + joinGame.getCurrentDimensionData(), joinGame.getLastDeathPosition())); // Now send a respawn packet in the correct dimension. player.getConnection().delayedWrite( new Respawn(joinGame.getDimension(), joinGame.getPartialHashedSeed(), joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType(), false, joinGame.getDimensionInfo(), joinGame.getPreviousGamemode(), - joinGame.getCurrentDimensionData())); + joinGame.getCurrentDimensionData(), joinGame.getLastDeathPosition())); } public List getServerBossBars() { @@ -619,8 +705,10 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { }); } + private CompletableFuture processCommandExecuteResult(String originalCommand, - CommandResult result) { + CommandResult result, + @Nullable SignedChatCommand signedCommand) { if (result == CommandResult.denied()) { return CompletableFuture.completedFuture(null); } @@ -628,13 +716,30 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { MinecraftConnection smc = player.ensureAndGetCurrentServer().ensureConnected(); String commandToRun = result.getCommand().orElse(originalCommand); if (result.isForwardToServer()) { - return CompletableFuture.runAsync(() -> smc.write(Chat.createServerbound("/" - + commandToRun)), smc.eventLoop()); + ChatBuilder write = ChatBuilder + .builder(player.getProtocolVersion()) + .asPlayer(player); + + if (signedCommand != null && commandToRun.equals(signedCommand.getBaseCommand())) { + write.message(signedCommand); + } else { + write.message("/" + commandToRun); + } + return CompletableFuture.runAsync(() -> smc.write(write.toServer()), smc.eventLoop()); } else { return server.getCommandManager().executeImmediatelyAsync(player, commandToRun) .thenAcceptAsync(hasRun -> { if (!hasRun) { - smc.write(Chat.createServerbound("/" + commandToRun)); + ChatBuilder write = ChatBuilder + .builder(player.getProtocolVersion()) + .asPlayer(player); + + if (signedCommand != null && commandToRun.equals(signedCommand.getBaseCommand())) { + write.message(signedCommand); + } else { + write.message("/" + commandToRun); + } + smc.write(write.toServer()); } }, smc.eventLoop()); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index f45735879..a369d79c9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -41,6 +41,8 @@ import com.velocitypowered.api.permission.Tristate; import com.velocitypowered.api.proxy.ConnectionRequestBuilder; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.crypto.IdentifiedKey; +import com.velocitypowered.api.proxy.crypto.KeyIdentifiable; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.player.PlayerSettings; import com.velocitypowered.api.proxy.player.ResourcePackInfo; @@ -56,13 +58,14 @@ import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.StateRegistry; -import com.velocitypowered.proxy.protocol.packet.Chat; import com.velocitypowered.proxy.protocol.packet.ClientSettings; import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; import com.velocitypowered.proxy.protocol.packet.KeepAlive; import com.velocitypowered.proxy.protocol.packet.PluginMessage; import com.velocitypowered.proxy.protocol.packet.ResourcePackRequest; +import com.velocitypowered.proxy.protocol.packet.chat.ChatBuilder; +import com.velocitypowered.proxy.protocol.packet.chat.LegacyChat; import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; import com.velocitypowered.proxy.server.VelocityRegisteredServer; import com.velocitypowered.proxy.tablist.VelocityTabList; @@ -109,7 +112,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.jetbrains.annotations.NotNull; -public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { +public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, KeyIdentifiable { private static final int MAX_PLUGIN_CHANNELS = 1024; private static final PlainTextComponentSerializer PASS_THRU_TRANSLATE = PlainTextComponentSerializer.builder() @@ -159,9 +162,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { .build(); private @Nullable String clientBrand; private @Nullable Locale effectiveLocale; + private @Nullable IdentifiedKey playerKey; ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection, - @Nullable InetSocketAddress virtualHost, boolean onlineMode) { + @Nullable InetSocketAddress virtualHost, boolean onlineMode, @Nullable IdentifiedKey playerKey) { this.server = server; this.profile = profile; this.connection = connection; @@ -176,6 +180,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { } else { this.tabList = new VelocityTabListLegacy(this); } + this.playerKey = playerKey; } @Override @@ -311,7 +316,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { @Override public void sendMessage(@NonNull Identity identity, @NonNull Component message) { Component translated = translateMessage(message); - connection.write(Chat.createClientbound(identity, translated, this.getProtocolVersion())); + + connection.write(ChatBuilder.builder(this.getProtocolVersion()) + .component(translated).forIdentity(identity).toClient()); } @Override @@ -321,9 +328,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { Preconditions.checkNotNull(type, "type"); Component translated = translateMessage(message); - Chat packet = Chat.createClientbound(identity, translated, this.getProtocolVersion()); - packet.setType(type == MessageType.CHAT ? Chat.CHAT_TYPE : Chat.SYSTEM_TYPE); - connection.write(packet); + + connection.write(ChatBuilder.builder(this.getProtocolVersion()) + .component(translated).forIdentity(identity) + .setType(type == MessageType.CHAT ? ChatBuilder.ChatType.CHAT : ChatBuilder.ChatType.SYSTEM) + .toClient()); } @Override @@ -344,10 +353,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { JsonObject object = new JsonObject(); object.addProperty("text", LegacyComponentSerializer.legacySection() .serialize(translated)); - Chat chat = new Chat(); - chat.setMessage(object.toString()); - chat.setType(Chat.GAME_INFO_TYPE); - connection.write(chat); + LegacyChat legacyChat = new LegacyChat(); + legacyChat.setMessage(object.toString()); + legacyChat.setType(LegacyChat.GAME_INFO_TYPE); + connection.write(legacyChat); } } @@ -883,10 +892,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { @Override public void spoofChatInput(String input) { - Preconditions.checkArgument(input.length() <= Chat.MAX_SERVERBOUND_MESSAGE_LENGTH, - "input cannot be greater than " + Chat.MAX_SERVERBOUND_MESSAGE_LENGTH + Preconditions.checkArgument(input.length() <= LegacyChat.MAX_SERVERBOUND_MESSAGE_LENGTH, + "input cannot be greater than " + LegacyChat.MAX_SERVERBOUND_MESSAGE_LENGTH + " characters in length"); - ensureBackendConnection().write(Chat.createServerbound(input)); + ensureBackendConnection().write(ChatBuilder.builder(getProtocolVersion()) + .asPlayer(this).message(input).toServer()); } @Override @@ -1055,6 +1065,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { return knownChannels; } + @Override + public IdentifiedKey getIdentifiedKey() { + return playerKey; + } + private class IdentityImpl implements Identity { @Override public @NonNull UUID uuid() { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java index cd20fb2a9..eb3b1befa 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java @@ -20,12 +20,15 @@ package com.velocitypowered.proxy.connection.client; import static com.google.common.net.UrlEscapers.urlFormParameterEscaper; import static com.velocitypowered.proxy.VelocityServer.GENERAL_GSON; import static com.velocitypowered.proxy.connection.VelocityConstants.EMPTY_BYTE_ARRAY; -import static com.velocitypowered.proxy.util.EncryptionUtils.decryptRsa; -import static com.velocitypowered.proxy.util.EncryptionUtils.generateServerId; +import static com.velocitypowered.proxy.crypto.EncryptionUtils.decryptRsa; +import static com.velocitypowered.proxy.crypto.EncryptionUtils.generateServerId; import com.google.common.base.Preconditions; +import com.google.common.primitives.Longs; import com.velocitypowered.api.event.connection.PreLoginEvent; import com.velocitypowered.api.event.connection.PreLoginEvent.PreLoginComponentResult; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; @@ -77,6 +80,23 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler { public boolean handle(ServerLogin packet) { assertState(LoginState.LOGIN_PACKET_EXPECTED); this.currentState = LoginState.LOGIN_PACKET_RECEIVED; + IdentifiedKey playerKey = packet.getPlayerKey(); + if (playerKey != null) { + if (playerKey.hasExpired()) { + inbound.disconnect(Component.translatable("multiplayer.disconnect.invalid_public_key_signature")); + return true; + } + + if (!playerKey.isSignatureValid()) { + inbound.disconnect(Component.translatable("multiplayer.disconnect.invalid_public_key")); + return true; + } + } else if (mcConnection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0 + && server.getConfiguration().isForceKeyAuthentication()) { + inbound.disconnect(Component.translatable("multiplayer.disconnect.missing_public_key")); + return true; + } + inbound.setPlayerKey(playerKey); this.login = packet; PreLoginEvent event = new PreLoginEvent(inbound, login.getUsername()); @@ -135,7 +155,6 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler { public boolean handle(EncryptionResponse packet) { assertState(LoginState.ENCRYPTION_REQUEST_SENT); this.currentState = LoginState.ENCRYPTION_RESPONSE_RECEIVED; - ServerLogin login = this.login; if (login == null) { throw new IllegalStateException("No ServerLogin packet received yet."); @@ -147,9 +166,16 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler { try { KeyPair serverKeyPair = server.getServerKeyPair(); - byte[] decryptedVerifyToken = decryptRsa(serverKeyPair, packet.getVerifyToken()); - if (!MessageDigest.isEqual(verify, decryptedVerifyToken)) { - throw new IllegalStateException("Unable to successfully decrypt the verification token."); + if (inbound.getIdentifiedKey() != null) { + IdentifiedKey playerKey = inbound.getIdentifiedKey(); + if (!playerKey.verifyDataSignature(packet.getVerifyToken(), verify, Longs.toByteArray(packet.getSalt()))) { + throw new IllegalStateException("Invalid client public signature."); + } + } else { + byte[] decryptedVerifyToken = decryptRsa(serverKeyPair, packet.getVerifyToken()); + if (!MessageDigest.isEqual(verify, decryptedVerifyToken)) { + throw new IllegalStateException("Unable to successfully decrypt the verification token."); + } } byte[] decryptedSharedSecret = decryptRsa(serverKeyPair, packet.getSharedSecret()); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginInboundConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginInboundConnection.java index f707c4cc7..955e28029 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginInboundConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginInboundConnection.java @@ -19,6 +19,8 @@ package com.velocitypowered.proxy.connection.client; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.LoginPhaseConnection; +import com.velocitypowered.api.proxy.crypto.IdentifiedKey; +import com.velocitypowered.api.proxy.crypto.KeyIdentifiable; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage; @@ -32,9 +34,10 @@ import java.util.Optional; import java.util.Queue; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import space.vectrix.flare.fastutil.Int2ObjectSyncMap; -public class LoginInboundConnection implements LoginPhaseConnection { +public class LoginInboundConnection implements LoginPhaseConnection, KeyIdentifiable { private static final AtomicIntegerFieldUpdater SEQUENCE_UPDATER = AtomicIntegerFieldUpdater.newUpdater(LoginInboundConnection.class, "sequenceCounter"); @@ -45,6 +48,7 @@ public class LoginInboundConnection implements LoginPhaseConnection { private final Queue loginMessagesToSend; private volatile Runnable onAllMessagesHandled; private volatile boolean loginEventFired; + private @MonotonicNonNull IdentifiedKey playerKey; LoginInboundConnection( InitialInboundConnection delegate) { @@ -149,4 +153,13 @@ public class LoginInboundConnection implements LoginPhaseConnection { MinecraftConnection delegatedConnection() { return delegate.getConnection(); } + + public void setPlayerKey(IdentifiedKey playerKey) { + this.playerKey = playerKey; + } + + @Override + public IdentifiedKey getIdentifiedKey() { + return playerKey; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/ChatData.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/ChatData.java new file mode 100644 index 000000000..bb09edcad --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/ChatData.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2018 Velocity 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 . + */ + +package com.velocitypowered.proxy.connection.registry; + +/* +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import net.kyori.adventure.nbt.*; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.format.TextFormat; +import net.kyori.adventure.translation.Translatable; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +*/ + +// TODO Implement +public class ChatData { + /* + private static final ListBinaryTag EMPTY_LIST_TAG = ListBinaryTag.empty(); + + + private final String identifier; + private final int id; + private final Map<> + + + public static class Decoration implements Translatable { + + private final List parameters; + private final List style; + private final String translationKey; + + public List getParameters() { + return parameters; + } + + public List getStyle() { + return style; + } + + @Override + public @NotNull String translationKey() { + return translationKey; + } + + public Decoration(List parameters, List style, String translationKey) { + this.parameters = Preconditions.checkNotNull(parameters); + this.style = Preconditions.checkNotNull(style); + this.translationKey = Preconditions.checkNotNull(translationKey); + Preconditions.checkArgument(translationKey.length() > 0); + } + + public static Decoration decodeRegistryEntry(CompoundBinaryTag toDecode) { + ImmutableList.Builder parameters = ImmutableList.builder(); + ListBinaryTag paramList = toDecode.getList("parameters", EMPTY_LIST_TAG); + if (paramList != EMPTY_LIST_TAG) { + paramList.forEach(binaryTag -> parameters.add(binaryTag.toString())); + } + + ImmutableList.Builder style = ImmutableList.builder(); + CompoundBinaryTag styleList = toDecode.getCompound("style"); + for (String key : styleList.keySet()) { + if ("color".equals(key)) { + NamedTextColor color = Preconditions.checkNotNull( + NamedTextColor.NAMES.value(styleList.getString(key))); + style.add(color); + } else { + // Key is a Style option instead + TextDecoration deco = TextDecoration.NAMES.value(key); + // This wouldn't be here if it wasn't applied, but here it goes anyway: + byte val = styleList.getByte(key); + if (val != 0) { + style.add(deco); + } + } + } + + String translationKey = toDecode.getString("translation_key"); + + return new Decoration(parameters.build(), style.build(), translationKey); + } + + public void encodeRegistryEntry(CompoundBinaryTag ) + + } + + public static enum Priority { + SYSTEM, + CHAT + } +*/ +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/DimensionData.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/DimensionData.java index 07b69baef..398c024d4 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/DimensionData.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/DimensionData.java @@ -45,42 +45,51 @@ public final class DimensionData { private final @Nullable String effects; private final @Nullable Integer minY; // Required and added by 1.17 private final @Nullable Integer height; // Required and added by 1.17 + private final @Nullable Integer monsterSpawnBlockLightLimit; // Required and added by 1.19 + private final @Nullable Integer monsterSpawnLightLevel; // Required and added by 1.19 /** * Initializes a new {@link DimensionData} instance. - * @param registryIdentifier the identifier for the dimension from the registry. - * @param dimensionId the dimension ID contained in the registry (the "id" tag) - * @param isNatural indicates if the dimension use natural world generation (e.g. overworld) - * @param ambientLight the light level the client sees without external lighting - * @param isShrunk indicates if the world is shrunk, aka not the full 256 blocks (e.g. nether) - * @param isUltrawarm internal dimension warmth flag - * @param hasCeiling indicates if the dimension has a ceiling layer - * @param hasSkylight indicates if the dimension should display the sun - * @param isPiglinSafe indicates if piglins should naturally zombify in this dimension - * @param doBedsWork indicates if players should be able to sleep in beds in this dimension - * @param doRespawnAnchorsWork indicates if player respawn points can be used in this dimension - * @param hasRaids indicates if raids can be spawned in the dimension - * @param logicalHeight the natural max height for the given dimension - * @param burningBehaviourIdentifier the identifier for how burning blocks work in the dimension - * @param fixedTime optional. If set to any game daytime value will deactivate time cycle - * @param createDragonFight optional. Internal flag used in the end dimension - * @param coordinateScale optional, unknown purpose - * @param effects optional, unknown purpose - * @param minY the world effective lowest build-level - * @param height the world height above zero + * + * @param registryIdentifier the identifier for the dimension from the registry. + * @param dimensionId the dimension ID contained in the registry (the "id" tag) + * @param isNatural indicates if the dimension use natural world generation (e.g. overworld) + * @param ambientLight the light level the client sees without external lighting + * @param isShrunk indicates if the world is shrunk, aka not the full 256 blocks (e.g. nether) + * @param isUltrawarm internal dimension warmth flag + * @param hasCeiling indicates if the dimension has a ceiling layer + * @param hasSkylight indicates if the dimension should display the sun + * @param isPiglinSafe indicates if piglins should naturally zombify in this dimension + * @param doBedsWork indicates if players should be able to sleep in beds in this dimension + * @param doRespawnAnchorsWork indicates if player respawn points can be used in this dimension + * @param hasRaids indicates if raids can be spawned in the dimension + * @param logicalHeight the natural max height for the given dimension + * @param burningBehaviourIdentifier the identifier for how burning blocks work in the dimension + * @param fixedTime optional. If set to any game daytime value will deactivate time cycle + * @param createDragonFight optional. Internal flag used in the end dimension + * @param coordinateScale optional, unknown purpose + * @param effects optional, unknown purpose + * @param minY the world effective lowest build-level + * @param height the world height above zero + * @param monsterSpawnBlockLightLimit an integer controlling the block light needed to prevent monster spawns. + * @param monsterSpawnLightLevel an int provider which is evaluated to find a value to compare the current + * overall brightness with to determine if a monster should be allowed to spawn. */ public DimensionData(String registryIdentifier, - @Nullable Integer dimensionId, - boolean isNatural, - float ambientLight, boolean isShrunk, boolean isUltrawarm, - boolean hasCeiling, boolean hasSkylight, - boolean isPiglinSafe, boolean doBedsWork, - boolean doRespawnAnchorsWork, boolean hasRaids, - int logicalHeight, String burningBehaviourIdentifier, - @Nullable Long fixedTime, @Nullable Boolean createDragonFight, - @Nullable Double coordinateScale, - @Nullable String effects, - @Nullable Integer minY, @Nullable Integer height) { + @Nullable Integer dimensionId, + boolean isNatural, + float ambientLight, boolean isShrunk, boolean isUltrawarm, + boolean hasCeiling, boolean hasSkylight, + boolean isPiglinSafe, boolean doBedsWork, + boolean doRespawnAnchorsWork, boolean hasRaids, + int logicalHeight, String burningBehaviourIdentifier, + @Nullable Long fixedTime, @Nullable Boolean createDragonFight, + @Nullable Double coordinateScale, + @Nullable String effects, + @Nullable Integer minY, @Nullable Integer height, @Nullable Integer monsterSpawnBlockLightLimit, + @Nullable Integer monsterSpawnLightLevel) { + this.monsterSpawnBlockLightLimit = monsterSpawnBlockLightLimit; + this.monsterSpawnLightLevel = monsterSpawnLightLevel; Preconditions.checkNotNull( registryIdentifier, "registryIdentifier cannot be null"); Preconditions.checkArgument(registryIdentifier.length() > 0, @@ -193,15 +202,15 @@ public final class DimensionData { * and {@code dimensionId}. * * @param registryIdentifier the identifier for the dimension from the registry - * @param dimensionId optional, dimension ID + * @param dimensionId optional, dimension ID * @return a new {@link DimensionData} */ public DimensionData annotateWith(String registryIdentifier, - @Nullable Integer dimensionId) { + @Nullable Integer dimensionId) { return new DimensionData(registryIdentifier, dimensionId, isNatural, ambientLight, isShrunk, isUltrawarm, hasCeiling, hasSkylight, isPiglinSafe, doBedsWork, doRespawnAnchorsWork, hasRaids, logicalHeight, burningBehaviourIdentifier, fixedTime, createDragonFight, - coordinateScale, effects, minY, height); + coordinateScale, effects, minY, height, monsterSpawnBlockLightLimit, monsterSpawnLightLevel); } public boolean isUnannotated() { @@ -217,7 +226,7 @@ public final class DimensionData { * @return game dimension data */ public static DimensionData decodeBaseCompoundTag(CompoundBinaryTag details, - ProtocolVersion version) { + ProtocolVersion version) { boolean isNatural = details.getBoolean("natural"); float ambientLight = details.getFloat("ambient_light"); boolean isShrunk = details.getBoolean("shrunk"); @@ -240,28 +249,40 @@ public final class DimensionData { : null; Integer minY = details.keySet().contains("min_y") ? details.getInt("min_y") : null; Integer height = details.keySet().contains("height") ? details.getInt("height") : null; + Integer monsterSpawnBlockLightLimit = details.keySet().contains("monster_spawn_block_light_limit") + ? details.getInt("monster_spawn_block_light_limit") : null; + Integer monsterSpawnLightLevel = + details.keySet().contains("monster_spawn_light_level") ? details.getInt("monster_spawn_block_light_limit") : + null; if (version.compareTo(ProtocolVersion.MINECRAFT_1_17) >= 0) { Preconditions.checkNotNull(height, - "DimensionData requires 'height' to be present for this version"); + "DimensionData requires 'height' to be present for this version"); Preconditions.checkNotNull(minY, - "DimensionData requires 'minY' to be present for this version"); + "DimensionData requires 'minY' to be present for this version"); + } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { + Preconditions.checkNotNull(monsterSpawnBlockLightLimit, + "DimensionData requires 'monster_spawn_block_light_limit' to be present for this version."); + Preconditions.checkNotNull(monsterSpawnLightLevel, + "DimensionData requires 'monster_spawn_light_level' to be present for this version."); } return new DimensionData( UNKNOWN_DIMENSION_ID, null, isNatural, ambientLight, isShrunk, isUltrawarm, hasCeiling, hasSkylight, isPiglinSafe, doBedsWork, doRespawnAnchorsWork, hasRaids, logicalHeight, burningBehaviourIdentifier, fixedTime, hasEnderdragonFight, - coordinateScale, effects, minY, height); + coordinateScale, effects, minY, height, monsterSpawnBlockLightLimit, monsterSpawnLightLevel); } /** * Parses a given CompoundTag to a DimensionData instance. Assumes the data is part of a * dimension registry. - * @param dimTag the compound from the registry to read + * + * @param dimTag the compound from the registry to read * @param version the protocol version * @return game dimension data */ public static DimensionData decodeRegistryEntry(CompoundBinaryTag dimTag, - ProtocolVersion version) { + ProtocolVersion version) { String registryIdentifier = dimTag.getString("name"); CompoundBinaryTag details; Integer dimensionId = null; @@ -278,6 +299,7 @@ public final class DimensionData { /** * Encodes the Dimension data as CompoundTag. + * * @param version the version to serialize as * @return compound containing the dimension data */ @@ -301,6 +323,7 @@ public final class DimensionData { /** * Serializes details of this dimension. + * * @return serialized details of this dimension */ public CompoundBinaryTag serializeDimensionDetails() { @@ -335,6 +358,12 @@ public final class DimensionData { if (height != null) { ret.putInt("height", height); } + if (monsterSpawnBlockLightLimit != null) { + ret.putInt("monster_spawn_block_light_limit", monsterSpawnBlockLightLimit); + } + if (monsterSpawnLightLevel != null) { + ret.putInt("monster_spawn_light_level", monsterSpawnLightLevel); + } return ret.build(); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/crypto/EncryptionUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/crypto/EncryptionUtils.java new file mode 100644 index 000000000..6bd61604b --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/crypto/EncryptionUtils.java @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2018 Velocity 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 . + */ + +package com.velocitypowered.proxy.crypto; + +import com.google.common.base.Preconditions; +import com.google.common.io.ByteStreams; +import com.velocitypowered.proxy.util.except.QuietDecoderException; +import it.unimi.dsi.fastutil.Pair; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; +import javax.crypto.Cipher; + +public enum EncryptionUtils { + ; + + public static final Pair PEM_RSA_PUBLIC_KEY_DESCRIPTOR = + Pair.of("-----BEGIN RSA PUBLIC KEY-----", "-----END RSA PUBLIC KEY-----"); + public static final Pair PEM_RSA_PRIVATE_KEY_DESCRIPTOR = + Pair.of("-----BEGIN RSA PRIVATE KEY-----", "-----END RSA PRIVATE KEY-----"); + + public static final String SHA1_WITH_RSA = "SHA1withRSA"; + public static final String SHA256_WITH_RSA = "SHA256withRSA"; + + public static final QuietDecoderException INVALID_SIGNATURE + = new QuietDecoderException("Incorrectly signed chat message"); + public static final QuietDecoderException PREVIEW_SIGNATURE_MISSING + = new QuietDecoderException("Unsigned chat message requested signed preview"); + public static final byte[] EMPTY = new byte[0]; + private static PublicKey YGGDRASIL_SESSION_KEY; + private static KeyFactory RSA_KEY_FACTORY; + + private static final Base64.Encoder MIME_SPECIAL_ENCODER + = Base64.getMimeEncoder(76, "\n".getBytes(StandardCharsets.UTF_8)); + + static { + try { + RSA_KEY_FACTORY = KeyFactory.getInstance("RSA"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + + try { + byte[] bytes = ByteStreams.toByteArray( + EncryptionUtils.class.getClassLoader().getResourceAsStream("yggdrasil_session_pubkey.der")); + YGGDRASIL_SESSION_KEY = parseRsaPublicKey(bytes); + } catch (IOException | NullPointerException err) { + throw new RuntimeException(err); + } + } + + public static PublicKey getYggdrasilSessionKey() { + return YGGDRASIL_SESSION_KEY; + } + + /** + * Verifies a key signature. + * + * @param algorithm the signature algorithm + * @param base the public key to verify with + * @param signature the signature to verify against + * @param toVerify the byte array(s) of data to verify + * @return validity of the signature + */ + public static boolean verifySignature(String algorithm, PublicKey base, byte[] signature, byte[]... toVerify) { + Preconditions.checkArgument(toVerify.length > 0); + try { + Signature construct = Signature.getInstance(algorithm); + construct.initVerify(base); + for (byte[] bytes : toVerify) { + construct.update(bytes); + } + return construct.verify(signature); + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException("Invalid signature parameters"); + } + } + + /** + * Generates a signature for input data. + * + * @param algorithm the signature algorithm + * @param base the private key to sign with + * @param toSign the byte array(s) of data to sign + * @return the generated signature + */ + public static byte[] generateSignature(String algorithm, PrivateKey base, byte[]... toSign) { + Preconditions.checkArgument(toSign.length > 0); + try { + Signature construct = Signature.getInstance(algorithm); + construct.initSign(base); + for (byte[] bytes : toSign) { + construct.update(bytes); + } + return construct.sign(); + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException("Invalid signature parameters"); + } + } + + /** + * Encodes a long array as Big-endian byte array. + * + * @param bits the long (array) of numbers to encode + * @return the encoded bytes + */ + public static byte[] longToBigEndianByteArray(long... bits) { + ByteBuffer ret = ByteBuffer.allocate(8 * bits.length).order(ByteOrder.BIG_ENDIAN); + for (long put : bits) { + ret.putLong(put); + } + return ret.array(); + } + + public static String encodeUrlEncoded(byte[] data) { + return MIME_SPECIAL_ENCODER.encodeToString(data); + } + + public static byte[] decodeUrlEncoded(String toParse) { + return Base64.getMimeDecoder().decode(toParse); + } + + /** + * Parse a cer-encoded RSA key into its key bytes. + * + * @param toParse the cer-encoded key String + * @param descriptors the type of key + * @return the parsed key bytes + */ + public static byte[] parsePemEncoded(String toParse, Pair descriptors) { + int startIdx = toParse.indexOf(descriptors.first()); + Preconditions.checkArgument(startIdx >= 0); + int firstLen = descriptors.first().length(); + int endIdx = toParse.indexOf(descriptors.second(), firstLen + startIdx) + 1; + Preconditions.checkArgument(endIdx > 0); + return decodeUrlEncoded(toParse.substring(startIdx + firstLen, endIdx)); + } + + /** + * Encodes an RSA key as String cer format. + * + * @param toEncode the private or public RSA key + * @return the encoded key cer + */ + public static String pemEncodeRsaKey(Key toEncode) { + Preconditions.checkNotNull(toEncode); + Pair encoder; + if (toEncode instanceof PublicKey) { + encoder = PEM_RSA_PUBLIC_KEY_DESCRIPTOR; + } else if (toEncode instanceof PrivateKey) { + encoder = PEM_RSA_PRIVATE_KEY_DESCRIPTOR; + } else { + throw new IllegalArgumentException("Invalid key type"); + } + + return encoder.first() + "\n" + + encodeUrlEncoded(toEncode.getEncoded()) + "\n" + + encoder.second() + "\n"; + } + + /** + * Parse an RSA public key from key bytes. + * + * @param keyValue the key bytes + * @return the generated key + */ + public static PublicKey parseRsaPublicKey(byte[] keyValue) { + try { + return RSA_KEY_FACTORY.generatePublic(new X509EncodedKeySpec(keyValue)); + } catch (InvalidKeySpecException e) { + throw new IllegalArgumentException("Invalid key bytes"); + } + } + + /** + * Generates an RSA key pair. + * + * @param keysize the key size (in bits) for the RSA key pair + * @return the generated key pair + */ + public static KeyPair createRsaKeyPair(final int keysize) { + try { + final KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(keysize); + return generator.generateKeyPair(); + } catch (final NoSuchAlgorithmException e) { + throw new RuntimeException("Unable to generate RSA keypair", e); + } + } + + /** + * Generates a hex digest in two's complement form for use with the Mojang joinedServer endpoint. + * + * @param digest the bytes to digest + * @return the hex digest + */ + public static String twosComplementHexdigest(byte[] digest) { + return new BigInteger(digest).toString(16); + } + + /** + * Decrypts an RSA message. + * + * @param keyPair the key pair to use + * @param bytes the bytes of the encrypted message + * @return the decrypted message + * @throws GeneralSecurityException if the message couldn't be decoded + */ + public static byte[] decryptRsa(KeyPair keyPair, byte[] bytes) throws GeneralSecurityException { + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate()); + return cipher.doFinal(bytes); + } + + /** + * Generates the server ID for the hasJoined endpoint. + * + * @param sharedSecret the shared secret between the client and the proxy + * @param key the RSA public key + * @return the server ID + */ + public static String generateServerId(byte[] sharedSecret, PublicKey key) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.update(sharedSecret); + digest.update(key.getEncoded()); + return twosComplementHexdigest(digest.digest()); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/crypto/IdentifiedKeyImpl.java b/proxy/src/main/java/com/velocitypowered/proxy/crypto/IdentifiedKeyImpl.java new file mode 100644 index 000000000..f9eb028cb --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/crypto/IdentifiedKeyImpl.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2018 Velocity 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 . + */ + +package com.velocitypowered.proxy.crypto; + +import com.velocitypowered.api.proxy.crypto.IdentifiedKey; +import java.nio.charset.StandardCharsets; +import java.security.PublicKey; +import java.time.Instant; +import java.util.Arrays; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +public class IdentifiedKeyImpl implements IdentifiedKey { + + private final PublicKey publicKey; + private final byte[] signature; + private final Instant expiryTemporal; + private @MonotonicNonNull Boolean isSignatureValid; + + public IdentifiedKeyImpl(byte[] keyBits, long expiry, + byte[] signature) { + this(EncryptionUtils.parseRsaPublicKey(keyBits), Instant.ofEpochMilli(expiry), signature); + } + + /** + * Creates an Identified key from data. + */ + public IdentifiedKeyImpl(PublicKey publicKey, Instant expiryTemporal, byte[] signature) { + this.publicKey = publicKey; + this.expiryTemporal = expiryTemporal; + this.signature = signature; + } + + @Override + public PublicKey getSignedPublicKey() { + return publicKey; + } + + @Override + public PublicKey getSigner() { + return EncryptionUtils.getYggdrasilSessionKey(); + } + + @Override + public Instant getExpiryTemporal() { + return expiryTemporal; + } + + @Override + public byte[] getSignature() { + return signature; + } + + @Override + public boolean isSignatureValid() { + if (isSignatureValid == null) { + String pemKey = EncryptionUtils.pemEncodeRsaKey(publicKey); + long expires = expiryTemporal.toEpochMilli(); + byte[] toVerify = ("" + expires + pemKey).getBytes(StandardCharsets.US_ASCII); + isSignatureValid = EncryptionUtils.verifySignature( + EncryptionUtils.SHA1_WITH_RSA, EncryptionUtils.getYggdrasilSessionKey(), signature, toVerify); + } + return isSignatureValid; + } + + @Override + public boolean verifyDataSignature(byte[] signature, byte[]... toVerify) { + try { + return EncryptionUtils.verifySignature(EncryptionUtils.SHA256_WITH_RSA, publicKey, signature, toVerify); + } catch (IllegalArgumentException e) { + return false; + } + } + + @Override + public String toString() { + return "IdentifiedKeyImpl{" + + "publicKey=" + publicKey + + ", signature=" + Arrays.toString(signature) + + ", expiryTemporal=" + expiryTemporal + + ", isSignatureValid=" + isSignatureValid + + '}'; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/crypto/SignedChatCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/crypto/SignedChatCommand.java new file mode 100644 index 000000000..9394a3ae4 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/crypto/SignedChatCommand.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2018 Velocity 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 . + */ + +package com.velocitypowered.proxy.crypto; + +import com.google.common.base.Preconditions; +import com.velocitypowered.api.proxy.crypto.KeySigned; +import java.security.PublicKey; +import java.time.Instant; +import java.util.Map; +import java.util.UUID; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class SignedChatCommand implements KeySigned { + private final String command; + private final PublicKey signer; + private final Instant expiry; + private final byte[] salt; + private final UUID sender; + //private final boolean isValid; + private final boolean isPreviewSigned; + + private final Map signatures; + + /** + * Create a signed command from data. + */ + public SignedChatCommand(String command, PublicKey signer, UUID sender, + Instant expiry, Map signature, byte[] salt, boolean isPreviewSigned) { + this.command = Preconditions.checkNotNull(command); + this.signer = Preconditions.checkNotNull(signer); + this.sender = Preconditions.checkNotNull(sender); + this.signatures = Preconditions.checkNotNull(signature); + this.expiry = Preconditions.checkNotNull(expiry); + this.salt = Preconditions.checkNotNull(salt); + this.isPreviewSigned = isPreviewSigned; + + } + + @Override + public PublicKey getSigner() { + return signer; + } + + @Override + public Instant getExpiryTemporal() { + return expiry; + } + + @Override + public @Nullable byte[] getSignature() { + return null; + } + + @Override + public byte[] getSalt() { + return salt; + } + + public String getBaseCommand() { + return command; + } + + + public Map getSignatures() { + return signatures; + } + + public boolean isPreviewSigned() { + return isPreviewSigned; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/crypto/SignedChatMessage.java b/proxy/src/main/java/com/velocitypowered/proxy/crypto/SignedChatMessage.java new file mode 100644 index 000000000..b108e5a74 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/crypto/SignedChatMessage.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2018 Velocity 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 . + */ + +package com.velocitypowered.proxy.crypto; + +import com.google.common.base.Preconditions; +import com.google.common.primitives.Longs; +import com.velocitypowered.api.proxy.crypto.SignedMessage; +import com.velocitypowered.proxy.util.except.QuietDecoderException; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.PublicKey; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.TemporalAmount; +import java.util.UUID; + +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class SignedChatMessage implements SignedMessage { + + private static final QuietDecoderException INVALID_SIGNED_CHAT = + new QuietDecoderException("Couldn't parse chat message"); + + public static final TemporalAmount EXPIRY_TIME = Duration.ofMinutes(2L); + + private final String message; + private final PublicKey signer; + private final byte[] signature; + private final Instant expiry; + private final byte[] salt; + private final UUID sender; + //private final boolean isValid; + private final boolean isPreviewSigned; + + /** + * Create a signed message from data. + */ + public SignedChatMessage(String message, PublicKey signer, UUID sender, + Instant expiry, byte[] signature, byte[] salt, boolean isPreviewSigned) { + this.message = Preconditions.checkNotNull(message); + this.signer = Preconditions.checkNotNull(signer); + this.sender = Preconditions.checkNotNull(sender); + this.signature = Preconditions.checkNotNull(signature); + this.expiry = Preconditions.checkNotNull(expiry); + this.salt = Preconditions.checkNotNull(salt); + this.isPreviewSigned = isPreviewSigned; + + + //this.isValid = EncryptionUtils.verifySignature(EncryptionUtils.SHA1_WITH_RSA, signer, + // signature, salt, EncryptionUtils.longToBigEndianByteArray( + // sender.getMostSignificantBits(), sender.getLeastSignificantBits() + // ), Longs.toByteArray(expiry.getEpochSecond()), message.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public PublicKey getSigner() { + return signer; + } + + @Override + public Instant getExpiryTemporal() { + return expiry; + } + + @Override + public @Nullable byte[] getSignature() { + return signature; + } + + //@Override + //public boolean isSignatureValid() { + // return isValid; + //} + + @Override + public String getMessage() { + return message; + } + + @Override + public UUID getSignerUuid() { + return sender; + } + + @Override + public boolean isPreviewSigned() { + return isPreviewSigned; + } + + @Override + public byte[] getSalt() { + return salt; + } + +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java index 53daae299..cb12737ef 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java @@ -21,7 +21,9 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.velocitypowered.proxy.protocol.util.NettyPreconditions.checkFrame; import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.api.util.GameProfile; +import com.velocitypowered.proxy.crypto.IdentifiedKeyImpl; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.util.VelocityLegacyHoverEventSerializer; import com.velocitypowered.proxy.util.except.QuietDecoderException; @@ -59,7 +61,7 @@ public enum ProtocolUtils { .legacyHoverEventSerializer(VelocityLegacyHoverEventSerializer.INSTANCE) .build(); - private static final int DEFAULT_MAX_STRING_SIZE = 65536; // 64KiB + public static final int DEFAULT_MAX_STRING_SIZE = 65536; // 64KiB private static final QuietDecoderException BAD_VARINT_CACHED = new QuietDecoderException("Bad VarInt decoded"); private static final int[] VARINT_EXACT_BYTE_LENGTHS = new int[33]; @@ -537,6 +539,31 @@ public enum ProtocolUtils { return PRE_1_16_SERIALIZER; } + /** + * Writes a players {@link IdentifiedKey} to the buffer. + * + * @param buf the buffer + * @param playerKey the key to write + */ + public static void writePlayerKey(ByteBuf buf, IdentifiedKey playerKey) { + buf.writeLong(playerKey.getExpiryTemporal().toEpochMilli()); + ProtocolUtils.writeByteArray(buf, playerKey.getSignedPublicKey().getEncoded()); + ProtocolUtils.writeByteArray(buf, playerKey.getSignature()); + } + + /** + * Reads a players {@link IdentifiedKey} from the buffer. + * + * @param buf the buffer + * @return the key + */ + public static IdentifiedKey readPlayerKey(ByteBuf buf) { + long expiry = buf.readLong(); + byte[] key = ProtocolUtils.readByteArray(buf); + byte[] signature = ProtocolUtils.readByteArray(buf, 4096); + return new IdentifiedKeyImpl(key, expiry, signature); + } + public enum Direction { SERVERBOUND, CLIENTBOUND; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java index bab367e49..ccedb7dc8 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -28,6 +28,8 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16_2; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16_4; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_17; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_18; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_18_2; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_7_2; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9; @@ -39,7 +41,6 @@ import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.protocol.packet.AvailableCommands; import com.velocitypowered.proxy.protocol.packet.BossBar; -import com.velocitypowered.proxy.protocol.packet.Chat; import com.velocitypowered.proxy.protocol.packet.ClientSettings; import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.EncryptionRequest; @@ -63,6 +64,10 @@ import com.velocitypowered.proxy.protocol.packet.StatusRequest; import com.velocitypowered.proxy.protocol.packet.StatusResponse; import com.velocitypowered.proxy.protocol.packet.TabCompleteRequest; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse; +import com.velocitypowered.proxy.protocol.packet.chat.LegacyChat; +import com.velocitypowered.proxy.protocol.packet.chat.PlayerChat; +import com.velocitypowered.proxy.protocol.packet.chat.PlayerCommand; +import com.velocitypowered.proxy.protocol.packet.chat.SystemChat; import com.velocitypowered.proxy.protocol.packet.title.LegacyTitlePacket; import com.velocitypowered.proxy.protocol.packet.title.TitleActionbarPacket; import com.velocitypowered.proxy.protocol.packet.title.TitleClearPacket; @@ -114,19 +119,25 @@ public enum StateRegistry { map(0x02, MINECRAFT_1_12, false), map(0x01, MINECRAFT_1_12_1, false), map(0x05, MINECRAFT_1_13, false), - map(0x06, MINECRAFT_1_14, false)); - serverbound.register(Chat.class, Chat::new, + map(0x06, MINECRAFT_1_14, false), + map(0x08, MINECRAFT_1_19, false)); + serverbound.register(LegacyChat.class, LegacyChat::new, map(0x01, MINECRAFT_1_7_2, false), map(0x02, MINECRAFT_1_9, false), map(0x03, MINECRAFT_1_12, false), map(0x02, MINECRAFT_1_12_1, false), - map(0x03, MINECRAFT_1_14, false)); + map(0x03, MINECRAFT_1_14, MINECRAFT_1_18_2, false)); + serverbound.register(PlayerCommand.class, PlayerCommand::new, + map(0x03, MINECRAFT_1_19, false)); + serverbound.register(PlayerChat.class, PlayerChat::new, + map(0x04, MINECRAFT_1_19, false)); serverbound.register(ClientSettings.class, ClientSettings::new, map(0x15, MINECRAFT_1_7_2, false), map(0x04, MINECRAFT_1_9, false), map(0x05, MINECRAFT_1_12, false), map(0x04, MINECRAFT_1_12_1, false), - map(0x05, MINECRAFT_1_14, false)); + map(0x05, MINECRAFT_1_14, false), + map(0x07, MINECRAFT_1_19, false)); serverbound.register(PluginMessage.class, PluginMessage::new, map(0x17, MINECRAFT_1_7_2, false), map(0x09, MINECRAFT_1_9, false), @@ -134,7 +145,8 @@ public enum StateRegistry { map(0x09, MINECRAFT_1_12_1, false), map(0x0A, MINECRAFT_1_13, false), map(0x0B, MINECRAFT_1_14, false), - map(0x0A, MINECRAFT_1_17, false)); + map(0x0A, MINECRAFT_1_17, false), + map(0x0C, MINECRAFT_1_19, false)); serverbound.register(KeepAlive.class, KeepAlive::new, map(0x00, MINECRAFT_1_7_2, false), map(0x0B, MINECRAFT_1_9, false), @@ -143,7 +155,8 @@ public enum StateRegistry { map(0x0E, MINECRAFT_1_13, false), map(0x0F, MINECRAFT_1_14, false), map(0x10, MINECRAFT_1_16, false), - map(0x0F, MINECRAFT_1_17, false)); + map(0x0F, MINECRAFT_1_17, false), + map(0x11, MINECRAFT_1_19, false)); serverbound.register(ResourcePackResponse.class, ResourcePackResponse::new, map(0x19, MINECRAFT_1_8, false), map(0x16, MINECRAFT_1_9, false), @@ -151,20 +164,22 @@ public enum StateRegistry { map(0x1D, MINECRAFT_1_13, false), map(0x1F, MINECRAFT_1_14, false), map(0x20, MINECRAFT_1_16, false), - map(0x21, MINECRAFT_1_16_2, false)); + map(0x21, MINECRAFT_1_16_2, false), + map(0x23, MINECRAFT_1_19, false)); clientbound.register(BossBar.class, BossBar::new, map(0x0C, MINECRAFT_1_9, false), map(0x0D, MINECRAFT_1_15, false), map(0x0C, MINECRAFT_1_16, false), - map(0x0D, MINECRAFT_1_17, false)); - clientbound.register(Chat.class, Chat::new, + map(0x0D, MINECRAFT_1_17, false), + map(0x0A, MINECRAFT_1_19, false)); + clientbound.register(LegacyChat.class, LegacyChat::new, map(0x02, MINECRAFT_1_7_2, true), map(0x0F, MINECRAFT_1_9, true), map(0x0E, MINECRAFT_1_13, true), map(0x0F, MINECRAFT_1_15, true), map(0x0E, MINECRAFT_1_16, true), - map(0x0F, MINECRAFT_1_17, true)); + map(0x0F, MINECRAFT_1_17, MINECRAFT_1_18_2, true)); clientbound.register(TabCompleteResponse.class, TabCompleteResponse::new, map(0x3A, MINECRAFT_1_7_2, false), map(0x0E, MINECRAFT_1_9, false), @@ -172,13 +187,15 @@ public enum StateRegistry { map(0x11, MINECRAFT_1_15, false), map(0x10, MINECRAFT_1_16, false), map(0x0F, MINECRAFT_1_16_2, false), - map(0x11, MINECRAFT_1_17, false)); + map(0x11, MINECRAFT_1_17, false), + map(0x0E, MINECRAFT_1_19, false)); clientbound.register(AvailableCommands.class, AvailableCommands::new, map(0x11, MINECRAFT_1_13, false), map(0x12, MINECRAFT_1_15, false), map(0x11, MINECRAFT_1_16, false), map(0x10, MINECRAFT_1_16_2, false), - map(0x12, MINECRAFT_1_17, false)); + map(0x12, MINECRAFT_1_17, false), + map(0x0F, MINECRAFT_1_19, false)); clientbound.register(PluginMessage.class, PluginMessage::new, map(0x3F, MINECRAFT_1_7_2, false), map(0x18, MINECRAFT_1_9, false), @@ -187,7 +204,8 @@ public enum StateRegistry { map(0x19, MINECRAFT_1_15, false), map(0x18, MINECRAFT_1_16, false), map(0x17, MINECRAFT_1_16_2, false), - map(0x18, MINECRAFT_1_17, false)); + map(0x18, MINECRAFT_1_17, false), + map(0x15, MINECRAFT_1_19, false)); clientbound.register(Disconnect.class, Disconnect::new, map(0x40, MINECRAFT_1_7_2, false), map(0x1A, MINECRAFT_1_9, false), @@ -196,7 +214,8 @@ public enum StateRegistry { map(0x1B, MINECRAFT_1_15, false), map(0x1A, MINECRAFT_1_16, false), map(0x19, MINECRAFT_1_16_2, false), - map(0x1A, MINECRAFT_1_17, false)); + map(0x1A, MINECRAFT_1_17, false), + map(0x17, MINECRAFT_1_19, false)); clientbound.register(KeepAlive.class, KeepAlive::new, map(0x00, MINECRAFT_1_7_2, false), map(0x1F, MINECRAFT_1_9, false), @@ -205,7 +224,8 @@ public enum StateRegistry { map(0x21, MINECRAFT_1_15, false), map(0x20, MINECRAFT_1_16, false), map(0x1F, MINECRAFT_1_16_2, false), - map(0x21, MINECRAFT_1_17, false)); + map(0x21, MINECRAFT_1_17, false), + map(0x1E, MINECRAFT_1_19, false)); clientbound.register(JoinGame.class, JoinGame::new, map(0x01, MINECRAFT_1_7_2, false), map(0x23, MINECRAFT_1_9, false), @@ -214,7 +234,8 @@ public enum StateRegistry { map(0x26, MINECRAFT_1_15, false), map(0x25, MINECRAFT_1_16, false), map(0x24, MINECRAFT_1_16_2, false), - map(0x26, MINECRAFT_1_17, false)); + map(0x26, MINECRAFT_1_17, false), + map(0x23, MINECRAFT_1_19, false)); clientbound.register(Respawn.class, Respawn::new, map(0x07, MINECRAFT_1_7_2, true), map(0x33, MINECRAFT_1_9, true), @@ -225,7 +246,8 @@ public enum StateRegistry { map(0x3B, MINECRAFT_1_15, true), map(0x3A, MINECRAFT_1_16, true), map(0x39, MINECRAFT_1_16_2, true), - map(0x3D, MINECRAFT_1_17, true)); + map(0x3D, MINECRAFT_1_17, true), + map(0x3B, MINECRAFT_1_19, true)); clientbound.register(ResourcePackRequest.class, ResourcePackRequest::new, map(0x48, MINECRAFT_1_8, false), map(0x32, MINECRAFT_1_9, false), @@ -236,7 +258,8 @@ public enum StateRegistry { map(0x3A, MINECRAFT_1_15, false), map(0x39, MINECRAFT_1_16, false), map(0x38, MINECRAFT_1_16_2, false), - map(0x3C, MINECRAFT_1_17, false)); + map(0x3C, MINECRAFT_1_17, false), + map(0x3A, MINECRAFT_1_19, false)); clientbound.register(HeaderAndFooter.class, HeaderAndFooter::new, map(0x47, MINECRAFT_1_8, true), map(0x48, MINECRAFT_1_9, true), @@ -248,7 +271,8 @@ public enum StateRegistry { map(0x54, MINECRAFT_1_15, true), map(0x53, MINECRAFT_1_16, true), map(0x5E, MINECRAFT_1_17, true), - map(0x5F, MINECRAFT_1_18, true)); + map(0x5F, MINECRAFT_1_18, true), + map(0x60, MINECRAFT_1_19, true)); clientbound.register(LegacyTitlePacket.class, LegacyTitlePacket::new, map(0x45, MINECRAFT_1_8, true), map(0x45, MINECRAFT_1_9, true), @@ -265,12 +289,14 @@ public enum StateRegistry { map(0x59, MINECRAFT_1_17, true), map(0x5A, MINECRAFT_1_18, true)); clientbound.register(TitleActionbarPacket.class, TitleActionbarPacket::new, - map(0x41, MINECRAFT_1_17, true)); + map(0x41, MINECRAFT_1_17, true), + map(0x40, MINECRAFT_1_19, true)); clientbound.register(TitleTimesPacket.class, TitleTimesPacket::new, map(0x5A, MINECRAFT_1_17, true), map(0x5B, MINECRAFT_1_18, true)); clientbound.register(TitleClearPacket.class, TitleClearPacket::new, - map(0x10, MINECRAFT_1_17, true)); + map(0x10, MINECRAFT_1_17, true), + map(0x0D, MINECRAFT_1_19, true)); clientbound.register(PlayerListItem.class, PlayerListItem::new, map(0x38, MINECRAFT_1_7_2, false), map(0x2D, MINECRAFT_1_9, false), @@ -280,7 +306,10 @@ public enum StateRegistry { map(0x34, MINECRAFT_1_15, false), map(0x33, MINECRAFT_1_16, false), map(0x32, MINECRAFT_1_16_2, false), - map(0x36, MINECRAFT_1_17, false)); + map(0x36, MINECRAFT_1_17, false), + map(0x34, MINECRAFT_1_19, false)); + clientbound.register(SystemChat.class, SystemChat::new, + map(0x5F, MINECRAFT_1_19, true)); } }, LOGIN { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/AvailableCommands.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/AvailableCommands.java index 39cfbdec3..c349dc65a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/AvailableCommands.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/AvailableCommands.java @@ -83,7 +83,7 @@ public class AvailableCommands implements MinecraftPacket { int commands = ProtocolUtils.readVarInt(buf); WireNode[] wireNodes = new WireNode[commands]; for (int i = 0; i < commands; i++) { - wireNodes[i] = deserializeNode(buf, i); + wireNodes[i] = deserializeNode(buf, i, protocolVersion); } // Iterate over the deserialized nodes and attempt to form a graph. We also resolve any cycles @@ -130,13 +130,13 @@ public class AvailableCommands implements MinecraftPacket { // Now serialize the children. ProtocolUtils.writeVarInt(buf, idMappings.size()); for (CommandNode child : idMappings.keySet()) { - serializeNode(child, buf, idMappings); + serializeNode(child, buf, idMappings, protocolVersion); } ProtocolUtils.writeVarInt(buf, idMappings.getInt(rootNode)); } private static void serializeNode(CommandNode node, ByteBuf buf, - Object2IntMap> idMappings) { + Object2IntMap> idMappings, ProtocolVersion protocolVersion) { byte flags = 0; if (node.getRedirect() != null) { flags |= FLAG_IS_REDIRECT; @@ -168,7 +168,7 @@ public class AvailableCommands implements MinecraftPacket { if (node instanceof ArgumentCommandNode) { ProtocolUtils.writeString(buf, node.getName()); ArgumentPropertyRegistry.serialize(buf, - ((ArgumentCommandNode) node).getType()); + ((ArgumentCommandNode) node).getType(), protocolVersion); if (((ArgumentCommandNode) node).getCustomSuggestions() != null) { SuggestionProvider provider = ((ArgumentCommandNode) node) @@ -189,7 +189,7 @@ public class AvailableCommands implements MinecraftPacket { return handler.handle(this); } - private static WireNode deserializeNode(ByteBuf buf, int idx) { + private static WireNode deserializeNode(ByteBuf buf, int idx, ProtocolVersion version) { byte flags = buf.readByte(); int[] children = ProtocolUtils.readIntegerArray(buf); int redirectTo = -1; @@ -205,14 +205,13 @@ public class AvailableCommands implements MinecraftPacket { .literal(ProtocolUtils.readString(buf))); case NODE_TYPE_ARGUMENT: String name = ProtocolUtils.readString(buf); - ArgumentType argumentType = ArgumentPropertyRegistry.deserialize(buf); + ArgumentType argumentType = ArgumentPropertyRegistry.deserialize(buf, version); RequiredArgumentBuilder argumentBuilder = RequiredArgumentBuilder .argument(name, argumentType); if ((flags & FLAG_HAS_SUGGESTIONS) != 0) { argumentBuilder.suggests(new ProtocolSuggestionProvider(ProtocolUtils.readString(buf))); } - return new WireNode(idx, flags, children, redirectTo, argumentBuilder); default: throw new IllegalArgumentException("Unknown node type " + (flags & FLAG_NODE_TYPE)); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/EncryptionResponse.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/EncryptionResponse.java index 905ea05ae..7a19b4962 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/EncryptionResponse.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/EncryptionResponse.java @@ -24,13 +24,19 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; +import com.velocitypowered.proxy.util.except.QuietDecoderException; import io.netty.buffer.ByteBuf; +import org.checkerframework.checker.nullness.qual.Nullable; + import java.util.Arrays; public class EncryptionResponse implements MinecraftPacket { + private final static QuietDecoderException NO_SALT = new QuietDecoderException("Encryption response didn't contain salt"); + private byte[] sharedSecret = EMPTY_BYTE_ARRAY; private byte[] verifyToken = EMPTY_BYTE_ARRAY; + private @Nullable Long salt; public byte[] getSharedSecret() { return sharedSecret.clone(); @@ -40,6 +46,13 @@ public class EncryptionResponse implements MinecraftPacket { return verifyToken.clone(); } + public long getSalt() { + if (salt == null) { + throw NO_SALT; + } + return salt; + } + @Override public String toString() { return "EncryptionResponse{" @@ -52,7 +65,14 @@ public class EncryptionResponse implements MinecraftPacket { public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { if (version.compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { this.sharedSecret = ProtocolUtils.readByteArray(buf, 128); - this.verifyToken = ProtocolUtils.readByteArray(buf, 128); + + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0 + && !buf.readBoolean()) { + salt = buf.readLong(); + } + + this.verifyToken = ProtocolUtils.readByteArray(buf, + version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0 ? 256 : 128); } else { this.sharedSecret = ProtocolUtils.readByteArray17(buf); this.verifyToken = ProtocolUtils.readByteArray17(buf); @@ -63,6 +83,14 @@ public class EncryptionResponse implements MinecraftPacket { public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { if (version.compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { ProtocolUtils.writeByteArray(buf, sharedSecret); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { + if (salt != null) { + buf.writeBoolean(false); + buf.writeLong(salt); + } else { + buf.writeBoolean(true); + } + } ProtocolUtils.writeByteArray(buf, verifyToken); } else { ProtocolUtils.writeByteArray17(sharedSecret, buf, false); @@ -79,11 +107,22 @@ public class EncryptionResponse implements MinecraftPacket { public int expectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) { // It turns out these come out to the same length, whether we're talking >=1.8 or not. // The length prefix always winds up being 2 bytes. - return 260; + int base = 256 + 2 + 2; + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { + // Verify token is twice as long on 1.19+ + // Additional 1 byte for left <> right and 8 bytes for salt + base += 128 + 8 + 1; + } + return base; } @Override public int expectedMinLength(ByteBuf buf, Direction direction, ProtocolVersion version) { - return expectedMaxLength(buf, direction, version); + int base = expectedMaxLength(buf, direction, version); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { + // These are "optional" + base -= 128 + 8; + } + return base; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java index 74e2956c0..da86363b7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java @@ -25,6 +25,7 @@ import com.velocitypowered.proxy.connection.registry.DimensionInfo; import com.velocitypowered.proxy.connection.registry.DimensionRegistry; import com.velocitypowered.proxy.protocol.*; import io.netty.buffer.ByteBuf; +import it.unimi.dsi.fastutil.Pair; import net.kyori.adventure.nbt.BinaryTagIO; import net.kyori.adventure.nbt.BinaryTagTypes; import net.kyori.adventure.nbt.CompoundBinaryTag; @@ -33,7 +34,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; public class JoinGame implements MinecraftPacket { - private static final BinaryTagIO.Reader JOINGAME_READER = BinaryTagIO.reader(2 * 1024 * 1024); + private static final BinaryTagIO.Reader JOINGAME_READER = BinaryTagIO.reader(4 * 1024 * 1024); private int entityId; private short gamemode; private int dimension; @@ -51,6 +52,8 @@ public class JoinGame implements MinecraftPacket { private short previousGamemode; // 1.16+ private CompoundBinaryTag biomeRegistry; // 1.16.2+ private int simulationDistance; // 1.18+ + private @Nullable Pair lastDeathPosition; + private CompoundBinaryTag chatTypeRegistry; // placeholder, 1.19+ public int getEntityId() { return entityId; @@ -172,6 +175,22 @@ public class JoinGame implements MinecraftPacket { this.simulationDistance = simulationDistance; } + public Pair getLastDeathPosition() { + return lastDeathPosition; + } + + public void setLastDeathPosition(Pair lastDeathPosition) { + this.lastDeathPosition = lastDeathPosition; + } + + public CompoundBinaryTag getChatTypeRegistry() { + return chatTypeRegistry; + } + + public void setChatTypeRegistry(CompoundBinaryTag chatTypeRegistry) { + this.chatTypeRegistry = chatTypeRegistry; + } + @Override public String toString() { return "JoinGame{" @@ -188,6 +207,7 @@ public class JoinGame implements MinecraftPacket { + ", dimensionInfo='" + dimensionInfo + '\'' + ", previousGamemode=" + previousGamemode + ", simulationDistance=" + simulationDistance + + ", lastDeathPosition='" + lastDeathPosition + '\'' + '}'; } @@ -253,6 +273,7 @@ public class JoinGame implements MinecraftPacket { dimensionRegistryContainer = registryContainer.getCompound("minecraft:dimension_type") .getList("value", BinaryTagTypes.COMPOUND); this.biomeRegistry = registryContainer.getCompound("minecraft:worldgen/biome"); + this.chatTypeRegistry = registryContainer.getCompound("minecraft:chat_type"); } else { dimensionRegistryContainer = registryContainer.getList("dimension", BinaryTagTypes.COMPOUND); @@ -263,7 +284,8 @@ public class JoinGame implements MinecraftPacket { String dimensionIdentifier; String levelName = null; - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) { + if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0 + && version.compareTo(ProtocolVersion.MINECRAFT_1_19) < 0) { CompoundBinaryTag currentDimDataTag = ProtocolUtils.readCompoundTag(buf, JOINGAME_READER); dimensionIdentifier = ProtocolUtils.readString(buf); this.currentDimensionData = DimensionData.decodeBaseCompoundTag(currentDimDataTag, version) @@ -290,6 +312,10 @@ public class JoinGame implements MinecraftPacket { boolean isDebug = buf.readBoolean(); boolean isFlat = buf.readBoolean(); this.dimensionInfo = new DimensionInfo(dimensionIdentifier, levelName, isFlat, isDebug); + // optional death location + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0 && buf.readBoolean()) { + this.lastDeathPosition = Pair.of(ProtocolUtils.readString(buf), buf.readLong()); + } } @Override @@ -356,11 +382,13 @@ public class JoinGame implements MinecraftPacket { dimensionRegistryEntry.put("value", encodedDimensionRegistry); registryContainer.put("minecraft:dimension_type", dimensionRegistryEntry.build()); registryContainer.put("minecraft:worldgen/biome", biomeRegistry); + registryContainer.put("minecraft:chat_type", chatTypeRegistry); } else { registryContainer.put("dimension", encodedDimensionRegistry); } ProtocolUtils.writeCompoundTag(buf, registryContainer.build()); - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) { + if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0 + && version.compareTo(ProtocolVersion.MINECRAFT_1_19) < 0) { ProtocolUtils.writeCompoundTag(buf, currentDimensionData.serializeDimensionDetails()); ProtocolUtils.writeString(buf, dimensionInfo.getRegistryIdentifier()); } else { @@ -382,6 +410,17 @@ public class JoinGame implements MinecraftPacket { buf.writeBoolean(showRespawnScreen); buf.writeBoolean(dimensionInfo.isDebugType()); buf.writeBoolean(dimensionInfo.isFlat()); + + // optional death location + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { + if (lastDeathPosition != null) { + buf.writeBoolean(true); + ProtocolUtils.writeString(buf, lastDeathPosition.key()); + buf.writeLong(lastDeathPosition.value()); + } else { + buf.writeBoolean(false); + } + } } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PlayerListItem.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PlayerListItem.java index 5aa667065..81ae855cc 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PlayerListItem.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PlayerListItem.java @@ -19,6 +19,7 @@ package com.velocitypowered.proxy.protocol.packet; import com.google.common.collect.ImmutableList; import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.api.proxy.player.TabListEntry; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; @@ -74,6 +75,12 @@ public class PlayerListItem implements MinecraftPacket { item.setGameMode(ProtocolUtils.readVarInt(buf)); item.setLatency(ProtocolUtils.readVarInt(buf)); item.setDisplayName(readOptionalComponent(buf, version)); + + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { + if (buf.readBoolean()) { + item.setPlayerKey(ProtocolUtils.readPlayerKey(buf)); + } + } break; case UPDATE_GAMEMODE: item.setGameMode(ProtocolUtils.readVarInt(buf)); @@ -124,8 +131,15 @@ public class PlayerListItem implements MinecraftPacket { ProtocolUtils.writeProperties(buf, item.getProperties()); ProtocolUtils.writeVarInt(buf, item.getGameMode()); ProtocolUtils.writeVarInt(buf, item.getLatency()); - writeDisplayName(buf, item.getDisplayName(), version); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { + if (item.getPlayerKey() != null) { + buf.writeBoolean(true); + ProtocolUtils.writePlayerKey(buf, item.getPlayerKey()); + } else { + buf.writeBoolean(false); + } + } break; case UPDATE_GAMEMODE: ProtocolUtils.writeVarInt(buf, item.getGameMode()); @@ -181,6 +195,7 @@ public class PlayerListItem implements MinecraftPacket { private int gameMode; private int latency; private @Nullable Component displayName; + private @Nullable IdentifiedKey playerKey; public Item() { uuid = null; @@ -196,6 +211,7 @@ public class PlayerListItem implements MinecraftPacket { .setProperties(entry.getProfile().getProperties()) .setLatency(entry.getLatency()) .setGameMode(entry.getGameMode()) + .setPlayerKey(entry.getIdentifiedKey()) .setDisplayName(entry.getDisplayNameComponent().orElse(null)); } @@ -247,5 +263,14 @@ public class PlayerListItem implements MinecraftPacket { this.displayName = displayName; return this; } + + public Item setPlayerKey(IdentifiedKey playerKey) { + this.playerKey = playerKey; + return this; + } + + public IdentifiedKey getPlayerKey() { + return playerKey; + } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Respawn.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Respawn.java index 5b94a1df2..e5db58bba 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Respawn.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Respawn.java @@ -24,8 +24,10 @@ import com.velocitypowered.proxy.connection.registry.DimensionInfo; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; +import it.unimi.dsi.fastutil.Pair; import net.kyori.adventure.nbt.BinaryTagIO; import net.kyori.adventure.nbt.CompoundBinaryTag; +import org.checkerframework.checker.nullness.qual.Nullable; public class Respawn implements MinecraftPacket { @@ -38,13 +40,14 @@ public class Respawn implements MinecraftPacket { private DimensionInfo dimensionInfo; // 1.16-1.16.1 private short previousGamemode; // 1.16+ private DimensionData currentDimensionData; // 1.16.2+ + private @Nullable Pair lastDeathPosition; // 1.19+ public Respawn() { } public Respawn(int dimension, long partialHashedSeed, short difficulty, short gamemode, String levelType, boolean shouldKeepPlayerData, DimensionInfo dimensionInfo, - short previousGamemode, DimensionData currentDimensionData) { + short previousGamemode, DimensionData currentDimensionData, @Nullable Pair lastDeathPosition) { this.dimension = dimension; this.partialHashedSeed = partialHashedSeed; this.difficulty = difficulty; @@ -54,6 +57,7 @@ public class Respawn implements MinecraftPacket { this.dimensionInfo = dimensionInfo; this.previousGamemode = previousGamemode; this.currentDimensionData = currentDimensionData; + this.lastDeathPosition = lastDeathPosition; } public int getDimension() { @@ -112,6 +116,14 @@ public class Respawn implements MinecraftPacket { this.previousGamemode = previousGamemode; } + public void setLastDeathPosition(Pair lastDeathPosition) { + this.lastDeathPosition = lastDeathPosition; + } + + public Pair getLastDeathPosition() { + return lastDeathPosition; + } + @Override public String toString() { return "Respawn{" @@ -133,7 +145,8 @@ public class Respawn implements MinecraftPacket { String dimensionIdentifier = null; String levelName = null; if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) { + if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0 + && version.compareTo(ProtocolVersion.MINECRAFT_1_19) < 0) { CompoundBinaryTag dimDataTag = ProtocolUtils.readCompoundTag(buf, BinaryTagIO.reader()); dimensionIdentifier = ProtocolUtils.readString(buf); this.currentDimensionData = DimensionData.decodeBaseCompoundTag(dimDataTag, version) @@ -161,12 +174,16 @@ public class Respawn implements MinecraftPacket { } else { this.levelType = ProtocolUtils.readString(buf, 16); } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0 && buf.readBoolean()) { + this.lastDeathPosition = Pair.of(ProtocolUtils.readString(buf), buf.readLong()); + } } @Override public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) { + if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0 + && version.compareTo(ProtocolVersion.MINECRAFT_1_19) < 0) { ProtocolUtils.writeCompoundTag(buf, currentDimensionData.serializeDimensionDetails()); ProtocolUtils.writeString(buf, dimensionInfo.getRegistryIdentifier()); } else { @@ -191,6 +208,17 @@ public class Respawn implements MinecraftPacket { } else { ProtocolUtils.writeString(buf, levelType); } + + // optional death location + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { + if (lastDeathPosition != null) { + buf.writeBoolean(true); + ProtocolUtils.writeString(buf, lastDeathPosition.key()); + buf.writeLong(lastDeathPosition.value()); + } else { + buf.writeBoolean(false); + } + } } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java index 57959c370..9b0c0f795 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java @@ -19,6 +19,7 @@ package com.velocitypowered.proxy.protocol.packet; import com.google.common.base.Preconditions; import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; @@ -32,11 +33,12 @@ public class ServerLogin implements MinecraftPacket { private static final QuietDecoderException EMPTY_USERNAME = new QuietDecoderException("Empty username!"); private @Nullable String username; + private @Nullable IdentifiedKey playerKey; // Introduced in 1.19 public ServerLogin() { } - public ServerLogin(String username) { + public ServerLogin(String username, @Nullable IdentifiedKey playerKey) { this.username = Preconditions.checkNotNull(username, "username"); } @@ -47,10 +49,19 @@ public class ServerLogin implements MinecraftPacket { return username; } + public IdentifiedKey getPlayerKey() { + return playerKey; + } + + public void setPlayerKey(IdentifiedKey playerKey) { + this.playerKey = playerKey; + } + @Override public String toString() { return "ServerLogin{" + "username='" + username + '\'' + + "playerKey='" + playerKey + '\'' + '}'; } @@ -60,6 +71,12 @@ public class ServerLogin implements MinecraftPacket { if (username.isEmpty()) { throw EMPTY_USERNAME; } + + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { + if (buf.readBoolean()) { + playerKey = ProtocolUtils.readPlayerKey(buf); + } + } } @Override @@ -68,13 +85,27 @@ public class ServerLogin implements MinecraftPacket { throw new IllegalStateException("No username found!"); } ProtocolUtils.writeString(buf, username); + + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { + if (playerKey != null) { + buf.writeBoolean(true); + ProtocolUtils.writePlayerKey(buf, playerKey); + } else { + buf.writeBoolean(false); + } + } } @Override public int expectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) { // Accommodate the rare (but likely malicious) use of UTF-8 usernames, since it is technically // legal on the protocol level. - return 1 + (16 * 4); + int base = 1 + (16 * 4); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { + return -1; + //TODO: ## 19 + } + return base; } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLoginSuccess.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLoginSuccess.java index d0f415694..c7789ee35 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLoginSuccess.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLoginSuccess.java @@ -18,11 +18,14 @@ package com.velocitypowered.proxy.protocol.packet; import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.api.util.UuidUtils; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; + +import java.util.List; import java.util.UUID; import org.checkerframework.checker.nullness.qual.Nullable; @@ -30,6 +33,7 @@ public class ServerLoginSuccess implements MinecraftPacket { private @Nullable UUID uuid; private @Nullable String username; + private @Nullable List properties; public UUID getUuid() { if (uuid == null) { @@ -53,17 +57,28 @@ public class ServerLoginSuccess implements MinecraftPacket { this.username = username; } + public List getProperties() { + return properties; + } + + public void setProperties(List properties) { + this.properties = properties; + } + @Override public String toString() { return "ServerLoginSuccess{" + "uuid=" + uuid + ", username='" + username + '\'' + + ", properties='" + properties + '\'' + '}'; } @Override public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { + uuid = ProtocolUtils.readUuid(buf); + } else if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { uuid = ProtocolUtils.readUuidIntArray(buf); } else if (version.compareTo(ProtocolVersion.MINECRAFT_1_7_6) >= 0) { uuid = UUID.fromString(ProtocolUtils.readString(buf, 36)); @@ -71,6 +86,10 @@ public class ServerLoginSuccess implements MinecraftPacket { uuid = UuidUtils.fromUndashed(ProtocolUtils.readString(buf, 32)); } username = ProtocolUtils.readString(buf, 16); + + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { + properties = ProtocolUtils.readProperties(buf); + } } @Override @@ -78,7 +97,9 @@ public class ServerLoginSuccess implements MinecraftPacket { if (uuid == null) { throw new IllegalStateException("No UUID specified!"); } - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { + ProtocolUtils.writeUuid(buf, uuid); + } else if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { ProtocolUtils.writeUuidIntArray(buf, uuid); } else if (version.compareTo(ProtocolVersion.MINECRAFT_1_7_6) >= 0) { ProtocolUtils.writeString(buf, uuid.toString()); @@ -89,6 +110,14 @@ public class ServerLoginSuccess implements MinecraftPacket { throw new IllegalStateException("No username specified!"); } ProtocolUtils.writeString(buf, username); + + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { + if (properties == null) { + ProtocolUtils.writeVarInt(buf, 0); + } else { + ProtocolUtils.writeProperties(buf, properties); + } + } } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentIdentifier.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentIdentifier.java new file mode 100644 index 000000000..78277f87b --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentIdentifier.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2018 Velocity 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 . + */ + +package com.velocitypowered.proxy.protocol.packet.brigadier; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.velocitypowered.api.network.ProtocolVersion; + +import java.util.HashMap; +import java.util.Map; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class ArgumentIdentifier { + + private final String identifier; + private final Map versionById; + + private ArgumentIdentifier(String identifier, VersionSet... versions) { + this.identifier = Preconditions.checkNotNull(identifier); + + Preconditions.checkNotNull(versions); + + Map temp = new HashMap<>(); + + ProtocolVersion previous = null; + for (int i = 0; i < versions.length; i++) { + VersionSet current = Preconditions.checkNotNull(versions[i]); + + Preconditions.checkArgument(current.getVersion().compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0, + "Version too old for ID index"); + Preconditions.checkArgument(previous == null || previous.compareTo(current.getVersion()) > 0, + "Invalid protocol version order"); + + for (ProtocolVersion v : ProtocolVersion.values()) { + if (v.compareTo(current.getVersion()) >= 0) { + temp.put(v, current.getId()); + } + } + previous = current.getVersion(); + + } + + this.versionById = ImmutableMap.copyOf(temp); + } + + public String getIdentifier() { + return identifier; + } + + public @Nullable Integer getIdByProtocolVersion(ProtocolVersion version) { + return versionById.get(Preconditions.checkNotNull(version)); + } + + public static VersionSet mapSet(ProtocolVersion version, int id) { + return new VersionSet(version, id); + } + + public static ArgumentIdentifier id(String identifier, VersionSet... versions) { + return new ArgumentIdentifier(identifier, versions); + } + + /** + * This class is purely for convenience. + */ + public static class VersionSet { + private final ProtocolVersion version; + private final int id; + + private VersionSet(ProtocolVersion version, int id) { + this.version = Preconditions.checkNotNull(version); + this.id = id; + } + + public int getId() { + return id; + } + + public ProtocolVersion getVersion() { + return version; + } + + } + +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertyRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertyRegistry.java index 1f5aa3678..ddd59c78e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertyRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertyRegistry.java @@ -17,6 +17,9 @@ package com.velocitypowered.proxy.protocol.packet.brigadier; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19; +import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.id; +import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.mapSet; import static com.velocitypowered.proxy.protocol.packet.brigadier.DoubleArgumentPropertySerializer.DOUBLE; import static com.velocitypowered.proxy.protocol.packet.brigadier.EmptyArgumentPropertySerializer.EMPTY; import static com.velocitypowered.proxy.protocol.packet.brigadier.FloatArgumentPropertySerializer.FLOAT; @@ -25,6 +28,7 @@ import static com.velocitypowered.proxy.protocol.packet.brigadier.LongArgumentPr import static com.velocitypowered.proxy.protocol.packet.brigadier.ModArgumentPropertySerializer.MOD; import static com.velocitypowered.proxy.protocol.packet.brigadier.StringArgumentPropertySerializer.STRING; +import com.google.common.base.Preconditions; import com.mojang.brigadier.arguments.ArgumentType; import com.mojang.brigadier.arguments.BoolArgumentType; import com.mojang.brigadier.arguments.DoubleArgumentType; @@ -32,35 +36,38 @@ import com.mojang.brigadier.arguments.FloatArgumentType; import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.arguments.LongArgumentType; import com.mojang.brigadier.arguments.StringArgumentType; +import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; import java.util.HashMap; import java.util.Map; -import org.checkerframework.checker.nullness.qual.Nullable; public class ArgumentPropertyRegistry { private ArgumentPropertyRegistry() { throw new AssertionError(); } - private static final Map> byId = new HashMap<>(); + private static final Map> byIdentifier = + new HashMap<>(); private static final Map, ArgumentPropertySerializer> byClass = new HashMap<>(); - private static final Map, String> classToId = new HashMap<>(); + private static final Map, ArgumentIdentifier> classToId = + new HashMap<>(); - private static > void register(String identifier, Class klazz, - ArgumentPropertySerializer serializer) { - byId.put(identifier, serializer); + private static > void register(ArgumentIdentifier identifier, + Class klazz, ArgumentPropertySerializer serializer) { + byIdentifier.put(identifier, serializer); byClass.put(klazz, serializer); classToId.put(klazz, identifier); } - private static void empty(String identifier) { + private static void empty(ArgumentIdentifier identifier) { empty(identifier, EMPTY); } - private static void empty(String identifier, ArgumentPropertySerializer serializer) { - byId.put(identifier, serializer); + private static void empty(ArgumentIdentifier identifier, + ArgumentPropertySerializer serializer) { + byIdentifier.put(identifier, serializer); } /** @@ -68,13 +75,14 @@ public class ArgumentPropertyRegistry { * @param buf the buffer to deserialize * @return the deserialized {@link ArgumentType} */ - public static ArgumentType deserialize(ByteBuf buf) { - String identifier = ProtocolUtils.readString(buf); - ArgumentPropertySerializer serializer = byId.get(identifier); + public static ArgumentType deserialize(ByteBuf buf, ProtocolVersion protocolVersion) { + ArgumentIdentifier identifier = readIdentifier(buf, protocolVersion); + + ArgumentPropertySerializer serializer = byIdentifier.get(identifier); if (serializer == null) { throw new IllegalArgumentException("Argument type identifier " + identifier + " unknown."); } - Object result = serializer.deserialize(buf); + Object result = serializer.deserialize(buf, protocolVersion); if (result instanceof ArgumentType) { return (ArgumentType) result; @@ -88,95 +96,144 @@ public class ArgumentPropertyRegistry { * @param buf the buffer to serialize into * @param type the type to serialize */ - public static void serialize(ByteBuf buf, ArgumentType type) { + public static void serialize(ByteBuf buf, ArgumentType type, + ProtocolVersion protocolVersion) { if (type instanceof PassthroughProperty) { PassthroughProperty property = (PassthroughProperty) type; - ProtocolUtils.writeString(buf, property.getIdentifier()); + writeIdentifier(buf, property.getIdentifier(), protocolVersion); if (property.getResult() != null) { - property.getSerializer().serialize(property.getResult(), buf); + property.getSerializer().serialize(property.getResult(), buf, protocolVersion); } } else if (type instanceof ModArgumentProperty) { ModArgumentProperty property = (ModArgumentProperty) type; - ProtocolUtils.writeString(buf, property.getIdentifier()); + writeIdentifier(buf, property.getIdentifier(), protocolVersion); buf.writeBytes(property.getData()); } else { ArgumentPropertySerializer serializer = byClass.get(type.getClass()); - String id = classToId.get(type.getClass()); + ArgumentIdentifier id = classToId.get(type.getClass()); if (serializer == null || id == null) { throw new IllegalArgumentException("Don't know how to serialize " + type.getClass().getName()); } - ProtocolUtils.writeString(buf, id); - serializer.serialize(type, buf); + writeIdentifier(buf, id, protocolVersion); + serializer.serialize(type, buf, protocolVersion); } } + /** + * Writes the {@link ArgumentIdentifier} to a version-specific buffer. + * @param buf the buffer to write to + * @param identifier the identifier to write + * @param protocolVersion the protocol version to use + */ + public static void writeIdentifier(ByteBuf buf, ArgumentIdentifier identifier, + ProtocolVersion protocolVersion) { + if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) { + Integer id = identifier.getIdByProtocolVersion(protocolVersion); + Preconditions.checkNotNull(id, "Don't know how to serialize type " + identifier); + + ProtocolUtils.writeVarInt(buf, id); + } else { + ProtocolUtils.writeString(buf, identifier.getIdentifier()); + } + + } + + /** + * Reads the {@link ArgumentIdentifier} from a version-specific buffer. + * @param buf the buffer to write to + * @param protocolVersion the protocol version to use + * @return the identifier read from the buffer + */ + public static ArgumentIdentifier readIdentifier(ByteBuf buf, ProtocolVersion protocolVersion) { + if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) { + int id = ProtocolUtils.readVarInt(buf); + for (ArgumentIdentifier i : byIdentifier.keySet()) { + Integer v = i.getIdByProtocolVersion(protocolVersion); + if (v != null && v == id) { + return i; + } + } + } else { + String identifier = ProtocolUtils.readString(buf); + for (ArgumentIdentifier i : byIdentifier.keySet()) { + if (i.getIdentifier().equals(identifier)) { + return i; + } + } + } + return null; + } + static { // Base Brigadier argument types - register("brigadier:string", StringArgumentType.class, STRING); - register("brigadier:integer", IntegerArgumentType.class, INTEGER); - register("brigadier:float", FloatArgumentType.class, FLOAT); - register("brigadier:double", DoubleArgumentType.class, DOUBLE); - register("brigadier:bool", BoolArgumentType.class, + register(id("brigadier:bool", mapSet(MINECRAFT_1_19, 0)), BoolArgumentType.class, new ArgumentPropertySerializer<>() { @Override - public BoolArgumentType deserialize(ByteBuf buf) { + public BoolArgumentType deserialize(ByteBuf buf, ProtocolVersion protocolVersion) { return BoolArgumentType.bool(); } @Override - public void serialize(BoolArgumentType object, ByteBuf buf) { + public void serialize(BoolArgumentType object, ByteBuf buf, + ProtocolVersion protocolVersion) { } }); - register("brigadier:long", LongArgumentType.class, LONG); - register("minecraft:resource", RegistryKeyArgument.class, RegistryKeyArgumentSerializer.REGISTRY); - register("minecraft:resource_or_tag", RegistryKeyArgument.class, RegistryKeyArgumentSerializer.REGISTRY); + register(id("brigadier:float", mapSet(MINECRAFT_1_19, 1)), FloatArgumentType.class, FLOAT); + register(id("brigadier:double", mapSet(MINECRAFT_1_19, 2)), DoubleArgumentType.class, DOUBLE); + register(id("brigadier:integer", mapSet(MINECRAFT_1_19, 3)), IntegerArgumentType.class, INTEGER); + register(id("brigadier:long", mapSet(MINECRAFT_1_19, 4)), LongArgumentType.class, LONG); + register(id("brigadier:string", mapSet(MINECRAFT_1_19, 5)), StringArgumentType.class, STRING); + + empty(id("minecraft:entity", mapSet(MINECRAFT_1_19, 6)), ByteArgumentPropertySerializer.BYTE); + empty(id("minecraft:game_profile", mapSet(MINECRAFT_1_19, 7))); + empty(id("minecraft:block_pos", mapSet(MINECRAFT_1_19, 8))); + empty(id("minecraft:column_pos", mapSet(MINECRAFT_1_19, 9))); + empty(id("minecraft:vec3", mapSet(MINECRAFT_1_19, 10))); + empty(id("minecraft:vec2", mapSet(MINECRAFT_1_19, 11))); + empty(id("minecraft:block_state", mapSet(MINECRAFT_1_19, 12))); + empty(id("minecraft:block_predicate", mapSet(MINECRAFT_1_19, 13))); + empty(id("minecraft:item_stack", mapSet(MINECRAFT_1_19, 14))); + empty(id("minecraft:item_predicate", mapSet(MINECRAFT_1_19, 15))); + empty(id("minecraft:color", mapSet(MINECRAFT_1_19, 16))); + empty(id("minecraft:component", mapSet(MINECRAFT_1_19, 17))); + empty(id("minecraft:message", mapSet(MINECRAFT_1_19, 18))); + empty(id("minecraft:nbt_compound_tag", mapSet(MINECRAFT_1_19, 19))); // added in 1.14 + empty(id("minecraft:nbt_tag", mapSet(MINECRAFT_1_19, 20))); // added in 1.14 + empty(id("minecraft:nbt_path", mapSet(MINECRAFT_1_19, 21))); + empty(id("minecraft:objective", mapSet(MINECRAFT_1_19, 22))); + empty(id("minecraft:objective_criteria", mapSet(MINECRAFT_1_19, 23))); + empty(id("minecraft:operation", mapSet(MINECRAFT_1_19, 24))); + empty(id("minecraft:particle", mapSet(MINECRAFT_1_19, 25))); + empty(id("minecraft:angle", mapSet(MINECRAFT_1_19, 26))); // added in 1.16.2 + empty(id("minecraft:rotation", mapSet(MINECRAFT_1_19, 27))); + empty(id("minecraft:scoreboard_slot", mapSet(MINECRAFT_1_19, 28))); + empty(id("minecraft:score_holder", mapSet(MINECRAFT_1_19, 29)), ByteArgumentPropertySerializer.BYTE); + empty(id("minecraft:swizzle", mapSet(MINECRAFT_1_19, 30))); + empty(id("minecraft:team", mapSet(MINECRAFT_1_19, 31))); + empty(id("minecraft:item_slot", mapSet(MINECRAFT_1_19, 32))); + empty(id("minecraft:resource_location", mapSet(MINECRAFT_1_19, 33))); + empty(id("minecraft:mob_effect", mapSet(MINECRAFT_1_19, 34))); + empty(id("minecraft:function", mapSet(MINECRAFT_1_19, 35))); + empty(id("minecraft:entity_anchor", mapSet(MINECRAFT_1_19, 36))); + empty(id("minecraft:int_range", mapSet(MINECRAFT_1_19, 37))); + empty(id("minecraft:float_range", mapSet(MINECRAFT_1_19, 38))); + empty(id("minecraft:item_enchantment", mapSet(MINECRAFT_1_19, 39))); + empty(id("minecraft:entity_summon", mapSet(MINECRAFT_1_19, 40))); + empty(id("minecraft:dimension", mapSet(MINECRAFT_1_19, 41))); + empty(id("minecraft:time", mapSet(MINECRAFT_1_19, 42))); // added in 1.14 + + register(id("minecraft:resource_or_tag", mapSet(MINECRAFT_1_19, 43)), + RegistryKeyArgument.class, RegistryKeyArgumentSerializer.REGISTRY); + register(id("minecraft:resource", mapSet(MINECRAFT_1_19, 44)), + RegistryKeyArgument.class, RegistryKeyArgumentSerializer.REGISTRY); + + empty(id("minecraft:uuid", mapSet(MINECRAFT_1_19, 45))); // added in 1.16 // Crossstitch support - register("crossstitch:mod_argument", ModArgumentProperty.class, MOD); + register(id("crossstitch:mod_argument", mapSet(MINECRAFT_1_19, -256)), ModArgumentProperty.class, MOD); - // Minecraft argument types with extra properties - empty("minecraft:entity", ByteArgumentPropertySerializer.BYTE); - empty("minecraft:score_holder", ByteArgumentPropertySerializer.BYTE); - - // Minecraft argument types - empty("minecraft:game_profile"); - empty("minecraft:block_pos"); - empty("minecraft:column_pos"); - empty("minecraft:vec3"); - empty("minecraft:vec2"); - empty("minecraft:block_state"); - empty("minecraft:block_predicate"); - empty("minecraft:item_stack"); - empty("minecraft:item_predicate"); - empty("minecraft:color"); - empty("minecraft:component"); - empty("minecraft:message"); - empty("minecraft:nbt"); - empty("minecraft:nbt_compound_tag"); // added in 1.14 - empty("minecraft:nbt_tag"); // added in 1.14 - empty("minecraft:nbt_path"); - empty("minecraft:objective"); - empty("minecraft:objective_criteria"); - empty("minecraft:operation"); - empty("minecraft:particle"); - empty("minecraft:rotation"); - empty("minecraft:scoreboard_slot"); - empty("minecraft:swizzle"); - empty("minecraft:team"); - empty("minecraft:item_slot"); - empty("minecraft:resource_location"); - empty("minecraft:mob_effect"); - empty("minecraft:function"); - empty("minecraft:entity_anchor"); - empty("minecraft:item_enchantment"); - empty("minecraft:entity_summon"); - empty("minecraft:dimension"); - empty("minecraft:int_range"); - empty("minecraft:float_range"); - empty("minecraft:time"); // added in 1.14 - empty("minecraft:uuid"); // added in 1.16 - empty("minecraft:angle"); // added in 1.16.2 + empty(id("minecraft:nbt")); // No longer in 1.19+ } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertySerializer.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertySerializer.java index 0dbc66dce..6b05377d6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertySerializer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertySerializer.java @@ -17,11 +17,12 @@ package com.velocitypowered.proxy.protocol.packet.brigadier; +import com.velocitypowered.api.network.ProtocolVersion; import io.netty.buffer.ByteBuf; import org.checkerframework.checker.nullness.qual.Nullable; public interface ArgumentPropertySerializer { - @Nullable T deserialize(ByteBuf buf); + @Nullable T deserialize(ByteBuf buf, ProtocolVersion protocolVersion); - void serialize(T object, ByteBuf buf); + void serialize(T object, ByteBuf buf, ProtocolVersion protocolVersion); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ByteArgumentPropertySerializer.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ByteArgumentPropertySerializer.java index a6d24c645..7385b7cf1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ByteArgumentPropertySerializer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ByteArgumentPropertySerializer.java @@ -17,6 +17,7 @@ package com.velocitypowered.proxy.protocol.packet.brigadier; +import com.velocitypowered.api.network.ProtocolVersion; import io.netty.buffer.ByteBuf; class ByteArgumentPropertySerializer implements ArgumentPropertySerializer { @@ -28,12 +29,12 @@ class ByteArgumentPropertySerializer implements ArgumentPropertySerializer } @Override - public Byte deserialize(ByteBuf buf) { + public Byte deserialize(ByteBuf buf, ProtocolVersion protocolVersion) { return buf.readByte(); } @Override - public void serialize(Byte object, ByteBuf buf) { + public void serialize(Byte object, ByteBuf buf, ProtocolVersion protocolVersion) { buf.writeByte(object); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/DoubleArgumentPropertySerializer.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/DoubleArgumentPropertySerializer.java index 2162a980e..e20b929f6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/DoubleArgumentPropertySerializer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/DoubleArgumentPropertySerializer.java @@ -22,6 +22,7 @@ import static com.velocitypowered.proxy.protocol.packet.brigadier.IntegerArgumen import static com.velocitypowered.proxy.protocol.packet.brigadier.IntegerArgumentPropertySerializer.getFlags; import com.mojang.brigadier.arguments.DoubleArgumentType; +import com.velocitypowered.api.network.ProtocolVersion; import io.netty.buffer.ByteBuf; class DoubleArgumentPropertySerializer implements ArgumentPropertySerializer { @@ -32,7 +33,7 @@ class DoubleArgumentPropertySerializer implements ArgumentPropertySerializer { @@ -33,7 +34,7 @@ class FloatArgumentPropertySerializer implements ArgumentPropertySerializer { @@ -32,7 +33,7 @@ class IntegerArgumentPropertySerializer implements ArgumentPropertySerializer { @@ -33,7 +34,7 @@ class LongArgumentPropertySerializer implements ArgumentPropertySerializer { - private final String identifier; + private final ArgumentIdentifier identifier; private final ByteBuf data; - public ModArgumentProperty(String identifier, ByteBuf data) { + public ModArgumentProperty(ArgumentIdentifier identifier, ByteBuf data) { this.identifier = identifier; this.data = Unpooled.unreleasableBuffer(data.asReadOnly()); } - public String getIdentifier() { + public ArgumentIdentifier getIdentifier() { return identifier; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ModArgumentPropertySerializer.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ModArgumentPropertySerializer.java index 58b65b98d..db4e6e31d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ModArgumentPropertySerializer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ModArgumentPropertySerializer.java @@ -17,6 +17,7 @@ package com.velocitypowered.proxy.protocol.packet.brigadier; +import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -30,14 +31,22 @@ class ModArgumentPropertySerializer implements ArgumentPropertySerializer= 0) { + int idx = ProtocolUtils.readVarInt(buf); + identifier = ArgumentIdentifier.id("crossstitch:identified_" + (idx < 0 ? "n" + (-idx) : idx), + ArgumentIdentifier.mapSet(version, idx)); + } else { + identifier = ArgumentIdentifier.id(ProtocolUtils.readString(buf)); + } + byte[] extraData = ProtocolUtils.readByteArray(buf); return new ModArgumentProperty(identifier, Unpooled.wrappedBuffer(extraData)); } @Override - public void serialize(ModArgumentProperty object, ByteBuf buf) { + public void serialize(ModArgumentProperty object, ByteBuf buf, ProtocolVersion version) { // This is special-cased by ArgumentPropertyRegistry throw new UnsupportedOperationException(); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/PassthroughProperty.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/PassthroughProperty.java index 71b91d45f..f7fc13e40 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/PassthroughProperty.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/PassthroughProperty.java @@ -23,18 +23,18 @@ import org.checkerframework.checker.nullness.qual.Nullable; class PassthroughProperty implements ArgumentType { - private final String identifier; + private final ArgumentIdentifier identifier; private final ArgumentPropertySerializer serializer; private final @Nullable T result; - PassthroughProperty(String identifier, ArgumentPropertySerializer serializer, + PassthroughProperty(ArgumentIdentifier identifier, ArgumentPropertySerializer serializer, @Nullable T result) { this.identifier = identifier; this.serializer = serializer; this.result = result; } - public String getIdentifier() { + public ArgumentIdentifier getIdentifier() { return identifier; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/RegistryKeyArgumentSerializer.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/RegistryKeyArgumentSerializer.java index e8ea99215..ff5decf4c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/RegistryKeyArgumentSerializer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/RegistryKeyArgumentSerializer.java @@ -17,6 +17,7 @@ package com.velocitypowered.proxy.protocol.packet.brigadier; +import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; @@ -25,12 +26,12 @@ public class RegistryKeyArgumentSerializer implements ArgumentPropertySerializer static final RegistryKeyArgumentSerializer REGISTRY = new RegistryKeyArgumentSerializer(); @Override - public RegistryKeyArgument deserialize(ByteBuf buf) { + public RegistryKeyArgument deserialize(ByteBuf buf, ProtocolVersion protocolVersion) { return new RegistryKeyArgument(ProtocolUtils.readString(buf)); } @Override - public void serialize(RegistryKeyArgument object, ByteBuf buf) { + public void serialize(RegistryKeyArgument object, ByteBuf buf, ProtocolVersion protocolVersion) { ProtocolUtils.writeString(buf, object.getIdentifier()); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/StringArgumentPropertySerializer.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/StringArgumentPropertySerializer.java index 790eee12d..83abb9e9b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/StringArgumentPropertySerializer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/StringArgumentPropertySerializer.java @@ -18,6 +18,7 @@ package com.velocitypowered.proxy.protocol.packet.brigadier; import com.mojang.brigadier.arguments.StringArgumentType; +import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; @@ -34,7 +35,7 @@ class StringArgumentPropertySerializer implements ArgumentPropertySerializer. + */ + +package com.velocitypowered.proxy.protocol.packet.chat; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.crypto.SignedChatCommand; +import com.velocitypowered.proxy.crypto.SignedChatMessage; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import java.time.Instant; +import java.util.UUID; +import net.kyori.adventure.identity.Identity; +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class ChatBuilder { + + private final ProtocolVersion version; + + private @MonotonicNonNull Component component; + private @MonotonicNonNull String message; + private @MonotonicNonNull SignedChatMessage signedChatMessage; + private @MonotonicNonNull SignedChatCommand signedCommand; + + private @Nullable Player sender; + private @Nullable Identity senderIdentity; + + private ChatType type = ChatType.CHAT; + + private ChatBuilder(ProtocolVersion version) { + this.version = version; + } + + public static ChatBuilder builder(ProtocolVersion version) { + return new ChatBuilder(Preconditions.checkNotNull(version)); + } + + public ChatBuilder component(Component message) { + this.component = Preconditions.checkNotNull(message); + return this; + } + + /** + * Sets the message to the provided message. + * + * @param message The message for the chat. + * @return {@code this} + */ + public ChatBuilder message(String message) { + Preconditions.checkArgument(this.message == null); + this.message = Preconditions.checkNotNull(message); + return this; + } + + /** + * Sets the signed message to the provided message. + * + * @param message The signed message for the chat. + * @return {@code this} + */ + public ChatBuilder message(SignedChatMessage message) { + Preconditions.checkNotNull(message); + Preconditions.checkArgument(this.message == null); + this.message = message.getMessage(); + this.signedChatMessage = message; + return this; + } + + /** + * Sets the signed command to the provided command. + * + * @param command The signed command for the chat. + * @return {@code this} + */ + public ChatBuilder message(SignedChatCommand command) { + Preconditions.checkNotNull(command); + Preconditions.checkArgument(this.message == null); + this.message = command.getBaseCommand(); + this.signedCommand = command; + return this; + } + + + public ChatBuilder setType(ChatType type) { + this.type = type; + return this; + } + + public ChatBuilder asPlayer(@Nullable Player player) { + this.sender = player; + return this; + } + + public ChatBuilder forIdentity(@Nullable Identity identity) { + this.senderIdentity = identity; + return this; + } + + public ChatBuilder asServer() { + this.sender = null; + return this; + } + + /** + * Creates a {@link MinecraftPacket} which can be sent to the client; using the provided information in the builder. + * + * @return The {@link MinecraftPacket} to send to the client. + */ + public MinecraftPacket toClient() { + // This is temporary + UUID identity = sender == null ? (senderIdentity == null ? Identity.nil().uuid() + : senderIdentity.uuid()) : sender.getUniqueId(); + Component msg = component == null ? Component.text(message) : component; + + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { + return new SystemChat(msg, type.getId()); + } else { + return new LegacyChat(ProtocolUtils.getJsonChatSerializer(version).serialize(msg), type.getId(), identity); + } + } + + /** + * Creates a {@link MinecraftPacket} which can be sent to the server; using the provided information in the builder. + * + * @return The {@link MinecraftPacket} to send to the server. + */ + public MinecraftPacket toServer() { + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { + if (signedChatMessage != null) { + return new PlayerChat(signedChatMessage); + } else if (signedCommand != null) { + return new PlayerCommand(signedCommand); + } else { + // Well crap + if (message.startsWith("/")) { + return new PlayerCommand(message.substring(1), ImmutableList.of(), Instant.now()); + } else { + // This will produce an error on the server, but needs to be here. + return new PlayerChat(message); + } + } + } + LegacyChat chat = new LegacyChat(); + chat.setMessage(message); + return chat; + } + + public static enum ChatType { + CHAT((byte) 0), + SYSTEM((byte) 1), + GAME_INFO((byte) 2); + + private final byte raw; + + ChatType(byte raw) { + this.raw = raw; + } + + public byte getId() { + return raw; + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Chat.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LegacyChat.java similarity index 80% rename from proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Chat.java rename to proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LegacyChat.java index 5c628337c..d16d4c5eb 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Chat.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LegacyChat.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.velocitypowered.proxy.protocol.packet; +package com.velocitypowered.proxy.protocol.packet.chat; import com.google.common.base.Preconditions; import com.velocitypowered.api.network.ProtocolVersion; @@ -23,12 +23,11 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; +import java.util.UUID; import net.kyori.adventure.identity.Identity; import org.checkerframework.checker.nullness.qual.Nullable; -import java.util.UUID; - -public class Chat implements MinecraftPacket { +public class LegacyChat implements MinecraftPacket { public static final byte CHAT_TYPE = (byte) 0; public static final byte SYSTEM_TYPE = (byte) 1; @@ -41,15 +40,21 @@ public class Chat implements MinecraftPacket { private byte type; private @Nullable UUID sender; - public Chat() { + public LegacyChat() { } - public Chat(String message, byte type, UUID sender) { + /** + * Creates a Chat packet. + */ + public LegacyChat(String message, byte type, UUID sender) { this.message = message; this.type = type; this.sender = sender; } + /** + * Retrieves the Chat message. + */ public String getMessage() { if (message == null) { throw new IllegalStateException("Message is not specified"); @@ -115,20 +120,4 @@ public class Chat implements MinecraftPacket { public boolean handle(MinecraftSessionHandler handler) { return handler.handle(this); } - - public static Chat createClientbound(Identity identity, - net.kyori.adventure.text.Component component, ProtocolVersion version) { - return createClientbound(component, CHAT_TYPE, identity.uuid(), version); - } - - public static Chat createClientbound(net.kyori.adventure.text.Component component, byte type, - UUID sender, ProtocolVersion version) { - Preconditions.checkNotNull(component, "component"); - return new Chat(ProtocolUtils.getJsonChatSerializer(version).serialize(component), type, - sender); - } - - public static Chat createServerbound(String message) { - return new Chat(message, CHAT_TYPE, EMPTY_SENDER); - } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/PlayerChat.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/PlayerChat.java new file mode 100644 index 000000000..71b922abb --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/PlayerChat.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2018 Velocity 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 . + */ + +package com.velocitypowered.proxy.protocol.packet.chat; + +import com.google.common.primitives.Longs; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.crypto.IdentifiedKey; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.crypto.EncryptionUtils; +import com.velocitypowered.proxy.crypto.SignedChatMessage; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; +import java.time.Instant; +import java.util.UUID; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class PlayerChat implements MinecraftPacket { + + private String message; + private boolean signedPreview; + private boolean unsigned = false; + private @Nullable Instant expiry; + private @Nullable byte[] signature; + private @Nullable byte[] salt; + + public PlayerChat() { + } + + public PlayerChat(String message) { + this.message = message; + this.unsigned = true; + } + + /** + * Create new {@link PlayerChat} based on a previously {@link SignedChatMessage}. + * + * @param message The {@link SignedChatMessage} to turn into {@link PlayerChat}. + */ + public PlayerChat(SignedChatMessage message) { + this.message = message.getMessage(); + this.expiry = message.getExpiryTemporal(); + this.salt = message.getSalt(); + this.signature = message.getSignature(); + this.signedPreview = message.isPreviewSigned(); + } + + public Instant getExpiry() { + return expiry; + } + + public boolean isUnsigned() { + return unsigned; + } + + public String getMessage() { + return message; + } + + public boolean isSignedPreview() { + return signedPreview; + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + message = ProtocolUtils.readString(buf, 256); + + long expiresAt = buf.readLong(); + long saltLong = buf.readLong(); + byte[] signatureBytes = ProtocolUtils.readByteArray(buf); + + if (saltLong != 0L && signatureBytes.length > 0) { + salt = Longs.toByteArray(saltLong); + signature = signatureBytes; + expiry = Instant.ofEpochMilli(expiresAt); + } else if (saltLong == 0L && signature.length == 0) { + unsigned = true; + } else { + throw EncryptionUtils.INVALID_SIGNATURE; + } + + signedPreview = buf.readBoolean(); + if (signedPreview && unsigned) { + throw EncryptionUtils.PREVIEW_SIGNATURE_MISSING; + } + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + ProtocolUtils.writeString(buf, message); + + buf.writeLong(unsigned ? Instant.now().toEpochMilli() : expiry.toEpochMilli()); + buf.writeLong(unsigned ? 0L : Longs.fromByteArray(salt)); + ProtocolUtils.writeByteArray(buf, unsigned ? EncryptionUtils.EMPTY : signature); + + buf.writeBoolean(signedPreview); + } + + /** + * Validates a signature and creates a {@link SignedChatMessage} from the given signature. + * + * @param signer the signer's information + * @param sender the sender of the message + * @param mustSign instructs the function to throw if the signature is invalid. + * @return The {@link SignedChatMessage} or null if the signature couldn't be verified. + * @throws com.velocitypowered.proxy.util.except.QuietDecoderException when mustSign is {@code true} and the signature + * is invalid. + */ + public SignedChatMessage signedContainer(IdentifiedKey signer, UUID sender, boolean mustSign) { + if (unsigned) { + if (mustSign) { + throw EncryptionUtils.INVALID_SIGNATURE; + } + return null; + } + + return new SignedChatMessage(message, signer.getSignedPublicKey(), sender, expiry, signature, salt, signedPreview); + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/PlayerChatPreview.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/PlayerChatPreview.java new file mode 100644 index 000000000..bf9079a11 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/PlayerChatPreview.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2018 Velocity 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 . + */ + +package com.velocitypowered.proxy.protocol.packet.chat; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class PlayerChatPreview implements MinecraftPacket { + + private int id; + private String query; + + public int getId() { + return id; + } + + public String getQuery() { + return query; + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + id = buf.readInt(); + query = ProtocolUtils.readString(buf, 256); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + buf.writeInt(id); + ProtocolUtils.writeString(buf, query); + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/PlayerCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/PlayerCommand.java new file mode 100644 index 000000000..580b7be3a --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/PlayerCommand.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2018 Velocity 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 . + */ + +package com.velocitypowered.proxy.protocol.packet.chat; + +import com.google.common.collect.ImmutableMap; +import com.google.common.primitives.Longs; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.crypto.IdentifiedKey; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.crypto.EncryptionUtils; +import com.velocitypowered.proxy.crypto.SignedChatCommand; +import com.velocitypowered.proxy.crypto.SignedChatMessage; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.util.except.QuietDecoderException; +import io.netty.buffer.ByteBuf; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class PlayerCommand implements MinecraftPacket { + + private static final int MAX_NUM_ARGUMENTS = 8; + private static final int MAX_LENGTH_ARGUMENTS = 16; + private static final QuietDecoderException LIMITS_VIOLATION = + new QuietDecoderException("Command arguments incorrect size"); + + private boolean unsigned = false; + private String command; + private Instant timestamp; + private long salt; + private boolean signedPreview; // Good god. Please no. + private Map arguments = ImmutableMap.of(); + + public boolean isSignedPreview() { + return signedPreview; + } + + public Instant getTimestamp() { + return timestamp; + } + + public boolean isUnsigned() { + return unsigned; + } + + public String getCommand() { + return command; + } + + public PlayerCommand() { + } + + /** + * Creates an {@link PlayerCommand} packet based on a command and list of arguments. + * + * @param command the command to run + * @param arguments the arguments of the command + * @param timestamp the timestamp of the command execution + */ + public PlayerCommand(String command, List arguments, Instant timestamp) { + this.unsigned = true; + ImmutableMap.Builder builder = ImmutableMap.builder(); + arguments.forEach(entry -> builder.put(entry, EncryptionUtils.EMPTY)); + this.arguments = builder.build(); + this.timestamp = timestamp; + this.command = command; + this.signedPreview = false; + this.salt = 0L; + } + + /** + * Create new {@link PlayerCommand} based on a previously {@link SignedChatCommand}. + * + * @param signedCommand The {@link SignedChatCommand} to turn into {@link PlayerCommand}. + */ + public PlayerCommand(SignedChatCommand signedCommand) { + this.command = signedCommand.getBaseCommand(); + this.arguments = ImmutableMap.copyOf(signedCommand.getSignatures()); + this.timestamp = signedCommand.getExpiryTemporal(); + this.salt = Longs.fromByteArray(signedCommand.getSalt()); + this.signedPreview = signedCommand.isPreviewSigned(); + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + command = ProtocolUtils.readString(buf, 256); + timestamp = Instant.ofEpochMilli(buf.readLong()); + + salt = buf.readLong(); + if (salt == 0L) { + unsigned = true; + } + + int mapSize = ProtocolUtils.readVarInt(buf); + if (mapSize > MAX_NUM_ARGUMENTS) { + throw LIMITS_VIOLATION; + } + // Mapped as Argument : signature + ImmutableMap.Builder entries = ImmutableMap.builderWithExpectedSize(mapSize); + for (int i = 0; i < mapSize; i++) { + entries.put(ProtocolUtils.readString(buf, MAX_LENGTH_ARGUMENTS), + ProtocolUtils.readByteArray(buf, unsigned ? 0 : ProtocolUtils.DEFAULT_MAX_STRING_SIZE)); + } + arguments = entries.build(); + + signedPreview = buf.readBoolean(); + if (unsigned && signedPreview) { + throw EncryptionUtils.PREVIEW_SIGNATURE_MISSING; + } + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + ProtocolUtils.writeString(buf, command); + buf.writeLong(timestamp.toEpochMilli()); + + buf.writeLong(unsigned ? 0L : salt); + + int size = arguments.size(); + if (size > MAX_NUM_ARGUMENTS) { + throw LIMITS_VIOLATION; + } + ProtocolUtils.writeVarInt(buf, size); + for (Map.Entry entry : arguments.entrySet()) { + // What annoys me is that this isn't "sorted" + ProtocolUtils.writeString(buf, entry.getKey()); + ProtocolUtils.writeByteArray(buf, unsigned ? EncryptionUtils.EMPTY : entry.getValue()); + } + + buf.writeBoolean(signedPreview); + + } + + /** + * Validates a signature and creates a {@link SignedChatCommand} from the given signature. + * + * @param signer the signer's information + * @param sender the sender of the message + * @param mustSign instructs the function to throw if the signature is invalid. + * @return The {@link SignedChatCommand} or null if the signature couldn't be verified. + * @throws com.velocitypowered.proxy.util.except.QuietDecoderException when mustSign is {@code true} and the signature + * is invalid. + */ + public SignedChatCommand signedContainer(IdentifiedKey signer, UUID sender, boolean mustSign) { + if (unsigned) { + if (mustSign) { + throw EncryptionUtils.INVALID_SIGNATURE; + } + return null; + } + + return new SignedChatCommand(command, signer.getSignedPublicKey(), sender, timestamp, + arguments, Longs.toByteArray(salt), signedPreview); + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ServerChatPreview.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ServerChatPreview.java new file mode 100644 index 000000000..f97410e5f --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ServerChatPreview.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2018 Velocity 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 . + */ + +package com.velocitypowered.proxy.protocol.packet.chat; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class ServerChatPreview implements MinecraftPacket { + + private int id; + private @Nullable Component preview; + + public Component getPreview() { + return preview; + } + + public int getId() { + return id; + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + id = buf.readInt(); + if (buf.readBoolean()) { + preview = GsonComponentSerializer.gson().deserialize(ProtocolUtils.readString(buf)); + } + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + buf.writeInt(id); + if (preview != null) { + buf.writeBoolean(true); + ProtocolUtils.writeString(buf, GsonComponentSerializer.gson().serialize(preview)); + } else { + buf.writeBoolean(false); + } + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ServerPlayerChat.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ServerPlayerChat.java new file mode 100644 index 000000000..eccfddfa3 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ServerPlayerChat.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2018 Velocity 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 . + */ + +package com.velocitypowered.proxy.protocol.packet.chat; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; +import java.time.Instant; +import java.util.UUID; +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class ServerPlayerChat implements MinecraftPacket { + + private Component component; + private @Nullable Component unsignedComponent; + private int type; + + private UUID sender; + private Component senderName; + private @Nullable Component teamName; + + private Instant expiry; + + public void setType(int type) { + this.type = type; + } + + public void setComponent(Component component) { + this.component = component; + } + + public int getType() { + return type; + } + + public Component getComponent() { + return component; + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + component = ProtocolUtils.getJsonChatSerializer(protocolVersion).deserialize(ProtocolUtils.readString(buf)); + if (buf.readBoolean()) { + unsignedComponent = component = ProtocolUtils.getJsonChatSerializer(protocolVersion) + .deserialize(ProtocolUtils.readString(buf)); + } + + type = ProtocolUtils.readVarInt(buf); + + sender = ProtocolUtils.readUuid(buf); + senderName = ProtocolUtils.getJsonChatSerializer(protocolVersion).deserialize(ProtocolUtils.readString(buf)); + if (buf.readBoolean()) { + teamName = ProtocolUtils.getJsonChatSerializer(protocolVersion).deserialize(ProtocolUtils.readString(buf)); + } + + expiry = Instant.ofEpochMilli(buf.readLong()); + + long salt = buf.readLong(); + byte[] signature = ProtocolUtils.readByteArray(buf); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + // TBD + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/SystemChat.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/SystemChat.java new file mode 100644 index 000000000..fdf0c6859 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/SystemChat.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2018 Velocity 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 . + */ + +package com.velocitypowered.proxy.protocol.packet.chat; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; +import net.kyori.adventure.text.Component; + +public class SystemChat implements MinecraftPacket { + + public SystemChat() {} + + public SystemChat(Component component, int type) { + this.component = component; + this.type = type; + } + + private Component component; + private int type; + + public int getType() { + return type; + } + + public Component getComponent() { + return component; + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + component = ProtocolUtils.getJsonChatSerializer(protocolVersion).deserialize(ProtocolUtils.readString(buf)); + type = ProtocolUtils.readVarInt(buf); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + ProtocolUtils.writeString(buf, ProtocolUtils.getJsonChatSerializer(protocolVersion).serialize(component)); + ProtocolUtils.writeVarInt(buf, type); + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java index 99124cdae..0f1e6e04c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java @@ -18,6 +18,7 @@ package com.velocitypowered.proxy.tablist; import com.google.common.base.Preconditions; +import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.api.proxy.player.TabList; import com.velocitypowered.api.proxy.player.TabListEntry; import com.velocitypowered.api.util.GameProfile; @@ -119,10 +120,16 @@ public class VelocityTabList implements TabList { return Collections.unmodifiableCollection(this.entries.values()); } + @Override + public TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, int gameMode) { + return buildEntry(profile, displayName, latency, gameMode, null); + } + @Override public TabListEntry buildEntry(GameProfile profile, - net.kyori.adventure.text.@Nullable Component displayName, int latency, int gameMode) { - return new VelocityTabListEntry(this, profile, displayName, latency, gameMode); + net.kyori.adventure.text.@Nullable Component displayName, + int latency, int gameMode, @Nullable IdentifiedKey key) { + return new VelocityTabListEntry(this, profile, displayName, latency, gameMode, key); } /** diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java index b368381f3..143b014ea 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java @@ -17,6 +17,7 @@ package com.velocitypowered.proxy.tablist; +import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.api.proxy.player.TabList; import com.velocitypowered.api.proxy.player.TabListEntry; import com.velocitypowered.api.util.GameProfile; @@ -31,14 +32,17 @@ public class VelocityTabListEntry implements TabListEntry { private net.kyori.adventure.text.Component displayName; private int latency; private int gameMode; + private @Nullable IdentifiedKey playerKey; VelocityTabListEntry(VelocityTabList tabList, GameProfile profile, - net.kyori.adventure.text.@Nullable Component displayName, int latency, int gameMode) { + net.kyori.adventure.text.@Nullable Component displayName, int latency, int gameMode, + @Nullable IdentifiedKey playerKey) { this.tabList = tabList; this.profile = profile; this.displayName = displayName; this.latency = latency; this.gameMode = gameMode; + this.playerKey = playerKey; } @Override @@ -98,4 +102,13 @@ public class VelocityTabListEntry implements TabListEntry { void setGameModeInternal(int gameMode) { this.gameMode = gameMode; } + + @Override + public IdentifiedKey getIdentifiedKey() { + return playerKey; + } + + void setPlayerKeyInternal(IdentifiedKey playerKey) { + this.playerKey = playerKey; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntryLegacy.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntryLegacy.java index 8aa00c9f7..bbe163aeb 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntryLegacy.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntryLegacy.java @@ -26,7 +26,7 @@ public class VelocityTabListEntryLegacy extends VelocityTabListEntry { VelocityTabListEntryLegacy(VelocityTabListLegacy tabList, GameProfile profile, @Nullable Component displayName, int latency, int gameMode) { - super(tabList, profile, displayName, latency, gameMode); + super(tabList, profile, displayName, latency, gameMode, null); } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/EncryptionUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/util/EncryptionUtils.java deleted file mode 100644 index 3c555889a..000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/EncryptionUtils.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2018 Velocity 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 . - */ - -package com.velocitypowered.proxy.util; - -import java.math.BigInteger; -import java.security.GeneralSecurityException; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import javax.crypto.Cipher; - -public enum EncryptionUtils { - ; - - /** - * Generates an RSA key pair. - * - * @param keysize the key size (in bits) for the RSA key pair - * @return the generated key pair - */ - public static KeyPair createRsaKeyPair(final int keysize) { - try { - final KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); - generator.initialize(keysize); - return generator.generateKeyPair(); - } catch (final NoSuchAlgorithmException e) { - throw new RuntimeException("Unable to generate RSA keypair", e); - } - } - - /** - * Generates a hex digest in two's complement form for use with the Mojang joinedServer endpoint. - * - * @param digest the bytes to digest - * @return the hex digest - */ - public static String twosComplementHexdigest(byte[] digest) { - return new BigInteger(digest).toString(16); - } - - /** - * Decrypts an RSA message. - * - * @param keyPair the key pair to use - * @param bytes the bytes of the encrypted message - * @return the decrypted message - * @throws GeneralSecurityException if the message couldn't be decoded - */ - public static byte[] decryptRsa(KeyPair keyPair, byte[] bytes) throws GeneralSecurityException { - Cipher cipher = Cipher.getInstance("RSA"); - cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate()); - return cipher.doFinal(bytes); - } - - /** - * Generates the server ID for the hasJoined endpoint. - * - * @param sharedSecret the shared secret between the client and the proxy - * @param key the RSA public key - * @return the server ID - */ - public static String generateServerId(byte[] sharedSecret, PublicKey key) { - try { - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - digest.update(sharedSecret); - digest.update(key.getEncoded()); - return twosComplementHexdigest(digest.digest()); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(e); - } - } -} diff --git a/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages.properties b/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages.properties index 5ac4ea9ea..14ea29d0c 100644 --- a/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages.properties +++ b/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages.properties @@ -29,7 +29,7 @@ velocity.error.modern-forwarding-needs-new-client=This server is only compatible velocity.error.modern-forwarding-failed=Your server did not send a forwarding request to the proxy. Make sure the server is configured for Velocity forwarding. velocity.error.moved-to-new-server=You were kicked from {0}: {1} velocity.error.no-available-servers=There are no available servers to connect you to. Try again later or contact an admin. -velocity.error.illegal-chat-characters=Illegal characters in chat +velocity.error.illegal-legacyChat-characters=Illegal characters in legacyChat # Commands velocity.command.generic-error=An error occurred while running this command. diff --git a/proxy/src/main/resources/default-velocity.toml b/proxy/src/main/resources/default-velocity.toml index b561c8ad9..5ed6a7dcd 100644 --- a/proxy/src/main/resources/default-velocity.toml +++ b/proxy/src/main/resources/default-velocity.toml @@ -15,6 +15,9 @@ show-max-players = 500 # Should we authenticate players with Mojang? By default, this is on. online-mode = true +# Should the proxy enforce the new public key security standard? By default, this is on. +force-key-authentication = true + # If client's ISP/AS sent from this proxy is different from the one from Mojang's # authentication server, the player is kicked. This disallows some VPN and proxy # connections but is a weak form of protection. diff --git a/proxy/src/main/resources/yggdrasil_session_pubkey.der b/proxy/src/main/resources/yggdrasil_session_pubkey.der new file mode 100644 index 000000000..9c79a3aa4 Binary files /dev/null and b/proxy/src/main/resources/yggdrasil_session_pubkey.der differ diff --git a/proxy/src/test/java/com/velocitypowered/proxy/util/EncryptionUtilsTest.java b/proxy/src/test/java/com/velocitypowered/proxy/util/EncryptionUtilsTest.java index 7aa347796..ad0175fd6 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/util/EncryptionUtilsTest.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/util/EncryptionUtilsTest.java @@ -19,8 +19,10 @@ package com.velocitypowered.proxy.util; import static org.junit.jupiter.api.Assertions.assertEquals; +import com.velocitypowered.proxy.crypto.EncryptionUtils; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; + import org.junit.jupiter.api.Test; class EncryptionUtilsTest {