Initial skin support

This commit is contained in:
DoctorMacc 2020-08-19 23:13:05 -04:00
parent d37113388b
commit ec87344a77
No known key found for this signature in database
GPG key ID: 6D6E7E059F186DB4
8 changed files with 129 additions and 16 deletions

View file

@ -48,7 +48,7 @@ public class GeyserSpigotConfiguration extends GeyserJacksonConfiguration {
private Path floodgateKey; private Path floodgateKey;
public void loadFloodgate(GeyserSpigotPlugin plugin) { 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); 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);
} }

View file

@ -93,7 +93,7 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
this.geyserLogger = new GeyserSpigotLogger(getLogger(), geyserConfig.isDebugMode()); this.geyserLogger = new GeyserSpigotLogger(getLogger(), geyserConfig.isDebugMode());
GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); 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")); geyserLogger.severe(LanguageUtils.getLocaleStringLog("geyser.bootstrap.floodgate.not_installed") + " " + LanguageUtils.getLocaleStringLog("geyser.bootstrap.floodgate.disabling"));
this.getPluginLoader().disablePlugin(this); this.getPluginLoader().disablePlugin(this);
return; return;

View file

@ -51,19 +51,21 @@ public final class BedrockData {
private final LinkedPlayer linkedPlayer; private final LinkedPlayer linkedPlayer;
private final int dataLength; private final int dataLength;
private RawSkin skin;
public BedrockData(String version, String username, String xuid, int deviceOs, public BedrockData(String version, String username, String xuid, int deviceOs,
String languageCode, int uiProfile, int inputMode, String ip, String languageCode, int uiProfile, int inputMode, String ip,
LinkedPlayer linkedPlayer) { LinkedPlayer linkedPlayer, RawSkin skin) {
this(version, username, xuid, deviceOs, languageCode, 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, public BedrockData(String version, String username, String xuid, int deviceOs,
String languageCode, int uiProfile, int inputMode, String ip) { String languageCode, int uiProfile, int inputMode, String ip, RawSkin skin) {
this(version, username, xuid, deviceOs, languageCode, uiProfile, inputMode, ip, null); 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"); String[] split = data.split("\0");
if (split.length != EXPECTED_LENGTH) return emptyData(split.length); if (split.length != EXPECTED_LENGTH) return emptyData(split.length);
@ -72,12 +74,12 @@ public final class BedrockData {
return new BedrockData( return new BedrockData(
split[0], split[1], split[2], Integer.parseInt(split[3]), split[4], split[0], split[1], split[2], Integer.parseInt(split[3]), split[4],
Integer.parseInt(split[5]), Integer.parseInt(split[6]), split[7], 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) { public static BedrockData fromRawData(byte[] data, String skin) {
return fromString(new String(data)); return fromString(new String(data), skin);
} }
@Override @Override
@ -93,6 +95,6 @@ public final class BedrockData {
} }
private static BedrockData emptyData(int dataLength) { 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);
} }
} }

View file

@ -58,9 +58,14 @@ public final class EncryptionUtil {
Base64.getEncoder().encodeToString(encryptedText); 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, public static String encryptBedrockData(Key key, BedrockData data) throws IllegalBlockSizeException,
InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException { InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException {
return encrypt(key, data.toString()); return encryptBedrockData(key, data, true);
} }
public static byte[] decrypt(Key key, String encryptedData) throws IllegalBlockSizeException, 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])); 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 { InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException {
return BedrockData.fromRawData(decrypt(key, encryptedData)); return BedrockData.fromRawData(decrypt(key, encryptedData), skin);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")

View file

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

View file

@ -371,7 +371,8 @@ public class GeyserSession implements CommandSender {
clientData.getLanguageCode(), clientData.getLanguageCode(),
clientData.getUiProfile().ordinal(), clientData.getUiProfile().ordinal(),
clientData.getCurrentInputMode().ordinal(), clientData.getCurrentInputMode().ordinal(),
upstream.getSession().getAddress().getAddress().getHostAddress() upstream.getSession().getAddress().getAddress().getHostAddress(),
clientData.getImage("Skin")
)); ));
} catch (Exception e) { } catch (Exception e) {
connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e); connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e);

View file

@ -25,18 +25,25 @@
package org.geysermc.connector.network.session.auth; package org.geysermc.connector.network.session.auth;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter; import lombok.Getter;
import net.minidev.json.JSONObject;
import org.geysermc.floodgate.util.DeviceOs; import org.geysermc.floodgate.util.DeviceOs;
import org.geysermc.floodgate.util.InputMode; import org.geysermc.floodgate.util.InputMode;
import org.geysermc.floodgate.util.RawSkin;
import org.geysermc.floodgate.util.UiProfile; import org.geysermc.floodgate.util.UiProfile;
import java.util.Base64;
import java.util.UUID; import java.util.UUID;
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
@Getter @Getter
public final class BedrockClientData { public final class BedrockClientData {
@JsonIgnore
private JSONObject jsonData;
@JsonProperty(value = "GameVersion") @JsonProperty(value = "GameVersion")
private String gameVersion; private String gameVersion;
@JsonProperty(value = "ServerAddress") @JsonProperty(value = "ServerAddress")
@ -104,4 +111,44 @@ public final class BedrockClientData {
private String skinColor; private String skinColor;
@JsonProperty(value = "ThirdPartyNameOnly") @JsonProperty(value = "ThirdPartyNameOnly")
private boolean 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<br>
* 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");
}
}
} }

View file

@ -131,7 +131,9 @@ public class LoginEncryptionUtils {
JWSObject clientJwt = JWSObject.parse(clientData); JWSObject clientJwt = JWSObject.parse(clientData);
EncryptionUtils.verifyJwt(clientJwt, identityPublicKey); 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()) { if (EncryptionUtils.canUseEncryption()) {
LoginEncryptionUtils.startEncryptionHandshake(session, identityPublicKey); LoginEncryptionUtils.startEncryptionHandshake(session, identityPublicKey);