Compare commits

...

1 Commits

13 changed files with 8 additions and 815 deletions

View File

@ -6,8 +6,6 @@ import com.github.games647.fastlogin.core.shared.LoginSession;
import java.util.Optional;
import org.apache.commons.lang.ArrayUtils;
/**
* Represents a client connecting to the server.
*
@ -15,40 +13,21 @@ import org.apache.commons.lang.ArrayUtils;
*/
public class BukkitLoginSession extends LoginSession {
private final String serverId;
private final byte[] verifyToken;
private SkinProperty skinProperty;
private boolean verified;
private SkinProperty skinProperty;
public BukkitLoginSession(String username, String serverId, byte[] verifyToken, boolean registered
, StoredProfile profile) {
public BukkitLoginSession(String username, boolean registered, StoredProfile profile) {
super(username, registered, profile);
this.serverId = serverId;
this.verifyToken = ArrayUtils.clone(verifyToken);
}
//available for BungeeCord
public BukkitLoginSession(String username, boolean registered) {
this(username, "", ArrayUtils.EMPTY_BYTE_ARRAY, registered, null);
this(username, registered, null);
}
//cracked player
public BukkitLoginSession(String username, StoredProfile profile) {
this(username, "", ArrayUtils.EMPTY_BYTE_ARRAY, false, profile);
}
/**
* Gets the verify token the server sent to the client.
*
* Empty if it's a BungeeCord connection
*
* @return the verify token from the server
*/
public byte[] getVerifyToken() {
return ArrayUtils.clone(verifyToken);
this(username, false, profile);
}
/**

View File

@ -1,121 +0,0 @@
package com.github.games647.fastlogin.bukkit;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.Key;
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.util.Random;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
/**
* Encryption and decryption minecraft util for connection between servers
* and paid Minecraft account clients.
*
* @see net.minecraft.server.MinecraftEncryption
*/
public class EncryptionUtil {
public static final int VERIFY_TOKEN_LENGTH = 4;
public static final String KEY_PAIR_ALGORITHM = "RSA";
private EncryptionUtil() {
//utility
}
/**
* Generate a RSA key pair
*
* @return The RSA key pair.
*/
public static KeyPair generateKeyPair() {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_PAIR_ALGORITHM);
keyPairGenerator.initialize(1_024);
return keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException nosuchalgorithmexception) {
//Should be existing in every vm
throw new ExceptionInInitializerError(nosuchalgorithmexception);
}
}
/**
* Generate a random token. This is used to verify that we are communicating with the same player
* in a login session.
*
* @param random random generator
* @return an error with 4 bytes long
*/
public static byte[] generateVerifyToken(Random random) {
byte[] token = new byte[VERIFY_TOKEN_LENGTH];
random.nextBytes(token);
return token;
}
/**
* Generate the server id based on client and server data.
*
* @param sessionId session for the current login attempt
* @param sharedSecret shared secret between the client and the server
* @param publicKey public key of the server
* @return the server id formatted as a hexadecimal string.
*/
public static String getServerIdHashString(String sessionId, Key sharedSecret, PublicKey publicKey) {
try {
byte[] serverHash = getServerIdHash(sessionId, sharedSecret, publicKey);
return (new BigInteger(serverHash)).toString(16);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return "";
}
/**
* Decrypts the content and extracts the key spec.
*
* @param cipher decryption cipher
* @param privateKey private key of the server
* @param sharedKey the encrypted shared key
* @return shared secret key
* @throws GeneralSecurityException
*/
public static SecretKey decryptSharedKey(Cipher cipher, PrivateKey privateKey, byte[] sharedKey)
throws GeneralSecurityException {
return new SecretKeySpec(decrypt(cipher, privateKey, sharedKey), "AES");
}
/**
* Decrypted the given data using the cipher.
*
* @param cipher decryption cypher
* @param key server private key
* @param data the encrypted data
* @return clear text data
* @throws GeneralSecurityException if it fails to initialize and decrypt the data
*/
public static byte[] decrypt(Cipher cipher, PrivateKey key, byte[] data) throws GeneralSecurityException {
cipher.init(Cipher.DECRYPT_MODE, key);
return cipher.doFinal(data);
}
private static byte[] getServerIdHash(String sessionId, Key sharedSecret, PublicKey publicKey)
throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.update(sessionId.getBytes(StandardCharsets.ISO_8859_1));
digest.update(sharedSecret.getEncoded());
digest.update(publicKey.getEncoded());
return digest.digest();
}
}

