diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeConfiguration.java b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeConfiguration.java index d9b86a2e8..500f342a6 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeConfiguration.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeConfiguration.java @@ -44,7 +44,7 @@ public class GeyserBungeeConfiguration extends GeyserJacksonConfiguration { private Path floodgateKey; public void loadFloodgate(GeyserBungeePlugin plugin, Configuration configuration) { - Plugin floodgate = plugin.getProxy().getPluginManager().getPlugin("floodgate-bungee"); + Plugin floodgate = plugin.getProxy().getPluginManager().getPlugin("floodgate"); floodgateKey = FloodgateKeyLoader.getKey(plugin.getGeyserLogger(), this, Paths.get(plugin.getDataFolder().toString(), configuration.getString("floodgate-key-file"), "public-key.pem"), floodgate, floodgate != null ? floodgate.getDataFolder().toPath() : null); } diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeePlugin.java b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeePlugin.java index 059e1dfd8..a600ebcd3 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeePlugin.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeePlugin.java @@ -94,7 +94,7 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { this.geyserLogger = new GeyserBungeeLogger(getLogger(), geyserConfig.isDebugMode()); GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); - if (geyserConfig.getRemote().getAuthType().equals("floodgate") && getProxy().getPluginManager().getPlugin("floodgate-bungee") == null) { + if (geyserConfig.getRemote().getAuthType().equals("floodgate") && getProxy().getPluginManager().getPlugin("floodgate") == null) { geyserLogger.severe(LanguageUtils.getLocaleStringLog("geyser.bootstrap.floodgate.not_installed") + " " + LanguageUtils.getLocaleStringLog("geyser.bootstrap.floodgate.disabling")); return; } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotConfiguration.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotConfiguration.java index 8667a692b..de4e58c3b 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotConfiguration.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotConfiguration.java @@ -48,7 +48,7 @@ public class GeyserSpigotConfiguration extends GeyserJacksonConfiguration { private Path floodgateKey; public void loadFloodgate(GeyserSpigotPlugin plugin) { - Plugin floodgate = Bukkit.getPluginManager().getPlugin("floodgate-spigot"); + Plugin floodgate = Bukkit.getPluginManager().getPlugin("floodgate"); floodgateKey = FloodgateKeyLoader.getKey(plugin.getGeyserLogger(), this, Paths.get(plugin.getDataFolder().toString(), plugin.getConfig().getString("floodgate-key-file", "public-key.pem")), floodgate, floodgate != null ? floodgate.getDataFolder().toPath() : null); } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java index b0cc5e6c3..dab63c4ea 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java @@ -93,7 +93,7 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { this.geyserLogger = new GeyserSpigotLogger(getLogger(), geyserConfig.isDebugMode()); GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); - if (geyserConfig.getRemote().getAuthType().equals("floodgate") && Bukkit.getPluginManager().getPlugin("floodgate-spigot") == null) { + if (geyserConfig.getRemote().getAuthType().equals("floodgate") && Bukkit.getPluginManager().getPlugin("floodgate") == null) { geyserLogger.severe(LanguageUtils.getLocaleStringLog("geyser.bootstrap.floodgate.not_installed") + " " + LanguageUtils.getLocaleStringLog("geyser.bootstrap.floodgate.disabling")); this.getPluginLoader().disablePlugin(this); return; diff --git a/common/src/main/java/org/geysermc/floodgate/crypto/AesCipher.java b/common/src/main/java/org/geysermc/floodgate/crypto/AesCipher.java index 3e6fef01a..2627584f6 100644 --- a/common/src/main/java/org/geysermc/floodgate/crypto/AesCipher.java +++ b/common/src/main/java/org/geysermc/floodgate/crypto/AesCipher.java @@ -26,7 +26,7 @@ package org.geysermc.floodgate.crypto; -import org.geysermc.floodgate.util.InvalidHeaderException; +import lombok.RequiredArgsConstructor; import javax.crypto.Cipher; import javax.crypto.SecretKey; @@ -35,12 +35,14 @@ import java.nio.ByteBuffer; import java.security.Key; import java.security.SecureRandom; +@RequiredArgsConstructor public final class AesCipher implements FloodgateCipher { - private static final int IV_LENGTH = 12; + public static final int IV_LENGTH = 12; private static final int TAG_BIT_LENGTH = 128; private static final String CIPHER_NAME = "AES/GCM/NoPadding"; private final SecureRandom secureRandom = new SecureRandom(); + private final Topping topping; private SecretKey secretKey; public void init(Key key) { @@ -62,32 +64,57 @@ public final class AesCipher implements FloodgateCipher { cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec); byte[] cipherText = cipher.doFinal(data); - return ByteBuffer.allocate(iv.length + cipherText.length + HEADER_LENGTH) - .put(IDENTIFIER).put(VERSION) // header + if (topping != null) { + iv = topping.encode(iv); + cipherText = topping.encode(cipherText); + } + + return ByteBuffer.allocate(iv.length + cipherText.length + HEADER_LENGTH + 1) + .put(IDENTIFIER) // header .put(iv) + .put((byte) 0x21) .put(cipherText) .array(); } public byte[] decrypt(byte[] cipherTextWithIv) throws Exception { - HeaderResult pair = checkHeader(cipherTextWithIv); - if (pair.getVersion() != VERSION) { - throw new InvalidHeaderException( - "Expected version " + VERSION + ", got " + pair.getVersion() - ); - } + checkHeader(cipherTextWithIv); Cipher cipher = Cipher.getInstance(CIPHER_NAME); int bufferLength = cipherTextWithIv.length - HEADER_LENGTH; ByteBuffer buffer = ByteBuffer.wrap(cipherTextWithIv, HEADER_LENGTH, bufferLength); - byte[] iv = new byte[IV_LENGTH]; + int ivLength = IV_LENGTH; + + if (topping != null) { + int mark = buffer.position(); + + // we need the first index, the second is for the optional RawSkin + boolean found = false; + while (buffer.hasRemaining() && !found) { + if (buffer.get() == 0x21) { + found = true; + } + } + + ivLength = buffer.position() - mark - 1; // don't include the splitter itself + buffer.position(mark); // reset to the pre-while index + } + + byte[] iv = new byte[ivLength]; buffer.get(iv); + buffer.position(buffer.position() + 1); // skip splitter + byte[] cipherText = new byte[buffer.remaining()]; buffer.get(cipherText); + if (topping != null) { + iv = topping.decode(iv); + cipherText = topping.decode(cipherText); + } + GCMParameterSpec spec = new GCMParameterSpec(TAG_BIT_LENGTH, iv); cipher.init(Cipher.DECRYPT_MODE, secretKey, spec); return cipher.doFinal(cipherText); diff --git a/common/src/main/java/org/geysermc/floodgate/crypto/Base64Topping.java b/common/src/main/java/org/geysermc/floodgate/crypto/Base64Topping.java new file mode 100644 index 000000000..fbec78a1d --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/crypto/Base64Topping.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019-2020 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.floodgate.crypto; + +import java.util.Base64; + +public final class Base64Topping implements Topping { + @Override + public byte[] encode(byte[] data) { + return Base64.getEncoder().encode(data); + } + + @Override + public byte[] decode(byte[] data) { + return Base64.getDecoder().decode(data); + } +} diff --git a/common/src/main/java/org/geysermc/floodgate/crypto/FloodgateCipher.java b/common/src/main/java/org/geysermc/floodgate/crypto/FloodgateCipher.java index 23e57fb64..4869531e2 100644 --- a/common/src/main/java/org/geysermc/floodgate/crypto/FloodgateCipher.java +++ b/common/src/main/java/org/geysermc/floodgate/crypto/FloodgateCipher.java @@ -28,7 +28,7 @@ package org.geysermc.floodgate.crypto; import lombok.AllArgsConstructor; import lombok.Data; -import org.geysermc.floodgate.util.InvalidHeaderException; +import org.geysermc.floodgate.util.InvalidFormatException; import java.nio.charset.StandardCharsets; import java.security.Key; @@ -38,9 +38,7 @@ import java.security.Key; */ public interface FloodgateCipher { byte[] IDENTIFIER = "Floodgate".getBytes(StandardCharsets.UTF_8); - byte VERSION = 2; - - int HEADER_LENGTH = IDENTIFIER.length + 1; // one byte for version + int HEADER_LENGTH = IDENTIFIER.length; /** * Initializes the instance by giving it the key it needs to encrypt or decrypt data @@ -110,19 +108,20 @@ public interface FloodgateCipher { } /** - * Checks if the header is valid and return a IntPair containing the header version - * and the index to start reading the actual encrypted data from. + * Checks if the header is valid. + * This method will throw an InvalidFormatException when the header is invalid. * * @param data the data to check - * @return IntPair. x = version number, y = the index to start reading from. - * @throws InvalidHeaderException when the header is invalid + * @throws InvalidFormatException when the header is invalid */ - default HeaderResult checkHeader(byte[] data) throws InvalidHeaderException { + default void checkHeader(byte[] data) throws InvalidFormatException { final int identifierLength = IDENTIFIER.length; if (data.length <= HEADER_LENGTH) { - throw new InvalidHeaderException("Data length is smaller then header." + - "Needed " + HEADER_LENGTH + ", got " + data.length); + throw new InvalidFormatException("Data length is smaller then header." + + "Needed " + HEADER_LENGTH + ", got " + data.length, + true + ); } for (int i = 0; i < identifierLength; i++) { @@ -132,15 +131,15 @@ public interface FloodgateCipher { receivedIdentifier.append(b); } - throw new InvalidHeaderException(String.format( - "Expected identifier %s, got %s", - new String(IDENTIFIER, StandardCharsets.UTF_8), - receivedIdentifier.toString() - )); + throw new InvalidFormatException( + String.format("Expected identifier %s, got %s", + new String(IDENTIFIER, StandardCharsets.UTF_8), + receivedIdentifier.toString() + ), + true + ); } } - - return new HeaderResult(data[identifierLength], HEADER_LENGTH); } static boolean hasHeader(String data) { diff --git a/common/src/main/java/org/geysermc/floodgate/crypto/Topping.java b/common/src/main/java/org/geysermc/floodgate/crypto/Topping.java new file mode 100644 index 000000000..f3dc0d4b8 --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/crypto/Topping.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019-2020 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.floodgate.crypto; + +public interface Topping { + byte[] encode(byte[] data); + byte[] decode(byte[] data); +} diff --git a/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java b/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java index dcc412f50..89eaf5395 100644 --- a/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java +++ b/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java @@ -50,25 +50,23 @@ public final class BedrockData { private final LinkedPlayer linkedPlayer; private final int dataLength; - private RawSkin skin; - public BedrockData(String version, String username, String xuid, int deviceOs, String languageCode, int uiProfile, int inputMode, String ip, - LinkedPlayer linkedPlayer, RawSkin skin) { + LinkedPlayer linkedPlayer) { this(version, username, xuid, deviceOs, languageCode, - inputMode, uiProfile, ip, linkedPlayer, EXPECTED_LENGTH, skin); + inputMode, uiProfile, ip, linkedPlayer, EXPECTED_LENGTH); } public BedrockData(String version, String username, String xuid, int deviceOs, - String languageCode, int uiProfile, int inputMode, String ip, RawSkin skin) { - this(version, username, xuid, deviceOs, languageCode, uiProfile, inputMode, ip, null, skin); + String languageCode, int uiProfile, int inputMode, String ip) { + this(version, username, xuid, deviceOs, languageCode, uiProfile, inputMode, ip, null); } public boolean hasPlayerLink() { return linkedPlayer != null; } - public static BedrockData fromString(String data, String skin) { + public static BedrockData fromString(String data) { String[] split = data.split("\0"); if (split.length != EXPECTED_LENGTH) { return emptyData(split.length); @@ -79,14 +77,10 @@ public final class BedrockData { return new BedrockData( split[0], split[1], split[2], Integer.parseInt(split[3]), split[4], Integer.parseInt(split[5]), Integer.parseInt(split[6]), split[7], - linkedPlayer, split.length, RawSkin.parse(skin) + linkedPlayer, split.length ); } - public static BedrockData fromRawData(byte[] data, String skin) { - return fromString(new String(data), skin); - } - @Override public String toString() { // The format is the same as the order of the fields in this class @@ -96,6 +90,6 @@ public final class BedrockData { } private static BedrockData emptyData(int dataLength) { - return new BedrockData(null, null, null, -1, null, -1, -1, null, null, dataLength, null); + return new BedrockData(null, null, null, -1, null, -1, -1, null, null, dataLength); } } diff --git a/common/src/main/java/org/geysermc/floodgate/util/InvalidHeaderException.java b/common/src/main/java/org/geysermc/floodgate/util/InvalidFormatException.java similarity index 77% rename from common/src/main/java/org/geysermc/floodgate/util/InvalidHeaderException.java rename to common/src/main/java/org/geysermc/floodgate/util/InvalidFormatException.java index 30dbf0726..9ec5b1710 100644 --- a/common/src/main/java/org/geysermc/floodgate/util/InvalidHeaderException.java +++ b/common/src/main/java/org/geysermc/floodgate/util/InvalidFormatException.java @@ -26,16 +26,26 @@ package org.geysermc.floodgate.util; -public class InvalidHeaderException extends Exception { - public InvalidHeaderException() { +import lombok.Getter; + +@Getter +public class InvalidFormatException extends Exception { + private boolean header = false; + + public InvalidFormatException() { super(); } - public InvalidHeaderException(String message) { + public InvalidFormatException(String message) { super(message); } - public InvalidHeaderException(String message, Throwable cause) { + public InvalidFormatException(String message, boolean header) { + super(message); + this.header = header; + } + + public InvalidFormatException(String message, Throwable cause) { super(message, cause); } } diff --git a/common/src/main/java/org/geysermc/floodgate/util/RawSkin.java b/common/src/main/java/org/geysermc/floodgate/util/RawSkin.java index ba22b632a..2ff0e3fb2 100644 --- a/common/src/main/java/org/geysermc/floodgate/util/RawSkin.java +++ b/common/src/main/java/org/geysermc/floodgate/util/RawSkin.java @@ -26,31 +26,62 @@ package org.geysermc.floodgate.util; import lombok.AllArgsConstructor; +import lombok.ToString; -import java.nio.charset.StandardCharsets; +import java.nio.ByteBuffer; +import java.util.Base64; @AllArgsConstructor -public class RawSkin { +@ToString +public final class RawSkin { public int width; public int height; public byte[] data; private RawSkin() {} - public static RawSkin parse(String data) { - if (data == null) return null; - String[] split = data.split(":"); - if (split.length != 3) return null; + public static RawSkin decode(byte[] data) throws InvalidFormatException { + if (data == null) { + return null; + } + + int maxEncodedLength = 4 * (((64 * 64 * 4 + 8) + 2) / 3); + // if the RawSkin is longer then the max Java Edition skin length + if (data.length > maxEncodedLength) { + throw new InvalidFormatException( + "Encoded data cannot be longer then " + maxEncodedLength + " bytes!" + ); + } + + // if the encoded data doesn't even contain the width and height (8 bytes, 2 ints) + if (data.length < 4 * ((8 + 2) / 3)) { + throw new InvalidFormatException("Encoded data must be at least 12 bytes long!"); + } + + data = Base64.getDecoder().decode(data); + + ByteBuffer buffer = ByteBuffer.wrap(data); RawSkin skin = new RawSkin(); - skin.width = Integer.parseInt(split[0]); - skin.height = Integer.parseInt(split[1]); - skin.data = split[2].getBytes(StandardCharsets.UTF_8); + skin.width = buffer.getInt(); + skin.height = buffer.getInt(); + if (buffer.remaining() != (skin.width * skin.height * 4)) { + throw new InvalidFormatException(String.format( + "Expected skin length to be %s, got %s", + (skin.width * skin.height * 4), buffer.remaining() + )); + } + skin.data = new byte[buffer.remaining()]; + buffer.get(skin.data); return skin; } - @Override - public String toString() { - return Integer.toString(width) + ':' + height + ':' + new String(data); + public byte[] encode() { + // 2 x int = 8 bytes + ByteBuffer buffer = ByteBuffer.allocate(8 + data.length); + buffer.putInt(width); + buffer.putInt(height); + buffer.put(data); + return Base64.getEncoder().encode(buffer.array()); } } diff --git a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java index 21fb5564f..9b6045be2 100644 --- a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java +++ b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java @@ -57,6 +57,7 @@ import org.geysermc.connector.utils.LanguageUtils; import org.geysermc.connector.utils.LocaleUtils; import org.geysermc.floodgate.crypto.AesCipher; import org.geysermc.floodgate.crypto.AesKeyProducer; +import org.geysermc.floodgate.crypto.Base64Topping; import org.geysermc.floodgate.crypto.FloodgateCipher; import javax.naming.directory.Attribute; @@ -180,7 +181,7 @@ public class GeyserConnector { if (authType == AuthType.FLOODGATE) { try { Key key = new AesKeyProducer().produceFrom(config.getFloodgateKeyFile()); - cipher = new AesCipher(); + cipher = new AesCipher(new Base64Topping()); cipher.init(key); logger.info(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.loaded_key")); } catch (Exception exception) { diff --git a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java index 3e4314fd4..9a2ebe56d 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java @@ -77,15 +77,8 @@ import org.geysermc.floodgate.crypto.FloodgateCipher; import org.geysermc.floodgate.util.BedrockData; import java.net.InetSocketAddress; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.spec.InvalidKeySpecException; -import java.util.*; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Base64; -import java.util.List; -import java.util.UUID; +import java.util.*; import java.util.concurrent.atomic.AtomicInteger; @Getter @@ -359,8 +352,7 @@ public class GeyserSession implements CommandSender { clientData.getLanguageCode(), clientData.getUiProfile().ordinal(), clientData.getCurrentInputMode().ordinal(), - upstream.getSession().getAddress().getAddress().getHostAddress(), - clientData.getImage("Skin") + upstream.getSession().getAddress().getAddress().getHostAddress() ).toString()); } catch (Exception e) { connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e); @@ -368,15 +360,18 @@ public class GeyserSession implements CommandSender { return; } - String encrypted = new String( - Base64.getEncoder().encode(encryptedData), - StandardCharsets.UTF_8 - ); + byte[] rawSkin = clientData.getAndTransformImage("Skin").encode(); + byte[] finalData = new byte[encryptedData.length + rawSkin.length + 1]; + System.arraycopy(encryptedData, 0, finalData, 0, encryptedData.length); + finalData[encryptedData.length] = 0x21; // splitter + System.arraycopy(rawSkin, 0, finalData, encryptedData.length + 1, rawSkin.length); + + String finalDataString = new String(finalData, StandardCharsets.UTF_8); HandshakePacket handshakePacket = event.getPacket(); event.setPacket(new HandshakePacket( handshakePacket.getProtocolVersion(), - handshakePacket.getHostname() + '\0' + encrypted, + handshakePacket.getHostname() + '\0' + finalDataString, handshakePacket.getPort(), handshakePacket.getIntent() )); @@ -626,7 +621,7 @@ public class GeyserSession implements CommandSender { /** * Send a packet immediately to the player. - * + * * @param packet the bedrock packet from the NukkitX protocol lib */ public void sendUpstreamPacketImmediately(BedrockPacket packet) { diff --git a/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java b/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java index 2a3e174ed..ab0fad845 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java @@ -28,13 +28,15 @@ package org.geysermc.connector.network.session.auth; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; import lombok.Getter; -import net.minidev.json.JSONObject; +import org.geysermc.connector.utils.SkinProvider; import org.geysermc.floodgate.util.DeviceOs; import org.geysermc.floodgate.util.InputMode; import org.geysermc.floodgate.util.RawSkin; import org.geysermc.floodgate.util.UiProfile; +import java.awt.image.BufferedImage; import java.util.Base64; import java.util.UUID; @@ -42,7 +44,7 @@ import java.util.UUID; @Getter public final class BedrockClientData { @JsonIgnore - private JSONObject jsonData; + private JsonNode jsonData; @JsonProperty(value = "GameVersion") private String gameVersion; @@ -112,31 +114,17 @@ public final class BedrockClientData { @JsonProperty(value = "ThirdPartyNameOnly") private boolean thirdPartyNameOnly; - public void setJsonData(JSONObject data) { - if (this.jsonData != null && data != null) { + public void setJsonData(JsonNode data) { + if (this.jsonData == null && data != null) { this.jsonData = data; } } - /** - * Taken from https://github.com/NukkitX/Nukkit/blob/master/src/main/java/cn/nukkit/network/protocol/LoginPacket.java
- * Internally only used for Skins, but can be used for Capes too - */ - public RawSkin getImage(String name) { - if (jsonData == null || !jsonData.containsKey(name + "Data")) return null; - byte[] image = Base64.getDecoder().decode(jsonData.getAsString(name + "Data")); - if (jsonData.containsKey(name + "ImageWidth") && jsonData.containsKey(name + "ImageHeight")) { - return new RawSkin( - (int) jsonData.getAsNumber(name + "ImageWidth"), - (int) jsonData.get(name + "ImageHeight"), - image - ); - } - return getLegacyImage(image); - } - private static RawSkin getLegacyImage(byte[] imageData) { - if (imageData == null) return null; + if (imageData == null) { + return null; + } + // width * height * 4 (rgba) switch (imageData.length) { case 8192: @@ -151,4 +139,44 @@ public final class BedrockClientData { throw new IllegalArgumentException("Unknown legacy skin size"); } } + + /** + * Taken from https://github.com/NukkitX/Nukkit/blob/master/src/main/java/cn/nukkit/network/protocol/LoginPacket.java
+ * Internally only used for Skins, but can be used for Capes too + */ + public RawSkin getImage(String name) { + System.out.println(jsonData.toString()); + if (jsonData == null || !jsonData.has(name + "Data")) { + return null; + } + + byte[] image = Base64.getDecoder().decode(jsonData.get(name + "Data").asText()); + if (jsonData.has(name + "ImageWidth") && jsonData.has(name + "ImageHeight")) { + return new RawSkin( + jsonData.get(name + "ImageWidth").asInt(), + jsonData.get(name + "ImageHeight").asInt(), + image + ); + } + return getLegacyImage(image); + } + + public RawSkin getAndTransformImage(String name) { + RawSkin skin = getImage(name); + if (skin != null && (skin.width > 64 || skin.height > 64)) { + BufferedImage scaledImage = SkinProvider.imageDataToBufferedImage(skin.data, skin.width, skin.height); + + int max = Math.max(skin.width, skin.height); + while (max > 64) { + max /= 2; + scaledImage = SkinProvider.scale(scaledImage); + } + + byte[] skinData = SkinProvider.bufferedImageToImageData(scaledImage); + skin.width = scaledImage.getWidth(); + skin.height = scaledImage.getHeight(); + skin.data = skinData; + } + return skin; + } } diff --git a/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java b/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java index 7e4989d6e..62d70f612 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java @@ -131,8 +131,9 @@ public class LoginEncryptionUtils { JWSObject clientJwt = JWSObject.parse(clientData); EncryptionUtils.verifyJwt(clientJwt, identityPublicKey); - BedrockClientData data = JSON_MAPPER.convertValue(JSON_MAPPER.readTree(clientJwt.getPayload().toBytes()), BedrockClientData.class); - data.setJsonData(clientJwt.getPayload().toJSONObject()); + JsonNode clientDataJson = JSON_MAPPER.readTree(clientJwt.getPayload().toBytes()); + BedrockClientData data = JSON_MAPPER.convertValue(clientDataJson, BedrockClientData.class); + data.setJsonData(clientDataJson); session.setClientData(data); if (EncryptionUtils.canUseEncryption()) { diff --git a/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java b/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java index 5551230b9..9b3d737e8 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java +++ b/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java @@ -460,7 +460,7 @@ public class SkinProvider { return null; } - private static BufferedImage scale(BufferedImage bufferedImage) { + public static BufferedImage scale(BufferedImage bufferedImage) { BufferedImage resized = new BufferedImage(bufferedImage.getWidth() / 2, bufferedImage.getHeight() / 2, BufferedImage.TYPE_INT_ARGB); Graphics2D g2 = resized.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);