From fdc2772f38c05f9f35168e7fc3960ebb2396bc1d Mon Sep 17 00:00:00 2001 From: games647 Date: Tue, 3 Nov 2015 17:44:57 +0100 Subject: [PATCH] Update description --- README.md | 116 +++++++++++++++++- pom.xml | 2 +- .../{Encryption.java => EncryptionUtil.java} | 28 ++--- .../github/games647/fastlogin/FastLogin.java | 4 +- .../listener/EncryptionPacketListener.java | 114 +++++++++-------- .../listener/StartPacketListener.java | 9 +- 6 files changed, 196 insertions(+), 77 deletions(-) rename src/main/java/com/github/games647/fastlogin/{Encryption.java => EncryptionUtil.java} (85%) diff --git a/README.md b/README.md index 22e8b52e..cf07e25a 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,125 @@ # FastLogin -Checks if a minecraft player has a valid premium (paid account). If so, they can skip offline authentication. +Checks if a minecraft player has a paid account (premium). If so, they can skip offline authentication (auth plugins). +So they don't need to enter passwords. This is also called auto login. + +###Features: +* Detect paid accounts from others +* Automatically login paid accounts (premium) +* Support various of auth plugins +* Experimental Cauldron support + +*** ###Commands: -* /premium Marks the invoker as paid account -* /premium [playername] Mark player specified as a paid account +* /premium Label the invoker as paid account +* /premium [playername] Label specified player as a paid account -###Premissions: +###Permissions: * fastlogin.command.premium * fastlogin.command.premium.others ###Requirements: -* [ProtocolLib](http://www.spigotmc.org/resources/protocollib.1997/) -* Tested Bukkit 1.8.8 (could also work with other versions) +* Plugin: [ProtocolLib](http://www.spigotmc.org/resources/protocollib.1997/) +* Tested Bukkit/[Spigot](https://www.spigotmc.org) 1.8.8 (could also work with other versions) * Java 7 or above +* Run in offline mode (see server.properties) * An auth plugin. Supported Plugins: * [AuthMe](http://dev.bukkit.org/bukkit-plugins/authme-reloaded/) * [xAuth](http://dev.bukkit.org/bukkit-plugins/xauth/) * [CrazyLogin](http://dev.bukkit.org/bukkit-plugins/crazylogin/) * [LoginSecurity](http://dev.bukkit.org/bukkit-plugins/loginsecurity/) + +###Downloads + +https://github.com/games647/FastLogin/releases + +*** + +###FAQ + +####Index +1. [How does minecraft logins work?](#how-does-minecraft-logins-work) +2. [How does this plugin work?](#how-does-this-plugin-work) +3. [Why does the plugin require offline mode?](#why-does-the-plugin-require-offline-mode) +4. [Can cracked player join with premium usernames?](#can-cracked-player-join-with-premium-usernames) +5. [Why do players have to invoke a command?](#why-do-players-have-to-invoke-a-command) +6. [What happens if a paid account joins with a used username?](#what-happens-if-a-paid-account-joins-with-a-used-username) +7. [Does the plugin have BungeeCord support?](#does-the-plugin-have-bungeecord-support) +8. [Could premium players have a premium UUID and Skin?](#could-premium-players-have-a-premium-uuid-and-skin) +9. [Is this plugin compatible with Cauldron?](#is-this-plugin-compatible-with-cauldron) + +####How does minecraft logins work? +######Online Mode +1. Client -> Server: I want to login, here is my username +2. Server -> Client: Okay. I'm in online mode so here is my public key for encryption and my serverid +3. Client -> Mojang: I'm player "xyz". I want to join a server with that serverid +4. Mojang -> Client: Session data checked. You can continue +5. Client -> Server: I received a successful response from Mojang. Heres our shared secret key +6. Server -> Mojang: Does the player "xyz" with this shared secret key has a valid account to join me? +7. Client and Server: encrypt all following communication packet +8. Server -> Client: Everything checked you can play now + + +######Offline Mode +In offline mode step 2-7 is skipped. So a login request is directly followed by 8. + +######More details +http://wiki.vg/Protocol#Login + +####How does this plugin work? +By using ProtocolLib, this plugin works as a proxy between the client and server. This plugin will fake that the server +runs in online mode. It does everything an online mode server would do. This will be for example, generating keys or +checking for valid sessions. Because everything is the same compared to an offline mode login after an encrypted +connection, we will intercept only **login** packets of **premium** players. + +1. Player is connecting to the server. +2. Plugin checks if the username we received activated the fast login method (i.e. using command) +3. Run a check if the username is currently used by a paid account. +(We don't know yet if the client connecting is premium) +4. Request an Mojang Session Server authentication +5. On response check if all data is correct +6. Encrypt the connection +7. On success intercept all related login packets and fake a new login packet as a normal offline login + +####Why does the plugin require offline mode? +1. As you can see in the question "how does minecraft login works", offline mode is equivalent to online mode except of +the encryption and session checks on login. So we can intercept and cancel the first packets for premium players and +enable an encrypted connection. Then we send a new fake packet in order to pretend that this a new login request from +a offline mode player. The server will handle the rest. +2. Some plugins check if the server is in online mode. If so, they could process the real offline (cracked) accounts +incorrectly. For example, a plugin tries to fetch the UUID from Mojang, but the name of the player is not associated to +a paid account. +3. Servers, who allow cracked players and just speed up logins for premium players, are **already** in offline mode. + +####Can cracked player join with premium usernames? +Yes, indeed. Therefore the command for toggling the fast login method exists. + +####Why do players have to invoke a command? +1. It's a secure way to make sure a person with a paid account cannot steal the account +of a cracked player that has the same username. +2. We only receive the username from the player on login. We could check if that username is associated +to a paid account but if we request a online mode login from a cracked player (who uses a username from +a paid account), the player will disconnect with the reason bad login. There is no way to change that message +on the server side (without client modifications), because it's a connection between the Client and the Sessionserver. + +###What happens if a paid account joins with a used username? +The player on the server have to activate the feature of this plugin by command. If a person buys the username +of his own account, it's still secured. A normal offline mode login makes sure he's the owner of the server account +and Mojang account. Then the command can be executed. So someone different cannot steal the account of cracked player +by buying the username. + +####Does the plugin have BungeeCord support? +Not yet, but I'm planning this. + +####Could premium players have a premium UUID and Skin? +Something like that is possible, but is not yet implemented. + +####Is this plugin compatible with Cauldron? +It's not yet tested, but all needed methods also exists in Cauldron so it could work together + +*** + +###Useful Links: +* [Login Protocol](http://wiki.vg/Protocol#Login) +* [Protocol Encryption](http://wiki.vg/Protocol_Encryption) \ No newline at end of file diff --git a/pom.xml b/pom.xml index b8558d1d..b158604c 100644 --- a/pom.xml +++ b/pom.xml @@ -126,7 +126,7 @@ fr.xephi authme - 5.0-SNAPSHOT + 5.1-SNAPSHOT true diff --git a/src/main/java/com/github/games647/fastlogin/Encryption.java b/src/main/java/com/github/games647/fastlogin/EncryptionUtil.java similarity index 85% rename from src/main/java/com/github/games647/fastlogin/Encryption.java rename to src/main/java/com/github/games647/fastlogin/EncryptionUtil.java index 44f41c50..8c1d7093 100644 --- a/src/main/java/com/github/games647/fastlogin/Encryption.java +++ b/src/main/java/com/github/games647/fastlogin/EncryptionUtil.java @@ -31,23 +31,23 @@ import javax.crypto.spec.SecretKeySpec; * * Remapped by: https://github.com/Techcable/MinecraftMappings/tree/master/1.8 */ -public class Encryption { +public class EncryptionUtil { public static KeyPair generateKeyPair() { try { - KeyPairGenerator keypairgenerator = KeyPairGenerator.getInstance("RSA"); + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); - keypairgenerator.initialize(1024); - return keypairgenerator.generateKeyPair(); + keyPairGenerator.initialize(1024); + return keyPairGenerator.generateKeyPair(); } catch (NoSuchAlgorithmException nosuchalgorithmexception) { //Should be existing in every vm - return null; + throw new ExceptionInInitializerError(nosuchalgorithmexception); } } - public static byte[] getServerIdHash(String serverId, PublicKey publickey, SecretKey secretkey) { + public static byte[] getServerIdHash(String serverId, PublicKey publicKey, SecretKey secretKey) { return digestOperation("SHA-1" - , new byte[][]{serverId.getBytes(Charsets.ISO_8859_1), secretkey.getEncoded(), publickey.getEncoded()}); + , new byte[][]{serverId.getBytes(Charsets.ISO_8859_1), secretKey.getEncoded(), publicKey.getEncoded()}); } private static byte[] digestOperation(String algo, byte[]... content) { @@ -66,24 +66,24 @@ public class Encryption { public static PublicKey decodePublicKey(byte[] encodedKey) { try { - X509EncodedKeySpec x509encodedkeyspec = new X509EncodedKeySpec(encodedKey); KeyFactory keyfactory = KeyFactory.getInstance("RSA"); + X509EncodedKeySpec x509encodedkeyspec = new X509EncodedKeySpec(encodedKey); return keyfactory.generatePublic(x509encodedkeyspec); } catch (NoSuchAlgorithmException | InvalidKeySpecException nosuchalgorithmexception) { - ; + //ignore } System.err.println("Public key reconstitute failed!"); return null; } - public static SecretKey decryptSharedKey(PrivateKey privatekey, byte[] encryptedSharedKey) { - return new SecretKeySpec(decryptData(privatekey, encryptedSharedKey), "AES"); + public static SecretKey decryptSharedKey(PrivateKey privateKey, byte[] encryptedSharedKey) { + return new SecretKeySpec(decryptData(privateKey, encryptedSharedKey), "AES"); } - public static byte[] decryptData(Key key, byte[] abyte) { - return cipherOperation(Cipher.DECRYPT_MODE, key, abyte); + public static byte[] decryptData(Key key, byte[] data) { + return cipherOperation(Cipher.DECRYPT_MODE, key, data); } private static byte[] cipherOperation(int operationMode, Key key, byte[] data) { @@ -122,7 +122,7 @@ public class Encryption { } } - private Encryption() { + private EncryptionUtil() { //utility } } diff --git a/src/main/java/com/github/games647/fastlogin/FastLogin.java b/src/main/java/com/github/games647/fastlogin/FastLogin.java index ad85e256..d2c61c16 100644 --- a/src/main/java/com/github/games647/fastlogin/FastLogin.java +++ b/src/main/java/com/github/games647/fastlogin/FastLogin.java @@ -33,7 +33,7 @@ public class FastLogin extends JavaPlugin { private static final String USER_AGENT = "Premium-Checker"; //provide a immutable key pair to be thread safe | used for encrypting and decrypting traffic - private final KeyPair keyPair = Encryption.generateKeyPair(); + private final KeyPair keyPair = EncryptionUtil.generateKeyPair(); //we need a thread-safe set because we access it async in the packet listener private final Set enabledPremium = Sets.newConcurrentHashSet(); @@ -102,7 +102,7 @@ public class FastLogin extends JavaPlugin { * * @return the server KeyPair */ - public KeyPair getKeyPair() { + public KeyPair getServerKey() { return keyPair; } diff --git a/src/main/java/com/github/games647/fastlogin/listener/EncryptionPacketListener.java b/src/main/java/com/github/games647/fastlogin/listener/EncryptionPacketListener.java index 53364152..8636f0b5 100644 --- a/src/main/java/com/github/games647/fastlogin/listener/EncryptionPacketListener.java +++ b/src/main/java/com/github/games647/fastlogin/listener/EncryptionPacketListener.java @@ -9,7 +9,7 @@ import com.comphenix.protocol.injector.server.TemporaryPlayerFactory; import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.wrappers.WrappedChatComponent; import com.comphenix.protocol.wrappers.WrappedGameProfile; -import com.github.games647.fastlogin.Encryption; +import com.github.games647.fastlogin.EncryptionUtil; import com.github.games647.fastlogin.FastLogin; import com.github.games647.fastlogin.PlayerSession; @@ -80,7 +80,6 @@ public class EncryptionPacketListener extends PacketAdapter { */ @Override public void onPacketReceiving(PacketEvent packetEvent) { - PacketContainer packet = packetEvent.getPacket(); Player player = packetEvent.getPlayer(); //the player name is unknown to ProtocolLib (so getName() doesn't work) - now uses ip:port as key @@ -88,48 +87,23 @@ public class EncryptionPacketListener extends PacketAdapter { PlayerSession session = plugin.getSessions().get(uniqueSessionKey); if (session == null) { disconnect(packetEvent, "Invalid request", Level.FINE - , "Player {0} tried to send encryption response on an invalid connection state" + , "Player {0} tried to send encryption response at invalid state" , player.getAddress()); return; } - byte[] sharedSecret = packet.getByteArrays().read(0); - //encrypted verify token - byte[] clientVerify = packet.getByteArrays().read(1); + PrivateKey privateKey = plugin.getServerKey().getPrivate(); - PrivateKey privateKey = plugin.getKeyPair().getPrivate(); - byte[] serverVerify = session.getVerifyToken(); - //https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L182 - if (!Arrays.equals(serverVerify, Encryption.decryptData(privateKey, clientVerify))) { - //check if the verify token are equal to the server sent one - disconnect(packetEvent, "Invalid token", Level.FINE - , "Player {0} ({1}) tried to login with an invalid verify token. " - + "Server: {2} Client: {3}" - , session.getUsername(), player.getAddress(), serverVerify, clientVerify); - return; - } - - SecretKey loginKey = Encryption.decryptSharedKey(privateKey, sharedSecret); - try { - //get the NMS connection handle of this player - Object networkManager = getNetworkManager(player); - - //try to detect the method by parameters - Method encryptConnectionMethod = FuzzyReflection.fromObject(networkManager) - .getMethodByParameters("a", SecretKey.class); - - //encrypt/decrypt following packets - //the client expects this behaviour - encryptConnectionMethod.invoke(networkManager, loginKey); - } catch (ReflectiveOperationException ex) { - disconnect(packetEvent, "Error occurred", Level.SEVERE, "Couldn't enable encryption", ex); + byte[] sharedSecret = packetEvent.getPacket().getByteArrays().read(0); + SecretKey loginKey = EncryptionUtil.decryptSharedKey(privateKey, sharedSecret); + if (!checkVerifyToken(session, privateKey, packetEvent) || !encryptConnection(player, loginKey, packetEvent)) { return; } //https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L193 //generate the server id based on client and server data - String serverId = (new BigInteger(Encryption.getServerIdHash("", plugin.getKeyPair().getPublic(), loginKey))) - .toString(16); + byte[] serverIdHash = EncryptionUtil.getServerIdHash("", plugin.getServerKey().getPublic(), loginKey); + String serverId = (new BigInteger(serverIdHash)).toString(16); String username = session.getUsername(); if (hasJoinedServer(username, serverId)) { @@ -144,9 +118,63 @@ public class EncryptionPacketListener extends PacketAdapter { , session.getUsername(), player.getAddress(), serverId); } + //this is a fake packet; it shouldn't be send to the server packetEvent.setCancelled(true); } + private boolean checkVerifyToken(PlayerSession session, PrivateKey privateKey, PacketEvent packetEvent) { + byte[] requestVerify = session.getVerifyToken(); + //encrypted verify token + byte[] responseVerify = packetEvent.getPacket().getByteArrays().read(1); + + //https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L182 + if (!Arrays.equals(requestVerify, EncryptionUtil.decryptData(privateKey, responseVerify))) { + //check if the verify token are equal to the server sent one + disconnect(packetEvent, "Invalid token", Level.FINE + , "Player {0} ({1}) tried to login with an invalid verify token. " + + "Server: {2} Client: {3}" + , session.getUsername(), packetEvent.getPlayer().getAddress(), requestVerify, responseVerify); + return false; + } + + return true; + } + + //try to get the networkManager from ProtocolLib + private Object getNetworkManager(Player player) + throws IllegalAccessException, NoSuchFieldException { + Object injector = TemporaryPlayerFactory.getInjectorFromPlayer(player); + Field injectorField = injector.getClass().getDeclaredField("injector"); + injectorField.setAccessible(true); + + Object rawInjector = injectorField.get(injector); + + injectorField = rawInjector.getClass().getDeclaredField("networkManager"); + injectorField.setAccessible(true); + return injectorField.get(rawInjector); + } + + private boolean encryptConnection(Player player, SecretKey loginKey, PacketEvent packetEvent) + throws IllegalArgumentException { + try { + //get the NMS connection handle of this player + Object networkManager = getNetworkManager(player); + + //try to detect the method by parameters + Method encryptConnectionMethod = FuzzyReflection.fromObject(networkManager) + .getMethodByParameters("a", SecretKey.class); + + //encrypt/decrypt following packets + //the client expects this behaviour + encryptConnectionMethod.invoke(networkManager, loginKey); + } catch (ReflectiveOperationException ex) { + disconnect(packetEvent, "Error occurred", Level.SEVERE, "Couldn't enable encryption", ex); + return false; + } + + return true; + } + private void disconnect(PacketEvent packetEvent, String kickReason, Level logLevel, String logMessage , Object... arguments) { plugin.getLogger().log(logLevel, logMessage, arguments); @@ -170,20 +198,6 @@ public class EncryptionPacketListener extends PacketAdapter { } } - //try to get the networkManager from ProtocolLib - private Object getNetworkManager(Player player) - throws SecurityException, IllegalAccessException, NoSuchFieldException { - Object injector = TemporaryPlayerFactory.getInjectorFromPlayer(player); - Field injectorField = injector.getClass().getDeclaredField("injector"); - injectorField.setAccessible(true); - - Object rawInjector = injectorField.get(injector); - - injectorField = rawInjector.getClass().getDeclaredField("networkManager"); - injectorField.setAccessible(true); - return injectorField.get(rawInjector); - } - private boolean hasJoinedServer(String username, String serverId) { try { String url = HAS_JOINED_URL + "username=" + username + "&serverId=" + serverId; @@ -192,7 +206,7 @@ public class EncryptionPacketListener extends PacketAdapter { BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); String line = reader.readLine(); - if (!line.equals("null")) { + if (!"null".equals(line)) { //validate parsing //http://wiki.vg/Protocol_Encryption#Server JSONObject userData = (JSONObject) JSONValue.parseWithException(line); @@ -220,7 +234,7 @@ public class EncryptionPacketListener extends PacketAdapter { //see StartPacketListener for packet information PacketContainer startPacket = protocolManager.createPacket(PacketType.Login.Client.START, true); - //uuid is ignored + //uuid is ignored by the packet definition WrappedGameProfile fakeProfile = new WrappedGameProfile(UUID.randomUUID(), username); startPacket.getGameProfiles().write(0, fakeProfile); try { diff --git a/src/main/java/com/github/games647/fastlogin/listener/StartPacketListener.java b/src/main/java/com/github/games647/fastlogin/listener/StartPacketListener.java index 57d7738d..72159517 100644 --- a/src/main/java/com/github/games647/fastlogin/listener/StartPacketListener.java +++ b/src/main/java/com/github/games647/fastlogin/listener/StartPacketListener.java @@ -68,7 +68,6 @@ public class StartPacketListener extends PacketAdapter { */ @Override public void onPacketReceiving(PacketEvent packetEvent) { - PacketContainer packet = packetEvent.getPacket(); Player player = packetEvent.getPlayer(); //this includes ip:port. Should be unique for an incoming login request with a timeout of 2 minutes @@ -78,17 +77,18 @@ public class StartPacketListener extends PacketAdapter { plugin.getSessions().remove(sessionKey); //player.getName() won't work at this state + PacketContainer packet = packetEvent.getPacket(); String username = packet.getGameProfiles().read(0).getName(); plugin.getLogger().log(Level.FINER, "Player {0} with {1} connecting to the server" , new Object[]{sessionKey, username}); - if (plugin.getEnabledPremium().contains(username) && isPremium(username)) { + if (plugin.getEnabledPremium().contains(username) && isPremiumName(username)) { //minecraft server implementation //https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L161 sentEncryptionRequest(sessionKey, username, player, packetEvent); } } - private boolean isPremium(String playerName) { + private boolean isPremiumName(String playerName) { //check if it's a valid playername if (playernameMatcher.matcher(playerName).matches()) { //only make a API call if the name is valid existing mojang account @@ -120,12 +120,13 @@ public class StartPacketListener extends PacketAdapter { PacketContainer newPacket = protocolManager .createPacket(PacketType.Login.Server.ENCRYPTION_BEGIN, true); - newPacket.getSpecificModifier(PublicKey.class).write(0, plugin.getKeyPair().getPublic()); + newPacket.getSpecificModifier(PublicKey.class).write(0, plugin.getServerKey().getPublic()); //generate a random token which should be the same when we receive it from the client byte[] verifyToken = new byte[VERIFY_TOKEN_LENGTH]; random.nextBytes(verifyToken); newPacket.getByteArrays().write(0, verifyToken); + //serverId is a empty string protocolManager.sendServerPacket(player, newPacket); //cancel only if the player has a paid account otherwise login as normal offline player