View File

@ -4,8 +4,6 @@ import com.github.games647.fastlogin.bukkit.commands.CrackedCommand;
import com.github.games647.fastlogin.bukkit.commands.PremiumCommand;
import com.github.games647.fastlogin.bukkit.listener.BungeeListener;
import com.github.games647.fastlogin.bukkit.listener.ConnectionListener;
import com.github.games647.fastlogin.bukkit.listener.protocollib.ProtocolLibListener;
import com.github.games647.fastlogin.bukkit.listener.protocollib.SkinApplyListener;
import com.github.games647.fastlogin.bukkit.listener.protocolsupport.ProtocolSupportListener;
import com.github.games647.fastlogin.bukkit.tasks.DelayedAuthHook;
import com.github.games647.fastlogin.core.CommonUtil;
@ -35,7 +33,7 @@ import org.slf4j.Logger;
public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<CommandSender> {
//1 minutes should be enough as a timeout for bad internet connection (Server, Client and Mojang)
private final ConcurrentMap<String, BukkitLoginSession> loginSession = CommonUtil.buildCache(1, -1);
private final ConcurrentMap<String, BukkitLoginSession> loginSession = new ConcurrentHashMap<>();
private final Logger logger = CommonUtil.createLoggerFromJDK(getLogger());
private final Map<UUID, PremiumStatus> premiumPlayers = new ConcurrentHashMap<>();
@ -64,8 +62,6 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
PluginManager pluginManager = getServer().getPluginManager();
if (bungeeCord) {
setServerStarted();
//check for incoming messages from the bungeecord version of this plugin
getServer().getMessenger().registerIncomingPluginChannel(this, getName(), new BungeeListener(this));
getServer().getMessenger().registerOutgoingPluginChannel(this, getName());
@ -77,11 +73,8 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
if (pluginManager.isPluginEnabled("ProtocolSupport")) {
pluginManager.registerEvents(new ProtocolSupportListener(this), this);
} else if (pluginManager.isPluginEnabled("ProtocolLib")) {
ProtocolLibListener.register(this);
pluginManager.registerEvents(new SkinApplyListener(this), this);
} else {
logger.warn("Either ProtocolLib or ProtocolSupport have to be installed if you don't use BungeeCord");
logger.warn("ProtocolSupport have to be installed if you don't use BungeeCord");
}
}
@ -148,22 +141,6 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
return premiumPlayers.getOrDefault(onlinePlayer, PremiumStatus.UNKNOWN);
}
/**
* Wait before the server is fully started. This is workaround, because connections right on startup are not
* injected by ProtocolLib
*
* @return true if ProtocolLib can now intercept packets
*/
public boolean isServerFullyStarted() {
return serverStarted;
}
public void setServerStarted() {
if (!this.serverStarted) {
this.serverStarted = true;
}
}
public void sendPluginMessage(PluginMessageRecipient player, ChannelMessage message) {
if (player != null) {
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();

View File

@ -1,12 +1,9 @@
package com.github.games647.fastlogin.bukkit;
import java.util.List;
import me.clip.placeholderapi.PlaceholderAPI;
import me.clip.placeholderapi.PlaceholderHook;
import org.bukkit.entity.Player;
import org.bukkit.metadata.MetadataValue;
public class PremiumPlaceholder extends PlaceholderHook {
@ -19,16 +16,7 @@ public class PremiumPlaceholder extends PlaceholderHook {
@Override
public String onPlaceholderRequest(Player player, String variable) {
if (player != null && "fastlogin_status".contains(variable)) {
List<MetadataValue> metadata = player.getMetadata(plugin.getName());
if (metadata == null) {
return "unknown";
}
if (metadata.isEmpty()) {
return "cracked";
} else {
return "premium";
}
return plugin.getStatus(player.getUniqueId()).name();
}
return "";

View File

@ -6,11 +6,8 @@ import com.github.games647.fastlogin.bukkit.tasks.ForceLoginTask;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerLoginEvent.Result;
import org.bukkit.event.player.PlayerQuitEvent;
/**
@ -27,13 +24,6 @@ public class ConnectionListener implements Listener {
this.plugin = plugin;
}
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerLogin(PlayerLoginEvent loginEvent) {
if (loginEvent.getResult() == Result.ALLOWED && !plugin.isServerFullyStarted()) {
loginEvent.disallow(Result.KICK_OTHER, plugin.getCore().getMessage("not-started"));
}
}
@EventHandler(ignoreCancelled = true)
public void onPlayerJoin(PlayerJoinEvent joinEvent) {
Player player = joinEvent.getPlayer();

View File

@ -1,80 +0,0 @@
package com.github.games647.fastlogin.bukkit.listener.protocollib;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.events.PacketEvent;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.JoinManagement;
import java.security.PublicKey;
import java.util.Random;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
public class NameCheckTask extends JoinManagement<Player, CommandSender, ProtocolLibLoginSource>
implements Runnable {
private final FastLoginBukkit plugin;
private final PacketEvent packetEvent;
private final PublicKey publicKey;
private final Random random;
private final Player player;
private final String username;
public NameCheckTask(FastLoginBukkit plugin, PacketEvent packetEvent, Random random,
Player player, String username, PublicKey publicKey) {
super(plugin.getCore(), plugin.getCore().getAuthPluginHook());
this.plugin = plugin;
this.packetEvent = packetEvent;
this.publicKey = publicKey;
this.random = random;
this.player = player;
this.username = username;
}
@Override
public void run() {
try {
super.onLogin(username, new ProtocolLibLoginSource(packetEvent, player, random, publicKey));
} finally {
ProtocolLibrary.getProtocolManager().getAsynchronousManager().signalPacketTransmission(packetEvent);
}
}
//Minecraft server implementation
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L161
@Override
public void requestPremiumLogin(ProtocolLibLoginSource source, StoredProfile profile
, String username, boolean registered) {
try {
source.setOnlineMode();
} catch (Exception ex) {
plugin.getLog().error("Cannot send encryption packet. Falling back to cracked login for: {}", profile, ex);
return;
}
String ip = player.getAddress().getAddress().getHostAddress();
core.getPendingLogin().put(ip + username, new Object());
String serverId = source.getServerId();
byte[] verify = source.getVerifyToken();
BukkitLoginSession playerSession = new BukkitLoginSession(username, serverId, verify, registered, profile);
plugin.getLoginSessions().put(player.getAddress().toString(), playerSession);
//cancel only if the player has a paid account otherwise login as normal offline player
synchronized (packetEvent.getAsyncMarker().getProcessingLock()) {
packetEvent.setCancelled(true);
}
}
@Override
public void startCrackedSession(ProtocolLibLoginSource source, StoredProfile profile, String username) {
BukkitLoginSession loginSession = new BukkitLoginSession(username, profile);
plugin.getLoginSessions().put(player.getAddress().toString(), loginSession);
}
}

View File

@ -1,90 +0,0 @@
package com.github.games647.fastlogin.bukkit.listener.protocollib;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.github.games647.fastlogin.bukkit.EncryptionUtil;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import java.security.KeyPair;
import java.security.SecureRandom;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import static com.comphenix.protocol.PacketType.Login.Client.ENCRYPTION_BEGIN;
import static com.comphenix.protocol.PacketType.Login.Client.START;
public class ProtocolLibListener extends PacketAdapter {
private static final int WORKER_THREADS = 3;
private final FastLoginBukkit plugin;
//just create a new once on plugin enable. This used for verify token generation
private final SecureRandom random = new SecureRandom();
private final KeyPair keyPair = EncryptionUtil.generateKeyPair();
public ProtocolLibListener(FastLoginBukkit plugin) {
//run async in order to not block the server, because we are making api calls to Mojang
super(params()
.plugin(plugin)
.types(START, ENCRYPTION_BEGIN)
.optionAsync());
this.plugin = plugin;
}
public static void register(FastLoginBukkit plugin) {
//they will be created with a static builder, because otherwise it will throw a NoClassDefFoundError
ProtocolLibrary.getProtocolManager()
.getAsynchronousManager()
.registerAsyncHandler(new ProtocolLibListener(plugin))
.start(WORKER_THREADS);
}
@Override
public void onPacketReceiving(PacketEvent packetEvent) {
if (packetEvent.isCancelled()
|| plugin.getCore().getAuthPluginHook()== null
|| !plugin.isServerFullyStarted()) {
return;
}
Player sender = packetEvent.getPlayer();
PacketType packetType = packetEvent.getPacketType();
if (packetType == START) {
onLogin(packetEvent, sender);
} else {
onEncryptionBegin(packetEvent, sender);
}
}
private void onEncryptionBegin(PacketEvent packetEvent, Player sender) {
byte[] sharedSecret = packetEvent.getPacket().getByteArrays().read(0);
packetEvent.getAsyncMarker().incrementProcessingDelay();
Runnable verifyTask = new VerifyResponseTask(plugin, packetEvent, sender, sharedSecret, keyPair);
Bukkit.getScheduler().runTaskAsynchronously(plugin, verifyTask);
}
private void onLogin(PacketEvent packetEvent, Player player) {
//this includes ip:port. Should be unique for an incoming login request with a timeout of 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.getLoginSessions().remove(sessionKey);
//player.getName() won't work at this state
PacketContainer packet = packetEvent.getPacket();
String username = packet.getGameProfiles().read(0).getName();
plugin.getLog().trace("GameProfile {} with {} connecting", sessionKey, username);
packetEvent.getAsyncMarker().incrementProcessingDelay();
Runnable nameCheckTask = new NameCheckTask(plugin, packetEvent, random, player, username, keyPair.getPublic());
Bukkit.getScheduler().runTaskAsynchronously(plugin, nameCheckTask);
}
}

View File

@ -1,101 +0,0 @@
package com.github.games647.fastlogin.bukkit.listener.protocollib;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import com.github.games647.fastlogin.bukkit.EncryptionUtil;
import com.github.games647.fastlogin.core.shared.LoginSource;
import java.lang.reflect.InvocationTargetException;
import java.net.InetSocketAddress;
import java.security.PublicKey;
import java.util.Arrays;
import java.util.Random;
import org.apache.commons.lang.ArrayUtils;
import org.bukkit.entity.Player;
import static com.comphenix.protocol.PacketType.Login.Server.DISCONNECT;
import static com.comphenix.protocol.PacketType.Login.Server.ENCRYPTION_BEGIN;
public class ProtocolLibLoginSource implements LoginSource {
private final PacketEvent packetEvent;
private final Player player;
private final Random random;
private final PublicKey publicKey;
private final String serverId = "";
private byte[] verifyToken;
public ProtocolLibLoginSource(PacketEvent packetEvent, Player player, Random random, PublicKey publicKey) {
this.packetEvent = packetEvent;
this.player = player;
this.random = random;
this.publicKey = publicKey;
}
@Override
public void setOnlineMode() throws Exception {
verifyToken = EncryptionUtil.generateVerifyToken(random);
/*
* Packet Information: http://wiki.vg/Protocol#Encryption_Request
*
* ServerID="" (String) key=public server key verifyToken=random 4 byte array
*/
PacketContainer newPacket = new PacketContainer(ENCRYPTION_BEGIN);
newPacket.getStrings().write(0, serverId);
newPacket.getSpecificModifier(PublicKey.class).write(0, publicKey);
newPacket.getByteArrays().write(0, verifyToken);
//serverId is a empty string
ProtocolLibrary.getProtocolManager().sendServerPacket(player, newPacket);
}
@Override
public void kick(String message) throws InvocationTargetException {
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
PacketContainer kickPacket = new PacketContainer(DISCONNECT);
kickPacket.getChatComponents().write(0, WrappedChatComponent.fromText(message));
try {
//send kick packet at login state
//the normal event.getPlayer.kickPlayer(String) method does only work at play state
protocolManager.sendServerPacket(player, kickPacket);
} finally {
//tell the server that we want to close the connection
player.kickPlayer("Disconnect");
}
}
@Override
public InetSocketAddress getAddress() {
return packetEvent.getPlayer().getAddress();
}
public String getServerId() {
return serverId;
}
public byte[] getVerifyToken() {
return ArrayUtils.clone(verifyToken);
}
@Override
public String toString() {
return this.getClass().getSimpleName() + '{' +
"packetEvent=" + packetEvent +
", player=" + player +
", random=" + random +
", serverId='" + serverId + '\'' +
", verifyToken=" + Arrays.toString(verifyToken) +
'}';
}
}

View File

@ -1,70 +0,0 @@
package com.github.games647.fastlogin.bukkit.listener.protocollib;
import com.comphenix.protocol.reflect.MethodUtils;
import com.comphenix.protocol.reflect.accessors.Accessors;
import com.comphenix.protocol.reflect.accessors.MethodAccessor;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.wrappers.WrappedGameProfile;
import com.comphenix.protocol.wrappers.WrappedSignedProperty;
import com.github.games647.craftapi.model.skin.SkinProperty;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import java.lang.reflect.InvocationTargetException;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerLoginEvent.Result;
public class SkinApplyListener implements Listener {
private static final Class<?> GAME_PROFILE = MinecraftReflection.getGameProfileClass();
private static final MethodAccessor GET_PROPERTIES = Accessors.getMethodAccessor(GAME_PROFILE, "getProperties");
private final FastLoginBukkit plugin;
public SkinApplyListener(FastLoginBukkit plugin) {
this.plugin = plugin;
}
@EventHandler(priority = EventPriority.LOW)
//run this on the loginEvent to let skins plugins see the skin like in normal Minecraft behaviour
public void onPlayerLogin(PlayerLoginEvent loginEvent) {
if (loginEvent.getResult() != Result.ALLOWED) {
return;
}
Player player = loginEvent.getPlayer();
if (plugin.getConfig().getBoolean("forwardSkin")) {
//go through every session, because player.getAddress is null
//loginEvent.getAddress is just a InetAddress not InetSocketAddress, so not unique enough
for (BukkitLoginSession session : plugin.getLoginSessions().values()) {
if (session.getUsername().equals(player.getName())) {
session.getSkin().ifPresent(skin -> applySkin(player, skin.getValue(), skin.getSignature()));
break;
}
}
}
}
private void applySkin(Player player, String skinData, String signature) {
WrappedGameProfile gameProfile = WrappedGameProfile.fromPlayer(player);
WrappedSignedProperty skin = WrappedSignedProperty.fromValues(SkinProperty.TEXTURE_KEY, skinData, signature);
try {
gameProfile.getProperties().put(SkinProperty.TEXTURE_KEY, skin);
} catch (ClassCastException castException) {
//Cauldron, MCPC, Thermos, ...
Object map = GET_PROPERTIES.invoke(gameProfile.getHandle());
try {
MethodUtils.invokeMethod(map, "put", new Object[]{SkinProperty.TEXTURE_KEY, skin.getHandle()});
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) {
plugin.getLog().error("Error setting premium skin of: {}", player, ex);
}
}
}
}

View File

@ -1,232 +0,0 @@
package com.github.games647.fastlogin.bukkit.listener.protocollib;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.injector.server.TemporaryPlayerFactory;
import com.comphenix.protocol.reflect.FieldUtils;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import com.comphenix.protocol.wrappers.WrappedGameProfile;
import com.github.games647.craftapi.model.auth.Verification;
import com.github.games647.craftapi.model.skin.SkinProperty;
import com.github.games647.craftapi.resolver.MojangResolver;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.EncryptionUtil;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.util.Arrays;
import java.util.Optional;
import java.util.UUID;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import org.bukkit.entity.Player;
import static com.comphenix.protocol.PacketType.Login.Client.START;
import static com.comphenix.protocol.PacketType.Login.Server.DISCONNECT;
public class VerifyResponseTask implements Runnable {
private final FastLoginBukkit plugin;
private final PacketEvent packetEvent;
private final KeyPair serverKey;
private final Player player;
private final byte[] sharedSecret;
public VerifyResponseTask(FastLoginBukkit plugin, PacketEvent packetEvent, Player player,
byte[] sharedSecret, KeyPair keyPair) {
this.plugin = plugin;
this.packetEvent = packetEvent;
this.player = player;
this.sharedSecret = Arrays.copyOf(sharedSecret, sharedSecret.length);
this.serverKey = keyPair;
}
@Override
public void run() {
try {
BukkitLoginSession session = plugin.getLoginSessions().get(player.getAddress().toString());
if (session == null) {
disconnect(plugin.getCore().getMessage("invalid-request"), true
, "GameProfile {0} tried to send encryption response at invalid state", player.getAddress());
} else {
verifyResponse(session);
}
} finally {
//this is a fake packet; it shouldn't be send to the server
synchronized (packetEvent.getAsyncMarker().getProcessingLock()) {
packetEvent.setCancelled(true);
}
ProtocolLibrary.getProtocolManager().getAsynchronousManager().signalPacketTransmission(packetEvent);
}
}
private void verifyResponse(BukkitLoginSession session) {
PrivateKey privateKey = serverKey.getPrivate();
Cipher cipher;
SecretKey loginKey;
try {
cipher = Cipher.getInstance(privateKey.getAlgorithm());
loginKey = EncryptionUtil.decryptSharedKey(cipher, privateKey, sharedSecret);
} catch (GeneralSecurityException securityEx) {
disconnect("error-kick", false, "Cannot decrypt received contents", securityEx);
return;
}
try {
if (!checkVerifyToken(session, cipher, privateKey) || !encryptConnection(loginKey)) {
return;
}
} catch (Exception ex) {
disconnect("error-kick", false, "Cannot decrypt received contents", ex);
return;
}
String serverId = EncryptionUtil.getServerIdHashString("", loginKey, serverKey.getPublic());
String username = session.getUsername();
InetSocketAddress socketAddress = player.getAddress();
try {
MojangResolver resolver = plugin.getCore().getResolver();
InetAddress address = socketAddress.getAddress();
Optional<Verification> response = resolver.hasJoined(username, serverId, address);
if (response.isPresent()) {
plugin.getLog().info("GameProfile {} has a verified premium account", username);
SkinProperty[] properties = response.get().getProperties();
if (properties.length > 0) {
session.setSkinProperty(properties[0]);
}
session.setVerified(true);
setPremiumUUID(session.getUuid());
receiveFakeStartPacket(username);
} else {
//user tried to fake a authentication
disconnect(plugin.getCore().getMessage("invalid-session"), true
, "GameProfile {0} ({1}) tried to log in with an invalid session ServerId: {2}"
, session.getUsername(), socketAddress, serverId);
}
} catch (IOException ioEx) {
disconnect("error-kick", false, "Failed to connect to sessionserver", ioEx);
}
}
private void setPremiumUUID(UUID premiumUUID) {
if (plugin.getConfig().getBoolean("premiumUuid") && premiumUUID != null) {
try {
Object networkManager = getNetworkManager();
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/NetworkManager.java#L69
FieldUtils.writeField(networkManager, "spoofedUUID", premiumUUID, true);
} catch (Exception exc) {
plugin.getLog().error("Error setting premium uuid of {}", player, exc);
}
}
}
private boolean checkVerifyToken(BukkitLoginSession session, Cipher cipher, PrivateKey privateKey)
throws GeneralSecurityException {
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.decrypt(cipher, privateKey, responseVerify))) {
//check if the verify token are equal to the server sent one
disconnect(plugin.getCore().getMessage("invalid-verify-token"), true
, "GameProfile {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() throws IllegalAccessException, ClassNotFoundException {
Object injectorContainer = TemporaryPlayerFactory.getInjectorFromPlayer(player);
//ChannelInjector
Class<?> injectorClass = Class.forName("com.comphenix.protocol.injector.netty.Injector");
Object rawInjector = FuzzyReflection.getFieldValue(injectorContainer, injectorClass, true);
return FieldUtils.readField(rawInjector, "networkManager", true);
}
private boolean encryptConnection(SecretKey loginKey) throws IllegalArgumentException {
try {
//get the NMS connection handle of this player
Object networkManager = getNetworkManager();
//try to detect the method by parameters
Method encryptMethod = FuzzyReflection
.fromObject(networkManager).getMethodByParameters("a", SecretKey.class);
//encrypt/decrypt following packets
//the client expects this behaviour
encryptMethod.invoke(networkManager, loginKey);
} catch (Exception ex) {
disconnect("error-kick", false, "Couldn't enable encryption", ex);
return false;
}
return true;
}
private void disconnect(String kickReason, boolean debug, String logMessage, Object... arguments) {
if (debug) {
plugin.getLog().debug(logMessage, arguments);
} else {
plugin.getLog().error(logMessage, arguments);
}
kickPlayer(plugin.getCore().getMessage(kickReason));
}
private void kickPlayer(String reason) {
PacketContainer kickPacket = new PacketContainer(DISCONNECT);
kickPacket.getChatComponents().write(0, WrappedChatComponent.fromText(reason));
try {
//send kick packet at login state
//the normal event.getPlayer.kickPlayer(String) method does only work at play state
ProtocolLibrary.getProtocolManager().sendServerPacket(player, kickPacket);
//tell the server that we want to close the connection
player.kickPlayer("Disconnect");
} catch (InvocationTargetException ex) {
plugin.getLog().error("Error sending kick packet for: {}", player, ex);
}
}
//fake a new login packet in order to let the server handle all the other stuff
private void receiveFakeStartPacket(String username) {
//see StartPacketListener for packet information
PacketContainer startPacket = new PacketContainer(START);
//uuid is ignored by the packet definition
WrappedGameProfile fakeProfile = new WrappedGameProfile(UUID.randomUUID(), username);
startPacket.getGameProfiles().write(0, fakeProfile);
try {
//we don't want to handle our own packets so ignore filters
ProtocolLibrary.getProtocolManager().recieveClientPacket(player, startPacket, false);
} catch (InvocationTargetException | IllegalAccessException ex) {
plugin.getLog().warn("Failed to fake a new start packet for: {}", username, ex);
//cancel the event in order to prevent the server receiving an invalid packet
kickPlayer(plugin.getCore().getMessage("error-kick"));
}
}
}

View File

@ -67,8 +67,7 @@ public class ProtocolSupportListener extends JoinManagement<Player, CommandSende
String ip = source.getAddress().getAddress().getHostAddress();
plugin.getCore().getPendingLogin().put(ip + username, new Object());
BukkitLoginSession playerSession = new BukkitLoginSession(username, null, null
, registered, profile);
BukkitLoginSession playerSession = new BukkitLoginSession(username, registered, profile);
plugin.getLoginSessions().put(source.getAddress().toString(), playerSession);
if (plugin.getConfig().getBoolean("premiumUuid")) {
source.getLoginStartEvent().setUseOnlineModeUUID(true);

View File

@ -36,10 +36,6 @@ public class DelayedAuthHook implements Runnable {
+ "and BungeeCord is deactivated. "
+ "Either one or both of the checks have to pass in order to use this plugin");
}
if (hookFound) {
plugin.setServerStarted();
}
}
private boolean isHookFound() {

View File

@ -1,42 +0,0 @@
package com.github.games647.fastlogin.bukkit;
import java.security.SecureRandom;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.junit.Assert.assertThat;
public class EncryptionUtilTest {
@Test
public void testVerifyToken() throws Exception {
SecureRandom random = new SecureRandom();
byte[] token = EncryptionUtil.generateVerifyToken(random);
assertThat(token, notNullValue());
assertThat(token.length, is(4));
}
// @Test
// public void testDecryptSharedSecret() throws Exception {
//
// }
//
// @Test
// public void testDecryptData() throws Exception {
//
// }
// private static SecretKey createNewSharedKey() {
// try {
// KeyGenerator keygenerator = KeyGenerator.getInstance("AES");
// keygenerator.init(128);
// return keygenerator.generateKey();
// } catch (NoSuchAlgorithmException nosuchalgorithmexception) {
// throw new Error(nosuchalgorithmexception);
// }
// }
}