diff --git a/README.md b/README.md index 53059c80..e92630d5 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,5 @@ Requirements: * 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/) diff --git a/lib/CrazyCore v10.7.7.jar b/lib/CrazyCore v10.7.7.jar new file mode 100644 index 00000000..3330015c Binary files /dev/null and b/lib/CrazyCore v10.7.7.jar differ diff --git a/lib/CrazyLogin v7.23.2.jar b/lib/CrazyLogin v7.23.2.jar new file mode 100644 index 00000000..746c3216 Binary files /dev/null and b/lib/CrazyLogin v7.23.2.jar differ diff --git a/lib/LoginSecurity v2.0.10.jar b/lib/LoginSecurity v2.0.10.jar new file mode 100644 index 00000000..d394352f Binary files /dev/null and b/lib/LoginSecurity v2.0.10.jar differ diff --git a/pom.xml b/pom.xml index 40161e20..daf4e1e7 100644 --- a/pom.xml +++ b/pom.xml @@ -8,11 +8,11 @@ jar FastLogin - 0.1 + 0.2 2015 http://dev.bukkit.org/bukkit-plugins/fastlogin - Automatically logins premium player on a offline mode server + Automatically logins premium (paid accounts) player on a offline mode server @@ -43,7 +43,6 @@ maven-compiler-plugin 3.2 - 1.8 1.8 true @@ -110,7 +109,7 @@ org.spigotmc - spigot + spigot-api 1.8.8-R0.1-SNAPSHOT provided @@ -119,7 +118,7 @@ com.comphenix.protocol ProtocolLib - 3.6.3-SNAPSHOT + 3.6.5-SNAPSHOT true @@ -134,6 +133,7 @@ de.luricos.bukkit xAuth 2.6 + net.gravitydevelopment.updater @@ -145,5 +145,29 @@ + + + de.st_ddt.crazy + CrazyCore + 10.7.7 + system + ${project.basedir}/lib/CrazyCore v10.7.7.jar + + + + de.st_ddt.crazy + CrazyLogin + 7.23 + system + ${project.basedir}/lib/CrazyLogin v7.23.2.jar + + + + me.lenis0012.ls + LoginSecurity + 2.0.10 + system + ${project.basedir}/lib/LoginSecurity v2.0.10.jar + diff --git a/src/main/java/com/github/games647/fastlogin/Encryption.java b/src/main/java/com/github/games647/fastlogin/Encryption.java new file mode 100644 index 00000000..2555d8e4 --- /dev/null +++ b/src/main/java/com/github/games647/fastlogin/Encryption.java @@ -0,0 +1,135 @@ +package com.github.games647.fastlogin; + +import java.io.UnsupportedEncodingException; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * Source: + * https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/MinecraftEncryption.java + * + * Remapped by: + * https://github.com/Techcable/MinecraftMappings/tree/master/1.8 + */ +public class Encryption { + + public static KeyPair generateKeyPair() { + try { + KeyPairGenerator keypairgenerator = KeyPairGenerator.getInstance("RSA"); + + keypairgenerator.initialize(1024); + return keypairgenerator.generateKeyPair(); + } catch (NoSuchAlgorithmException nosuchalgorithmexception) { + //Should be existing in every vm + return null; + } + } + + public static byte[] getServerIdHash(String serverId, PublicKey publickey, SecretKey secretkey) { + try { + return digestOperation("SHA-1" + , new byte[][]{serverId.getBytes("ISO_8859_1"), secretkey.getEncoded(), publickey.getEncoded()}); + } catch (UnsupportedEncodingException unsupportedencodingexception) { + unsupportedencodingexception.printStackTrace(); + return null; + } + } + + private static byte[] digestOperation(String algo, byte[]... content) { + try { + MessageDigest messagedigest = MessageDigest.getInstance(algo); + int dataLength = content.length; + + for (int i = 0; i < dataLength; ++i) { + byte[] abyte1 = content[i]; + + messagedigest.update(abyte1); + } + + return messagedigest.digest(); + } catch (NoSuchAlgorithmException nosuchalgorithmexception) { + nosuchalgorithmexception.printStackTrace(); + return null; + } + } + + public static PublicKey decodePublicKey(byte[] encodedKey) { + try { + X509EncodedKeySpec x509encodedkeyspec = new X509EncodedKeySpec(encodedKey); + KeyFactory keyfactory = KeyFactory.getInstance("RSA"); + + return keyfactory.generatePublic(x509encodedkeyspec); + } catch (NoSuchAlgorithmException | InvalidKeySpecException nosuchalgorithmexception) { + ; + } + + 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 byte[] decryptData(Key key, byte[] abyte) { + return cipherOperation(Cipher.DECRYPT_MODE, key, abyte); + } + + private static byte[] cipherOperation(int operationMode, Key key, byte[] data) { + try { + return createCipherInstance(operationMode, key.getAlgorithm(), key).doFinal(data); + } catch (IllegalBlockSizeException | BadPaddingException illegalblocksizeexception) { + illegalblocksizeexception.printStackTrace(); + } + + System.err.println("Cipher data failed!"); + return null; + } + + private static Cipher createCipherInstance(int operationMode, String cipherName, Key key) { + try { + Cipher cipher = Cipher.getInstance(cipherName); + + cipher.init(operationMode, key); + return cipher; + } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException invalidkeyexception) { + invalidkeyexception.printStackTrace(); + } + + System.err.println("Cipher creation failed!"); + return null; + } + + public static Cipher createBufferedBlockCipher(int operationMode, Key key) { + try { + Cipher cipher = Cipher.getInstance("AES/CFB8/NoPadding"); + + cipher.init(operationMode, key, new IvParameterSpec(key.getEncoded())); + return cipher; + } catch (GeneralSecurityException generalsecurityexception) { + throw new RuntimeException(generalsecurityexception); + } + } + + private Encryption() { + //utility + } +} diff --git a/src/main/java/com/github/games647/fastlogin/FastLogin.java b/src/main/java/com/github/games647/fastlogin/FastLogin.java index 4b6f7a2c..c2903e4c 100644 --- a/src/main/java/com/github/games647/fastlogin/FastLogin.java +++ b/src/main/java/com/github/games647/fastlogin/FastLogin.java @@ -3,91 +3,144 @@ package com.github.games647.fastlogin; import com.github.games647.fastlogin.listener.PlayerListener; import com.comphenix.protocol.ProtocolLibrary; import com.comphenix.protocol.ProtocolManager; +import com.comphenix.protocol.utility.SafeCacheBuilder; +import com.github.games647.fastlogin.hooks.AuthPlugin; import com.github.games647.fastlogin.listener.EncryptionPacketListener; import com.github.games647.fastlogin.listener.StartPacketListener; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.reflect.ClassPath; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; - import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; +import java.util.logging.Level; import org.bukkit.plugin.java.JavaPlugin; public class FastLogin extends JavaPlugin { - private final KeyPair keyPair = generateKey(); - private final Cache session = CacheBuilder.newBuilder() + private static final int TIMEOUT = 15000; + private static final String USER_AGENT = "Premium-Checker"; + + //provide a immutable key pair to be thread safe + private final KeyPair keyPair = Encryption.generateKeyPair(); + //this map is thread-safe for async access (Packet Listener) + //SafeCacheBuilder is used in order to be version independent + private final ConcurrentMap session = SafeCacheBuilder.newBuilder() + //mapped by ip:port .expireAfterWrite(2, TimeUnit.MINUTES) - .build(); + //2 minutes should be enough as a timeout for bad internet connection (Server, Client and Mojang) + .build(new CacheLoader() { - @Override - public void onEnable() { - if (!isEnabled()) { - return; - } - - if (!getServer().getPluginManager().isPluginEnabled("AuthMe") - && !getServer().getPluginManager().isPluginEnabled("xAuth")) { - getLogger().warning("No support offline Auth plugin found. "); - getLogger().warning("Disabling this plugin..."); - - setEnabled(false); - return; - } - - ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager(); - protocolManager.addPacketListener(new EncryptionPacketListener(this, protocolManager)); - protocolManager.addPacketListener(new StartPacketListener(this, protocolManager)); - - getServer().getPluginManager().registerEvents(new PlayerListener(this), this); - } + @Override + public PlayerSession load(String key) throws Exception { + //A key should be inserted manually on start packet + throw new UnsupportedOperationException("Not supported"); + } + }); @Override public void onLoad() { - //online mode is only changeable aftter a restart + //online mode is only changeable after a restart so check it here if (getServer().getOnlineMode()) { getLogger().severe("Server have to be in offline mode"); setEnabled(false); } - - generateKey(); } - private KeyPair generateKey() { - try { - KeyPairGenerator keypairgenerator = KeyPairGenerator.getInstance("RSA"); - - keypairgenerator.initialize(1024); - return keypairgenerator.generateKeyPair(); - } catch (NoSuchAlgorithmException noSuchAlgorithmException) { - //Should be default existing in every vm + @Override + public void onEnable() { + if (!isEnabled() || !registerHooks()) { + return; } - return null; + //register packet listeners on success + ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager(); + protocolManager.addPacketListener(new EncryptionPacketListener(this, protocolManager)); + protocolManager.addPacketListener(new StartPacketListener(this, protocolManager)); } - public Cache getSession() { + @Override + public void onDisable() { + //clean up + session.clear(); + } + + /** + * Gets a thread-safe map about players which are connecting + * to the server are being checked to be premium (paid account) + * + * @return a thread-safe session map + */ + public ConcurrentMap getSessions() { return session; } + /** + * Gets the server KeyPair + * + * @return the server KeyPair + */ public KeyPair getKeyPair() { return keyPair; } + /** + * Prepares a Mojang API connection. The connection is not + * started in this method + * + * @param url the url connecting to + * @return the prepared connection + * + * @throws IOException on invalid url format or on {@link java.net.URL#openConnection() } + */ public HttpURLConnection getConnection(String url) throws IOException { - final HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); - connection.setConnectTimeout(15000); - connection.setReadTimeout(15000); + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + connection.setConnectTimeout(TIMEOUT); + connection.setReadTimeout(TIMEOUT); + //the new Mojang API just uses json as response connection.setRequestProperty("Content-Type", "application/json"); - connection.setRequestProperty("User-Agent", "Premium-Checker"); + connection.setRequestProperty("User-Agent", USER_AGENT); return connection; } + + private boolean registerHooks() { + AuthPlugin authPluginHook = null; + try { + String hooksPackage = this.getClass().getPackage().getName() + ".hooks"; + //Look through all classes in the hooks package and look for supporting plugins on the server + for (ClassPath.ClassInfo clazzInfo : ClassPath.from(getClassLoader()).getTopLevelClasses(hooksPackage)) { + //remove the hook suffix + String pluginName = clazzInfo.getSimpleName().replace("Hook", ""); + Class clazz = clazzInfo.load(); + //uses only member classes which uses AuthPlugin interface (skip interfaces) + if (AuthPlugin.class.isAssignableFrom(clazz) + && getServer().getPluginManager().isPluginEnabled(pluginName)) { + authPluginHook = (AuthPlugin) clazz.newInstance(); + getLogger().log(Level.INFO, "Hooking into auth plugin: {0}", pluginName); + break; + } + } + } catch (InstantiationException | IllegalAccessException | IOException ex) { + getLogger().log(Level.SEVERE, "Couldn't load the integration class", ex); + } + + if (authPluginHook == null) { + //run this check for exceptions and not found plugins + getLogger().warning("No support offline Auth plugin found. "); + getLogger().warning("Disabling this plugin..."); + + setEnabled(false); + return false; + } + + //We found a supporting plugin - we can now register a forwarding listener + getServer().getPluginManager().registerEvents(new PlayerListener(this, authPluginHook), this); + return true; + } } diff --git a/src/main/java/com/github/games647/fastlogin/PlayerData.java b/src/main/java/com/github/games647/fastlogin/PlayerData.java deleted file mode 100644 index aabe2ff6..00000000 --- a/src/main/java/com/github/games647/fastlogin/PlayerData.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.github.games647.fastlogin; - -public class PlayerData { - - private final byte[] verifyToken; - private final String username; - - public PlayerData(byte[] verifyToken, String username) { - this.username = username; - this.verifyToken = verifyToken; - } - - public byte[] getVerifyToken() { - return verifyToken; - } - - public String getUsername() { - return username; - } -} diff --git a/src/main/java/com/github/games647/fastlogin/PlayerSession.java b/src/main/java/com/github/games647/fastlogin/PlayerSession.java new file mode 100644 index 00000000..4b74f55e --- /dev/null +++ b/src/main/java/com/github/games647/fastlogin/PlayerSession.java @@ -0,0 +1,56 @@ +package com.github.games647.fastlogin; + +/** + * Represents a client connecting to the server. + * + * This session is invalid if the player disconnects or the login was successful + */ +public class PlayerSession { + + private final byte[] verifyToken; + private final String username; + private boolean verified; + + public PlayerSession(byte[] verifyToken, String username) { + this.username = username; + this.verifyToken = verifyToken; + } + + /** + * Gets the verify token the server sent to the client. + * + * @return the verify token from the server + */ + public byte[] getVerifyToken() { + return verifyToken; + } + + /** + * Gets the username the player sent to the server + * + * @return the client sent username + */ + public String getUsername() { + return username; + } + + /** + * Sets whether the player has a premium (paid account) account + * and valid session + * + * @param verified whether the player has valid session + */ + public synchronized void setVerified(boolean verified) { + this.verified = verified; + } + + /** + * Get whether the player has a premium (paid account) account + * and valid session + * + * @return whether the player has a valid session + */ + public synchronized boolean isVerified() { + return verified; + } +} diff --git a/src/main/java/com/github/games647/fastlogin/hooks/AuthMeHook.java b/src/main/java/com/github/games647/fastlogin/hooks/AuthMeHook.java new file mode 100644 index 00000000..33158df2 --- /dev/null +++ b/src/main/java/com/github/games647/fastlogin/hooks/AuthMeHook.java @@ -0,0 +1,25 @@ +package com.github.games647.fastlogin.hooks; + +import fr.xephi.authme.api.NewAPI; +import fr.xephi.authme.cache.limbo.LimboCache; + +import org.bukkit.entity.Player; + +/** + * Github: https://github.com/Xephi/AuthMeReloaded/ + * Project page: dev.bukkit.org/bukkit-plugins/authme-reloaded/ + */ +public class AuthMeHook implements AuthPlugin { + + @Override + public void forceLogin(Player player) { + //here is the gamemode, inventory ... saved + if (!LimboCache.getInstance().hasLimboPlayer(player.getName().toLowerCase())) { + //add cache entry - otherwise logging in wouldn't work + LimboCache.getInstance().addLimboPlayer(player); + } + + //skips registration and login + NewAPI.getInstance().forceLogin(player); + } +} diff --git a/src/main/java/com/github/games647/fastlogin/hooks/AuthPlugin.java b/src/main/java/com/github/games647/fastlogin/hooks/AuthPlugin.java new file mode 100644 index 00000000..73cf66d8 --- /dev/null +++ b/src/main/java/com/github/games647/fastlogin/hooks/AuthPlugin.java @@ -0,0 +1,16 @@ +package com.github.games647.fastlogin.hooks; + +import org.bukkit.entity.Player; + +/** + * Represents a supporting authentication plugin + */ +public interface AuthPlugin { + + /** + * Login the premium (paid account) player + * + * @param player the player that needs to be logged in + */ + void forceLogin(Player player); +} diff --git a/src/main/java/com/github/games647/fastlogin/hooks/CrazyLoginHook.java b/src/main/java/com/github/games647/fastlogin/hooks/CrazyLoginHook.java new file mode 100644 index 00000000..c632c9ae --- /dev/null +++ b/src/main/java/com/github/games647/fastlogin/hooks/CrazyLoginHook.java @@ -0,0 +1,31 @@ +package com.github.games647.fastlogin.hooks; + +import de.st_ddt.crazylogin.CrazyLogin; +import de.st_ddt.crazylogin.data.LoginPlayerData; +import de.st_ddt.crazylogin.databases.CrazyLoginDataDatabase; + +import org.bukkit.entity.Player; + +/** + * Github: https://github.com/ST-DDT/CrazyLogin + * Project page: http://dev.bukkit.org/server-mods/crazylogin/ + */ +public class CrazyLoginHook implements AuthPlugin { + + @Override + public void forceLogin(Player player) { + CrazyLogin crazyLoginPlugin = CrazyLogin.getPlugin(); + CrazyLoginDataDatabase crazyDatabase = crazyLoginPlugin.getCrazyDatabase(); + + LoginPlayerData playerData = crazyLoginPlugin.getPlayerData(player.getName()); + if (playerData == null) { + //create a fake account - this will be saved to the database with the password=FAILEDLOADING + //user cannot login with that password unless the admin uses plain text + playerData = new LoginPlayerData(player); + crazyDatabase.save(playerData); + } else { + //mark the account as logged in + playerData.setLoggedIn(true); + } + } +} diff --git a/src/main/java/com/github/games647/fastlogin/hooks/LoginSecurityHook.java b/src/main/java/com/github/games647/fastlogin/hooks/LoginSecurityHook.java new file mode 100644 index 00000000..aea19697 --- /dev/null +++ b/src/main/java/com/github/games647/fastlogin/hooks/LoginSecurityHook.java @@ -0,0 +1,29 @@ +package com.github.games647.fastlogin.hooks; + +import com.lenis0012.bukkit.ls.LoginSecurity; + +import org.bukkit.entity.Player; + +/** + * Github: http://dev.bukkit.org/bukkit-plugins/loginsecurity/ + * Project page: https://github.com/lenis0012/LoginSecurity-2 + * + * on join: https://github.com/lenis0012/LoginSecurity-2/blob/master/src/main/java/com/lenis0012/bukkit/ls/LoginSecurity.java#L282 + */ +public class LoginSecurityHook implements AuthPlugin { + + @Override + public void forceLogin(Player player) { + //Login command of this plugin: (How the plugin logs the player in) + //https://github.com/lenis0012/LoginSecurity-2/blob/master/src/main/java/com/lenis0012/bukkit/ls/commands/LoginCommand.java#L39 + LoginSecurity securityPlugin = LoginSecurity.instance; + String name = player.getName().toLowerCase(); + + //mark the user as logged in + securityPlugin.authList.remove(name); + //cancel timeout timer + securityPlugin.thread.timeout.remove(name); + //remove effects + securityPlugin.rehabPlayer(player, name); + } +} diff --git a/src/main/java/com/github/games647/fastlogin/hooks/xAuthHook.java b/src/main/java/com/github/games647/fastlogin/hooks/xAuthHook.java new file mode 100644 index 00000000..8a297424 --- /dev/null +++ b/src/main/java/com/github/games647/fastlogin/hooks/xAuthHook.java @@ -0,0 +1,36 @@ +package com.github.games647.fastlogin.hooks; + +import de.luricos.bukkit.xAuth.xAuth; +import de.luricos.bukkit.xAuth.xAuthPlayer; +import de.luricos.bukkit.xAuth.xAuthPlayer.Status; + +import java.sql.Timestamp; + +import org.bukkit.entity.Player; + +/** + * Github: https://github.com/LycanDevelopment/xAuth/ + * Project page: http://dev.bukkit.org/bukkit-plugins/xauth/ + */ +public class xAuthHook implements AuthPlugin { + + @Override + public void forceLogin(Player player) { + xAuth xAuthPlugin = xAuth.getPlugin(); + + xAuthPlayer xAuthPlayer = xAuthPlugin.getPlayerManager().getPlayer(player); + //we checked that the player is premium (paid account) + xAuthPlayer.setPremium(true); + //mark the player online + xAuthPlugin.getAuthClass(xAuthPlayer).online(xAuthPlayer.getName()); + + //update last login time + xAuthPlayer.setLoginTime(new Timestamp(System.currentTimeMillis())); + + //mark the player as logged in + xAuthPlayer.setStatus(Status.AUTHENTICATED); + + //restore inventory + xAuthPlugin.getPlayerManager().unprotect(xAuthPlayer); + } +} 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 75b43e60..8eaf70a8 100644 --- a/src/main/java/com/github/games647/fastlogin/listener/EncryptionPacketListener.java +++ b/src/main/java/com/github/games647/fastlogin/listener/EncryptionPacketListener.java @@ -7,15 +7,17 @@ import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.injector.server.SocketInjector; import com.comphenix.protocol.injector.server.TemporaryPlayerFactory; +import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.wrappers.WrappedGameProfile; +import com.github.games647.fastlogin.Encryption; import com.github.games647.fastlogin.FastLogin; -import com.github.games647.fastlogin.PlayerData; +import com.github.games647.fastlogin.PlayerSession; import java.io.BufferedReader; -import java.io.IOException; import java.io.InputStreamReader; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.math.BigInteger; import java.net.HttpURLConnection; import java.security.PrivateKey; @@ -24,29 +26,35 @@ import java.util.logging.Level; import javax.crypto.SecretKey; -import net.minecraft.server.v1_8_R3.MinecraftEncryption; -import net.minecraft.server.v1_8_R3.NetworkManager; - import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.json.simple.JSONObject; import org.json.simple.JSONValue; +/** + * Receiving packet information: + * http://wiki.vg/Protocol#Encryption_Response + * + * sharedSecret=encrypted byte array + * verify token=encrypted byte array + */ public class EncryptionPacketListener extends PacketAdapter { private static final String HAS_JOINED_URL = "https://sessionserver.mojang.com/session/minecraft/hasJoined?"; private final ProtocolManager protocolManager; - private final FastLogin fastLogin; + //hides the inherit Plugin plugin field, but we need this type + private final FastLogin plugin; public EncryptionPacketListener(FastLogin plugin, ProtocolManager protocolManger) { + //run async in order to not block the server, because we make api calls to Mojang super(params(plugin, PacketType.Login.Client.ENCRYPTION_BEGIN).optionAsync()); - this.fastLogin = plugin; + this.plugin = plugin; this.protocolManager = protocolManger; } - /* + /** * C->S : Handshake State=2 * C->S : Login Start * S->C : Encryption Key Request @@ -54,92 +62,138 @@ public class EncryptionPacketListener extends PacketAdapter { * C->S : Encryption Key Response * (Server Auth, Both enable encryption) * S->C : Login Success (*) + * + * On offline logins is Login Start followed by Login Success + * + * Minecraft Server implementation + * https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L180 */ @Override - public void onPacketReceiving(PacketEvent event) { - PacketContainer packet = event.getPacket(); - Player player = event.getPlayer(); + public void onPacketReceiving(PacketEvent packetEvent) { + PacketContainer packet = packetEvent.getPacket(); + Player player = packetEvent.getPlayer(); - final byte[] sharedSecret = packet.getByteArrays().read(0); + //the player name is unknown to ProtocolLib - now uses ip:port as key + String uniqueSessionKey = player.getAddress().toString(); + 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.getAddress()); + return; + } + + byte[] sharedSecret = packet.getByteArrays().read(0); byte[] clientVerify = packet.getByteArrays().read(1); - PrivateKey privateKey = fastLogin.getKeyPair().getPrivate(); - - String addressString = player.getAddress().toString(); - PlayerData cachedEntry = fastLogin.getSession().asMap().get(addressString); - byte[] serverVerify = cachedEntry.getVerifyToken(); - if (!Arrays.equals(serverVerify, MinecraftEncryption.b(privateKey, clientVerify))) { - player.kickPlayer("Invalid token"); - event.setCancelled(true); + 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; } - //encrypt all following packets - NetworkManager networkManager = getNetworkManager(event); - SecretKey loginKey = MinecraftEncryption.a(privateKey, sharedSecret); - networkManager.a(loginKey); - String serverId = (new BigInteger(MinecraftEncryption.a("", fastLogin.getKeyPair().getPublic(), loginKey))) + 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); + 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); - String username = cachedEntry.getUsername(); - if (!hasJoinedServer(username, serverId)) { + String username = session.getUsername(); + if (hasJoinedServer(username, serverId)) { + session.setVerified(true); + + receiveFakeStartPacket(username, player); + } else { //user tried to fake a authentification - player.kickPlayer("Invalid session"); - event.setCancelled(true); - return; + disconnect(packetEvent, "Invalid session", Level.FINE + , "Player {0} ({1}) tried to log in with an invalid session ServerId: {2}" + , session.getUsername(), player.getAddress(), serverId); } - //fake a new login packet - PacketContainer startPacket = protocolManager.createPacket(PacketType.Login.Client.START, true); - WrappedGameProfile fakeProfile = WrappedGameProfile.fromOfflinePlayer(Bukkit.getOfflinePlayer(username)); - startPacket.getGameProfiles().write(0, fakeProfile); - try { - protocolManager.recieveClientPacket(event.getPlayer(), startPacket, false); - } catch (InvocationTargetException | IllegalAccessException ex) { - plugin.getLogger().log(Level.WARNING, null, ex); - } - - event.setCancelled(true); + packetEvent.setCancelled(true); } - private NetworkManager getNetworkManager(PacketEvent event) throws IllegalArgumentException { - SocketInjector injector = TemporaryPlayerFactory.getInjectorFromPlayer(event.getPlayer()); - NetworkManager networkManager = null; - try { - Field declaredField = injector.getClass().getDeclaredField("injector"); - declaredField.setAccessible(true); + private void disconnect(PacketEvent packetEvent, String kickMessage, Level logLevel, String logMessage + , Object... arguments) { + plugin.getLogger().log(logLevel, logMessage, arguments); + packetEvent.getPlayer().kickPlayer(kickMessage); + //cancel the event in order to prevent the server receiving an invalid packet + packetEvent.setCancelled(true); + } - Object rawInjector = declaredField.get(injector); + private Object getNetworkManager(Player player) + throws SecurityException, IllegalAccessException, NoSuchFieldException { + SocketInjector injector = TemporaryPlayerFactory.getInjectorFromPlayer(player); + Field declaredField = injector.getClass().getDeclaredField("injector"); + declaredField.setAccessible(true); - declaredField = rawInjector.getClass().getDeclaredField("networkManager"); - declaredField.setAccessible(true); - networkManager = (NetworkManager) declaredField.get(rawInjector); - } catch (IllegalAccessException | NoSuchFieldException ex) { - plugin.getLogger().log(Level.WARNING, null, ex); - } + Object rawInjector = declaredField.get(injector); - return networkManager; + declaredField = rawInjector.getClass().getDeclaredField("networkManager"); + declaredField.setAccessible(true); + return declaredField.get(rawInjector); } private boolean hasJoinedServer(String username, String serverId) { try { String url = HAS_JOINED_URL + "username=" + username + "&serverId=" + serverId; - HttpURLConnection conn = fastLogin.getConnection(url); + HttpURLConnection conn = plugin.getConnection(url); BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); String line = reader.readLine(); if (!line.equals("null")) { - JSONObject object = (JSONObject) JSONValue.parse(line); + //validate parsing + JSONObject object = (JSONObject) JSONValue.parseWithException(line); String uuid = (String) object.get("id"); String name = (String) object.get("name"); return true; } - } catch (IOException ex) { - plugin.getLogger().log(Level.WARNING, null, ex); + } catch (Exception ex) { + //catch not only ioexceptions also parse and NPE on unexpected json format + plugin.getLogger().log(Level.WARNING, "Failed to verify if session is valid", ex); } return false; } + + private void receiveFakeStartPacket(String username, Player player) { + //fake a new login packet + //see StartPacketListener for packet information + PacketContainer startPacket = protocolManager.createPacket(PacketType.Login.Client.START, true); + + WrappedGameProfile fakeProfile = WrappedGameProfile.fromOfflinePlayer(Bukkit.getOfflinePlayer(username)); + startPacket.getGameProfiles().write(0, fakeProfile); + try { + protocolManager.recieveClientPacket(player, startPacket, false); + } catch (InvocationTargetException | IllegalAccessException ex) { + plugin.getLogger().log(Level.WARNING, "Failed to fake a new start packet", ex); + //cancel the event in order to prevent the server receiving an invalid packet + player.kickPlayer("Error occurred"); + } + } } diff --git a/src/main/java/com/github/games647/fastlogin/listener/PlayerListener.java b/src/main/java/com/github/games647/fastlogin/listener/PlayerListener.java index c8e816a7..9d886846 100644 --- a/src/main/java/com/github/games647/fastlogin/listener/PlayerListener.java +++ b/src/main/java/com/github/games647/fastlogin/listener/PlayerListener.java @@ -1,16 +1,10 @@ package com.github.games647.fastlogin.listener; import com.github.games647.fastlogin.FastLogin; -import com.github.games647.fastlogin.PlayerData; +import com.github.games647.fastlogin.PlayerSession; +import com.github.games647.fastlogin.hooks.AuthPlugin; -import de.luricos.bukkit.xAuth.xAuth; -import de.luricos.bukkit.xAuth.xAuthPlayer; -import de.luricos.bukkit.xAuth.xAuthPlayer.Status; - -import fr.xephi.authme.api.NewAPI; -import fr.xephi.authme.cache.limbo.LimboCache; - -import java.sql.Timestamp; +import java.util.logging.Level; import org.bukkit.Bukkit; import org.bukkit.entity.Player; @@ -21,9 +15,11 @@ import org.bukkit.event.player.PlayerJoinEvent; public class PlayerListener implements Listener { private final FastLogin plugin; + private final AuthPlugin authPlugin; - public PlayerListener(FastLogin plugin) { + public PlayerListener(FastLogin plugin, AuthPlugin authPlugin) { this.plugin = plugin; + this.authPlugin = authPlugin; } @EventHandler(ignoreCancelled = true) @@ -31,32 +27,17 @@ public class PlayerListener implements Listener { final Player player = joinEvent.getPlayer(); String address = player.getAddress().toString(); - PlayerData session = plugin.getSession().asMap().get(address); - if (session != null && session.getUsername().equals(player.getName())) { + PlayerSession session = plugin.getSessions().get(address); + //check if it's the same player as we checked before + if (session != null && session.getUsername().equals(player.getName()) + && session.isVerified()) { Bukkit.getScheduler().runTaskLater(plugin, () -> { - doLogin(player); + if (player.isOnline()) { + plugin.getLogger().log(Level.FINER, "Logging player {0} in", player.getName()); + authPlugin.forceLogin(player); + } + //Wait before auth plugin initializes the player }, 1 * 20L); } } - - private void doLogin(Player player) { - if (Bukkit.getPluginManager().isPluginEnabled("AuthMe")) { - //add cache entry - otherwise loggin wouldn't work - LimboCache.getInstance().addLimboPlayer(player); - - //skips registration and login - NewAPI.getInstance().forceLogin(player); - } else if (Bukkit.getPluginManager().isPluginEnabled("xAuth")) { - xAuth xAuthPlugin = xAuth.getPlugin(); - - xAuthPlayer xAuthPlayer = xAuthPlugin.getPlayerManager().getPlayer(player); - xAuthPlayer.setPremium(true); - xAuthPlugin.getAuthClass(xAuthPlayer).online(xAuthPlayer.getName()); - xAuthPlayer.setLoginTime(new Timestamp(System.currentTimeMillis())); - - xAuthPlayer.setStatus(Status.AUTHENTICATED); - - xAuthPlugin.getPlayerManager().unprotect(xAuthPlayer); - } - } } 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 b0f3dfb5..680f920b 100644 --- a/src/main/java/com/github/games647/fastlogin/listener/StartPacketListener.java +++ b/src/main/java/com/github/games647/fastlogin/listener/StartPacketListener.java @@ -6,7 +6,7 @@ import com.comphenix.protocol.events.PacketAdapter; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketEvent; import com.github.games647.fastlogin.FastLogin; -import com.github.games647.fastlogin.PlayerData; +import com.github.games647.fastlogin.PlayerSession; import java.io.IOException; import java.lang.reflect.InvocationTargetException; @@ -15,27 +15,40 @@ import java.security.PublicKey; import java.util.Random; import java.util.logging.Level; +import java.util.regex.Pattern; import org.bukkit.entity.Player; +/** + * Receiving packet information: + * http://wiki.vg/Protocol#Login_Start + * + * String=Username + */ public class StartPacketListener extends PacketAdapter { - //only premium members have a uuid from there + //only premium (paid account) users have a uuid from there private static final String UUID_LINK = "https://api.mojang.com/users/profiles/minecraft/"; + private static final String VALID_PLAYERNAME = "^\\w{2,16}$"; private final ProtocolManager protocolManager; - private final FastLogin fastLogin; + //hides the inherit Plugin plugin field, but we need a more detailed type than just Plugin + private final FastLogin plugin; + //just create a new once on plugin enable private final Random random = new Random(); + //compile the pattern on plugin enable + private final Pattern playernameMatcher = Pattern.compile(VALID_PLAYERNAME); public StartPacketListener(FastLogin plugin, ProtocolManager protocolManger) { + //run async in order to not block the server, because we make api calls to Mojang super(params(plugin, PacketType.Login.Client.START).optionAsync()); - this.fastLogin = plugin; + this.plugin = plugin; this.protocolManager = protocolManger; } - /* + /** * C->S : Handshake State=2 * C->S : Login Start * S->C : Encryption Key Request @@ -43,48 +56,72 @@ public class StartPacketListener extends PacketAdapter { * C->S : Encryption Key Response * (Server Auth, Both enable encryption) * S->C : Login Success (*) + * + * On offline logins is Login Start followed by Login Success */ @Override public void onPacketReceiving(PacketEvent packetEvent) { PacketContainer packet = packetEvent.getPacket(); Player player = packetEvent.getPlayer(); + //this includes ip and port. Should be unique for 2 Minutes + String sessionKey = player.getAddress().toString(); + + //remove old data every time on a new login in order to keep the session only for one person + plugin.getSessions().remove(sessionKey); + String username = packet.getGameProfiles().read(0).getName(); + plugin.getLogger().log(Level.FINER, "Player {0} with {1} connecting to the server" + , new Object[]{sessionKey, username}); + //do premium login process if (isPremium(username)) { - //do premium login process - try { - PacketContainer newPacket = protocolManager.createPacket(PacketType.Login.Server.ENCRYPTION_BEGIN, true); - - //constr ServerID="" - //public key=plugin.getPublic - newPacket.getSpecificModifier(PublicKey.class).write(0, fastLogin.getKeyPair().getPublic()); - byte[] verifyToken = new byte[4]; - random.nextBytes(verifyToken); - newPacket.getByteArrays().write(0, verifyToken); - - String addressString = player.getAddress().toString(); - fastLogin.getSession().asMap().put(addressString, new PlayerData(verifyToken, username)); - - protocolManager.sendServerPacket(player, newPacket, false); - } catch (InvocationTargetException ex) { - plugin.getLogger().log(Level.SEVERE, null, ex); - } - - //cancel only if the player is premium - packetEvent.setCancelled(true); + //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) { - try { - final HttpURLConnection connection = fastLogin.getConnection(UUID_LINK + playerName); - final int responseCode = connection.getResponseCode(); + if (playernameMatcher.matcher(playerName).matches()) { + //only make a API call if the name is valid existing mojang account + try { + HttpURLConnection connection = plugin.getConnection(UUID_LINK + playerName); + int responseCode = connection.getResponseCode(); - return responseCode == HttpURLConnection.HTTP_OK; - } catch (IOException ex) { - plugin.getLogger().log(Level.SEVERE, null, ex); + return responseCode == HttpURLConnection.HTTP_OK; + //204 - no content for not found + } catch (IOException ex) { + plugin.getLogger().log(Level.SEVERE, "Failed to check if player has a paid account", ex); + } } return false; } + + private void sentEncryptionRequest(String sessionKey, String username, Player player, PacketEvent packetEvent) { + try { + /** + * Packet Information: http://wiki.vg/Protocol#Encryption_Request + * + * ServerID="" (String) + * key=public server key + * verifyToken=random 4 byte array + */ + PacketContainer newPacket = protocolManager + .createPacket(PacketType.Login.Server.ENCRYPTION_BEGIN, true); + + newPacket.getSpecificModifier(PublicKey.class).write(0, plugin.getKeyPair().getPublic()); + byte[] verifyToken = new byte[4]; + random.nextBytes(verifyToken); + newPacket.getByteArrays().write(0, verifyToken); + + protocolManager.sendServerPacket(player, newPacket, false); + + //cancel only if the player has a paid account otherwise login as normal offline player + packetEvent.setCancelled(true); + plugin.getSessions().put(sessionKey, new PlayerSession(verifyToken, username)); + } catch (InvocationTargetException ex) { + plugin.getLogger().log(Level.SEVERE, "Cannot send encryption packet. Falling back to normal login", ex); + } + } } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 6a5457db..332912ed 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -11,4 +11,9 @@ description: | website: ${project.url} dev-url: ${project.url} -depend: [ProtocolLib] \ No newline at end of file +depend: [ProtocolLib] +softdepend: + - xAuth + - AuthMe + - CrazyLogin + - LoginSecurity