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