From ec87344a772254e04e381cc313789f6b142d6594 Mon Sep 17 00:00:00 2001 From: DoctorMacc Date: Wed, 19 Aug 2020 23:13:05 -0400 Subject: [PATCH] Initial skin support --- .../spigot/GeyserSpigotConfiguration.java | 2 +- .../platform/spigot/GeyserSpigotPlugin.java | 2 +- .../geysermc/floodgate/util/BedrockData.java | 20 ++++--- .../floodgate/util/EncryptionUtil.java | 11 +++- .../org/geysermc/floodgate/util/RawSkin.java | 56 +++++++++++++++++++ .../network/session/GeyserSession.java | 3 +- .../session/auth/BedrockClientData.java | 47 ++++++++++++++++ .../connector/utils/LoginEncryptionUtils.java | 4 +- 8 files changed, 129 insertions(+), 16 deletions(-) create mode 100644 common/src/main/java/org/geysermc/floodgate/util/RawSkin.java 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 380f70376..8667a692b 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-bukkit"); + Plugin floodgate = Bukkit.getPluginManager().getPlugin("floodgate-spigot"); 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 496681d33..b0cc5e6c3 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-bukkit") == null) { + if (geyserConfig.getRemote().getAuthType().equals("floodgate") && Bukkit.getPluginManager().getPlugin("floodgate-spigot") == 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/util/BedrockData.java b/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java index 3d80c0f9b..1f1b3bc88 100644 --- a/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java +++ b/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java @@ -51,19 +51,21 @@ 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) { + LinkedPlayer linkedPlayer, RawSkin skin) { this(version, username, xuid, deviceOs, languageCode, - inputMode, uiProfile, ip, linkedPlayer, EXPECTED_LENGTH); + inputMode, uiProfile, ip, linkedPlayer, EXPECTED_LENGTH, skin); } public BedrockData(String version, String username, String xuid, int deviceOs, - String languageCode, int uiProfile, int inputMode, String ip) { - this(version, username, xuid, deviceOs, languageCode, uiProfile, inputMode, ip, null); + String languageCode, int uiProfile, int inputMode, String ip, RawSkin skin) { + this(version, username, xuid, deviceOs, languageCode, uiProfile, inputMode, ip, null, skin); } - public static BedrockData fromString(String data) { + public static BedrockData fromString(String data, String skin) { String[] split = data.split("\0"); if (split.length != EXPECTED_LENGTH) return emptyData(split.length); @@ -72,12 +74,12 @@ 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 + linkedPlayer, split.length, RawSkin.parse(skin) ); } - public static BedrockData fromRawData(byte[] data) { - return fromString(new String(data)); + public static BedrockData fromRawData(byte[] data, String skin) { + return fromString(new String(data), skin); } @Override @@ -93,6 +95,6 @@ public final class BedrockData { } private static BedrockData emptyData(int dataLength) { - return new BedrockData(null, null, null, -1, null, -1, -1, null, null, dataLength); + return new BedrockData(null, null, null, -1, null, -1, -1, null, null, dataLength, null); } } diff --git a/common/src/main/java/org/geysermc/floodgate/util/EncryptionUtil.java b/common/src/main/java/org/geysermc/floodgate/util/EncryptionUtil.java index ccad8d1c2..2f3048bd9 100644 --- a/common/src/main/java/org/geysermc/floodgate/util/EncryptionUtil.java +++ b/common/src/main/java/org/geysermc/floodgate/util/EncryptionUtil.java @@ -58,9 +58,14 @@ public final class EncryptionUtil { Base64.getEncoder().encodeToString(encryptedText); } + public static String encryptBedrockData(Key key, BedrockData data, boolean includeSkin) throws IllegalBlockSizeException, + InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException { + return encrypt(key, data.toString()) + (includeSkin ? data.getSkin() : ""); + } + public static String encryptBedrockData(Key key, BedrockData data) throws IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException { - return encrypt(key, data.toString()); + return encryptBedrockData(key, data, true); } public static byte[] decrypt(Key key, String encryptedData) throws IllegalBlockSizeException, @@ -80,9 +85,9 @@ public final class EncryptionUtil { return cipher.doFinal(Base64.getDecoder().decode(split[1])); } - public static BedrockData decryptBedrockData(Key key, String encryptedData) throws IllegalBlockSizeException, + public static BedrockData decryptBedrockData(Key key, String encryptedData, String skin) throws IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException { - return BedrockData.fromRawData(decrypt(key, encryptedData)); + return BedrockData.fromRawData(decrypt(key, encryptedData), skin); } @SuppressWarnings("unchecked") diff --git a/common/src/main/java/org/geysermc/floodgate/util/RawSkin.java b/common/src/main/java/org/geysermc/floodgate/util/RawSkin.java new file mode 100644 index 000000000..ba22b632a --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/util/RawSkin.java @@ -0,0 +1,56 @@ +/* + * 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.util; + +import lombok.AllArgsConstructor; + +import java.nio.charset.StandardCharsets; + +@AllArgsConstructor +public 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; + + RawSkin skin = new RawSkin(); + skin.width = Integer.parseInt(split[0]); + skin.height = Integer.parseInt(split[1]); + skin.data = split[2].getBytes(StandardCharsets.UTF_8); + return skin; + } + + @Override + public String toString() { + return Integer.toString(width) + ':' + height + ':' + new String(data); + } +} 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 06c669c9d..e53dd03aa 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 @@ -371,7 +371,8 @@ public class GeyserSession implements CommandSender { clientData.getLanguageCode(), clientData.getUiProfile().ordinal(), clientData.getCurrentInputMode().ordinal(), - upstream.getSession().getAddress().getAddress().getHostAddress() + upstream.getSession().getAddress().getAddress().getHostAddress(), + clientData.getImage("Skin") )); } catch (Exception e) { connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e); 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 ad1fb2fb1..2a3e174ed 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 @@ -25,18 +25,25 @@ 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 lombok.Getter; +import net.minidev.json.JSONObject; 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.util.Base64; import java.util.UUID; @JsonIgnoreProperties(ignoreUnknown = true) @Getter public final class BedrockClientData { + @JsonIgnore + private JSONObject jsonData; + @JsonProperty(value = "GameVersion") private String gameVersion; @JsonProperty(value = "ServerAddress") @@ -104,4 +111,44 @@ public final class BedrockClientData { private String skinColor; @JsonProperty(value = "ThirdPartyNameOnly") private boolean thirdPartyNameOnly; + + public void setJsonData(JSONObject 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; + // width * height * 4 (rgba) + switch (imageData.length) { + case 8192: + return new RawSkin(64, 32, imageData); + case 16384: + return new RawSkin(64, 64, imageData); + case 32768: + return new RawSkin(64, 128, imageData); + case 65536: + return new RawSkin(128, 128, imageData); + default: + throw new IllegalArgumentException("Unknown legacy skin size"); + } + } } 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 4bc997bdf..7e4989d6e 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java @@ -131,7 +131,9 @@ public class LoginEncryptionUtils { JWSObject clientJwt = JWSObject.parse(clientData); EncryptionUtils.verifyJwt(clientJwt, identityPublicKey); - session.setClientData(JSON_MAPPER.convertValue(JSON_MAPPER.readTree(clientJwt.getPayload().toBytes()), BedrockClientData.class)); + BedrockClientData data = JSON_MAPPER.convertValue(JSON_MAPPER.readTree(clientJwt.getPayload().toBytes()), BedrockClientData.class); + data.setJsonData(clientJwt.getPayload().toJSONObject()); + session.setClientData(data); if (EncryptionUtils.canUseEncryption()) { LoginEncryptionUtils.startEncryptionHandshake(session, identityPublicKey);