diff --git a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java index 2c52de7de..4fbb520d9 100644 --- a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java +++ b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java @@ -30,7 +30,7 @@ import com.nukkitx.protocol.bedrock.packet.*; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.Registry; -import org.geysermc.connector.utils.AuthenticationUtils; +import org.geysermc.connector.utils.LoginEncryptionUtils; public class UpstreamPacketHandler extends LoggingPacketHandler { @@ -52,7 +52,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { return true; } - AuthenticationUtils.encryptPlayerConnection(connector, session, loginPacket); + LoginEncryptionUtils.encryptPlayerConnection(connector, session, loginPacket); PlayStatusPacket playStatus = new PlayStatusPacket(); playStatus.setStatus(PlayStatusPacket.Status.LOGIN_SUCCESS); @@ -88,14 +88,14 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { @Override public boolean handle(ModalFormResponsePacket packet) { connector.getLogger().debug("Handled packet: " + packet.getClass().getSimpleName()); - return AuthenticationUtils.authenticateFromForm(session, connector, packet.getFormData()); + return LoginEncryptionUtils.authenticateFromForm(session, connector, packet.getFormData()); } @Override public boolean handle(MovePlayerPacket packet) { connector.getLogger().debug("Handled packet: " + packet.getClass().getSimpleName()); if (!session.isLoggedIn()) { - AuthenticationUtils.showLoginWindow(session); + LoginEncryptionUtils.showLoginWindow(session); return true; } return false; diff --git a/connector/src/main/java/org/geysermc/connector/utils/AuthenticationUtils.java b/connector/src/main/java/org/geysermc/connector/utils/AuthenticationUtils.java deleted file mode 100644 index 226dcb7fc..000000000 --- a/connector/src/main/java/org/geysermc/connector/utils/AuthenticationUtils.java +++ /dev/null @@ -1,139 +0,0 @@ -package org.geysermc.connector.utils; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.JsonNodeType; -import com.nimbusds.jose.JWSObject; -import com.nukkitx.protocol.bedrock.packet.LoginPacket; -import com.nukkitx.protocol.bedrock.packet.ServerToClientHandshakePacket; -import com.nukkitx.protocol.bedrock.util.EncryptionUtils; -import net.minidev.json.JSONObject; -import org.geysermc.api.events.player.PlayerFormResponseEvent; -import org.geysermc.api.window.CustomFormBuilder; -import org.geysermc.api.window.CustomFormWindow; -import org.geysermc.api.window.FormWindow; -import org.geysermc.api.window.component.InputComponent; -import org.geysermc.api.window.component.LabelComponent; -import org.geysermc.api.window.response.CustomFormResponse; -import org.geysermc.connector.GeyserConnector; -import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.network.session.auth.BedrockAuthData; -import org.geysermc.connector.network.session.cache.WindowCache; - -import javax.crypto.SecretKey; -import java.io.IOException; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.PublicKey; -import java.security.interfaces.ECPublicKey; -import java.security.spec.ECGenParameterSpec; -import java.util.UUID; - -public class AuthenticationUtils { - public static void encryptPlayerConnection(GeyserConnector connector, GeyserSession session, LoginPacket loginPacket) { - JsonNode certData; - try { - certData = LoginEncryptionUtils.JSON_MAPPER.readTree(loginPacket.getChainData().toByteArray()); - } catch (IOException ex) { - throw new RuntimeException("Certificate JSON can not be read."); - } - - JsonNode certChainData = certData.get("chain"); - if (certChainData.getNodeType() != JsonNodeType.ARRAY) { - throw new RuntimeException("Certificate data is not valid"); - } - - encryptConnectionWithCert(connector, session, loginPacket.getSkinData().toString(), certData); - } - - private static void encryptConnectionWithCert(GeyserConnector connector, GeyserSession session, String playerSkin, JsonNode certChainData) { - try { - boolean validChain = LoginEncryptionUtils.validateChainData(certChainData); - - connector.getLogger().debug(String.format("Is player data valid? %s", validChain)); - - JWSObject jwt = JWSObject.parse(certChainData.get(certChainData.size() - 1).asText()); - JsonNode payload = LoginEncryptionUtils.JSON_MAPPER.readTree(jwt.getPayload().toBytes()); - - if (payload.get("extraData").getNodeType() != JsonNodeType.OBJECT) { - throw new RuntimeException("AuthData was not found!"); - } - - JSONObject extraData = (JSONObject) jwt.getPayload().toJSONObject().get("extraData"); - session.setAuthenticationData(new BedrockAuthData(extraData.getAsString("displayName"), UUID.fromString(extraData.getAsString("identity")), extraData.getAsString("XUID"))); - - if (payload.get("identityPublicKey").getNodeType() != JsonNodeType.STRING) { - throw new RuntimeException("Identity Public Key was not found!"); - } - - ECPublicKey identityPublicKey = EncryptionUtils.generateKey(payload.get("identityPublicKey").textValue()); - JWSObject clientJwt = JWSObject.parse(playerSkin); - EncryptionUtils.verifyJwt(clientJwt, identityPublicKey); - - if (EncryptionUtils.canUseEncryption()) { - AuthenticationUtils.startEncryptionHandshake(session, identityPublicKey); - } - } catch (Exception ex) { - session.disconnect("disconnectionScreen.internalError.cantConnect"); - throw new RuntimeException("Unable to complete login", ex); - } - } - - private static void startEncryptionHandshake(GeyserSession session, PublicKey key) throws Exception { - KeyPairGenerator generator = KeyPairGenerator.getInstance("EC"); - generator.initialize(new ECGenParameterSpec("secp384r1")); - KeyPair serverKeyPair = generator.generateKeyPair(); - - byte[] token = EncryptionUtils.generateRandomToken(); - SecretKey encryptionKey = EncryptionUtils.getSecretKey(serverKeyPair.getPrivate(), key, token); - session.getUpstream().enableEncryption(encryptionKey); - - ServerToClientHandshakePacket packet = new ServerToClientHandshakePacket(); - packet.setJwt(EncryptionUtils.createHandshakeJwt(serverKeyPair, token).serialize()); - session.getUpstream().sendPacketImmediately(packet); - } - - private static int AUTH_FORM_ID = 1337; - - public static void showLoginWindow(GeyserSession session) { - CustomFormWindow window = new CustomFormBuilder("Login") - .addComponent(new LabelComponent("Minecraft: Java Edition account authentication.")) - .addComponent(new LabelComponent("Enter the credentials for your Minecraft: Java Edition account below.")) - .addComponent(new InputComponent("Email/Username", "account@geysermc.org", "")) - .addComponent(new InputComponent("Password", "123456", "")) - .build(); - - session.sendForm(window, AUTH_FORM_ID); - } - - public static boolean authenticateFromForm(GeyserSession session, GeyserConnector connector, String formData) { - WindowCache windowCache = session.getWindowCache(); - if (!windowCache.getWindows().containsKey(AUTH_FORM_ID)) - return false; - - FormWindow window = windowCache.getWindows().remove(AUTH_FORM_ID); - window.setResponse(formData.trim()); - - if (session.isLoggedIn()) { - PlayerFormResponseEvent event = new PlayerFormResponseEvent(session, AUTH_FORM_ID, window); - connector.getPluginManager().runEvent(event); - } else { - if (window instanceof CustomFormWindow) { - CustomFormWindow customFormWindow = (CustomFormWindow) window; - if (!customFormWindow.getTitle().equals("Login")) - return false; - - CustomFormResponse response = (CustomFormResponse) customFormWindow.getResponse(); - String username = response.getInputResponses().get(2); - String password = response.getInputResponses().get(3); - - session.authenticate(username, password); - - // TODO should we clear the window cache in all cases or just if not already logged in? - // Clear windows so authentication data isn't accidentally cached - windowCache.getWindows().clear(); - } - } - return true; - } - -} 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 47584fcc2..868d7b490 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java @@ -6,15 +6,35 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.JsonNodeType; import com.nimbusds.jose.JWSObject; import com.nukkitx.network.util.Preconditions; +import com.nukkitx.protocol.bedrock.packet.LoginPacket; +import com.nukkitx.protocol.bedrock.packet.ServerToClientHandshakePacket; import com.nukkitx.protocol.bedrock.util.EncryptionUtils; +import net.minidev.json.JSONObject; +import org.geysermc.api.events.player.PlayerFormResponseEvent; +import org.geysermc.api.window.CustomFormBuilder; +import org.geysermc.api.window.CustomFormWindow; +import org.geysermc.api.window.FormWindow; +import org.geysermc.api.window.component.InputComponent; +import org.geysermc.api.window.component.LabelComponent; +import org.geysermc.api.window.response.CustomFormResponse; +import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.session.auth.BedrockAuthData; +import org.geysermc.connector.network.session.cache.WindowCache; +import javax.crypto.SecretKey; +import java.io.IOException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PublicKey; import java.security.interfaces.ECPublicKey; +import java.security.spec.ECGenParameterSpec; +import java.util.UUID; public class LoginEncryptionUtils { + private static final ObjectMapper JSON_MAPPER = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - public static final ObjectMapper JSON_MAPPER = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - - public static boolean validateChainData(JsonNode data) throws Exception { + private static boolean validateChainData(JsonNode data) throws Exception { ECPublicKey lastKey = null; boolean validChain = false; for (JsonNode node : data) { @@ -35,4 +55,112 @@ public class LoginEncryptionUtils { } return validChain; } + + public static void encryptPlayerConnection(GeyserConnector connector, GeyserSession session, LoginPacket loginPacket) { + JsonNode certData; + try { + certData = JSON_MAPPER.readTree(loginPacket.getChainData().toByteArray()); + } catch (IOException ex) { + throw new RuntimeException("Certificate JSON can not be read."); + } + + JsonNode certChainData = certData.get("chain"); + if (certChainData.getNodeType() != JsonNodeType.ARRAY) { + throw new RuntimeException("Certificate data is not valid"); + } + + encryptConnectionWithCert(connector, session, loginPacket.getSkinData().toString(), certData); + } + + private static void encryptConnectionWithCert(GeyserConnector connector, GeyserSession session, String playerSkin, JsonNode certChainData) { + try { + boolean validChain = validateChainData(certChainData); + + connector.getLogger().debug(String.format("Is player data valid? %s", validChain)); + + JWSObject jwt = JWSObject.parse(certChainData.get(certChainData.size() - 1).asText()); + JsonNode payload = JSON_MAPPER.readTree(jwt.getPayload().toBytes()); + + if (payload.get("extraData").getNodeType() != JsonNodeType.OBJECT) { + throw new RuntimeException("AuthData was not found!"); + } + + JSONObject extraData = (JSONObject) jwt.getPayload().toJSONObject().get("extraData"); + session.setAuthenticationData(new BedrockAuthData(extraData.getAsString("displayName"), UUID.fromString(extraData.getAsString("identity")), extraData.getAsString("XUID"))); + + if (payload.get("identityPublicKey").getNodeType() != JsonNodeType.STRING) { + throw new RuntimeException("Identity Public Key was not found!"); + } + + ECPublicKey identityPublicKey = EncryptionUtils.generateKey(payload.get("identityPublicKey").textValue()); + JWSObject clientJwt = JWSObject.parse(playerSkin); + EncryptionUtils.verifyJwt(clientJwt, identityPublicKey); + + if (EncryptionUtils.canUseEncryption()) { + LoginEncryptionUtils.startEncryptionHandshake(session, identityPublicKey); + } + } catch (Exception ex) { + session.disconnect("disconnectionScreen.internalError.cantConnect"); + throw new RuntimeException("Unable to complete login", ex); + } + } + + private static void startEncryptionHandshake(GeyserSession session, PublicKey key) throws Exception { + KeyPairGenerator generator = KeyPairGenerator.getInstance("EC"); + generator.initialize(new ECGenParameterSpec("secp384r1")); + KeyPair serverKeyPair = generator.generateKeyPair(); + + byte[] token = EncryptionUtils.generateRandomToken(); + SecretKey encryptionKey = EncryptionUtils.getSecretKey(serverKeyPair.getPrivate(), key, token); + session.getUpstream().enableEncryption(encryptionKey); + + ServerToClientHandshakePacket packet = new ServerToClientHandshakePacket(); + packet.setJwt(EncryptionUtils.createHandshakeJwt(serverKeyPair, token).serialize()); + session.getUpstream().sendPacketImmediately(packet); + } + + private static int AUTH_FORM_ID = 1337; + + public static void showLoginWindow(GeyserSession session) { + CustomFormWindow window = new CustomFormBuilder("Login") + .addComponent(new LabelComponent("Minecraft: Java Edition account authentication.")) + .addComponent(new LabelComponent("Enter the credentials for your Minecraft: Java Edition account below.")) + .addComponent(new InputComponent("Email/Username", "account@geysermc.org", "")) + .addComponent(new InputComponent("Password", "123456", "")) + .build(); + + session.sendForm(window, AUTH_FORM_ID); + } + + public static boolean authenticateFromForm(GeyserSession session, GeyserConnector connector, String formData) { + WindowCache windowCache = session.getWindowCache(); + if (!windowCache.getWindows().containsKey(AUTH_FORM_ID)) + return false; + + FormWindow window = windowCache.getWindows().remove(AUTH_FORM_ID); + window.setResponse(formData.trim()); + + if (session.isLoggedIn()) { + PlayerFormResponseEvent event = new PlayerFormResponseEvent(session, AUTH_FORM_ID, window); + connector.getPluginManager().runEvent(event); + } else { + if (window instanceof CustomFormWindow) { + CustomFormWindow customFormWindow = (CustomFormWindow) window; + if (!customFormWindow.getTitle().equals("Login")) + return false; + + CustomFormResponse response = (CustomFormResponse) customFormWindow.getResponse(); + String username = response.getInputResponses().get(2); + String password = response.getInputResponses().get(3); + + session.authenticate(username, password); + + // TODO should we clear the window cache in all cases or just if not already logged in? + // Clear windows so authentication data isn't accidentally cached + windowCache.getWindows().clear(); + } + } + return true; + } + }