Compare commits

..

100 Commits

Author SHA1 Message Date
9249c1ea8c Fix equality null test 2022-07-28 13:30:42 +02:00
fd33523189 Perform equality check 2022-07-28 13:25:13 +02:00
5af43170da Try to skip filtering 2022-07-28 13:16:25 +02:00
eb51f0c83c Debug sending fake packet out 2022-07-28 13:05:46 +02:00
5bdf2f4c9e Move debug message up 2022-07-27 15:08:03 +02:00
155c98d601 Fix using asynchronous manager in synchronous mode 2022-07-27 14:56:59 +02:00
a8f3155fc5 Add experimental synchronous processing 2022-07-27 14:56:59 +02:00
c886ccb9ef Remove commented out code 2022-07-27 14:37:11 +02:00
bbb615bb0f Merge branch 'tcpshield' 2022-07-27 14:31:55 +02:00
0e1774a175 Comment out potential debug code 2022-07-27 14:30:48 +02:00
f50bef3eb2 Fix TCPShield compat by using raw address for sessions
TCPShield overwrites the IP address during connection. ProtocolLib
doesn't notice this change, because it uses the end-to-end connection
which is the proxy IP. This causes getAddress calls during Spigot play
state and ProtocolLib auth state not match and then have conflicting
session ids.

A solution is also to hold onto the temporary player object. However
since we don't get a notification for a disconnect, holding it will
prevent to get GCed until the timeout occurs (1 minute).

Fixes #595
2022-07-27 14:30:42 +02:00
0e2e431d87 Use the address field for verify task 2022-07-27 14:30:04 +02:00
1e732d62cb Replace Field- and MethodUtils from breaking changes in ProtocolLib 2022-07-27 10:06:46 +02:00
94979a3cf6 Fix reading username with 1.19 and Floodgate
Related #856
2022-07-27 09:34:36 +02:00
564f713fce Simplify version setting 2022-07-25 16:28:38 +02:00
05708a256c Perform check for available reflection during compile time 2022-07-25 13:02:30 +02:00
1d45cad2ea Add connection debug messages for #855 2022-07-25 10:30:48 +02:00
e82e7c7856 Fix manual name change workaround for floodgate with 1.19
Fixes #854
2022-07-25 10:27:09 +02:00
8df5b11276 Fix Java 8 API compatibility 2022-07-22 18:46:46 +02:00
c090278f82 Reset Hikari to the Java 8 version 2022-07-22 13:31:43 +02:00
23d30eee1a Migrate to Java 8 (sponsored contribution)
Java >8 includes many helpful features and API additions. Current
phoronix benchmarks indicated a very similar performance. Furthermore,
the fact that Java 18 even works with 1.8.8 contributed to the
decision to move to Java 17 like vanilla Java in 1.19.

This also helps us to learn about the newer features added for our
personal interests. As this is a free project, this motivated us to make
this step.

Nevertheless, many server owners were frustrated about this decision.
Thanks to financial contribution, we revised this decision until Java 8
is end of life or no longer used actively used according to bstats.org
2022-07-22 13:26:52 +02:00
bd65c792d0 Use correct generics 2022-07-22 13:26:39 +02:00
79a83ce84d Throw exception for utilities 2022-07-22 13:26:39 +02:00
9fdbdeaf9a Use Lombok to create a record-like struct 2022-07-22 13:26:39 +02:00
dd2fca2ea1 Replace var with val to prevent conflicts with newer Java version
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtil.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtil.java
index 81abad1..d148bc1 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtil.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtil.java
@@ -59,7 +59,7 @@ import javax.crypto.NoSuchPaddingException;
 import javax.crypto.SecretKey;
 import javax.crypto.spec.SecretKeySpec;

-import lombok.var;
+import lombok.val;

 /**
  * Encryption and decryption minecraft util for connection between servers
@@ -179,9 +179,9 @@ final class EncryptionUtil {

     private static PublicKey loadMojangSessionKey()
             throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
-        var keyUrl = FastLoginBukkit.class.getClassLoader().getResource("yggdrasil_session_pubkey.der");
-        var keyData = Resources.toByteArray(keyUrl);
-        var keySpec = new X509EncodedKeySpec(keyData);
+        val keyUrl = FastLoginBukkit.class.getClassLoader().getResource("yggdrasil_session_pubkey.der");
+        val keyData = Resources.toByteArray(keyUrl);
+        val keySpec = new X509EncodedKeySpec(keyData);

         return KeyFactory.getInstance("RSA").generatePublic(keySpec);
     }
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java
index dc208ff..1363cb3 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java
@@ -60,7 +60,7 @@ import java.util.UUID;
 import javax.crypto.Cipher;
 import javax.crypto.SecretKey;

-import lombok.var;
+import lombok.val;
 import org.bukkit.entity.Player;

 import static com.comphenix.protocol.PacketType.Login.Client.START;
@@ -269,7 +269,7 @@ public class VerifyResponseTask implements Runnable {
             startPacket.getStrings().write(0, username);

             EquivalentConverter<WrappedProfileKeyData> converter = BukkitConverters.getWrappedPublicKeyDataConverter();
-            var wrappedKey = Optional.ofNullable(clientKey).map(key ->
+            val wrappedKey = Optional.ofNullable(clientKey).map(key ->
                 new WrappedProfileKeyData(clientKey.expiry(), clientKey.key(), clientKey.signature())
             );

diff --git a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/FastLoginBukkitTest.java b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/FastLoginBukkitTest.java
index e4ce55a..9d6832e 100644
--- a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/FastLoginBukkitTest.java
+++ b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/FastLoginBukkitTest.java
@@ -30,7 +30,7 @@ import com.github.games647.fastlogin.core.CommonUtil;
 import net.md_5.bungee.api.chat.TextComponent;
 import net.md_5.bungee.chat.ComponentSerializer;

-import lombok.var;
+import lombok.val;
 import org.junit.jupiter.api.Test;

 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -39,12 +39,12 @@ class FastLoginBukkitTest {

     @Test
     void testRGB() {
-        var message = "&x00002a00002b&lText";
-        var msg = CommonUtil.translateColorCodes(message);
+        val message = "&x00002a00002b&lText";
+        val msg = CommonUtil.translateColorCodes(message);
         assertEquals(msg, "§x00002a00002b§lText");

-        var components = TextComponent.fromLegacyText(msg);
-        var expected = "{\"bold\":true,\"color\":\"#00a00b\",\"text\":\"Text\"}";
+        val components = TextComponent.fromLegacyText(msg);
+        val expected = "{\"bold\":true,\"color\":\"#00a00b\",\"text\":\"Text\"}";
         assertEquals(ComponentSerializer.toString(components), expected);
     }
 }
diff --git a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/Base64Adapter.java b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/Base64Adapter.java
index 29b2f3d..c2fb34d 100644
--- a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/Base64Adapter.java
+++ b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/Base64Adapter.java
@@ -32,13 +32,13 @@ import com.google.gson.stream.JsonWriter;
 import java.io.IOException;
 import java.util.Base64;

-import lombok.var;
+import lombok.val;

 public class Base64Adapter extends TypeAdapter<byte[]> {

     @Override
     public void write(JsonWriter out, byte[] value) throws IOException {
-        var encoded = Base64.getEncoder().encodeToString(value);
+        val encoded = Base64.getEncoder().encodeToString(value);
         out.value(encoded);
     }

diff --git a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtilTest.java b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtilTest.java
index df3888e..1bbe864 100644
--- a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtilTest.java
+++ b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtilTest.java
@@ -50,7 +50,7 @@ import javax.crypto.NoSuchPaddingException;
 import javax.crypto.SecretKey;
 import javax.crypto.spec.SecretKeySpec;

-import lombok.var;
+import lombok.val;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
@@ -61,7 +61,7 @@ class EncryptionUtilTest {

     @Test
     void testVerifyToken() {
-        var random = ThreadLocalRandom.current();
+        val random = ThreadLocalRandom.current();
         byte[] token = EncryptionUtil.generateVerifyToken(random);

         assertAll(
@@ -89,10 +89,10 @@ class EncryptionUtilTest {

     @Test
     void testExpiredClientKey() throws Exception {
-        var clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
+        val clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");

         // Client expires at the exact second mentioned, so use it for verification
-        var expiredTimestamp = clientKey.expiry();
+        val expiredTimestamp = clientKey.expiry();
         assertFalse(EncryptionUtil.verifyClientKey(clientKey, expiredTimestamp));
     }

@@ -106,7 +106,7 @@ class EncryptionUtilTest {
             "client_keys/invalid_wrong_signature.json"
     })
     void testInvalidClientKey(String clientKeySource) throws Exception {
-        var clientKey = ResourceLoader.loadClientKey(clientKeySource);
+        val clientKey = ResourceLoader.loadClientKey(clientKeySource);
         Instant expireTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS);

         assertFalse(EncryptionUtil.verifyClientKey(clientKey, expireTimestamp));
@@ -114,8 +114,8 @@ class EncryptionUtilTest {

     @Test
     void testValidClientKey() throws Exception {
-        var clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
-        var verificationTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS);
+        val clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
+        val verificationTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS);

         assertTrue(EncryptionUtil.verifyClientKey(clientKey, verificationTimestamp));
     }
@@ -123,7 +123,7 @@ class EncryptionUtilTest {
     @Test
     void testDecryptSharedSecret() throws Exception {
         KeyPair serverPair = EncryptionUtil.generateKeyPair();
-        var serverPK = serverPair.getPublic();
+        val serverPK = serverPair.getPublic();

         SecretKey secretKey = generateSharedKey();
         byte[] encryptedSecret = encrypt(serverPK, secretKey.getEncoded());
@@ -135,7 +135,7 @@ class EncryptionUtilTest {
     private static byte[] encrypt(PublicKey receiverKey, byte... message)
             throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
             IllegalBlockSizeException, BadPaddingException {
-        var encryptCipher = Cipher.getInstance(receiverKey.getAlgorithm());
+        val encryptCipher = Cipher.getInstance(receiverKey.getAlgorithm());
         encryptCipher.init(Cipher.ENCRYPT_MODE, receiverKey);
         return encryptCipher.doFinal(message);
     }
@@ -151,9 +151,9 @@ class EncryptionUtilTest {

     @Test
     void testServerIdHash() throws Exception {
-        var serverId = "";
-        var sharedSecret = generateSharedKey();
-        var serverPK = ResourceLoader.loadClientKey("client_keys/valid_public_key.json").key();
+        val serverId = "";
+        val sharedSecret = generateSharedKey();
+        val serverPK = ResourceLoader.loadClientKey("client_keys/valid_public_key.json").key();

         String sessionHash = getServerHash(serverId, sharedSecret, serverPK);
         assertEquals(EncryptionUtil.getServerIdHashString(serverId, sharedSecret, serverPK), sessionHash);
@@ -167,7 +167,7 @@ class EncryptionUtilTest {
         // sha1.update(server's encoded public key from Encryption Request)
         // hash := sha1.hexdigest() # String of hex characters
         @SuppressWarnings("deprecation")
-        var hasher = Hashing.sha1().newHasher();
+        val hasher = Hashing.sha1().newHasher();
         hasher.putString(serverId, StandardCharsets.US_ASCII);
         hasher.putBytes(sharedSecret.getEncoded());
         hasher.putBytes(serverPK.getEncoded());
@@ -180,9 +180,9 @@ class EncryptionUtilTest {

     @Test
     void testServerIdHashWrongSecret() throws Exception {
-        var serverId = "";
-        var sharedSecret = generateSharedKey();
-        var serverPK = ResourceLoader.loadClientKey("client_keys/valid_public_key.json").key();
+        val serverId = "";
+        val sharedSecret = generateSharedKey();
+        val serverPK = ResourceLoader.loadClientKey("client_keys/valid_public_key.json").key();

         String sessionHash = getServerHash(serverId, sharedSecret, serverPK);
         assertNotEquals(EncryptionUtil.getServerIdHashString("", generateSharedKey(), serverPK), sessionHash);
@@ -190,12 +190,12 @@ class EncryptionUtilTest {

     @Test
     void testServerIdHashWrongServerKey() {
-        var serverId = "";
-        var sharedSecret = generateSharedKey();
-        var serverPK = EncryptionUtil.generateKeyPair().getPublic();
+        val serverId = "";
+        val sharedSecret = generateSharedKey();
+        val serverPK = EncryptionUtil.generateKeyPair().getPublic();

         String sessionHash = getServerHash(serverId, sharedSecret, serverPK);
-        var wrongPK = EncryptionUtil.generateKeyPair().getPublic();
+        val wrongPK = EncryptionUtil.generateKeyPair().getPublic();
         assertNotEquals(EncryptionUtil.getServerIdHashString("", sharedSecret, wrongPK), sessionHash);
     }

@@ -239,8 +239,8 @@ class EncryptionUtilTest {
     @Test
     void testNonce() throws Exception {
         byte[] expected = {1, 2, 3, 4};
-        var serverKey = EncryptionUtil.generateKeyPair();
-        var encryptedNonce = encrypt(serverKey.getPublic(), expected);
+        val serverKey = EncryptionUtil.generateKeyPair();
+        val encryptedNonce = encrypt(serverKey.getPublic(), expected);

         assertTrue(EncryptionUtil.verifyNonce(expected, serverKey.getPrivate(), encryptedNonce));
     }
@@ -248,19 +248,19 @@ class EncryptionUtilTest {
     @Test
     void testNonceIncorrect() throws Exception {
         byte[] expected = {1, 2, 3, 4};
-        var serverKey = EncryptionUtil.generateKeyPair();
+        val serverKey = EncryptionUtil.generateKeyPair();

         // flipped first character
-        var encryptedNonce = encrypt(serverKey.getPublic(), new byte[]{0, 2, 3, 4});
+        val encryptedNonce = encrypt(serverKey.getPublic(), new byte[]{0, 2, 3, 4});
         assertFalse(EncryptionUtil.verifyNonce(expected, serverKey.getPrivate(), encryptedNonce));
     }

     @Test
     void testNonceFailedDecryption() throws Exception {
         byte[] expected = {1, 2, 3, 4};
-        var serverKey = EncryptionUtil.generateKeyPair();
+        val serverKey = EncryptionUtil.generateKeyPair();
         // generate a new keypair that is different
-        var encryptedNonce = encrypt(EncryptionUtil.generateKeyPair().getPublic(), expected);
+        val encryptedNonce = encrypt(EncryptionUtil.generateKeyPair().getPublic(), expected);

         assertThrows(GeneralSecurityException.class,
                 () -> EncryptionUtil.verifyNonce(expected, serverKey.getPrivate(), encryptedNonce)
@@ -270,7 +270,7 @@ class EncryptionUtilTest {
     @Test
     void testNonceIncorrectEmpty() {
         byte[] expected = {1, 2, 3, 4};
-        var serverKey = EncryptionUtil.generateKeyPair();
+        val serverKey = EncryptionUtil.generateKeyPair();
         byte[] encryptedNonce = {};

         assertThrows(GeneralSecurityException.class,
diff --git a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/SignatureTestData.java b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/SignatureTestData.java
index 9a75fc5..01b734e 100644
--- a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/SignatureTestData.java
+++ b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/SignatureTestData.java
@@ -32,13 +32,13 @@ import com.google.gson.annotations.JsonAdapter;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;

-import lombok.var;
+import lombok.val;

 public class SignatureTestData {

     public static SignatureTestData fromResource(String resourceName) throws IOException {
-        var keyUrl = Resources.getResource(resourceName);
-        var encodedSignature = Resources.toString(keyUrl, StandardCharsets.US_ASCII);
+        val keyUrl = Resources.getResource(resourceName);
+        val encodedSignature = Resources.toString(keyUrl, StandardCharsets.US_ASCII);

         return new Gson().fromJson(encodedSignature, SignatureTestData.class);
     }
diff --git a/bungee/src/main/java/com/github/games647/fastlogin/bungee/FastLoginBungee.java b/bungee/src/main/java/com/github/games647/fastlogin/bungee/FastLoginBungee.java
index 148282c..c83c14d 100644
--- a/bungee/src/main/java/com/github/games647/fastlogin/bungee/FastLoginBungee.java
+++ b/bungee/src/main/java/com/github/games647/fastlogin/bungee/FastLoginBungee.java
@@ -57,6 +57,7 @@ import net.md_5.bungee.api.chat.TextComponent;
 import net.md_5.bungee.api.connection.PendingConnection;
 import net.md_5.bungee.api.connection.ProxiedPlayer;
 import net.md_5.bungee.api.connection.Server;
+import net.md_5.bungee.api.plugin.Listener;
 import net.md_5.bungee.api.plugin.Plugin;
 import net.md_5.bungee.api.plugin.PluginManager;
 import net.md_5.bungee.api.scheduler.GroupedThreadFactory;
@@ -100,7 +101,7 @@ public class FastLoginBungee extends Plugin implements PlatformPlugin<CommandSen
         //events
         PluginManager pluginManager = getProxy().getPluginManager();

-        ConnectListener connectListener = new ConnectListener(this, core.getAntiBot());
+        Listener connectListener = new ConnectListener(this, core.getAntiBot());
         pluginManager.registerListener(this, connectListener);
         pluginManager.registerListener(this, new PluginMessageListener(this));
2022-07-22 13:26:38 +02:00
7e9da9fd7c Migrate to Java 8 (sponsored contribution)
Java >8 includes many helpful features and API additions. Current
phoronix benchmarks indicated a very similar performance. Furthermore,
the fact that Java 18 even works with 1.8.8 contributed to the
decision to move to Java 17 like vanilla Java in 1.19.

This also helps us to learn about the newer features added for our
personal interests. As this is a free project, this motivated us to make
this step.

Nevertheless, many server owners were frustrated about this decision.
Thanks to financial contribution, we revised this decision until Java 8
is end of life or no longer used actively used according to bstats.org

diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtil.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtil.java
index 68095f6..81abad1 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtil.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtil.java
@@ -50,7 +50,7 @@ import java.time.Instant;
 import java.util.Arrays;
 import java.util.Base64;
 import java.util.Base64.Encoder;
-import java.util.random.RandomGenerator;
+import java.util.Random;

 import javax.crypto.BadPaddingException;
 import javax.crypto.Cipher;
@@ -59,6 +59,8 @@ import javax.crypto.NoSuchPaddingException;
 import javax.crypto.SecretKey;
 import javax.crypto.spec.SecretKeySpec;

+import lombok.var;
+
 /**
  * Encryption and decryption minecraft util for connection between servers
  * and paid Minecraft account clients.
@@ -112,7 +114,7 @@ final class EncryptionUtil {
      * @param random random generator
      * @return a token with 4 bytes long
      */
-    public static byte[] generateVerifyToken(RandomGenerator random) {
+    public static byte[] generateVerifyToken(Random random) {
         byte[] token = new byte[VERIFY_TOKEN_LENGTH];
         random.nextBytes(token);
         return token;
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
index 2e21ccb..4ce14ff 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
@@ -56,6 +56,7 @@ import javax.crypto.BadPaddingException;
 import javax.crypto.IllegalBlockSizeException;
 import javax.crypto.NoSuchPaddingException;

+import lombok.var;
 import org.bukkit.entity.Player;

 import static com.comphenix.protocol.PacketType.Login.Client.ENCRYPTION_BEGIN;
@@ -171,7 +172,7 @@ public class ProtocolLibListener extends PacketAdapter {
                 Either<byte[], ?> either = packet.getSpecificModifier(Either.class).read(0);
                 if (clientPublicKey == null) {
                     Optional<byte[]> left = either.left();
-                    if (left.isEmpty()) {
+                    if (!left.isPresent()) {
                         plugin.getLog().error("No verify token sent if requested without player signed key {}", sender);
                         return false;
                     }
@@ -179,7 +180,7 @@ public class ProtocolLibListener extends PacketAdapter {
                     return EncryptionUtil.verifyNonce(expectedToken, keyPair.getPrivate(), left.get());
                 } else {
                     Optional<?> optSignatureData = either.right();
-                    if (optSignatureData.isEmpty()) {
+                    if (!optSignatureData.isPresent()) {
                         plugin.getLog().error("No signature given to sent player signing key {}", sender);
                         return false;
                     }
@@ -219,7 +220,7 @@ public class ProtocolLibListener extends PacketAdapter {
             .optionRead(0);

         var clientKey = profileKey.flatMap(opt -> opt).flatMap(this::verifyPublicKey);
-        if (verifyClientKeys && clientKey.isEmpty()) {
+        if (verifyClientKeys && !clientKey.isPresent()) {
             // missing or incorrect
             // expired always not allowed
             player.kickPlayer(plugin.getCore().getMessage("invalid-public-key"));
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java
index 829d764..dc208ff 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java
@@ -60,6 +60,7 @@ import java.util.UUID;
 import javax.crypto.Cipher;
 import javax.crypto.SecretKey;

+import lombok.var;
 import org.bukkit.entity.Player;

 import static com.comphenix.protocol.PacketType.Login.Client.START;
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/packet/ClientPublicKey.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/packet/ClientPublicKey.java
index e375e29..619d41d 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/packet/ClientPublicKey.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/packet/ClientPublicKey.java
@@ -27,8 +27,62 @@ package com.github.games647.fastlogin.bukkit.listener.protocollib.packet;

 import java.security.PublicKey;
 import java.time.Instant;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.StringJoiner;

-public record ClientPublicKey(Instant expiry, PublicKey key, byte[] signature) {
+public final class ClientPublicKey {
+    private final Instant expiry;
+    private final PublicKey key;
+    private final byte[] signature;
+
+    public ClientPublicKey(Instant expiry, PublicKey key, byte[] signature) {
+        this.expiry = expiry;
+        this.key = key;
+        this.signature = signature;
+    }
+
+    public Instant expiry() {
+        return expiry;
+    }
+
+    public PublicKey key() {
+        return key;
+    }
+
+    public byte[] signature() {
+        return signature;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        }
+
+        if (obj == null || obj.getClass() != this.getClass()) {
+            return false;
+        }
+
+        ClientPublicKey that = (ClientPublicKey) obj;
+        return Objects.equals(this.expiry, that.expiry)
+                && Objects.equals(this.key, that.key)
+                && Arrays.equals(this.signature, that.signature);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(expiry, key, signature);
+    }
+
+    @Override
+    public String toString() {
+        return new StringJoiner(", ", ClientPublicKey.class.getSimpleName() + "[", "]")
+                .add("expiry=" + expiry)
+                .add("key=" + key)
+                .add("signature=" + Arrays.toString(signature))
+                .toString();
+    }

     public boolean isExpired(Instant verifyTimestamp) {
         return !verifyTimestamp.isBefore(expiry);
diff --git a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/FastLoginBukkitTest.java b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/FastLoginBukkitTest.java
index 07ff623..e4ce55a 100644
--- a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/FastLoginBukkitTest.java
+++ b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/FastLoginBukkitTest.java
@@ -30,6 +30,7 @@ import com.github.games647.fastlogin.core.CommonUtil;
 import net.md_5.bungee.api.chat.TextComponent;
 import net.md_5.bungee.chat.ComponentSerializer;

+import lombok.var;
 import org.junit.jupiter.api.Test;

 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -43,8 +44,7 @@ class FastLoginBukkitTest {
         assertEquals(msg, "§x00002a00002b§lText");

         var components = TextComponent.fromLegacyText(msg);
-        var expected = """
-            {"bold":true,"color":"#00a00b","text":"Text"}""";
+        var expected = "{\"bold\":true,\"color\":\"#00a00b\",\"text\":\"Text\"}";
         assertEquals(ComponentSerializer.toString(components), expected);
     }
 }
diff --git a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/Base64Adapter.java b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/Base64Adapter.java
index c2a2d1a..29b2f3d 100644
--- a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/Base64Adapter.java
+++ b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/Base64Adapter.java
@@ -32,6 +32,8 @@ import com.google.gson.stream.JsonWriter;
 import java.io.IOException;
 import java.util.Base64;

+import lombok.var;
+
 public class Base64Adapter extends TypeAdapter<byte[]> {

     @Override
diff --git a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtilTest.java b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtilTest.java
index e1b8bea..df3888e 100644
--- a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtilTest.java
+++ b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtilTest.java
@@ -50,6 +50,7 @@ import javax.crypto.NoSuchPaddingException;
 import javax.crypto.SecretKey;
 import javax.crypto.spec.SecretKeySpec;

+import lombok.var;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
diff --git a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ResourceLoader.java b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ResourceLoader.java
index 4361fe9..ac0991e 100644
--- a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ResourceLoader.java
+++ b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ResourceLoader.java
@@ -33,6 +33,7 @@ import com.google.gson.JsonObject;
 import java.io.IOException;
 import java.io.Reader;
 import java.io.StringReader;
+import java.net.URL;
 import java.nio.charset.StandardCharsets;
 import java.security.KeyFactory;
 import java.security.NoSuchAlgorithmException;
@@ -57,19 +58,19 @@ public class ResourceLoader {
         ) {
             PemObject pemObject = pemReader.readPemObject();
             byte[] content = pemObject.getContent();
-            var privateKeySpec = new PKCS8EncodedKeySpec(content);
+            PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(content);

-            var factory = KeyFactory.getInstance("RSA");
+            KeyFactory factory = KeyFactory.getInstance("RSA");
             return (RSAPrivateKey) factory.generatePrivate(privateKeySpec);
         }
     }

     protected static ClientPublicKey loadClientKey(String path)
         throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
-        var keyUrl = Resources.getResource(path);
+        URL keyUrl = Resources.getResource(path);

-        var lines = Resources.toString(keyUrl, StandardCharsets.US_ASCII);
-        var object = new Gson().fromJson(lines, JsonObject.class);
+        String lines = Resources.toString(keyUrl, StandardCharsets.US_ASCII);
+        JsonObject object = new Gson().fromJson(lines, JsonObject.class);

         Instant expires = Instant.parse(object.getAsJsonPrimitive("expires_at").getAsString());
         String key = object.getAsJsonPrimitive("key").getAsString();
@@ -87,9 +88,9 @@ public class ResourceLoader {
         ) {
             PemObject pemObject = pemReader.readPemObject();
             byte[] content = pemObject.getContent();
-            var pubKeySpec = new X509EncodedKeySpec(content);
+            X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(content);

-            var factory = KeyFactory.getInstance("RSA");
+            KeyFactory factory = KeyFactory.getInstance("RSA");
             return (RSAPublicKey) factory.generatePublic(pubKeySpec);
         }
     }
diff --git a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/SignatureTestData.java b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/SignatureTestData.java
index ee62e24..9a75fc5 100644
--- a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/SignatureTestData.java
+++ b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/SignatureTestData.java
@@ -32,6 +32,8 @@ import com.google.gson.annotations.JsonAdapter;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;

+import lombok.var;
+
 public class SignatureTestData {

     public static SignatureTestData fromResource(String resourceName) throws IOException {
diff --git a/core/src/main/java/com/github/games647/fastlogin/core/CommonUtil.java b/core/src/main/java/com/github/games647/fastlogin/core/CommonUtil.java
index 324e240..89dc7ee 100644
--- a/core/src/main/java/com/github/games647/fastlogin/core/CommonUtil.java
+++ b/core/src/main/java/com/github/games647/fastlogin/core/CommonUtil.java
@@ -25,8 +25,7 @@
  */
 package com.github.games647.fastlogin.core;

-import com.github.games647.craftapi.cache.SafeCacheBuilder;
-import com.google.common.cache.CacheLoader;
+import com.google.common.cache.CacheBuilder;

 import java.lang.reflect.Constructor;
 import java.util.concurrent.ConcurrentMap;
@@ -43,7 +42,7 @@ public final class CommonUtil {
     private static final char TRANSLATED_CHAR = '§';

     public static <K, V> ConcurrentMap<K, V> buildCache(int expireAfterWrite, int maxSize) {
-        SafeCacheBuilder<Object, Object> builder = SafeCacheBuilder.newBuilder();
+        CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();

         if (expireAfterWrite > 0) {
             builder.expireAfterWrite(expireAfterWrite, TimeUnit.MINUTES);
@@ -53,9 +52,7 @@ public final class CommonUtil {
             builder.maximumSize(maxSize);
         }

-        return builder.build(CacheLoader.from(() -> {
-            throw new UnsupportedOperationException();
-        }));
+        return (ConcurrentMap<K, V>) builder.build().asMap();
     }

     public static String translateColorCodes(String rawMessage) {
diff --git a/core/src/main/java/com/github/games647/fastlogin/core/StoredProfile.java b/core/src/main/java/com/github/games647/fastlogin/core/StoredProfile.java
index ee58bde..26492d3 100644
--- a/core/src/main/java/com/github/games647/fastlogin/core/StoredProfile.java
+++ b/core/src/main/java/com/github/games647/fastlogin/core/StoredProfile.java
@@ -118,10 +118,11 @@ public class StoredProfile extends Profile {
             return true;
         }

-        if (!(o instanceof StoredProfile that)) {
+        if (!(o instanceof StoredProfile)) {
             return false;
         }

+        StoredProfile that = (StoredProfile) o;
         if (!super.equals(o)) {
             return false;
         }
diff --git a/pom.xml b/pom.xml
index f2f3950..e1b26b0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -48,7 +48,7 @@
         <!-- Set default for non-git clones -->
         <git.commit.id>Unknown</git.commit.id>

-        <java.version>17</java.version>
+        <java.version>8</java.version>
         <maven.compiler.source>${java.version}</maven.compiler.source>
         <maven.compiler.target>${java.version}</maven.compiler.target>

@@ -189,5 +189,12 @@
             <version>5.8.2</version>
             <scope>test</scope>
         </dependency>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <version>1.18.24</version>
+            <scope>provided</scope>
+        </dependency>
     </dependencies>
 </project>
2022-07-22 13:26:38 +02:00
f513cffbaf Migrate tests to use Junit5 2022-07-22 13:24:57 +02:00
cdb3c50b87 Merge pull request #837 from games647/dependabot/maven/org.mariadb.jdbc-mariadb-java-client-3.0.6
Bump mariadb-java-client from 3.0.5 to 3.0.6
2022-07-17 16:56:31 +02:00
e338a768ca Use class loader for loading the session key
Related #849
2022-07-17 16:54:06 +02:00
18a8d7a5dc Replace Guava with Java joiner 2022-07-17 16:54:06 +02:00
6cf1a79ae1 Reset ignore list of dependabot 2022-07-17 16:54:06 +02:00
0e8b51e148 Minimize dependency jars 2022-07-17 16:54:06 +02:00
ef34550a53 Merge pull request #848 from Smart123s/fix/duplicate/forwardccommand
Remove duplicate forwardCrackedCommand()
2022-07-15 08:56:45 +02:00
fe1d4944a8 Remove duplicate forwardCrackedCommand()
The forwardCrackedCommand() method is invoked before, the removed code, that does the exact same thing.
2022-07-14 17:11:25 +02:00
423bfa2275 Re-enable force checkstyle check 2022-07-11 13:00:15 +02:00
06b0cf9e02 Merge pull request #779 from Smart123s/feature/codeformatter
Add a code formatter
2022-07-11 12:20:42 +02:00
752600f0e2 Fix code formatting according to checkstyle config 2022-07-11 12:14:31 +02:00
cf53ecacdf Disable hard to meet checkstyle requirements
This is temporary.
2022-07-11 12:14:31 +02:00
b740331d31 Add checkstyle.xml
Source: 0332cbe0ba/src/main/resources/sun_checks.xml
2022-07-11 12:14:30 +02:00
13b2f22e31 Add code formatter
Add checkstyle as maven goal
This will fail the build if the code is not formatted properly
2022-07-11 12:14:30 +02:00
d5d4a32c3f Fix forwarding the correct player signature
Fixes #843
2022-07-11 12:13:36 +02:00
bea3503eff Merge pull request #821 from games647/809-119-compatibility-1
1.19 Support
2022-07-11 09:43:17 +02:00
adfae507ac Flip velocity check to scan for newer Paper configurations first
The PaperConfig class file still exists in newer versions
2022-07-08 16:43:02 +02:00
e89cb3293a Disable verify client keys by default for older compatibility
This also mimics the default vanilla configuration.
2022-07-08 16:30:41 +02:00
b4ddf4fb19 Reduce dependency list to improve on build time 2022-07-08 16:29:25 +02:00
eb47dd3254 Restore compatibility with older Minecraft versions 2022-07-08 16:28:56 +02:00
0b0a46a18a Make client public key verification optional 2022-07-06 17:08:58 +02:00
7c8de84a34 Bump dependencies 2022-07-06 17:08:57 +02:00
c448be8c5f Bump mariadb-java-client from 3.0.5 to 3.0.6
Bumps [mariadb-java-client](https://github.com/mariadb-corporation/mariadb-connector-j) from 3.0.5 to 3.0.6.
- [Release notes](https://github.com/mariadb-corporation/mariadb-connector-j/releases)
- [Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mariadb-corporation/mariadb-connector-j/compare/3.0.5...3.0.6)

---
updated-dependencies:
- dependency-name: org.mariadb.jdbc:mariadb-java-client
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-04 07:04:41 +00:00
339156be18 Use multiple threads to build the project 2022-07-02 12:37:49 +02:00
944db748e8 Upgrade GitHub actions to use Java 18 2022-07-02 12:36:58 +02:00
bb1cbb79f2 Support newer Paper configuration with clearer error messages 2022-06-28 18:40:51 +02:00
cf356099a0 Fix public key timestamp verification. 2022-06-28 18:34:25 +02:00
91c01e3422 Reuse verify token tick message for signature verification
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
index 6c578ff..92e1dcd 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
@@ -153,11 +153,11 @@ public class ProtocolLibListener extends PacketAdapter {
                     Runnable verifyTask = new VerifyResponseTask(plugin, packetEvent, sender, sharedSecret, keyPair);
                     plugin.getScheduler().runAsync(verifyTask);
                 } else {
-                    sender.kickPlayer("Invalid signature");
+                    sender.kickPlayer(plugin.getCore().getMessage("invalid-verify-token"));
                     plugin.getLog().error("Invalid signature from player {}", sender);
                 }
             } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException signatureEx) {
-                sender.kickPlayer("Invalid signature");
+                sender.kickPlayer(plugin.getCore().getMessage("invalid-verify-token"));
                 plugin.getLog().error("Invalid signature from player {}", sender, signatureEx);
             }
         }
2022-06-28 18:34:25 +02:00
c118430bf5 Kick players using an invalid public key 2022-06-28 18:34:25 +02:00
ce59172839 Log signature verification errors to help administrators
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
index c3e159a..da28f38 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
@@ -154,9 +154,11 @@ public class ProtocolLibListener extends PacketAdapter {
                     plugin.getScheduler().runAsync(verifyTask);
                 } else {
                     sender.kickPlayer("Invalid signature");
+                    plugin.getLog().error("Invalid signature from player {}", sender);
                 }
-            } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
+            } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException signatureEx) {
                 sender.kickPlayer("Invalid signature");
+                plugin.getLog().error("Invalid signature from player {}", sender, signatureEx);
             }
         }
     }
2022-06-28 18:34:25 +02:00
34e63b7eae Make profile key optional 2022-06-28 18:34:25 +02:00
e8bb3ec7a9 Validate other encryption methods 2022-06-28 18:34:24 +02:00
1d46640b42 Limit length of server keys 2022-06-28 18:34:24 +02:00
a0fddd69aa Decrease necessary entropy for running tests 2022-06-28 18:34:24 +02:00
700b889aa9 Improve precision and flexibility of encrypt methods 2022-06-28 18:34:24 +02:00
3767b022a9 Migrate to guava hashing to replace unneeded exceptions 2022-06-28 18:34:24 +02:00
11077a002d Migrate public key to record 2022-06-28 18:34:24 +02:00
d9bf7267a6 Test valid server key pairs 2022-06-28 18:34:23 +02:00
53e6fe6ddf Missing synchronization access to the username 2022-06-28 18:34:23 +02:00
1c528fb9cb Clean up
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/BungeeManager.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/BungeeManager.java
index 49ff879..7149238 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/BungeeManager.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/BungeeManager.java
@@ -36,6 +36,7 @@ import java.io.IOException;
 import java.lang.reflect.InvocationTargetException;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
@@ -62,7 +63,7 @@ public class BungeeManager {
     private final FastLoginBukkit plugin;
     private boolean enabled;

-    private final Set<UUID> firedJoinEvents = new HashSet<>();
+    private final Collection<UUID> firedJoinEvents = new HashSet<>();

     public BungeeManager(FastLoginBukkit plugin) {
         this.plugin = plugin;
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/command/CrackedCommand.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/command/CrackedCommand.java
index a6ac9b7..6153338 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/command/CrackedCommand.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/command/CrackedCommand.java
@@ -44,7 +44,7 @@ public class CrackedCommand extends ToggleCommand {
     @Override
     public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
         if (args.length == 0) {
-            onCrackedSelf(sender, command, args);
+            onCrackedSelf(sender);
         } else {
             onCrackedOther(sender, command, args);
         }
@@ -52,7 +52,7 @@ public class CrackedCommand extends ToggleCommand {
         return true;
     }

-    private void onCrackedSelf(CommandSender sender, Command cmd, String[] args) {
+    private void onCrackedSelf(CommandSender sender) {
         if (isConsole(sender)) {
             return;
         }
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/command/PremiumCommand.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/command/PremiumCommand.java
index 26b6d31..9f1ef98 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/command/PremiumCommand.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/command/PremiumCommand.java
@@ -51,7 +51,7 @@ public class PremiumCommand extends ToggleCommand {
     @Override
     public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
         if (args.length == 0) {
-            onPremiumSelf(sender, command, args);
+            onPremiumSelf(sender);
         } else {
             onPremiumOther(sender, command, args);
         }
@@ -59,7 +59,7 @@ public class PremiumCommand extends ToggleCommand {
         return true;
     }

-    private void onPremiumSelf(CommandSender sender, Command cmd, String[] args) {
+    private void onPremiumSelf(CommandSender sender) {
         if (isConsole(sender)) {
             return;
         }
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/event/BukkitFastLoginAutoLoginEvent.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/event/BukkitFastLoginAutoLoginEvent.java
index 8c61330..6412c6d 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/event/BukkitFastLoginAutoLoginEvent.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/event/BukkitFastLoginAutoLoginEvent.java
@@ -28,6 +28,7 @@ package com.github.games647.fastlogin.bukkit.event;
 import com.github.games647.fastlogin.core.StoredProfile;
 import com.github.games647.fastlogin.core.shared.LoginSession;
 import com.github.games647.fastlogin.core.shared.event.FastLoginAutoLoginEvent;
+
 import org.bukkit.event.Cancellable;
 import org.bukkit.event.Event;
 import org.bukkit.event.HandlerList;
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/event/BukkitFastLoginPreLoginEvent.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/event/BukkitFastLoginPreLoginEvent.java
index 6b2edfc..5bf6df9 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/event/BukkitFastLoginPreLoginEvent.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/event/BukkitFastLoginPreLoginEvent.java
@@ -28,6 +28,7 @@ package com.github.games647.fastlogin.bukkit.event;
 import com.github.games647.fastlogin.core.StoredProfile;
 import com.github.games647.fastlogin.core.shared.LoginSource;
 import com.github.games647.fastlogin.core.shared.event.FastLoginPreLoginEvent;
+
 import org.bukkit.event.Event;
 import org.bukkit.event.HandlerList;
 import org.jetbrains.annotations.NotNull;
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/event/BukkitFastLoginPremiumToggleEvent.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/event/BukkitFastLoginPremiumToggleEvent.java
index 146efb0..0904826 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/event/BukkitFastLoginPremiumToggleEvent.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/event/BukkitFastLoginPremiumToggleEvent.java
@@ -27,6 +27,7 @@ package com.github.games647.fastlogin.bukkit.event;

 import com.github.games647.fastlogin.core.StoredProfile;
 import com.github.games647.fastlogin.core.shared.event.FastLoginPremiumToggleEvent;
+
 import org.bukkit.event.Event;
 import org.bukkit.event.HandlerList;
 import org.jetbrains.annotations.NotNull;
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/AuthMeHook.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/AuthMeHook.java
index fecfb73..45d5a5f 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/AuthMeHook.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/AuthMeHook.java
@@ -28,18 +28,20 @@ package com.github.games647.fastlogin.bukkit.hook;
 import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
 import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
 import com.github.games647.fastlogin.core.hooks.AuthPlugin;
+
 import fr.xephi.authme.api.v3.AuthMeApi;
 import fr.xephi.authme.events.RestoreSessionEvent;
 import fr.xephi.authme.process.Management;
 import fr.xephi.authme.process.register.executors.ApiPasswordRegisterParams;
 import fr.xephi.authme.process.register.executors.RegistrationMethod;
+
+import java.lang.reflect.Field;
+
 import org.bukkit.entity.Player;
 import org.bukkit.event.EventHandler;
 import org.bukkit.event.EventPriority;
 import org.bukkit.event.Listener;

-import java.lang.reflect.Field;
-
 /**
  * GitHub: <a href="https://github.com/Xephi/AuthMeReloaded/">...</a>
  * <p>
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/PaperCacheListener.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/PaperCacheListener.java
index e444ed9..44ff6ea 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/PaperCacheListener.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/PaperCacheListener.java
@@ -29,6 +29,7 @@ import com.destroystokyo.paper.profile.ProfileProperty;
 import com.github.games647.craftapi.model.skin.Textures;
 import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
 import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
+
 import org.bukkit.event.EventHandler;
 import org.bukkit.event.EventPriority;
 import org.bukkit.event.Listener;
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ManualNameChange.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ManualNameChange.java
index cdf3999..2a36e88 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ManualNameChange.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ManualNameChange.java
@@ -25,6 +25,7 @@
  */
 package com.github.games647.fastlogin.bukkit.listener.protocollib;

+import com.comphenix.protocol.ProtocolLibrary;
 import com.comphenix.protocol.events.PacketAdapter;
 import com.comphenix.protocol.events.PacketContainer;
 import com.comphenix.protocol.events.PacketEvent;
@@ -36,8 +37,6 @@ import org.geysermc.floodgate.api.FloodgateApi;

 import static com.comphenix.protocol.PacketType.Login.Client.START;

-import com.comphenix.protocol.ProtocolLibrary;
-
 /**
  * Manually inject Floodgate player name prefixes.
  * <br>
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/NameCheckTask.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/NameCheckTask.java
index 485c065..d3b38ea 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/NameCheckTask.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/NameCheckTask.java
@@ -49,7 +49,7 @@ public class NameCheckTask extends JoinManagement<Player, CommandSender, Protoco

     private final FastLoginBukkit plugin;
     private final PacketEvent packetEvent;
-    private final PublicKey publicKey;
+    private final PublicKey serverKey;

     private final Random random;

@@ -57,12 +57,12 @@ public class NameCheckTask extends JoinManagement<Player, CommandSender, Protoco
     private final String username;

     public NameCheckTask(FastLoginBukkit plugin, Random random, Player player, PacketEvent packetEvent,
-                         String username, PublicKey publicKey) {
+                         String username, PublicKey serverKey) {
         super(plugin.getCore(), plugin.getCore().getAuthPluginHook(), plugin.getBedrockService());

         this.plugin = plugin;
         this.packetEvent = packetEvent;
-        this.publicKey = publicKey;
+        this.serverKey = serverKey;
         this.random = random;
         this.player = player;
         this.username = username;
@@ -71,9 +71,9 @@ public class NameCheckTask extends JoinManagement<Player, CommandSender, Protoco
     @Override
     public void run() {
         try {
-            Optional<WrappedProfileKeyData> publicKey = packetEvent.getPacket().getOptionals(BukkitConverters.getWrappedPublicKeyDataConverter()).read(0);
+            Optional<WrappedProfileKeyData> clientKey = packetEvent.getPacket().getOptionals(BukkitConverters.getWrappedPublicKeyDataConverter()).read(0);

-            super.onLogin(username, new ProtocolLibLoginSource(player, random, publicKey.get(), this.publicKey));
+            super.onLogin(username, new ProtocolLibLoginSource(player, random, clientKey.get(), serverKey));
         } finally {
             ProtocolLibrary.getProtocolManager().getAsynchronousManager().signalPacketTransmission(packetEvent);
         }
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
index 55a8f33..89d855d 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
@@ -127,7 +127,7 @@ public class ProtocolLibListener extends PacketAdapter {
         }
     }

-    private Boolean isFastLoginPacket(PacketEvent packetEvent) {
+    private boolean isFastLoginPacket(PacketEvent packetEvent) {
         return packetEvent.getPacket().getMeta(SOURCE_META_KEY)
                 .map(val -> val.equals(plugin.getName()))
                 .orElse(false);
@@ -146,7 +146,7 @@ public class ProtocolLibListener extends PacketAdapter {
             long salt = FuzzyReflection.getFieldValue(signatureData, Long.TYPE, true);
             byte[] signature = FuzzyReflection.getFieldValue(signatureData, byte[].class, true);

-            PublicKey publicKey = session.getClientPublicKey().getKey();
+            PublicKey publicKey = session.getClientPublicKey().key();
             try {
                 if (EncryptionUtil.verifySignedNonce(session.getVerifyToken(), publicKey, salt, signature)) {
                     packetEvent.getAsyncMarker().incrementProcessingDelay();
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibLoginSource.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibLoginSource.java
index d87b602..d4c1130 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibLoginSource.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibLoginSource.java
@@ -33,7 +33,6 @@ import com.comphenix.protocol.wrappers.WrappedChatComponent;
 import com.comphenix.protocol.wrappers.WrappedProfilePublicKey.WrappedProfileKeyData;
 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;
@@ -64,7 +63,7 @@ class ProtocolLibLoginSource implements LoginSource {
     }

     @Override
-    public void enableOnlinemode() throws InvocationTargetException {
+    public void enableOnlinemode() {
         verifyToken = EncryptionUtil.generateVerifyToken(random);

         /*
@@ -92,7 +91,7 @@ class ProtocolLibLoginSource implements LoginSource {
     }

     @Override
-    public void kick(String message) throws InvocationTargetException {
+    public void kick(String message) {
         ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();

         PacketContainer kickPacket = new PacketContainer(DISCONNECT);
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/SkinApplyListener.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/SkinApplyListener.java
index 7d43835..3d4a807 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/SkinApplyListener.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/SkinApplyListener.java
@@ -34,6 +34,9 @@ import com.comphenix.protocol.wrappers.WrappedSignedProperty;
 import com.github.games647.craftapi.model.skin.Textures;
 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;
@@ -41,8 +44,6 @@ import org.bukkit.event.Listener;
 import org.bukkit.event.player.PlayerLoginEvent;
 import org.bukkit.event.player.PlayerLoginEvent.Result;

-import java.lang.reflect.InvocationTargetException;
-
 public class SkinApplyListener implements Listener {

     private static final Class<?> GAME_PROFILE = MinecraftReflection.getGameProfileClass();
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/task/FloodgateAuthTask.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/task/FloodgateAuthTask.java
index acb3597..ef9fed0 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/task/FloodgateAuthTask.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/task/FloodgateAuthTask.java
@@ -25,6 +25,11 @@
  */
 package com.github.games647.fastlogin.bukkit.task;

+import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
+import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
+import com.github.games647.fastlogin.core.shared.FastLoginCore;
+import com.github.games647.fastlogin.core.shared.FloodgateManagement;
+
 import java.net.InetSocketAddress;
 import java.util.UUID;

@@ -33,11 +38,6 @@ import org.bukkit.command.CommandSender;
 import org.bukkit.entity.Player;
 import org.geysermc.floodgate.api.player.FloodgatePlayer;

-import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
-import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
-import com.github.games647.fastlogin.core.shared.FastLoginCore;
-import com.github.games647.fastlogin.core.shared.FloodgateManagement;
-
 public class FloodgateAuthTask extends FloodgateManagement<Player, CommandSender, BukkitLoginSession, FastLoginBukkit> {

     public FloodgateAuthTask(FastLoginCore<Player, CommandSender, FastLoginBukkit> core, Player player, FloodgatePlayer floodgatePlayer) {
diff --git a/bungee/src/main/java/com/github/games647/fastlogin/bungee/event/BungeeFastLoginAutoLoginEvent.java b/bungee/src/main/java/com/github/games647/fastlogin/bungee/event/BungeeFastLoginAutoLoginEvent.java
index 365a198..6503ff9 100644
--- a/bungee/src/main/java/com/github/games647/fastlogin/bungee/event/BungeeFastLoginAutoLoginEvent.java
+++ b/bungee/src/main/java/com/github/games647/fastlogin/bungee/event/BungeeFastLoginAutoLoginEvent.java
@@ -28,6 +28,7 @@ package com.github.games647.fastlogin.bungee.event;
 import com.github.games647.fastlogin.core.StoredProfile;
 import com.github.games647.fastlogin.core.shared.LoginSession;
 import com.github.games647.fastlogin.core.shared.event.FastLoginAutoLoginEvent;
+
 import net.md_5.bungee.api.plugin.Cancellable;
 import net.md_5.bungee.api.plugin.Event;

diff --git a/bungee/src/main/java/com/github/games647/fastlogin/bungee/event/BungeeFastLoginPreLoginEvent.java b/bungee/src/main/java/com/github/games647/fastlogin/bungee/event/BungeeFastLoginPreLoginEvent.java
index b350763..2128cab 100644
--- a/bungee/src/main/java/com/github/games647/fastlogin/bungee/event/BungeeFastLoginPreLoginEvent.java
+++ b/bungee/src/main/java/com/github/games647/fastlogin/bungee/event/BungeeFastLoginPreLoginEvent.java
@@ -28,6 +28,7 @@ package com.github.games647.fastlogin.bungee.event;
 import com.github.games647.fastlogin.core.StoredProfile;
 import com.github.games647.fastlogin.core.shared.LoginSource;
 import com.github.games647.fastlogin.core.shared.event.FastLoginPreLoginEvent;
+
 import net.md_5.bungee.api.plugin.Event;

 public class BungeeFastLoginPreLoginEvent extends Event implements FastLoginPreLoginEvent {
diff --git a/bungee/src/main/java/com/github/games647/fastlogin/bungee/task/AsyncToggleMessage.java b/bungee/src/main/java/com/github/games647/fastlogin/bungee/task/AsyncToggleMessage.java
index 98c384c..2965855 100644
--- a/bungee/src/main/java/com/github/games647/fastlogin/bungee/task/AsyncToggleMessage.java
+++ b/bungee/src/main/java/com/github/games647/fastlogin/bungee/task/AsyncToggleMessage.java
@@ -29,8 +29,8 @@ import com.github.games647.fastlogin.bungee.FastLoginBungee;
 import com.github.games647.fastlogin.bungee.event.BungeeFastLoginPremiumToggleEvent;
 import com.github.games647.fastlogin.core.StoredProfile;
 import com.github.games647.fastlogin.core.shared.FastLoginCore;
-
 import com.github.games647.fastlogin.core.shared.event.FastLoginPremiumToggleEvent.PremiumToggleReason;
+
 import net.md_5.bungee.api.CommandSender;
 import net.md_5.bungee.api.ProxyServer;
 import net.md_5.bungee.api.chat.TextComponent;
diff --git a/bungee/src/main/java/com/github/games647/fastlogin/bungee/task/FloodgateAuthTask.java b/bungee/src/main/java/com/github/games647/fastlogin/bungee/task/FloodgateAuthTask.java
index 72719cf..5133198 100644
--- a/bungee/src/main/java/com/github/games647/fastlogin/bungee/task/FloodgateAuthTask.java
+++ b/bungee/src/main/java/com/github/games647/fastlogin/bungee/task/FloodgateAuthTask.java
@@ -25,19 +25,19 @@
  */
 package com.github.games647.fastlogin.bungee.task;

+import com.github.games647.fastlogin.bungee.BungeeLoginSession;
+import com.github.games647.fastlogin.bungee.FastLoginBungee;
+import com.github.games647.fastlogin.core.shared.FastLoginCore;
+import com.github.games647.fastlogin.core.shared.FloodgateManagement;
+
 import java.net.InetSocketAddress;
 import java.util.UUID;

-import org.geysermc.floodgate.api.player.FloodgatePlayer;
-
 import net.md_5.bungee.api.CommandSender;
 import net.md_5.bungee.api.connection.ProxiedPlayer;
 import net.md_5.bungee.api.connection.Server;

-import com.github.games647.fastlogin.bungee.BungeeLoginSession;
-import com.github.games647.fastlogin.bungee.FastLoginBungee;
-import com.github.games647.fastlogin.core.shared.FastLoginCore;
-import com.github.games647.fastlogin.core.shared.FloodgateManagement;
+import org.geysermc.floodgate.api.player.FloodgatePlayer;

 public class FloodgateAuthTask
         extends FloodgateManagement<ProxiedPlayer, CommandSender, BungeeLoginSession, FastLoginBungee> {
diff --git a/core/src/main/java/com/github/games647/fastlogin/core/hooks/DefaultPasswordGenerator.java b/core/src/main/java/com/github/games647/fastlogin/core/hooks/DefaultPasswordGenerator.java
index b6802a6..9caadef 100644
--- a/core/src/main/java/com/github/games647/fastlogin/core/hooks/DefaultPasswordGenerator.java
+++ b/core/src/main/java/com/github/games647/fastlogin/core/hooks/DefaultPasswordGenerator.java
@@ -26,7 +26,7 @@
 package com.github.games647.fastlogin.core.hooks;

 import java.security.SecureRandom;
-import java.util.Random;
+import java.util.random.RandomGenerator;
 import java.util.stream.IntStream;

 public class DefaultPasswordGenerator<P> implements PasswordGenerator<P> {
@@ -35,7 +35,7 @@ public class DefaultPasswordGenerator<P> implements PasswordGenerator<P> {
     private static final char[] PASSWORD_CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
             .toCharArray();

-    private final Random random = new SecureRandom();
+    private final RandomGenerator random = new SecureRandom();

     @Override
     public String getRandomPassword(P player) {
diff --git a/core/src/main/java/com/github/games647/fastlogin/core/hooks/bedrock/FloodgateService.java b/core/src/main/java/com/github/games647/fastlogin/core/hooks/bedrock/FloodgateService.java
index 7e6a1c2..1b555f1 100644
--- a/core/src/main/java/com/github/games647/fastlogin/core/hooks/bedrock/FloodgateService.java
+++ b/core/src/main/java/com/github/games647/fastlogin/core/hooks/bedrock/FloodgateService.java
@@ -54,14 +54,13 @@ public class FloodgateService extends BedrockService<FloodgatePlayer> {
      * <li>autoLoginFloodgate
      * <li>autoRegisterFloodgate
      * </ul>
-     * </p>
      *
      * @param key the key of the entry in config.yml
      * @return <b>true</b> if the entry's value is "true", "false", or "linked"
      */
     public boolean isValidFloodgateConfigString(String key) {
         String value = core.getConfig().get(key).toString().toLowerCase(Locale.ENGLISH);
-        if (!value.equals("true") && !value.equals("linked") && !value.equals("false") && !value.equals("no-conflict")) {
+        if (!"true".equals(value) && !"linked".equals(value) && !"false".equals(value) && !"no-conflict".equals(value)) {
             core.getPlugin().getLog().error("Invalid value detected for {} in FastLogin/config.yml.", key);
             return false;
         }
@@ -87,7 +86,7 @@ public class FloodgateService extends BedrockService<FloodgatePlayer> {
         } else {
             core.getPlugin().getLog().info("Skipping name conflict checking for player {}", username);
         }
-
+
         //Floodgate users don't need Java specific checks
         return true;
     }
@@ -98,7 +97,7 @@ public class FloodgateService extends BedrockService<FloodgatePlayer> {
      * username can be found
      * <br>
      * <i>Falls back to non-prefixed name checks, if ProtocolLib is installed</i>
-     *
+     *
      * @param prefixedUsername the name of the player with the prefix appended
      * @return FloodgatePlayer if found, null otherwise
      */
diff --git a/core/src/main/java/com/github/games647/fastlogin/core/shared/ForceLoginManagement.java b/core/src/main/java/com/github/games647/fastlogin/core/shared/ForceLoginManagement.java
index c8c7bea..873a855 100644
--- a/core/src/main/java/com/github/games647/fastlogin/core/shared/ForceLoginManagement.java
+++ b/core/src/main/java/com/github/games647/fastlogin/core/shared/ForceLoginManagement.java
@@ -25,10 +25,10 @@
  */
 package com.github.games647.fastlogin.core.shared;

-import com.github.games647.fastlogin.core.storage.SQLStorage;
 import com.github.games647.fastlogin.core.StoredProfile;
 import com.github.games647.fastlogin.core.hooks.AuthPlugin;
 import com.github.games647.fastlogin.core.shared.event.FastLoginAutoLoginEvent;
+import com.github.games647.fastlogin.core.storage.SQLStorage;

 public abstract class ForceLoginManagement<P extends C, C, L extends LoginSession, T extends PlatformPlugin<C>>
         implements Runnable {
diff --git a/core/src/test/java/com/github/games647/fastlogin/core/TickingRateLimiterTest.java b/core/src/test/java/com/github/games647/fastlogin/core/TickingRateLimiterTest.java
index 3f753cd..34f196a 100644
--- a/core/src/test/java/com/github/games647/fastlogin/core/TickingRateLimiterTest.java
+++ b/core/src/test/java/com/github/games647/fastlogin/core/TickingRateLimiterTest.java
@@ -32,8 +32,8 @@ import java.util.concurrent.TimeUnit;

 import org.junit.Test;

-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;

 public class TickingRateLimiterTest {

@@ -43,7 +43,7 @@ public class TickingRateLimiterTest {
      * Always expired
      */
     @Test
-    public void allowExpire() throws InterruptedException {
+    public void allowExpire() {
         int size = 3;

         FakeTicker ticker = new FakeTicker(5_000_000L);
@@ -51,17 +51,17 @@ public class TickingRateLimiterTest {
         // run twice the size to fill it first and then test it
         TickingRateLimiter rateLimiter = new TickingRateLimiter(ticker, size, 0);
         for (int i = 0; i < size; i++) {
-            assertTrue("Filling up", rateLimiter.tryAcquire());
+            assertThat("Filling up", rateLimiter.tryAcquire(), is(true));
         }

         for (int i = 0; i < size; i++) {
             ticker.add(Duration.ofSeconds(1));
-            assertTrue("Should be expired", rateLimiter.tryAcquire());
+            assertThat("Should be expired", rateLimiter.tryAcquire(), is(true));
         }
     }

     @Test
-    public void allowExpireNegative() throws InterruptedException {
+    public void allowExpireNegative() {
         int size = 3;

         FakeTicker ticker = new FakeTicker(-5_000_000L);
@@ -69,12 +69,12 @@ public class TickingRateLimiterTest {
         // run twice the size to fill it first and then test it
         TickingRateLimiter rateLimiter = new TickingRateLimiter(ticker, size, 0);
         for (int i = 0; i < size; i++) {
-            assertTrue("Filling up", rateLimiter.tryAcquire());
+            assertThat("Filling up", rateLimiter.tryAcquire(), is(true));
         }

         for (int i = 0; i < size; i++) {
             ticker.add(Duration.ofSeconds(1));
-            assertTrue("Should be expired", rateLimiter.tryAcquire());
+            assertThat("Should be expired", rateLimiter.tryAcquire(), is(true));
         }
     }

@@ -90,10 +90,10 @@ public class TickingRateLimiterTest {
         // fill the size
         TickingRateLimiter rateLimiter = new TickingRateLimiter(ticker, size, TimeUnit.SECONDS.toMillis(30));
         for (int i = 0; i < size; i++) {
-            assertTrue("Filling up", rateLimiter.tryAcquire());
+            assertThat("Filling up", rateLimiter.tryAcquire(), is(true));
         }

-        assertFalse("Should be full and no entry should be expired", rateLimiter.tryAcquire());
+        assertThat("Should be full and no entry should be expired", rateLimiter.tryAcquire(), is(false));
     }

     /**
@@ -108,51 +108,51 @@ public class TickingRateLimiterTest {
         // fill the size
         TickingRateLimiter rateLimiter = new TickingRateLimiter(ticker, size, TimeUnit.SECONDS.toMillis(30));
         for (int i = 0; i < size; i++) {
-            assertTrue("Filling up", rateLimiter.tryAcquire());
+            assertThat("Filling up", rateLimiter.tryAcquire(), is(true));
         }

-        assertFalse("Should be full and no entry should be expired", rateLimiter.tryAcquire());
+        assertThat("Should be full and no entry should be expired", rateLimiter.tryAcquire(), is(false));
     }

     /**
      * Blocked attempts shouldn't replace existing ones.
      */
     @Test
-    public void blockedNotAdded() throws InterruptedException {
+    public void blockedNotAdded() {
         FakeTicker ticker = new FakeTicker(5_000_000L);

         // fill the size - 100ms should be reasonable high
         TickingRateLimiter rateLimiter = new TickingRateLimiter(ticker, 1, 100);
-        assertTrue("Filling up", rateLimiter.tryAcquire());
+        assertThat("Filling up", rateLimiter.tryAcquire(), is(true));

         ticker.add(Duration.ofMillis(50));

         // still is full - should fail
-        assertFalse("Expired too early", rateLimiter.tryAcquire());
+        assertThat("Expired too early", rateLimiter.tryAcquire(), is(false));

         // wait the remaining time and add a threshold, because
         ticker.add(Duration.ofMillis(50));
-        assertTrue("Request not released", rateLimiter.tryAcquire());
+        assertThat("Request not released", rateLimiter.tryAcquire(), is(true));
     }

     /**
      * Blocked attempts shouldn't replace existing ones.
      */
     @Test
-    public void blockedNotAddedNegative() throws InterruptedException {
+    public void blockedNotAddedNegative() {
         FakeTicker ticker = new FakeTicker(-5_000_000L);

         // fill the size - 100ms should be reasonable high
         TickingRateLimiter rateLimiter = new TickingRateLimiter(ticker, 1, 100);
-        assertTrue("Filling up", rateLimiter.tryAcquire());
+        assertThat("Filling up", rateLimiter.tryAcquire(), is(true));

         ticker.add(Duration.ofMillis(50));

         // still is full - should fail
-        assertFalse("Expired too early", rateLimiter.tryAcquire());
+        assertThat("Expired too early", rateLimiter.tryAcquire(), is(false));

         // wait the remaining time and add a threshold, because
         ticker.add(Duration.ofMillis(50));
-        assertTrue("Request not released", rateLimiter.tryAcquire());
+        assertThat("Request not released", rateLimiter.tryAcquire(), is(true));
     }
 }
diff --git a/velocity/src/main/java/com/github/games647/fastlogin/velocity/listener/ConnectListener.java b/velocity/src/main/java/com/github/games647/fastlogin/velocity/listener/ConnectListener.java
index 33c8c31..902fb03 100644
--- a/velocity/src/main/java/com/github/games647/fastlogin/velocity/listener/ConnectListener.java
+++ b/velocity/src/main/java/com/github/games647/fastlogin/velocity/listener/ConnectListener.java
@@ -45,9 +45,11 @@ import com.velocitypowered.api.proxy.InboundConnection;
 import com.velocitypowered.api.proxy.Player;
 import com.velocitypowered.api.proxy.server.RegisteredServer;
 import com.velocitypowered.api.util.GameProfile;
+import com.velocitypowered.api.util.GameProfile.Property;

 import java.net.InetSocketAddress;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
@@ -121,7 +123,7 @@ public class ConnectListener {
         }
     }

-    private List<GameProfile.Property> removeSkin(List<GameProfile.Property> oldProperties) {
+    private List<GameProfile.Property> removeSkin(Collection<Property> oldProperties) {
         List<GameProfile.Property> newProperties = new ArrayList<>(oldProperties.size() - 1);
         for (GameProfile.Property property : oldProperties) {
             if (!"textures".equals(property.getName()))
diff --git a/velocity/src/main/java/com/github/games647/fastlogin/velocity/task/AsyncToggleMessage.java b/velocity/src/main/java/com/github/games647/fastlogin/velocity/task/AsyncToggleMessage.java
index 12444fc..8dc7d1c 100644
--- a/velocity/src/main/java/com/github/games647/fastlogin/velocity/task/AsyncToggleMessage.java
+++ b/velocity/src/main/java/com/github/games647/fastlogin/velocity/task/AsyncToggleMessage.java
@@ -32,6 +32,7 @@ import com.github.games647.fastlogin.velocity.FastLoginVelocity;
 import com.github.games647.fastlogin.velocity.event.VelocityFastLoginPremiumToggleEvent;
 import com.velocitypowered.api.command.CommandSource;
 import com.velocitypowered.api.proxy.Player;
+
 import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;

 public class AsyncToggleMessage implements Runnable {
2022-06-28 18:34:23 +02:00
b041a89209 Typo fixes 2022-06-28 18:34:23 +02:00
a5942cba74 Use HTML links 2022-06-28 18:34:23 +02:00
f44d7a6780 Forward client key to server
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
index a3bb3d0..55a8f33 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
@@ -119,7 +119,7 @@ public class ProtocolLibListener extends PacketAdapter {
                 case Continue:
                 default:
                     //player.getName() won't work at this state
-                    onLogin(packetEvent, sender, username);
+                    onLoginStart(packetEvent, sender, username);
                     break;
             }
         } else {
@@ -146,7 +146,6 @@ public class ProtocolLibListener extends PacketAdapter {
             long salt = FuzzyReflection.getFieldValue(signatureData, Long.TYPE, true);
             byte[] signature = FuzzyReflection.getFieldValue(signatureData, byte[].class, true);

-            BukkitLoginSession session = plugin.getSession(sender.getAddress());
             PublicKey publicKey = session.getClientPublicKey().getKey();
             try {
                 if (EncryptionUtil.verifySignedNonce(session.getVerifyToken(), publicKey, salt, signature)) {
@@ -162,7 +161,7 @@ public class ProtocolLibListener extends PacketAdapter {
         }
     }

-    private void onLogin(PacketEvent packetEvent, Player player, String username) {
+    private void onLoginStart(PacketEvent packetEvent, Player player, String username) {
         //this includes ip:port. Should be unique for an incoming login request with a timeout of 2 minutes
         String sessionKey = player.getAddress().toString();

diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java
index d13a5c9..ed84298 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java
@@ -43,6 +43,7 @@ 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.FastLoginBukkit;
+import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;

 import java.io.IOException;
 import java.lang.reflect.Method;
@@ -168,7 +169,7 @@ public class VerifyResponseTask implements Runnable {
         session.setVerified(true);

         setPremiumUUID(session.getUuid());
-        receiveFakeStartPacket(realUsername);
+        receiveFakeStartPacket(realUsername, session.getClientPublicKey());
     }

     private void setPremiumUUID(UUID premiumUUID) {
@@ -253,7 +254,7 @@ public class VerifyResponseTask implements Runnable {
     }

     //fake a new login packet in order to let the server handle all the other stuff
-    private void receiveFakeStartPacket(String username) {
+    private void receiveFakeStartPacket(String username, ClientPublicKey clientKey) {
         //see StartPacketListener for packet information
         PacketContainer startPacket = new PacketContainer(START);

@@ -261,7 +262,8 @@ public class VerifyResponseTask implements Runnable {
             startPacket.getStrings().write(0, username);

             EquivalentConverter<WrappedProfileKeyData> converter = BukkitConverters.getWrappedPublicKeyDataConverter();
-            startPacket.getOptionals(converter).write(0, Optional.empty());
+            var key = new WrappedProfileKeyData(clientKey.getExpiry(), clientKey.getKey(), sharedSecret);
+            startPacket.getOptionals(converter).write(0, Optional.of(key));
         } else {
             //uuid is ignored by the packet definition
             WrappedGameProfile fakeProfile = new WrappedGameProfile(UUID.randomUUID(), username);
2022-06-28 18:34:23 +02:00
0f17fe18f9 Document origin of signing keys 2022-06-28 18:34:22 +02:00
dac5cd7639 Verify signed nonce
diff --git a/bukkit/pom.xml b/bukkit/pom.xml
index 9b3c5c5..c1f63ba 100644
--- a/bukkit/pom.xml
+++ b/bukkit/pom.xml
@@ -179,6 +179,19 @@
             <version>1.0.7</version>
         </dependency>

+        <dependency>
+            <groupId>com.mojang</groupId>
+            <artifactId>datafixerupper</artifactId>
+            <version>5.0.28</version>
+            <scope>provided</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>*</groupId>
+                    <artifactId>*</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
         <!--Library for listening and sending Minecraft packets-->
         <dependency>
             <groupId>com.comphenix.protocol</groupId>
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/BukkitLoginSession.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/BukkitLoginSession.java
index 556def1..4f4af4d 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/BukkitLoginSession.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/BukkitLoginSession.java
@@ -26,6 +26,7 @@
 package com.github.games647.fastlogin.bukkit;

 import com.github.games647.craftapi.model.skin.SkinProperty;
+import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;
 import com.github.games647.fastlogin.core.StoredProfile;
 import com.github.games647.fastlogin.core.shared.LoginSession;

@@ -42,30 +43,33 @@ public class BukkitLoginSession extends LoginSession {

     private final byte[] verifyToken;

+    private final ClientPublicKey clientPublicKey;
+
     private boolean verified;

     private SkinProperty skinProperty;

-    public BukkitLoginSession(String username, byte[] verifyToken, boolean registered
+    public BukkitLoginSession(String username, byte[] verifyToken, ClientPublicKey publicKey, boolean registered
             , StoredProfile profile) {
         super(username, registered, profile);

+        this.clientPublicKey = publicKey;
         this.verifyToken = verifyToken.clone();
     }

     //available for BungeeCord
     public BukkitLoginSession(String username, boolean registered) {
-        this(username, EMPTY_ARRAY, registered, null);
+        this(username, EMPTY_ARRAY, null, registered, null);
     }

     //cracked player
     public BukkitLoginSession(String username, StoredProfile profile) {
-        this(username, EMPTY_ARRAY, false, profile);
+        this(username, EMPTY_ARRAY, null, false, profile);
     }

     //ProtocolSupport
     public BukkitLoginSession(String username, boolean registered, StoredProfile profile) {
-        this(username, EMPTY_ARRAY, registered, profile);
+        this(username, EMPTY_ARRAY, null, registered, profile);
     }

     /**
@@ -79,6 +83,10 @@ public class BukkitLoginSession extends LoginSession {
         return verifyToken.clone();
     }

+    public ClientPublicKey getClientPublicKey() {
+        return clientPublicKey;
+    }
+
     /**
      * @return premium skin if available
      */
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtil.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtil.java
index 956a60f..713fa68 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtil.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtil.java
@@ -27,6 +27,7 @@ package com.github.games647.fastlogin.bukkit.listener.protocollib;

 import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;
 import com.google.common.io.Resources;
+import com.google.common.primitives.Longs;

 import java.io.IOException;
 import java.math.BigInteger;
@@ -115,9 +116,9 @@ class EncryptionUtil {
     /**
      * Generate the server id based on client and server data.
      *
-     * @param sessionId session for the current login attempt
+     * @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
+     * @param publicKey    public key of the server
      * @return the server id formatted as a hexadecimal string.
      */
     public static String getServerIdHashString(String sessionId, SecretKey sharedSecret, PublicKey publicKey) {
@@ -136,7 +137,7 @@ class EncryptionUtil {
      * Decrypts the content and extracts the key spec.
      *
      * @param privateKey private server key
-     * @param sharedKey the encrypted shared key
+     * @param sharedKey  the encrypted shared key
      * @return shared secret key
      * @throws GeneralSecurityException if it fails to decrypt the data
      */
@@ -151,10 +152,20 @@ class EncryptionUtil {
             return false;
         }

-        Signature signature = Signature.getInstance("SHA1withRSA");
-        signature.initVerify(mojangSessionKey);
-        signature.update(toSignable(clientKey).getBytes(StandardCharsets.US_ASCII));
-        return signature.verify(clientKey.getSignature());
+        Signature verifier = Signature.getInstance("SHA1withRSA");
+        verifier.initVerify(mojangSessionKey);
+        verifier.update(toSignable(clientKey).getBytes(StandardCharsets.US_ASCII));
+        return verifier.verify(clientKey.getSignature());
+    }
+
+    public static boolean verifySignedNonce(byte[] nonce, PublicKey clientKey, long signatureSalt, byte[] signature)
+        throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+        Signature verifier = Signature.getInstance("SHA256withRSA");
+        verifier.initVerify(clientKey);
+
+        verifier.update(nonce);
+        verifier.update(Longs.toByteArray(signatureSalt));
+        return verifier.verify(signature);
     }

     private static PublicKey loadMojangSessionKey()
@@ -183,7 +194,7 @@ class EncryptionUtil {
      * Decrypted the given data using the cipher.
      *
      * @param cipher decryption cypher initialized with the private key
-     * @param data the encrypted data
+     * @param data   the encrypted data
      * @return clear text data
      * @throws GeneralSecurityException if it fails to decrypt the data
      */
@@ -194,7 +205,7 @@ class EncryptionUtil {
     }

     private static byte[] getServerIdHash(String sessionId, PublicKey publicKey, SecretKey sharedSecret)
-            throws NoSuchAlgorithmException {
+        throws NoSuchAlgorithmException {
         // byte[] a(String var0, PublicKey var1, SecretKey var2)
         MessageDigest digest = MessageDigest.getInstance("SHA-1");

diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/NameCheckTask.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/NameCheckTask.java
index f0a4083..485c065 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/NameCheckTask.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/NameCheckTask.java
@@ -27,21 +27,25 @@ package com.github.games647.fastlogin.bukkit.listener.protocollib;

 import com.comphenix.protocol.ProtocolLibrary;
 import com.comphenix.protocol.events.PacketEvent;
+import com.comphenix.protocol.wrappers.BukkitConverters;
+import com.comphenix.protocol.wrappers.WrappedProfilePublicKey.WrappedProfileKeyData;
 import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
 import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
 import com.github.games647.fastlogin.bukkit.event.BukkitFastLoginPreLoginEvent;
+import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;
 import com.github.games647.fastlogin.core.StoredProfile;
 import com.github.games647.fastlogin.core.shared.JoinManagement;
 import com.github.games647.fastlogin.core.shared.event.FastLoginPreLoginEvent;

 import java.security.PublicKey;
+import java.util.Optional;
 import java.util.Random;

 import org.bukkit.command.CommandSender;
 import org.bukkit.entity.Player;

 public class NameCheckTask extends JoinManagement<Player, CommandSender, ProtocolLibLoginSource>
-        implements Runnable {
+    implements Runnable {

     private final FastLoginBukkit plugin;
     private final PacketEvent packetEvent;
@@ -67,7 +71,9 @@ public class NameCheckTask extends JoinManagement<Player, CommandSender, Protoco
     @Override
     public void run() {
         try {
-            super.onLogin(username, new ProtocolLibLoginSource(player, random, publicKey));
+            Optional<WrappedProfileKeyData> publicKey = packetEvent.getPacket().getOptionals(BukkitConverters.getWrappedPublicKeyDataConverter()).read(0);
+
+            super.onLogin(username, new ProtocolLibLoginSource(player, random, publicKey.get(), this.publicKey));
         } finally {
             ProtocolLibrary.getProtocolManager().getAsynchronousManager().signalPacketTransmission(packetEvent);
         }
@@ -85,7 +91,7 @@ public class NameCheckTask extends JoinManagement<Player, CommandSender, Protoco
     //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) {
+        , String username, boolean registered) {
         try {
             source.enableOnlinemode();
         } catch (Exception ex) {
@@ -97,8 +103,10 @@ public class NameCheckTask extends JoinManagement<Player, CommandSender, Protoco
         core.getPendingLogin().put(ip + username, new Object());

         byte[] verify = source.getVerifyToken();
+        WrappedProfileKeyData key = source.getClientPublicKey();
+        ClientPublicKey clientKey = new ClientPublicKey(key.getExpireTime(), key.getKey(), key.getSignature());

-        BukkitLoginSession playerSession = new BukkitLoginSession(username, verify, registered, profile);
+        BukkitLoginSession playerSession = new BukkitLoginSession(username, verify, clientKey, registered, profile);
         plugin.putSession(player.getAddress(), playerSession);
         //cancel only if the player has a paid account otherwise login as normal offline player
         synchronized (packetEvent.getAsyncMarker().getProcessingLock()) {
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
index 27c0335..a3bb3d0 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
@@ -30,6 +30,7 @@ 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.comphenix.protocol.reflect.FuzzyReflection;
 import com.comphenix.protocol.wrappers.WrappedGameProfile;
 import com.comphenix.protocol.wrappers.WrappedProfilePublicKey;
 import com.comphenix.protocol.wrappers.WrappedProfilePublicKey.WrappedProfileKeyData;
@@ -38,6 +39,7 @@ import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
 import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;
 import com.github.games647.fastlogin.core.antibot.AntiBotService;
 import com.github.games647.fastlogin.core.antibot.AntiBotService.Action;
+import com.mojang.datafixers.util.Either;

 import java.net.InetSocketAddress;
 import java.security.InvalidKeyException;
@@ -139,9 +141,24 @@ public class ProtocolLibListener extends PacketAdapter {
             plugin.getLog().warn("GameProfile {} tried to send encryption response at invalid state", sender.getAddress());
             sender.kickPlayer(plugin.getCore().getMessage("invalid-request"));
         } else {
-            packetEvent.getAsyncMarker().incrementProcessingDelay();
-            Runnable verifyTask = new VerifyResponseTask(plugin, packetEvent, sender, session, sharedSecret, keyPair);
-            plugin.getScheduler().runAsync(verifyTask);
+            Either<byte[], ?> either = packetEvent.getPacket().getSpecificModifier(Either.class).read(0);
+            Object signatureData = either.right().get();
+            long salt = FuzzyReflection.getFieldValue(signatureData, Long.TYPE, true);
+            byte[] signature = FuzzyReflection.getFieldValue(signatureData, byte[].class, true);
+
+            BukkitLoginSession session = plugin.getSession(sender.getAddress());
+            PublicKey publicKey = session.getClientPublicKey().getKey();
+            try {
+                if (EncryptionUtil.verifySignedNonce(session.getVerifyToken(), publicKey, salt, signature)) {
+                    packetEvent.getAsyncMarker().incrementProcessingDelay();
+                    Runnable verifyTask = new VerifyResponseTask(plugin, packetEvent, sender, sharedSecret, keyPair);
+                    plugin.getScheduler().runAsync(verifyTask);
+                } else {
+                    sender.kickPlayer("Invalid signature");
+                }
+            } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
+                sender.kickPlayer("Invalid signature");
+            }
         }
     }

diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibLoginSource.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibLoginSource.java
index dd191f1..d87b602 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibLoginSource.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibLoginSource.java
@@ -30,6 +30,7 @@ import com.comphenix.protocol.ProtocolManager;
 import com.comphenix.protocol.events.PacketContainer;
 import com.comphenix.protocol.reflect.StructureModifier;
 import com.comphenix.protocol.wrappers.WrappedChatComponent;
+import com.comphenix.protocol.wrappers.WrappedProfilePublicKey.WrappedProfileKeyData;
 import com.github.games647.fastlogin.core.shared.LoginSource;

 import java.lang.reflect.InvocationTargetException;
@@ -48,15 +49,18 @@ class ProtocolLibLoginSource implements LoginSource {
     private final Player player;

     private final Random random;
+
+    private final WrappedProfileKeyData clientPublicKey;
     private final PublicKey publicKey;

     private final String serverId = "";
     private byte[] verifyToken;

-    public ProtocolLibLoginSource(Player player, Random random, PublicKey publicKey) {
+    public ProtocolLibLoginSource(Player player, Random random, WrappedProfileKeyData clientPublicKey, PublicKey serverPublicKey) {
         this.player = player;
         this.random = random;
-        this.publicKey = publicKey;
+        this.clientPublicKey = clientPublicKey;
+        this.publicKey = serverPublicKey;
     }

     @Override
@@ -109,6 +113,10 @@ class ProtocolLibLoginSource implements LoginSource {
         return player.getAddress();
     }

+    public WrappedProfileKeyData getClientPublicKey() {
+        return clientPublicKey;
+    }
+
     public String getServerId() {
         return serverId;
     }
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java
index e974b09..d13a5c9 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java
@@ -29,12 +29,15 @@ import com.comphenix.protocol.ProtocolLibrary;
 import com.comphenix.protocol.events.PacketContainer;
 import com.comphenix.protocol.events.PacketEvent;
 import com.comphenix.protocol.injector.temporary.TemporaryPlayerFactory;
+import com.comphenix.protocol.reflect.EquivalentConverter;
 import com.comphenix.protocol.reflect.FieldUtils;
 import com.comphenix.protocol.reflect.FuzzyReflection;
 import com.comphenix.protocol.utility.MinecraftReflection;
 import com.comphenix.protocol.utility.MinecraftVersion;
+import com.comphenix.protocol.wrappers.BukkitConverters;
 import com.comphenix.protocol.wrappers.WrappedChatComponent;
 import com.comphenix.protocol.wrappers.WrappedGameProfile;
+import com.comphenix.protocol.wrappers.WrappedProfilePublicKey.WrappedProfileKeyData;
 import com.github.games647.craftapi.model.auth.Verification;
 import com.github.games647.craftapi.model.skin.SkinProperty;
 import com.github.games647.craftapi.resolver.MojangResolver;
@@ -120,7 +123,7 @@ public class VerifyResponseTask implements Runnable {
         }

         try {
-            if (!checkVerifyToken(session) || !enableEncryption(loginKey)) {
+            if (!enableEncryption(loginKey)) {
                 return;
             }
         } catch (Exception ex) {
@@ -180,23 +183,6 @@ public class VerifyResponseTask implements Runnable {
         }
     }

-    private boolean checkVerifyToken(BukkitLoginSession session) 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(serverKey.getPrivate(), responseVerify))) {
-            //check if the verify-token are equal to the server sent one
-            disconnect("invalid-verify-token",
-                "GameProfile {0} ({1}) tried to login with an invalid verify token. Server: {2} Client: {3}",
-                session.getRequestUsername(), 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);
@@ -273,6 +259,9 @@ public class VerifyResponseTask implements Runnable {

         if (MinecraftVersion.atOrAbove(new MinecraftVersion(1, 19, 0))) {
             startPacket.getStrings().write(0, username);
+
+            EquivalentConverter<WrappedProfileKeyData> converter = BukkitConverters.getWrappedPublicKeyDataConverter();
+            startPacket.getOptionals(converter).write(0, Optional.empty());
         } else {
             //uuid is ignored by the packet definition
             WrappedGameProfile fakeProfile = new WrappedGameProfile(UUID.randomUUID(), username);
diff --git a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/Base64Adapter.java b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/Base64Adapter.java
new file mode 100644
index 0000000..c2a2d1a
--- /dev/null
+++ b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/Base64Adapter.java
@@ -0,0 +1,48 @@
+/*
+ * SPDX-License-Identifier: MIT
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015-2022 games647 and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package com.github.games647.fastlogin.bukkit.listener.protocollib;
+
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+
+import java.io.IOException;
+import java.util.Base64;
+
+public class Base64Adapter extends TypeAdapter<byte[]> {
+
+    @Override
+    public void write(JsonWriter out, byte[] value) throws IOException {
+        var encoded = Base64.getEncoder().encodeToString(value);
+        out.value(encoded);
+    }
+
+    @Override
+    public byte[] read(JsonReader in) throws IOException {
+        String encoded = in.nextString();
+        return Base64.getDecoder().decode(encoded);
+    }
+}
diff --git a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtilTest.java b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtilTest.java
index 904405a..63a24ba 100644
--- a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtilTest.java
+++ b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtilTest.java
@@ -25,6 +25,7 @@
  */
 package com.github.games647.fastlogin.bukkit.listener.protocollib;

+import com.github.games647.fastlogin.bukkit.listener.protocollib.SignatureTestData.SignatureData;
 import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;
 import com.google.common.io.Resources;
 import com.google.gson.Gson;
@@ -36,9 +37,12 @@ import java.io.StringReader;
 import java.nio.charset.StandardCharsets;
 import java.security.KeyFactory;
 import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
 import java.security.SecureRandom;
+import java.security.interfaces.RSAPrivateKey;
 import java.security.interfaces.RSAPublicKey;
 import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
 import java.security.spec.X509EncodedKeySpec;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
@@ -65,7 +69,7 @@ public class EncryptionUtilTest {

     @Test
     public void testExpiredClientKey() throws Exception {
-        var clientKey = loadClientKey("client_keys/valid.json");
+        var clientKey = loadClientKey("client_keys/valid_public_key.json");

         // Client expires at the exact second mentioned, so use it for verification
         var expiredTimestamp = clientKey.getExpiry();
@@ -78,7 +82,9 @@ public class EncryptionUtilTest {
         // expiration date changed should make the signature invalid
         // expiration should still be valid
         var clientKey = loadClientKey("client_keys/invalid_wrong_expiration.json");
-        assertThat(EncryptionUtil.verifyClientKey(clientKey, clientKey.getExpiry().minus(5, ChronoUnit.HOURS)), is(false));
+        Instant expireTimestamp = clientKey.getExpiry().minus(5, ChronoUnit.HOURS);
+
+        assertThat(EncryptionUtil.verifyClientKey(clientKey, expireTimestamp), is(false));
     }

     // @Test(expected = Exception.class)
@@ -86,31 +92,119 @@ public class EncryptionUtilTest {
     public void testInvalidChangedKey() throws Exception {
         // changed public key no longer corresponding to the signature
         var clientKey = loadClientKey("client_keys/invalid_wrong_key.json");
-        assertThat(EncryptionUtil.verifyClientKey(clientKey, clientKey.getExpiry().minus(5, ChronoUnit.HOURS)), is(false));
+        Instant expireTimestamp = clientKey.getExpiry().minus(5, ChronoUnit.HOURS);
+
+        assertThat(EncryptionUtil.verifyClientKey(clientKey, expireTimestamp), is(false));
     }

     @Test
     public void testInvalidChangedSignature() throws Exception {
         // signature modified no longer corresponding to key and expiration date
         var clientKey = loadClientKey("client_keys/invalid_wrong_signature.json");
-        assertThat(EncryptionUtil.verifyClientKey(clientKey, clientKey.getExpiry().minus(5, ChronoUnit.HOURS)), is(false));
+        Instant expireTimestamp = clientKey.getExpiry().minus(5, ChronoUnit.HOURS);
+
+        assertThat(EncryptionUtil.verifyClientKey(clientKey, expireTimestamp), is(false));
     }

     @Test
     public void testValidClientKey() throws Exception {
-        var clientKey = loadClientKey("client_keys/valid.json");
-
+        var clientKey = loadClientKey("client_keys/valid_public_key.json");
         var verificationTimestamp = clientKey.getExpiry().minus(5, ChronoUnit.HOURS);
+
         assertThat(EncryptionUtil.verifyClientKey(clientKey, verificationTimestamp), is(true));
     }

+    @Test
+    public void testValidSignedNonce() throws Exception {
+        ClientPublicKey clientKey = loadClientKey("client_keys/valid_public_key.json");
+        PublicKey clientPublicKey = clientKey.getKey();
+
+        SignatureTestData testData = loadSignatureResource("signature/valid_signature.json");
+        byte[] nonce = testData.getNonce();
+        SignatureData signature = testData.getSignature();
+        long salt = signature.getSalt();
+        assertThat(EncryptionUtil.verifySignedNonce(nonce, clientPublicKey, salt, signature.getSignature()), is(true));
+    }
+
+    @Test
+    public void testIncorrectNonce() throws Exception {
+        ClientPublicKey clientKey = loadClientKey("client_keys/valid_public_key.json");
+        PublicKey clientPublicKey = clientKey.getKey();
+
+        SignatureTestData testData = loadSignatureResource("signature/incorrect_nonce.json");
+        byte[] nonce = testData.getNonce();
+        SignatureData signature = testData.getSignature();
+        long salt = signature.getSalt();
+        assertThat(EncryptionUtil.verifySignedNonce(nonce, clientPublicKey, salt, signature.getSignature()), is(false));
+    }
+
+    @Test
+    public void testIncorrectSalt() throws Exception {
+        // client generated
+        ClientPublicKey clientKey = loadClientKey("client_keys/valid_public_key.json");
+        PublicKey clientPublicKey = clientKey.getKey();
+
+        SignatureTestData testData = loadSignatureResource("signature/incorrect_salt.json");
+        byte[] nonce = testData.getNonce();
+        SignatureData signature = testData.getSignature();
+        long salt = signature.getSalt();
+        assertThat(EncryptionUtil.verifySignedNonce(nonce, clientPublicKey, salt, signature.getSignature()), is(false));
+    }
+
+    @Test
+    public void testIncorrectSignature() throws Exception {
+        // client generated
+        ClientPublicKey clientKey = loadClientKey("client_keys/valid_public_key.json");
+        PublicKey clientPublicKey = clientKey.getKey();
+
+        SignatureTestData testData = loadSignatureResource("signature/incorrect_signature.json");
+        byte[] nonce = testData.getNonce();
+        SignatureData signature = testData.getSignature();
+        long salt = signature.getSalt();
+        assertThat(EncryptionUtil.verifySignedNonce(nonce, clientPublicKey, salt, signature.getSignature()), is(false));
+    }
+
+    @Test
+    public void testWrongPublicKeySigned() throws Exception {
+        // load a different public key
+        ClientPublicKey clientKey = loadClientKey("client_keys/invalid_wrong_key.json");
+        PublicKey clientPublicKey = clientKey.getKey();
+
+        SignatureTestData testData = loadSignatureResource("signature/valid_signature.json");
+        byte[] nonce = testData.getNonce();
+        SignatureData signature = testData.getSignature();
+        long salt = signature.getSalt();
+        assertThat(EncryptionUtil.verifySignedNonce(nonce, clientPublicKey, salt, signature.getSignature()), is(false));
+    }
+
+    private SignatureTestData loadSignatureResource(String resourceName) throws IOException {
+        var keyUrl = Resources.getResource(resourceName);
+        var encodedSignature = Resources.toString(keyUrl, StandardCharsets.US_ASCII);
+
+        return new Gson().fromJson(encodedSignature, SignatureTestData.class);
+    }
+
+    private RSAPrivateKey parsePrivateKey(String keySpec)
+        throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
+        try (
+            Reader reader = new StringReader(keySpec);
+            PemReader pemReader = new PemReader(reader)
+        ) {
+            PemObject pemObject = pemReader.readPemObject();
+            byte[] content = pemObject.getContent();
+            var privateKeySpec = new PKCS8EncodedKeySpec(content);
+
+            var factory = KeyFactory.getInstance("RSA");
+            return (RSAPrivateKey) factory.generatePrivate(privateKeySpec);
+        }
+    }
+
     private ClientPublicKey loadClientKey(String path)
         throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
         var keyUrl = Resources.getResource(path);
-        var gson = new Gson();

         var lines = Resources.toString(keyUrl, StandardCharsets.US_ASCII);
-        var object = gson.fromJson(lines, JsonObject.class);
+        var object = new Gson().fromJson(lines, JsonObject.class);

         Instant expires = Instant.parse(object.getAsJsonPrimitive("expires_at").getAsString());
         String key = object.getAsJsonPrimitive("key").getAsString();
@@ -120,10 +214,10 @@ public class EncryptionUtilTest {
         return new ClientPublicKey(expires, publicKey, signature);
     }

-    private RSAPublicKey parsePublicKey(String lines)
+    private RSAPublicKey parsePublicKey(String keySpec)
         throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
         try (
-            Reader reader = new StringReader(lines);
+            Reader reader = new StringReader(keySpec);
             PemReader pemReader = new PemReader(reader)
         ) {
             PemObject pemObject = pemReader.readPemObject();
diff --git a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/SignatureTestData.java b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/SignatureTestData.java
new file mode 100644
index 0000000..8ea85f6
--- /dev/null
+++ b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/SignatureTestData.java
@@ -0,0 +1,60 @@
+/*
+ * SPDX-License-Identifier: MIT
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015-2022 games647 and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package com.github.games647.fastlogin.bukkit.listener.protocollib;
+
+import com.google.gson.annotations.JsonAdapter;
+
+public class SignatureTestData {
+
+    @JsonAdapter(Base64Adapter.class)
+    private byte[] nonce;
+
+    private SignatureData signature;
+
+    public byte[] getNonce() {
+        return nonce;
+    }
+
+    public SignatureData getSignature() {
+        return signature;
+    }
+
+    public static class SignatureData {
+
+        private long salt;
+
+        @JsonAdapter(Base64Adapter.class)
+        private byte[] signature;
+
+        public long getSalt() {
+            return salt;
+        }
+
+        public byte[] getSignature() {
+            return signature;
+        }
+    }
+}
diff --git a/bukkit/src/test/resources/client_keys/README.md b/bukkit/src/test/resources/client_keys/README.md
new file mode 100644
index 0000000..1165e23
--- /dev/null
+++ b/bukkit/src/test/resources/client_keys/README.md
@@ -0,0 +1,27 @@
+# About
+
+This contains test resources for the unit tests. The file are extracted from the Minecraft directory with slight
+modifications. The files are found in `$MINECRAFT_HOME$/profilekeys/`, where `$MINECRAFT_HOME$` represents the
+OS-dependent minecraft folder.
+
+**Notable the files in this folder do not contain the private key information. It should be explicitly
+stripped before including it.**
+
+## Minecraft folder
+
+* Windows: `%appdata%\.minecraft`
+* Linux: `/home/username/.minecraft`
+* Mac: `~/Library/Application Support/minecraft`
+
+## Directory structure
+
+* `invalid_wrong_expiration.json`: Changed the expiration date
+* `invalid_wrong_key.json`: Modified public key while keeping the RSA structure valid
+* `invalid_wrong_signature.json`: Changed a character in the public key signature
+* `valid_public_key.json`: Extracted from actual file
+
+## File content
+
+* `expires_at`: Expiration date
+* `key`: Public key from the original file out of `public_key.key`
+* `signature`: Mojang signed signature of this public key
diff --git a/bukkit/src/test/resources/client_keys/invalid_wrong_expiration.json b/bukkit/src/test/resources/client_keys/invalid_wrong_expiration.json
index 7ecc12e..ea509fe 100644
--- a/bukkit/src/test/resources/client_keys/invalid_wrong_expiration.json
+++ b/bukkit/src/test/resources/client_keys/invalid_wrong_expiration.json
@@ -1,5 +1,5 @@
 {
-    "expires_at": "2022-06-12T09:46:26.421156927Z",
-    "key": "-----BEGIN RSA PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoOv23jt2QPyab6bPRBwH2ggmzQU4I+xm\nDpi3X5ZB5Em/4uzyZqNVLJc0gShpk0XsdoB28Nq1bPxczOTBxuXi3rg5ax5gL+iymDSU27DLM8s/\n33lOofzGPJUEQGKlFm0QelDKZ/q5Y/9inHE3hEJKf7h0tnmGahXFmZSF/nRz9GHnfSYpjtDr9bsZ\nOzQuLhHXT5E4ksNRTFW41h0MlZ1qOhO+NiiVgk7LmgVYiV7RRbgO8U6RaCEqg5n28Ewo6QtzB+DF\n4NTDeu3E9BLH5G0npdUrVNhdRUWCFDmH6n9hqSIz2J7o6GvWqEvp0h9e/3qtLsoS60hnQXunrcWc\nPaEIYQIDAQAB\n-----END RSA PUBLIC KEY-----\n",
-    "signature": "BYv2mKJvz5O3Wo5V5sbJI0L6zAjfzQSkTNd7ykd/MB7KPPDg4zoTuOqphmh042xz1EYbMcfqEZvP04NTaoZDx+IxGieBB+LuxqnmYKIgtpcR2SEpzbSKznSHkupr1hKwF7kCVWLlwSbxc/XRlWPPyT6FE9m628A/jFb/obgfzLLQWfTFWp6kq2oBoUUQV5om2ihdrJ8oLCsw10SGdcFtK4+UuLzz+wjwv3JpvIX93IKdjFnw0KNd110HOPWZgp2n8+f6GsecysorqvwaE1rJC0m9Qa/wFsK2TY7twSMreCrbXPaBiWrkRovtm9gnaBwD+iuZYlnLvO0ld8qW928LL2vFBTi3TsPUWC3i/xYnAR2m8YP2hiCLHuPfSJmgfxHsM2iRdrR8qdOUkiC9h34STEA7Q2+rWkNWJ+YKYVTIkyqHEuXqU87txhVTaRJi6UDGDn49cMKmZwQnn+23JQf1chcn1YFkrivDaJPhm17GhoEldQHSLQfxb0ifja5WBNDbkKBF/h9JqvG3Ya9anxlyxY6g7/m2zP73xfkvUnejoX4GKjffEqezQmTe9RIeuWyz94nfZNLr0Ps363kAfP4KSW+f4zkTU/UVg19ccAY0ZhiwDetKyksU5WqLO8xMPZ6PNFYhNeBb2yhGdT8PidkRYkC4XBn1k7F7apiNUuZU8aA="
+    "expires_at": "2022-06-20T07:31:47.318722344Z",
+    "key": "-----BEGIN RSA PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApd3ZxDhcRWWru1XEBke6uYqmbnS2Oxyk\nOMj+QDKrkwUqhVJYciyXGsMx46Mgr/KIoGCcokP5OtIxc6+69/ZLqJ9PvM81kLIxAqyvfBMKMGjP\n376LgxTF1FeDpbe5zXaNRxfmnvQhS5YTLbzgk36qWVjqxJMG4VLVmh7RV5zWsb7XlckZb2zRHM2Y\nMHbEC+ggX+l6zQyfG1KK0MH5k+O6b0xD0rv1wm24sLOesTXH6RZG8cNE3ofdnavxjFodTOnra6w1\naiVcoUTdEPSS86wQwq9j0YCcAKOwMXsqbk9NhpujrdyJ94dev+ELwkNS7P0pPrcfiyFTQeJCZTXz\nJB36MwIDAQAB\n-----END RSA PUBLIC KEY-----\n",
+    "signature": "lfRXK4zL213wBKg760eiPV7yvnLZ6a6v9Iohmw78yxIzqXO3tfrC5Z+P2LGiO1BdI4xckx8yz4ktn82zX97+r2zktBw0As7g71H/FjInpoZ76j3gMUaiFNrQJ0vKCCI7xsjonemroWAVDCAqlvdyqwUu/Fnz85+WoR2kCQ721vwy6IjWA3xhq8XrWjkI/AlBmoS/kVqnvjjjc9vocdddJXbUYzCse/hWWIbsFeBXyiGCd3v7apgtXwQfM++tt87fq7444zQskiYb14oQP8/uNwqZWQ9jAs00i1BZ0MNM6+TZYGHOfS6rbHZ1bcX34VZdcCwpapK/Z2HBRIgDN4QOcgJkyq1GcjvlM2wjfhN8gXTsmbF9Ee+5Y6a4ONRkxRZK2sT8oAXdm0OlTEGB0P0+WRRFOQ/PnRqbI7lvANao2METT2EUHHrtqFMe53kqCHdzy5qyuHxdCEa6l/gSR08fybx9DdRRmhOlhSPGxhgwqyi1fEMrN4CsSKNrv5u+sMqhspA05b3DQJeLDX+UV5ujRHwm0A49NF+h1ZYlrcefz5IMUUisOOw6HiLc/YGLD2jHwSePGdfMwMnrIxbxjCta7/7A91aaN7eYm16KW9erCOwAfJmBSQC6Pbmg5f7rd7rAKVOPxglq7nayXmd+BK53Mal5tltMSgd/0iY6SEtGSEU="
 }
diff --git a/bukkit/src/test/resources/client_keys/invalid_wrong_key.json b/bukkit/src/test/resources/client_keys/invalid_wrong_key.json
index 37bb3ad..98ea33f 100644
--- a/bukkit/src/test/resources/client_keys/invalid_wrong_key.json
+++ b/bukkit/src/test/resources/client_keys/invalid_wrong_key.json
@@ -1,5 +1,5 @@
 {
-    "expires_at": "2022-06-12T10:46:26.421156927Z",
+    "expires_at": "2022-06-20T08:31:47.318722344Z",
     "key": "-----BEGIN RSA PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoOv23jt2QPyab6bPRBwH2ggmzQU3I+xmDpi3X5ZB5Em/4uzyZqNVLJc0gShpk0XsdoB28Nq1bPxczOTBxuXi3rg5ax5gL+iymDSU27DLM8s/33lOofzGPJUEQGKlFm0QelDKZ/q5Y/9inHE3hEJKf7h0tnmGahXFmZSF/nRz9GHnfSYpjtDr9bsZOzQuLhHXT5E4ksNRTFW41h0MlZ1qOhO+NiiVgk7LmgVYiV7RRbgO8U6RaCEqg5n28Ewo6QtzB+DF4NTDeu3E9BLH5G0npdUrVNhdRUWCFDmH6n9hqSIz2J7o6GvWqEvp0h9e/3qtLsoS60hnQXunrcWcPaEIYQIDAQAB\n-----END RSA PUBLIC KEY-----\n",
-    "signature": "BYv2mKJvz5O3Wo5V5sbJI0L6zAjfzQSkTNd7ykd/MB7KPPDg4zoTuOqphmh042xz1EYbMcfqEZvP04NTaoZDx+IxGieBB+LuxqnmYKIgtpcR2SEpzbSKznSHkupr1hKwF7kCVWLlwSbxc/XRlWPPyT6FE9m628A/jFb/obgfzLLQWfTFWp6kq2oBoUUQV5om2ihdrJ8oLCsw10SGdcFtK4+UuLzz+wjwv3JpvIX93IKdjFnw0KNd110HOPWZgp2n8+f6GsecysorqvwaE1rJC0m9Qa/wFsK2TY7twSMreCrbXPaBiWrkRovtm9gnaBwD+iuZYlnLvO0ld8qW928LL2vFBTi3TsPUWC3i/xYnAR2m8YP2hiCLHuPfSJmgfxHsM2iRdrR8qdOUkiC9h34STEA7Q2+rWkNWJ+YKYVTIkyqHEuXqU87txhVTaRJi6UDGDn49cMKmZwQnn+23JQf1chcn1YFkrivDaJPhm17GhoEldQHSLQfxb0ifja5WBNDbkKBF/h9JqvG3Ya9anxlyxY6g7/m2zP73xfkvUnejoX4GKjffEqezQmTe9RIeuWyz94nfZNLr0Ps363kAfP4KSW+f4zkTU/UVg19ccAY0ZhiwDetKyksU5WqLO8xMPZ6PNFYhNeBb2yhGdT8PidkRYkC4XBn1k7F7apiNUuZU8aA="
+    "signature": "lfRXK4zL213wBKg760eiPV7yvnLZ6a6v9Iohmw78yxIzqXO3tfrC5Z+P2LGiO1BdI4xckx8yz4ktn82zX97+r2zktBw0As7g71H/FjInpoZ76j3gMUaiFNrQJ0vKCCI7xsjonemroWAVDCAqlvdyqwUu/Fnz85+WoR2kCQ721vwy6IjWA3xhq8XrWjkI/AlBmoS/kVqnvjjjc9vocdddJXbUYzCse/hWWIbsFeBXyiGCd3v7apgtXwQfM++tt87fq7444zQskiYb14oQP8/uNwqZWQ9jAs00i1BZ0MNM6+TZYGHOfS6rbHZ1bcX34VZdcCwpapK/Z2HBRIgDN4QOcgJkyq1GcjvlM2wjfhN8gXTsmbF9Ee+5Y6a4ONRkxRZK2sT8oAXdm0OlTEGB0P0+WRRFOQ/PnRqbI7lvANao2METT2EUHHrtqFMe53kqCHdzy5qyuHxdCEa6l/gSR08fybx9DdRRmhOlhSPGxhgwqyi1fEMrN4CsSKNrv5u+sMqhspA05b3DQJeLDX+UV5ujRHwm0A49NF+h1ZYlrcefz5IMUUisOOw6HiLc/YGLD2jHwSePGdfMwMnrIxbxjCta7/7A91aaN7eYm16KW9erCOwAfJmBSQC6Pbmg5f7rd7rAKVOPxglq7nayXmd+BK53Mal5tltMSgd/0iY6SEtGSEU="
 }
diff --git a/bukkit/src/test/resources/client_keys/invalid_wrong_signature.json b/bukkit/src/test/resources/client_keys/invalid_wrong_signature.json
index cbca4b1..2b80c2c 100644
--- a/bukkit/src/test/resources/client_keys/invalid_wrong_signature.json
+++ b/bukkit/src/test/resources/client_keys/invalid_wrong_signature.json
@@ -1,5 +1,5 @@
 {
-    "expires_at": "2022-06-12T10:46:26.421156927Z",
-    "key": "-----BEGIN RSA PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoOv23jt2QPyab6bPRBwH2ggmzQU4I+xm\nDpi3X5ZB5Em/4uzyZqNVLJc0gShpk0XsdoB28Nq1bPxczOTBxuXi3rg5ax5gL+iymDSU27DLM8s/\n33lOofzGPJUEQGKlFm0QelDKZ/q5Y/9inHE3hEJKf7h0tnmGahXFmZSF/nRz9GHnfSYpjtDr9bsZ\nOzQuLhHXT5E4ksNRTFW41h0MlZ1qOhO+NiiVgk7LmgVYiV7RRbgO8U6RaCEqg5n28Ewo6QtzB+DF\n4NTDeu3E9BLH5G0npdUrVNhdRUWCFDmH6n9hqSIz2J7o6GvWqEvp0h9e/3qtLsoS60hnQXunrcWc\nPaEIYQIDAQAB\n-----END RSA PUBLIC KEY-----\n",
-    "signature": "bYv2mKJvz5O3Wo5V5sbJI0L6zAjfzQSkTNd7ykd/MB7KPPDg4zoTuOqphmh042xz1EYbMcfqEZvP04NTaoZDx+IxGieBB+LuxqnmYKIgtpcR2SEpzbSKznSHkupr1hKwF7kCVWLlwSbxc/XRlWPPyT6FE9m628A/jFb/obgfzLLQWfTFWp6kq2oBoUUQV5om2ihdrJ8oLCsw10SGdcFtK4+UuLzz+wjwv3JpvIX93IKdjFnw0KNd110HOPWZgp2n8+f6GsecysorqvwaE1rJC0m9Qa/wFsK2TY7twSMreCrbXPaBiWrkRovtm9gnaBwD+iuZYlnLvO0ld8qW928LL2vFBTi3TsPUWC3i/xYnAR2m8YP2hiCLHuPfSJmgfxHsM2iRdrR8qdOUkiC9h34STEA7Q2+rWkNWJ+YKYVTIkyqHEuXqU87txhVTaRJi6UDGDn49cMKmZwQnn+23JQf1chcn1YFkrivDaJPhm17GhoEldQHSLQfxb0ifja5WBNDbkKBF/h9JqvG3Ya9anxlyxY6g7/m2zP73xfkvUnejoX4GKjffEqezQmTe9RIeuWyz94nfZNLr0Ps363kAfP4KSW+f4zkTU/UVg19ccAY0ZhiwDetKyksU5WqLO8xMPZ6PNFYhNeBb2yhGdT8PidkRYkC4XBn1k7F7apiNUuZU8aA="
+    "expires_at": "2022-06-20T08:31:47.318722344Z",
+    "key": "-----BEGIN RSA PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApd3ZxDhcRWWru1XEBke6uYqmbnS2Oxyk\nOMj+QDKrkwUqhVJYciyXGsMx46Mgr/KIoGCcokP5OtIxc6+69/ZLqJ9PvM81kLIxAqyvfBMKMGjP\n376LgxTF1FeDpbe5zXaNRxfmnvQhS5YTLbzgk36qWVjqxJMG4VLVmh7RV5zWsb7XlckZb2zRHM2Y\nMHbEC+ggX+l6zQyfG1KK0MH5k+O6b0xD0rv1wm24sLOesTXH6RZG8cNE3ofdnavxjFodTOnra6w1\naiVcoUTdEPSS86wQwq9j0YCcAKOwMXsqbk9NhpujrdyJ94dev+ELwkNS7P0pPrcfiyFTQeJCZTXz\nJB36MwIDAQAB\n-----END RSA PUBLIC KEY-----\n",
+    "signature": "lfRxK4zL213wBKg760eiPV7yvnLZ6a6v9Iohmw78yxIzqXO3tfrC5Z+P2LGiO1BdI4xckx8yz4ktn82zX97+r2zktBw0As7g71H/FjInpoZ76j3gMUaiFNrQJ0vKCCI7xsjonemroWAVDCAqlvdyqwUu/Fnz85+WoR2kCQ721vwy6IjWA3xhq8XrWjkI/AlBmoS/kVqnvjjjc9vocdddJXbUYzCse/hWWIbsFeBXyiGCd3v7apgtXwQfM++tt87fq7444zQskiYb14oQP8/uNwqZWQ9jAs00i1BZ0MNM6+TZYGHOfS6rbHZ1bcX34VZdcCwpapK/Z2HBRIgDN4QOcgJkyq1GcjvlM2wjfhN8gXTsmbF9Ee+5Y6a4ONRkxRZK2sT8oAXdm0OlTEGB0P0+WRRFOQ/PnRqbI7lvANao2METT2EUHHrtqFMe53kqCHdzy5qyuHxdCEa6l/gSR08fybx9DdRRmhOlhSPGxhgwqyi1fEMrN4CsSKNrv5u+sMqhspA05b3DQJeLDX+UV5ujRHwm0A49NF+h1ZYlrcefz5IMUUisOOw6HiLc/YGLD2jHwSePGdfMwMnrIxbxjCta7/7A91aaN7eYm16KW9erCOwAfJmBSQC6Pbmg5f7rd7rAKVOPxglq7nayXmd+BK53Mal5tltMSgd/0iY6SEtGSEU="
 }
diff --git a/bukkit/src/test/resources/client_keys/valid.json b/bukkit/src/test/resources/client_keys/valid.json
deleted file mode 100644
index a2d6a41..0000000
--- a/bukkit/src/test/resources/client_keys/valid.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-    "expires_at": "2022-06-12T10:46:26.421156927Z",
-    "key": "-----BEGIN RSA PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoOv23jt2QPyab6bPRBwH2ggmzQU4I+xmDpi3X5ZB5Em/4uzyZqNVLJc0gShpk0XsdoB28Nq1bPxczOTBxuXi3rg5ax5gL+iymDSU27DLM8s/33lOofzGPJUEQGKlFm0QelDKZ/q5Y/9inHE3hEJKf7h0tnmGahXFmZSF/nRz9GHnfSYpjtDr9bsZOzQuLhHXT5E4ksNRTFW41h0MlZ1qOhO+NiiVgk7LmgVYiV7RRbgO8U6RaCEqg5n28Ewo6QtzB+DF4NTDeu3E9BLH5G0npdUrVNhdRUWCFDmH6n9hqSIz2J7o6GvWqEvp0h9e/3qtLsoS60hnQXunrcWcPaEIYQIDAQAB\n-----END RSA PUBLIC KEY-----\n",
-    "signature": "BYv2mKJvz5O3Wo5V5sbJI0L6zAjfzQSkTNd7ykd/MB7KPPDg4zoTuOqphmh042xz1EYbMcfqEZvP04NTaoZDx+IxGieBB+LuxqnmYKIgtpcR2SEpzbSKznSHkupr1hKwF7kCVWLlwSbxc/XRlWPPyT6FE9m628A/jFb/obgfzLLQWfTFWp6kq2oBoUUQV5om2ihdrJ8oLCsw10SGdcFtK4+UuLzz+wjwv3JpvIX93IKdjFnw0KNd110HOPWZgp2n8+f6GsecysorqvwaE1rJC0m9Qa/wFsK2TY7twSMreCrbXPaBiWrkRovtm9gnaBwD+iuZYlnLvO0ld8qW928LL2vFBTi3TsPUWC3i/xYnAR2m8YP2hiCLHuPfSJmgfxHsM2iRdrR8qdOUkiC9h34STEA7Q2+rWkNWJ+YKYVTIkyqHEuXqU87txhVTaRJi6UDGDn49cMKmZwQnn+23JQf1chcn1YFkrivDaJPhm17GhoEldQHSLQfxb0ifja5WBNDbkKBF/h9JqvG3Ya9anxlyxY6g7/m2zP73xfkvUnejoX4GKjffEqezQmTe9RIeuWyz94nfZNLr0Ps363kAfP4KSW+f4zkTU/UVg19ccAY0ZhiwDetKyksU5WqLO8xMPZ6PNFYhNeBb2yhGdT8PidkRYkC4XBn1k7F7apiNUuZU8aA="
-}
diff --git a/bukkit/src/test/resources/client_keys/valid_public_key.json b/bukkit/src/test/resources/client_keys/valid_public_key.json
new file mode 100644
index 0000000..8943e87
--- /dev/null
+++ b/bukkit/src/test/resources/client_keys/valid_public_key.json
@@ -0,0 +1,5 @@
+{
+    "expires_at": "2022-06-20T08:31:47.318722344Z",
+    "key": "-----BEGIN RSA PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApd3ZxDhcRWWru1XEBke6uYqmbnS2Oxyk\nOMj+QDKrkwUqhVJYciyXGsMx46Mgr/KIoGCcokP5OtIxc6+69/ZLqJ9PvM81kLIxAqyvfBMKMGjP\n376LgxTF1FeDpbe5zXaNRxfmnvQhS5YTLbzgk36qWVjqxJMG4VLVmh7RV5zWsb7XlckZb2zRHM2Y\nMHbEC+ggX+l6zQyfG1KK0MH5k+O6b0xD0rv1wm24sLOesTXH6RZG8cNE3ofdnavxjFodTOnra6w1\naiVcoUTdEPSS86wQwq9j0YCcAKOwMXsqbk9NhpujrdyJ94dev+ELwkNS7P0pPrcfiyFTQeJCZTXz\nJB36MwIDAQAB\n-----END RSA PUBLIC KEY-----\n",
+    "signature": "lfRXK4zL213wBKg760eiPV7yvnLZ6a6v9Iohmw78yxIzqXO3tfrC5Z+P2LGiO1BdI4xckx8yz4ktn82zX97+r2zktBw0As7g71H/FjInpoZ76j3gMUaiFNrQJ0vKCCI7xsjonemroWAVDCAqlvdyqwUu/Fnz85+WoR2kCQ721vwy6IjWA3xhq8XrWjkI/AlBmoS/kVqnvjjjc9vocdddJXbUYzCse/hWWIbsFeBXyiGCd3v7apgtXwQfM++tt87fq7444zQskiYb14oQP8/uNwqZWQ9jAs00i1BZ0MNM6+TZYGHOfS6rbHZ1bcX34VZdcCwpapK/Z2HBRIgDN4QOcgJkyq1GcjvlM2wjfhN8gXTsmbF9Ee+5Y6a4ONRkxRZK2sT8oAXdm0OlTEGB0P0+WRRFOQ/PnRqbI7lvANao2METT2EUHHrtqFMe53kqCHdzy5qyuHxdCEa6l/gSR08fybx9DdRRmhOlhSPGxhgwqyi1fEMrN4CsSKNrv5u+sMqhspA05b3DQJeLDX+UV5ujRHwm0A49NF+h1ZYlrcefz5IMUUisOOw6HiLc/YGLD2jHwSePGdfMwMnrIxbxjCta7/7A91aaN7eYm16KW9erCOwAfJmBSQC6Pbmg5f7rd7rAKVOPxglq7nayXmd+BK53Mal5tltMSgd/0iY6SEtGSEU="
+}
diff --git a/bukkit/src/test/resources/signature/README.md b/bukkit/src/test/resources/signature/README.md
new file mode 100644
index 0000000..cc603bc
--- /dev/null
+++ b/bukkit/src/test/resources/signature/README.md
@@ -0,0 +1,16 @@
+# About
+
+This contains test resources for the unit tests. Files in this folder include pre-made cryptographic signatures.
+
+## Directory structure
+
+* `valid_signature.json`: Extracted using packet extract from actual authentication
+* `incorrect_nonce.json`: Different nonce token simulating that the server expected a different token than signed
+* `incorrect_salt.json`: Salt sent is different to the content signed by the signature (changed salt field)
+* `incorrect_signature.json`: Changed signature
+
+## File content
+
+* `nonce`: Server generated nonce token
+* `salt`: Client generated random token
+* `signature`: Nonce and salt signed using the client key from `valid_public_key.json`
diff --git a/bukkit/src/test/resources/signature/incorrect_nonce.json b/bukkit/src/test/resources/signature/incorrect_nonce.json
new file mode 100644
index 0000000..b88c0f5
--- /dev/null
+++ b/bukkit/src/test/resources/signature/incorrect_nonce.json
@@ -0,0 +1,7 @@
+{
+    "nonce": "galNig\u003d\u003d",
+    "signature": {
+        "signature": "JlXAUtIGDjxUOnF5vkg/NUEN2wlzXcqADyYIw2WRTb5hgKwIgxyUPO5v/2M7xU3hxz2Zf0iYHM97h8qNMGQ43cLgfVH9VWZ1kGMuOby2LNSb6nDaMzm0b02ftThaWOWj9kJXbR8fN7qdpB+28t2CTW5ILT+2AZYI/Sn8gFFR+LvJJt1ENMfEj2ZIIkHecpNBuKyLz1aDCZ5BEASSLfAqHEAA3dpHV1DIgzfpO6xwo7bVFDtcBEeusl/Nc3KyGyT8sDFTsZWgitgz53xNKrZUK8Q2BaJfP0zrGAX36rpYURJSVD0AtI1ic9s5aG+OFUC1YhLXb/1cDv37ZjHcdV2ppw\u003d\u003d",
+        "salt": -2985008842905108412
+    }
+}
diff --git a/bukkit/src/test/resources/signature/incorrect_salt.json b/bukkit/src/test/resources/signature/incorrect_salt.json
new file mode 100644
index 0000000..8edffb6
--- /dev/null
+++ b/bukkit/src/test/resources/signature/incorrect_salt.json
@@ -0,0 +1,7 @@
+{
+    "nonce": "GalNig\u003d\u003d",
+    "signature": {
+        "signature": "JlXAUtIGDjxUOnF5vkg/NUEN2wlzXcqADyYIw2WRTb5hgKwIgxyUPO5v/2M7xU3hxz2Zf0iYHM97h8qNMGQ43cLgfVH9VWZ1kGMuOby2LNSb6nDaMzm0b02ftThaWOWj9kJXbR8fN7qdpB+28t2CTW5ILT+2AZYI/Sn8gFFR+LvJJt1ENMfEj2ZIIkHecpNBuKyLz1aDCZ5BEASSLfAqHEAA3dpHV1DIgzfpO6xwo7bVFDtcBEeusl/Nc3KyGyT8sDFTsZWgitgz53xNKrZUK8Q2BaJfP0zrGAX36rpYURJSVD0AtI1ic9s5aG+OFUC1YhLXb/1cDv37ZjHcdV2ppw\u003d\u003d",
+        "salt": -1985008842905108412
+    }
+}
diff --git a/bukkit/src/test/resources/signature/incorrect_signature.json b/bukkit/src/test/resources/signature/incorrect_signature.json
new file mode 100644
index 0000000..ba6bac5
--- /dev/null
+++ b/bukkit/src/test/resources/signature/incorrect_signature.json
@@ -0,0 +1,7 @@
+{
+    "nonce": "GalNig\u003d\u003d",
+    "signature": {
+        "signature": "jlXAUtIGDjxUOnF5vkg/NUEN2wlzXcqADyYIw2WRTb5hgKwIgxyUPO5v/2M7xU3hxz2Zf0iYHM97h8qNMGQ43cLgfVH9VWZ1kGMuOby2LNSb6nDaMzm0b02ftThaWOWj9kJXbR8fN7qdpB+28t2CTW5ILT+2AZYI/Sn8gFFR+LvJJt1ENMfEj2ZIIkHecpNBuKyLz1aDCZ5BEASSLfAqHEAA3dpHV1DIgzfpO6xwo7bVFDtcBEeusl/Nc3KyGyT8sDFTsZWgitgz53xNKrZUK8Q2BaJfP0zrGAX36rpYURJSVD0AtI1ic9s5aG+OFUC1YhLXb/1cDv37ZjHcdV2ppw\u003d\u003d",
+        "salt": -2985008842905108412
+    }
+}
diff --git a/bukkit/src/test/resources/signature/valid_signature.json b/bukkit/src/test/resources/signature/valid_signature.json
new file mode 100644
index 0000000..7f4f4ad
--- /dev/null
+++ b/bukkit/src/test/resources/signature/valid_signature.json
@@ -0,0 +1,7 @@
+{
+    "nonce": "GalNig\u003d\u003d",
+    "signature": {
+        "signature": "JlXAUtIGDjxUOnF5vkg/NUEN2wlzXcqADyYIw2WRTb5hgKwIgxyUPO5v/2M7xU3hxz2Zf0iYHM97h8qNMGQ43cLgfVH9VWZ1kGMuOby2LNSb6nDaMzm0b02ftThaWOWj9kJXbR8fN7qdpB+28t2CTW5ILT+2AZYI/Sn8gFFR+LvJJt1ENMfEj2ZIIkHecpNBuKyLz1aDCZ5BEASSLfAqHEAA3dpHV1DIgzfpO6xwo7bVFDtcBEeusl/Nc3KyGyT8sDFTsZWgitgz53xNKrZUK8Q2BaJfP0zrGAX36rpYURJSVD0AtI1ic9s5aG+OFUC1YhLXb/1cDv37ZjHcdV2ppw\u003d\u003d",
+        "salt": -2985008842905108412
+    }
+}
2022-06-28 18:34:22 +02:00
bc7c4eea60 Use PublicKey instead of byte representation 2022-06-28 18:34:22 +02:00
456342c686 Migrate to ProtocolLib field extractors for better compatibility
diff --git a/bukkit/pom.xml b/bukkit/pom.xml
index c4caae8..9b3c5c5 100644
--- a/bukkit/pom.xml
+++ b/bukkit/pom.xml
@@ -183,7 +183,7 @@
         <dependency>
             <groupId>com.comphenix.protocol</groupId>
             <artifactId>ProtocolLib</artifactId>
-            <version>5.0.0-20220603.031413-2</version>
+            <version>5.0.0-SNAPSHOT</version>
             <scope>provided</scope>
             <exclusions>
                 <exclusion>
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
index c9e72fc..3c66e7b 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
@@ -27,12 +27,12 @@ package com.github.games647.fastlogin.bukkit.listener.protocollib;

 import com.comphenix.protocol.PacketType;
 import com.comphenix.protocol.ProtocolLibrary;
-import com.comphenix.protocol.events.InternalStructure;
 import com.comphenix.protocol.events.PacketAdapter;
 import com.comphenix.protocol.events.PacketContainer;
 import com.comphenix.protocol.events.PacketEvent;
-import com.comphenix.protocol.reflect.FuzzyReflection;
 import com.comphenix.protocol.wrappers.WrappedGameProfile;
+import com.comphenix.protocol.wrappers.WrappedProfilePublicKey;
+import com.comphenix.protocol.wrappers.WrappedProfilePublicKey.WrappedProfileKeyData;
 import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
 import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
 import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;
@@ -47,7 +47,6 @@ import java.security.PublicKey;
 import java.security.SecureRandom;
 import java.security.SignatureException;
 import java.time.Instant;
-import java.util.Optional;

 import org.bukkit.entity.Player;

@@ -171,15 +170,15 @@ public class ProtocolLibListener extends PacketAdapter {
     }

     private boolean verifyPublicKey(PacketContainer packet) {
-        Optional<InternalStructure> internalStructure = packet.getOptionalStructures().readSafely(0);
-        if (internalStructure == null) {
+        WrappedProfileKeyData profileKey = packet.getProfilePublicKeys().optionRead(0)
+            .map(WrappedProfilePublicKey::getKeyData).orElse(null);
+        if (profileKey == null) {
             return true;
         }

-        Object instance = internalStructure.get().getHandle();
-        Instant expires = FuzzyReflection.getFieldValue(instance, Instant.class, true);
-        PublicKey key = FuzzyReflection.getFieldValue(instance, PublicKey.class, true);
-        byte[] signature = FuzzyReflection.getFieldValue(instance, byte[].class, true);
+        Instant expires = profileKey.getExpireTime();
+        PublicKey key = profileKey.getKey();
+        byte[] signature = profileKey.getSignature();
         ClientPublicKey clientKey = new ClientPublicKey(expires, key.getEncoded(), signature);
         try {
             return EncryptionUtil.verifyClientKey(clientKey, Instant.now());
2022-06-28 18:34:22 +02:00
e85d0d3f0e Add session key to plugin 2022-06-28 18:34:22 +02:00
d6c6108a7e Do not modify certificate file during packaging phase 2022-06-28 18:34:22 +02:00
d353ac66ab Verify public keys from players
diff --git a/bukkit/pom.xml b/bukkit/pom.xml
index 2e1ccc4..c4caae8 100644
--- a/bukkit/pom.xml
+++ b/bukkit/pom.xml
@@ -119,9 +119,6 @@
         <repository>
             <id>dmulloy2-repo</id>
             <url>https://repo.dmulloy2.net/repository/public/</url>
-            <snapshots>
-                <enabled>false</enabled>
-            </snapshots>
         </repository>

         <!-- AuthMe Reloaded, xAuth and LoginSecurity -->
@@ -186,7 +183,7 @@
         <dependency>
             <groupId>com.comphenix.protocol</groupId>
             <artifactId>ProtocolLib</artifactId>
-            <version>5.0.0-SNAPSHOT</version>
+            <version>5.0.0-20220603.031413-2</version>
             <scope>provided</scope>
             <exclusions>
                 <exclusion>
@@ -351,5 +348,12 @@
             <scope>system</scope>
             <systemPath>${project.basedir}/lib/UltraAuth v2.1.2.jar</systemPath>
         </dependency>
+
+        <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcpkix-jdk18on</artifactId>
+            <version>1.71</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtil.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtil.java
index 1197514..143dda9 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtil.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtil.java
@@ -25,15 +25,28 @@
  */
 package com.github.games647.fastlogin.bukkit.listener.protocollib;

+import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;
+import com.google.common.io.Resources;
+
+import java.io.IOException;
 import java.math.BigInteger;
 import java.nio.charset.StandardCharsets;
 import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+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.Signature;
+import java.security.SignatureException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.Base64.Encoder;
 import java.util.Random;

 import javax.crypto.Cipher;
@@ -46,11 +59,25 @@ import javax.crypto.spec.SecretKeySpec;
  *
  * @see net.minecraft.server.MinecraftEncryption
  */
-public class EncryptionUtil {
+class EncryptionUtil {

     public static final int VERIFY_TOKEN_LENGTH = 4;
     public static final String KEY_PAIR_ALGORITHM = "RSA";

+    private static final int RSA_LENGTH = 1_024;
+
+    private static final PublicKey mojangSessionKey;
+    private static final int LINE_LENGTH = 76;
+    private static final Encoder KEY_ENCODER = Base64.getMimeEncoder(LINE_LENGTH, "\n".getBytes(StandardCharsets.UTF_8));
+
+    static {
+        try {
+            mojangSessionKey = loadMojangSessionKey();
+        } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException ex) {
+            throw new RuntimeException("Failed to load Mojang session key", ex);
+        }
+    }
+
     private EncryptionUtil() {
         // utility
     }
@@ -65,7 +92,7 @@ public class EncryptionUtil {
         try {
             KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_PAIR_ALGORITHM);

-            keyPairGenerator.initialize(1_024);
+            keyPairGenerator.initialize(RSA_LENGTH);
             return keyPairGenerator.generateKeyPair();
         } catch (NoSuchAlgorithmException nosuchalgorithmexception) {
             // Should be existing in every vm
@@ -120,6 +147,33 @@ public class EncryptionUtil {
         return new SecretKeySpec(decrypt(privateKey, sharedKey), "AES");
     }

+    public static boolean verifyClientKey(ClientPublicKey clientKey, Instant verifyTimstamp)
+        throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
+        if (!verifyTimstamp.isBefore(clientKey.getExpiry())) {
+            return false;
+        }
+
+        Signature signature = Signature.getInstance("SHA1withRSA");
+        signature.initVerify(mojangSessionKey);
+        signature.update(toSignable(clientKey).getBytes(StandardCharsets.US_ASCII));
+        return signature.verify(clientKey.getSignature());
+    }
+
+    private static PublicKey loadMojangSessionKey()
+        throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
+        var keyUrl = Resources.getResource("yggdrasil_session_pubkey.der");
+        var keyData = Resources.toByteArray(keyUrl);
+        var keySpec = new X509EncodedKeySpec(keyData);
+
+        return KeyFactory.getInstance("RSA").generatePublic(keySpec);
+    }
+
+    private static String toSignable(ClientPublicKey clientPublicKey) {
+        long expiry = clientPublicKey.getExpiry().toEpochMilli();
+        String encoded = KEY_ENCODER.encodeToString(clientPublicKey.getKey());
+        return expiry + "-----BEGIN RSA PUBLIC KEY-----\n" + encoded + "\n-----END RSA PUBLIC KEY-----\n";
+    }
+
     public static byte[] decrypt(PrivateKey key, byte[] data) throws GeneralSecurityException {
         // b(Key var0, byte[] var1)
         Cipher cipher = Cipher.getInstance(key.getAlgorithm());
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
index f8b8de2..c9e72fc 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
@@ -27,18 +27,27 @@ package com.github.games647.fastlogin.bukkit.listener.protocollib;

 import com.comphenix.protocol.PacketType;
 import com.comphenix.protocol.ProtocolLibrary;
+import com.comphenix.protocol.events.InternalStructure;
 import com.comphenix.protocol.events.PacketAdapter;
 import com.comphenix.protocol.events.PacketContainer;
 import com.comphenix.protocol.events.PacketEvent;
-import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
+import com.comphenix.protocol.reflect.FuzzyReflection;
 import com.comphenix.protocol.wrappers.WrappedGameProfile;
+import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
 import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
+import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;
 import com.github.games647.fastlogin.core.antibot.AntiBotService;
 import com.github.games647.fastlogin.core.antibot.AntiBotService.Action;

 import java.net.InetSocketAddress;
+import java.security.InvalidKeyException;
 import java.security.KeyPair;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
 import java.security.SecureRandom;
+import java.security.SignatureException;
+import java.time.Instant;
+import java.util.Optional;

 import org.bukkit.entity.Player;

@@ -149,6 +158,11 @@ public class ProtocolLibListener extends PacketAdapter {
             username = (String) packetEvent.getPacket().getMeta("original_name").get();
         }

+        if (!verifyPublicKey(packet)) {
+            plugin.getLog().warn("Invalid public key from player {}", username);
+            return;
+        }
+
         plugin.getLog().trace("GameProfile {} with {} connecting", sessionKey, username);

         packetEvent.getAsyncMarker().incrementProcessingDelay();
@@ -156,6 +170,24 @@ public class ProtocolLibListener extends PacketAdapter {
         plugin.getScheduler().runAsync(nameCheckTask);
     }

+    private boolean verifyPublicKey(PacketContainer packet) {
+        Optional<InternalStructure> internalStructure = packet.getOptionalStructures().readSafely(0);
+        if (internalStructure == null) {
+            return true;
+        }
+
+        Object instance = internalStructure.get().getHandle();
+        Instant expires = FuzzyReflection.getFieldValue(instance, Instant.class, true);
+        PublicKey key = FuzzyReflection.getFieldValue(instance, PublicKey.class, true);
+        byte[] signature = FuzzyReflection.getFieldValue(instance, byte[].class, true);
+        ClientPublicKey clientKey = new ClientPublicKey(expires, key.getEncoded(), signature);
+        try {
+            return EncryptionUtil.verifyClientKey(clientKey, Instant.now());
+        } catch (SignatureException | InvalidKeyException | NoSuchAlgorithmException ex) {
+            return false;
+        }
+    }
+
     private String getUsername(PacketContainer packet) {
         WrappedGameProfile profile = packet.getGameProfiles().readSafely(0);
         if (profile == null) {
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/packet/ClientPublicKey.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/packet/ClientPublicKey.java
new file mode 100644
index 0000000..495adb1
--- /dev/null
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/packet/ClientPublicKey.java
@@ -0,0 +1,53 @@
+/*
+ * SPDX-License-Identifier: MIT
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015-2022 games647 and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package com.github.games647.fastlogin.bukkit.listener.protocollib.packet;
+
+import java.time.Instant;
+
+public class ClientPublicKey {
+
+    private final Instant expiry;
+    private final byte[] key;
+    private final byte[] signature;
+
+    public ClientPublicKey(Instant expiry, byte[] key, byte[] signature) {
+        this.expiry = expiry;
+        this.key = key;
+        this.signature = signature;
+    }
+
+    public Instant getExpiry() {
+        return expiry;
+    }
+
+    public byte[] getKey() {
+        return key;
+    }
+
+    public byte[] getSignature() {
+        return signature;
+    }
+}
diff --git a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtilTest.java b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtilTest.java
index 7a097f1..11a52f7 100644
--- a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtilTest.java
+++ b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtilTest.java
@@ -25,8 +25,27 @@
  */
 package com.github.games647.fastlogin.bukkit.listener.protocollib;

+import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;
+import com.google.common.io.Resources;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Base64;

+import org.bouncycastle.util.io.pem.PemObject;
+import org.bouncycastle.util.io.pem.PemReader;
 import org.junit.Test;

 import static org.hamcrest.CoreMatchers.is;
@@ -43,4 +62,77 @@ public class EncryptionUtilTest {
         assertThat(token, notNullValue());
         assertThat(token.length, is(4));
     }
+
+    @Test
+    public void testExpiredClientKey() throws Exception {
+        var clientKey = loadClientKey("client_keys/valid.json");
+
+        // Client expires at the exact second mentioned, so use it for verification
+        var expiredTimestamp = clientKey.getExpiry();
+        assertThat(EncryptionUtil.verifyClientKey(clientKey, expiredTimestamp), is(false));
+    }
+
+    // @Test(expected = Exception.class)
+    @Test
+    public void testInvalidChangedExpiration() throws Exception {
+        // expiration date changed should make the signature invalid
+        // expiration should still be valid
+        var clientKey = loadClientKey("client_keys/invalid_wrong_expiration.json");
+        assertThat(EncryptionUtil.verifyClientKey(clientKey, clientKey.getExpiry().minus(5, ChronoUnit.HOURS)), is(false));
+    }
+
+    // @Test(expected = Exception.class)
+    @Test
+    public void testInvalidChangedKey() throws Exception {
+        // changed public key no longer corresponding to the signature
+        var clientKey = loadClientKey("client_keys/invalid_wrong_key.json");
+        assertThat(EncryptionUtil.verifyClientKey(clientKey, clientKey.getExpiry().minus(5, ChronoUnit.HOURS)), is(false));
+    }
+
+    @Test
+    public void testInvalidChangedSignature() throws Exception {
+        // signature modified no longer corresponding to key and expiration date
+        var clientKey = loadClientKey("client_keys/invalid_wrong_signature.json");
+        assertThat(EncryptionUtil.verifyClientKey(clientKey, clientKey.getExpiry().minus(5, ChronoUnit.HOURS)), is(false));
+    }
+
+    @Test
+    public void testValidClientKey() throws Exception {
+        var clientKey = loadClientKey("client_keys/valid.json");
+
+        var verificationTimestamp = clientKey.getExpiry().minus(5, ChronoUnit.HOURS);
+        assertThat(EncryptionUtil.verifyClientKey(clientKey, verificationTimestamp), is(true));
+    }
+
+    private ClientPublicKey loadClientKey(String path)
+        throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
+        var keyUrl = Resources.getResource(path);
+        var gson = new Gson();
+
+        var lines = Resources.toString(keyUrl, StandardCharsets.US_ASCII);
+        var object = gson.fromJson(lines, JsonObject.class);
+
+        Instant expires = Instant.parse(object.getAsJsonPrimitive("expires_at").getAsString());
+        String key = object.getAsJsonPrimitive("key").getAsString();
+        RSAPublicKey publicKey = parsePublicKey(key);
+
+        byte[] signature = Base64.getDecoder().decode(object.getAsJsonPrimitive("signature").getAsString());
+        return new ClientPublicKey(expires, publicKey.getEncoded(), signature);
+    }
+
+    private RSAPublicKey parsePublicKey(String lines)
+        throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
+        try (
+            Reader reader = new StringReader(lines);
+            PemReader pemReader = new PemReader(reader)
+        ) {
+
+            PemObject pemObject = pemReader.readPemObject();
+            byte[] content = pemObject.getContent();
+            var pubKeySpec = new X509EncodedKeySpec(content);
+
+            var factory = KeyFactory.getInstance("RSA");
+            return (RSAPublicKey) factory.generatePublic(pubKeySpec);
+        }
+    }
 }
diff --git a/bukkit/src/test/resources/client_keys/invalid_wrong_expiration.json b/bukkit/src/test/resources/client_keys/invalid_wrong_expiration.json
new file mode 100644
index 0000000..7ecc12e
--- /dev/null
+++ b/bukkit/src/test/resources/client_keys/invalid_wrong_expiration.json
@@ -0,0 +1,5 @@
+{
+    "expires_at": "2022-06-12T09:46:26.421156927Z",
+    "key": "-----BEGIN RSA PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoOv23jt2QPyab6bPRBwH2ggmzQU4I+xm\nDpi3X5ZB5Em/4uzyZqNVLJc0gShpk0XsdoB28Nq1bPxczOTBxuXi3rg5ax5gL+iymDSU27DLM8s/\n33lOofzGPJUEQGKlFm0QelDKZ/q5Y/9inHE3hEJKf7h0tnmGahXFmZSF/nRz9GHnfSYpjtDr9bsZ\nOzQuLhHXT5E4ksNRTFW41h0MlZ1qOhO+NiiVgk7LmgVYiV7RRbgO8U6RaCEqg5n28Ewo6QtzB+DF\n4NTDeu3E9BLH5G0npdUrVNhdRUWCFDmH6n9hqSIz2J7o6GvWqEvp0h9e/3qtLsoS60hnQXunrcWc\nPaEIYQIDAQAB\n-----END RSA PUBLIC KEY-----\n",
+    "signature": "BYv2mKJvz5O3Wo5V5sbJI0L6zAjfzQSkTNd7ykd/MB7KPPDg4zoTuOqphmh042xz1EYbMcfqEZvP04NTaoZDx+IxGieBB+LuxqnmYKIgtpcR2SEpzbSKznSHkupr1hKwF7kCVWLlwSbxc/XRlWPPyT6FE9m628A/jFb/obgfzLLQWfTFWp6kq2oBoUUQV5om2ihdrJ8oLCsw10SGdcFtK4+UuLzz+wjwv3JpvIX93IKdjFnw0KNd110HOPWZgp2n8+f6GsecysorqvwaE1rJC0m9Qa/wFsK2TY7twSMreCrbXPaBiWrkRovtm9gnaBwD+iuZYlnLvO0ld8qW928LL2vFBTi3TsPUWC3i/xYnAR2m8YP2hiCLHuPfSJmgfxHsM2iRdrR8qdOUkiC9h34STEA7Q2+rWkNWJ+YKYVTIkyqHEuXqU87txhVTaRJi6UDGDn49cMKmZwQnn+23JQf1chcn1YFkrivDaJPhm17GhoEldQHSLQfxb0ifja5WBNDbkKBF/h9JqvG3Ya9anxlyxY6g7/m2zP73xfkvUnejoX4GKjffEqezQmTe9RIeuWyz94nfZNLr0Ps363kAfP4KSW+f4zkTU/UVg19ccAY0ZhiwDetKyksU5WqLO8xMPZ6PNFYhNeBb2yhGdT8PidkRYkC4XBn1k7F7apiNUuZU8aA="
+}
diff --git a/bukkit/src/test/resources/client_keys/invalid_wrong_key.json b/bukkit/src/test/resources/client_keys/invalid_wrong_key.json
new file mode 100644
index 0000000..37bb3ad
--- /dev/null
+++ b/bukkit/src/test/resources/client_keys/invalid_wrong_key.json
@@ -0,0 +1,5 @@
+{
+    "expires_at": "2022-06-12T10:46:26.421156927Z",
+    "key": "-----BEGIN RSA PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoOv23jt2QPyab6bPRBwH2ggmzQU3I+xmDpi3X5ZB5Em/4uzyZqNVLJc0gShpk0XsdoB28Nq1bPxczOTBxuXi3rg5ax5gL+iymDSU27DLM8s/33lOofzGPJUEQGKlFm0QelDKZ/q5Y/9inHE3hEJKf7h0tnmGahXFmZSF/nRz9GHnfSYpjtDr9bsZOzQuLhHXT5E4ksNRTFW41h0MlZ1qOhO+NiiVgk7LmgVYiV7RRbgO8U6RaCEqg5n28Ewo6QtzB+DF4NTDeu3E9BLH5G0npdUrVNhdRUWCFDmH6n9hqSIz2J7o6GvWqEvp0h9e/3qtLsoS60hnQXunrcWcPaEIYQIDAQAB\n-----END RSA PUBLIC KEY-----\n",
+    "signature": "BYv2mKJvz5O3Wo5V5sbJI0L6zAjfzQSkTNd7ykd/MB7KPPDg4zoTuOqphmh042xz1EYbMcfqEZvP04NTaoZDx+IxGieBB+LuxqnmYKIgtpcR2SEpzbSKznSHkupr1hKwF7kCVWLlwSbxc/XRlWPPyT6FE9m628A/jFb/obgfzLLQWfTFWp6kq2oBoUUQV5om2ihdrJ8oLCsw10SGdcFtK4+UuLzz+wjwv3JpvIX93IKdjFnw0KNd110HOPWZgp2n8+f6GsecysorqvwaE1rJC0m9Qa/wFsK2TY7twSMreCrbXPaBiWrkRovtm9gnaBwD+iuZYlnLvO0ld8qW928LL2vFBTi3TsPUWC3i/xYnAR2m8YP2hiCLHuPfSJmgfxHsM2iRdrR8qdOUkiC9h34STEA7Q2+rWkNWJ+YKYVTIkyqHEuXqU87txhVTaRJi6UDGDn49cMKmZwQnn+23JQf1chcn1YFkrivDaJPhm17GhoEldQHSLQfxb0ifja5WBNDbkKBF/h9JqvG3Ya9anxlyxY6g7/m2zP73xfkvUnejoX4GKjffEqezQmTe9RIeuWyz94nfZNLr0Ps363kAfP4KSW+f4zkTU/UVg19ccAY0ZhiwDetKyksU5WqLO8xMPZ6PNFYhNeBb2yhGdT8PidkRYkC4XBn1k7F7apiNUuZU8aA="
+}
diff --git a/bukkit/src/test/resources/client_keys/invalid_wrong_signature.json b/bukkit/src/test/resources/client_keys/invalid_wrong_signature.json
new file mode 100644
index 0000000..cbca4b1
--- /dev/null
+++ b/bukkit/src/test/resources/client_keys/invalid_wrong_signature.json
@@ -0,0 +1,5 @@
+{
+    "expires_at": "2022-06-12T10:46:26.421156927Z",
+    "key": "-----BEGIN RSA PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoOv23jt2QPyab6bPRBwH2ggmzQU4I+xm\nDpi3X5ZB5Em/4uzyZqNVLJc0gShpk0XsdoB28Nq1bPxczOTBxuXi3rg5ax5gL+iymDSU27DLM8s/\n33lOofzGPJUEQGKlFm0QelDKZ/q5Y/9inHE3hEJKf7h0tnmGahXFmZSF/nRz9GHnfSYpjtDr9bsZ\nOzQuLhHXT5E4ksNRTFW41h0MlZ1qOhO+NiiVgk7LmgVYiV7RRbgO8U6RaCEqg5n28Ewo6QtzB+DF\n4NTDeu3E9BLH5G0npdUrVNhdRUWCFDmH6n9hqSIz2J7o6GvWqEvp0h9e/3qtLsoS60hnQXunrcWc\nPaEIYQIDAQAB\n-----END RSA PUBLIC KEY-----\n",
+    "signature": "bYv2mKJvz5O3Wo5V5sbJI0L6zAjfzQSkTNd7ykd/MB7KPPDg4zoTuOqphmh042xz1EYbMcfqEZvP04NTaoZDx+IxGieBB+LuxqnmYKIgtpcR2SEpzbSKznSHkupr1hKwF7kCVWLlwSbxc/XRlWPPyT6FE9m628A/jFb/obgfzLLQWfTFWp6kq2oBoUUQV5om2ihdrJ8oLCsw10SGdcFtK4+UuLzz+wjwv3JpvIX93IKdjFnw0KNd110HOPWZgp2n8+f6GsecysorqvwaE1rJC0m9Qa/wFsK2TY7twSMreCrbXPaBiWrkRovtm9gnaBwD+iuZYlnLvO0ld8qW928LL2vFBTi3TsPUWC3i/xYnAR2m8YP2hiCLHuPfSJmgfxHsM2iRdrR8qdOUkiC9h34STEA7Q2+rWkNWJ+YKYVTIkyqHEuXqU87txhVTaRJi6UDGDn49cMKmZwQnn+23JQf1chcn1YFkrivDaJPhm17GhoEldQHSLQfxb0ifja5WBNDbkKBF/h9JqvG3Ya9anxlyxY6g7/m2zP73xfkvUnejoX4GKjffEqezQmTe9RIeuWyz94nfZNLr0Ps363kAfP4KSW+f4zkTU/UVg19ccAY0ZhiwDetKyksU5WqLO8xMPZ6PNFYhNeBb2yhGdT8PidkRYkC4XBn1k7F7apiNUuZU8aA="
+}
diff --git a/bukkit/src/test/resources/client_keys/valid.json b/bukkit/src/test/resources/client_keys/valid.json
new file mode 100644
index 0000000..a2d6a41
--- /dev/null
+++ b/bukkit/src/test/resources/client_keys/valid.json
@@ -0,0 +1,5 @@
+{
+    "expires_at": "2022-06-12T10:46:26.421156927Z",
+    "key": "-----BEGIN RSA PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoOv23jt2QPyab6bPRBwH2ggmzQU4I+xmDpi3X5ZB5Em/4uzyZqNVLJc0gShpk0XsdoB28Nq1bPxczOTBxuXi3rg5ax5gL+iymDSU27DLM8s/33lOofzGPJUEQGKlFm0QelDKZ/q5Y/9inHE3hEJKf7h0tnmGahXFmZSF/nRz9GHnfSYpjtDr9bsZOzQuLhHXT5E4ksNRTFW41h0MlZ1qOhO+NiiVgk7LmgVYiV7RRbgO8U6RaCEqg5n28Ewo6QtzB+DF4NTDeu3E9BLH5G0npdUrVNhdRUWCFDmH6n9hqSIz2J7o6GvWqEvp0h9e/3qtLsoS60hnQXunrcWcPaEIYQIDAQAB\n-----END RSA PUBLIC KEY-----\n",
+    "signature": "BYv2mKJvz5O3Wo5V5sbJI0L6zAjfzQSkTNd7ykd/MB7KPPDg4zoTuOqphmh042xz1EYbMcfqEZvP04NTaoZDx+IxGieBB+LuxqnmYKIgtpcR2SEpzbSKznSHkupr1hKwF7kCVWLlwSbxc/XRlWPPyT6FE9m628A/jFb/obgfzLLQWfTFWp6kq2oBoUUQV5om2ihdrJ8oLCsw10SGdcFtK4+UuLzz+wjwv3JpvIX93IKdjFnw0KNd110HOPWZgp2n8+f6GsecysorqvwaE1rJC0m9Qa/wFsK2TY7twSMreCrbXPaBiWrkRovtm9gnaBwD+iuZYlnLvO0ld8qW928LL2vFBTi3TsPUWC3i/xYnAR2m8YP2hiCLHuPfSJmgfxHsM2iRdrR8qdOUkiC9h34STEA7Q2+rWkNWJ+YKYVTIkyqHEuXqU87txhVTaRJi6UDGDn49cMKmZwQnn+23JQf1chcn1YFkrivDaJPhm17GhoEldQHSLQfxb0ifja5WBNDbkKBF/h9JqvG3Ya9anxlyxY6g7/m2zP73xfkvUnejoX4GKjffEqezQmTe9RIeuWyz94nfZNLr0Ps363kAfP4KSW+f4zkTU/UVg19ccAY0ZhiwDetKyksU5WqLO8xMPZ6PNFYhNeBb2yhGdT8PidkRYkC4XBn1k7F7apiNUuZU8aA="
+}
2022-06-28 18:34:21 +02:00
4fa28f2c69 Support username extraction in 1.19
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
index d1f83a6..f8b8de2 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
@@ -31,6 +31,7 @@ import com.comphenix.protocol.events.PacketAdapter;
 import com.comphenix.protocol.events.PacketContainer;
 import com.comphenix.protocol.events.PacketEvent;
 import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
+import com.comphenix.protocol.wrappers.WrappedGameProfile;
 import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
 import com.github.games647.fastlogin.core.antibot.AntiBotService;
 import com.github.games647.fastlogin.core.antibot.AntiBotService.Action;
@@ -94,7 +95,7 @@ public class ProtocolLibListener extends PacketAdapter {
             PacketContainer packet = packetEvent.getPacket();

             InetSocketAddress address = sender.getAddress();
-            String username = packet.getGameProfiles().read(0).getName();
+            String username = getUsername(packet);

             Action action = antiBotService.onIncomingConnection(address, username);
             switch (action) {
@@ -154,4 +155,14 @@ public class ProtocolLibListener extends PacketAdapter {
         Runnable nameCheckTask = new NameCheckTask(plugin, random, player, packetEvent, username, keyPair.getPublic());
         plugin.getScheduler().runAsync(nameCheckTask);
     }
+
+    private String getUsername(PacketContainer packet) {
+        WrappedGameProfile profile = packet.getGameProfiles().readSafely(0);
+        if (profile == null) {
+            return packet.getStrings().read(0);
+        }
+
+        //player.getName() won't work at this state
+        return profile.getName();
+    }
 }
2022-06-28 18:34:21 +02:00
d8a6e2a6fa Update ProtocolLib to 5.0
diff --git a/bukkit/pom.xml b/bukkit/pom.xml
index 13e466e..2494a2b 100644
--- a/bukkit/pom.xml
+++ b/bukkit/pom.xml
@@ -186,7 +186,7 @@
         <dependency>
             <groupId>com.comphenix.protocol</groupId>
             <artifactId>ProtocolLib</artifactId>
-            <version>4.8.0</version>
+            <version>5.0.0-SNAPSHOT</version>
             <scope>provided</scope>
             <exclusions>
                 <exclusion>
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java
index 5a2794d..2e60cd1 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java
@@ -28,7 +28,7 @@ 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.injector.temporary.TemporaryPlayerFactory;
 import com.comphenix.protocol.reflect.FieldUtils;
 import com.comphenix.protocol.reflect.FuzzyReflection;
 import com.comphenix.protocol.utility.MinecraftReflection;
@@ -37,18 +37,14 @@ 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.AbstractResolver;
 import com.github.games647.craftapi.resolver.MojangResolver;
 import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
 import com.github.games647.fastlogin.bukkit.FastLoginBukkit;

 import java.io.IOException;
-import java.io.InputStream;
-import java.io.Reader;
-import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
-import java.net.*;
-import java.nio.charset.StandardCharsets;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
 import java.security.GeneralSecurityException;
 import java.security.Key;
 import java.security.KeyPair;
@@ -263,15 +259,11 @@ public class VerifyResponseTask implements Runnable {
     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);
-        }
+        //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");
     }

     //fake a new login packet in order to let the server handle all the other stuff
@@ -287,14 +279,8 @@ public class VerifyResponseTask implements Runnable {
             startPacket.getGameProfiles().write(0, fakeProfile);
         }

-        try {
-            //we don't want to handle our own packets so ignore filters
-            startPacket.setMeta(ProtocolLibListener.SOURCE_META_KEY, plugin.getName());
-            ProtocolLibrary.getProtocolManager().recieveClientPacket(player, startPacket, true);
-        } 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"));
-        }
+        //we don't want to handle our own packets so ignore filters
+        startPacket.setMeta(ProtocolLibListener.SOURCE_META_KEY, plugin.getName());
+        ProtocolLibrary.getProtocolManager().receiveClientPacket(player, startPacket, true);
     }
 }
2022-06-28 18:34:21 +02:00
219ebb1248 Bump version 2022-06-28 18:34:21 +02:00
87281be8cf Fix start packet containing username in 1.19 2022-06-28 18:34:21 +02:00
e781eb6c68 Revert "Add support for checking velocity support with newer paper configs"
This reverts commit 6f9e8a49f1.

Fixes #825
2022-06-28 18:12:01 +02:00
9ba6e9ba90 Add missing license header 2022-06-24 16:28:18 +02:00
f182adc3bc Add support for RGB colors using the color char
Examples include:
`&x00002a00002b&lText`
2022-06-24 16:21:53 +02:00
5e159cac64 Explicitly disable ProtocolLib listener on stop
This will stop Paper complaining about still running asynchronous tasks
which is a useful feature.
2022-06-19 12:10:31 +02:00
6f9e8a49f1 Add support for checking velocity support with newer paper configs
Fixes #802
2022-06-18 21:23:17 +02:00
57b7fe949f Merge pull request #823 from games647/805-more-antibot-features
Add support for blocking incoming connection on AntiBot detection
2022-06-18 18:15:52 +02:00
32fe2cdf4e Check for valid session before starting a new thread
This could reduce load for malicious senders as there have to be session
active to start the expensive operation of creating a new thread.
2022-06-18 18:09:35 +02:00
7cab5993b7 Add license
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/FastLoginBukkit.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/FastLoginBukkit.java
index ab4ceed..9c2fa3a 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/FastLoginBukkit.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/FastLoginBukkit.java
@@ -114,9 +114,9 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
             }

             if (pluginManager.isPluginEnabled("ProtocolSupport")) {
-                pluginManager.registerEvents(new ProtocolSupportListener(this, core.getRateLimiter()), this);
+                pluginManager.registerEvents(new ProtocolSupportListener(this, core.getAntiBot()), this);
             } else if (pluginManager.isPluginEnabled("ProtocolLib")) {
-                ProtocolLibListener.register(this, core.getRateLimiter());
+                ProtocolLibListener.register(this, core.getAntiBot());

                 if (isPluginInstalled("floodgate")) {
                     if (getConfig().getBoolean("floodgatePrefixWorkaround")){
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
index 8b3d601..3c161b7 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java
@@ -31,8 +31,10 @@ import com.comphenix.protocol.events.PacketAdapter;
 import com.comphenix.protocol.events.PacketContainer;
 import com.comphenix.protocol.events.PacketEvent;
 import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
-import com.github.games647.fastlogin.core.RateLimiter;
+import com.github.games647.fastlogin.core.antibot.AntiBotService;
+import com.github.games647.fastlogin.core.antibot.AntiBotService.Action;

+import java.net.InetSocketAddress;
 import java.security.KeyPair;
 import java.security.SecureRandom;

@@ -50,9 +52,9 @@ public class ProtocolLibListener extends PacketAdapter {
     //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();
-    private final RateLimiter rateLimiter;
+    private final AntiBotService antiBotService;

-    public ProtocolLibListener(FastLoginBukkit plugin, RateLimiter rateLimiter) {
+    public ProtocolLibListener(FastLoginBukkit plugin, AntiBotService antiBotService) {
         //run async in order to not block the server, because we are making api calls to Mojang
         super(params()
                 .plugin(plugin)
@@ -60,15 +62,15 @@ public class ProtocolLibListener extends PacketAdapter {
                 .optionAsync());

         this.plugin = plugin;
-        this.rateLimiter = rateLimiter;
+        this.antiBotService = antiBotService;
     }

-    public static void register(FastLoginBukkit plugin, RateLimiter rateLimiter) {
+    public static void register(FastLoginBukkit plugin, AntiBotService antiBotService) {
         // they will be created with a static builder, because otherwise it will throw a NoClassDefFoundError
         // TODO: make synchronous processing, but do web or database requests async
         ProtocolLibrary.getProtocolManager()
                 .getAsynchronousManager()
-                .registerAsyncHandler(new ProtocolLibListener(plugin, rateLimiter))
+                .registerAsyncHandler(new ProtocolLibListener(plugin, antiBotService))
                 .start();
     }

@@ -88,12 +90,26 @@ public class ProtocolLibListener extends PacketAdapter {
         Player sender = packetEvent.getPlayer();
         PacketType packetType = packetEvent.getPacketType();
         if (packetType == START) {
-            if (!rateLimiter.tryAcquire()) {
-                plugin.getLog().warn("Simple Anti-Bot join limit - Ignoring {}", sender);
-                return;
+            PacketContainer packet = packetEvent.getPacket();
+
+            InetSocketAddress address = sender.getAddress();
+            String username = packet.getGameProfiles().read(0).getName();
+
+            Action action = antiBotService.onIncomingConnection(address, username);
+            switch (action) {
+                case Ignore:
+                    // just ignore
+                    return;
+                case Block:
+                    String message = plugin.getCore().getMessage("kick-antibot");
+                    sender.kickPlayer(message);
+                    break;
+                case Continue:
+                default:
+                    //player.getName() won't work at this state
+                    onLogin(packetEvent, sender, username);
+                    break;
             }
-
-            onLogin(packetEvent, sender);
         } else {
             onEncryptionBegin(packetEvent, sender);
         }
@@ -113,18 +129,13 @@ public class ProtocolLibListener extends PacketAdapter {
         plugin.getScheduler().runAsync(verifyTask);
     }

-    private void onLogin(PacketEvent packetEvent, Player player) {
+    private void onLogin(PacketEvent packetEvent, Player player, String username) {
         //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.removeSession(player.getAddress());

-        //player.getName() won't work at this state
-        PacketContainer packet = packetEvent.getPacket();
-
-        String username = packet.getGameProfiles().read(0).getName();
-
         if (packetEvent.getPacket().getMeta("original_name").isPresent()) {
             //username has been injected by ManualNameChange.java
             username = (String) packetEvent.getPacket().getMeta("original_name").get();
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocolsupport/ProtocolSupportListener.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocolsupport/ProtocolSupportListener.java
index 608078c..ce84824 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocolsupport/ProtocolSupportListener.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocolsupport/ProtocolSupportListener.java
@@ -29,8 +29,9 @@ import com.github.games647.craftapi.UUIDAdapter;
 import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
 import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
 import com.github.games647.fastlogin.bukkit.event.BukkitFastLoginPreLoginEvent;
-import com.github.games647.fastlogin.core.RateLimiter;
 import com.github.games647.fastlogin.core.StoredProfile;
+import com.github.games647.fastlogin.core.antibot.AntiBotService;
+import com.github.games647.fastlogin.core.antibot.AntiBotService.Action;
 import com.github.games647.fastlogin.core.shared.JoinManagement;
 import com.github.games647.fastlogin.core.shared.event.FastLoginPreLoginEvent;

@@ -49,13 +50,13 @@ public class ProtocolSupportListener extends JoinManagement<Player, CommandSende
         implements Listener {

     private final FastLoginBukkit plugin;
-    private final RateLimiter rateLimiter;
+    private final AntiBotService antiBotService;

-    public ProtocolSupportListener(FastLoginBukkit plugin, RateLimiter rateLimiter) {
+    public ProtocolSupportListener(FastLoginBukkit plugin, AntiBotService antiBotService) {
         super(plugin.getCore(), plugin.getCore().getAuthPluginHook(), plugin.getBedrockService());

         this.plugin = plugin;
-        this.rateLimiter = rateLimiter;
+        this.antiBotService = antiBotService;
     }

     @EventHandler
@@ -64,19 +65,28 @@ public class ProtocolSupportListener extends JoinManagement<Player, CommandSende
             return;
         }

-        if (!rateLimiter.tryAcquire()) {
-            plugin.getLog().warn("Simple Anti-Bot join limit - Ignoring {}", loginStartEvent.getConnection());
-            return;
-        }
-
         String username = loginStartEvent.getConnection().getProfile().getName();
         InetSocketAddress address = loginStartEvent.getAddress();
-
-        //remove old data every time on a new login in order to keep the session only for one person
-        plugin.removeSession(address);
-
-        ProtocolLoginSource source = new ProtocolLoginSource(loginStartEvent);
-        super.onLogin(username, source);
+        plugin.getLog().info("Incoming login request for {} from {}", username, address);
+
+        Action action = antiBotService.onIncomingConnection(address, username);
+        switch (action) {
+            case Ignore:
+                // just ignore
+                return;
+            case Block:
+                String message = plugin.getCore().getMessage("kick-antibot");
+                loginStartEvent.denyLogin(message);
+                break;
+            case Continue:
+            default:
+                //remove old data every time on a new login in order to keep the session only for one person
+                plugin.removeSession(address);
+
+                ProtocolLoginSource source = new ProtocolLoginSource(loginStartEvent);
+                super.onLogin(username, source);
+                break;
+        }
     }

     @EventHandler
diff --git a/bungee/src/main/java/com/github/games647/fastlogin/bungee/listener/ConnectListener.java b/bungee/src/main/java/com/github/games647/fastlogin/bungee/listener/ConnectListener.java
index a8c894d..ddd7e97 100644
--- a/bungee/src/main/java/com/github/games647/fastlogin/bungee/listener/ConnectListener.java
+++ b/bungee/src/main/java/com/github/games647/fastlogin/bungee/listener/ConnectListener.java
@@ -32,7 +32,8 @@ import com.github.games647.fastlogin.bungee.task.AsyncPremiumCheck;
 import com.github.games647.fastlogin.bungee.task.FloodgateAuthTask;
 import com.github.games647.fastlogin.bungee.task.ForceLoginTask;
 import com.github.games647.fastlogin.core.StoredProfile;
-import com.github.games647.fastlogin.core.antibot.RateLimiter;
+import com.github.games647.fastlogin.core.antibot.AntiBotService;
+import com.github.games647.fastlogin.core.antibot.AntiBotService.Action;
 import com.github.games647.fastlogin.core.hooks.bedrock.FloodgateService;
 import com.github.games647.fastlogin.core.shared.LoginSession;
 import com.google.common.base.Throwables;
@@ -41,8 +42,10 @@ import java.lang.invoke.MethodHandle;
 import java.lang.invoke.MethodHandles;
 import java.lang.invoke.MethodHandles.Lookup;
 import java.lang.reflect.Field;
+import java.net.InetSocketAddress;
 import java.util.UUID;

+import net.md_5.bungee.api.chat.TextComponent;
 import net.md_5.bungee.api.connection.PendingConnection;
 import net.md_5.bungee.api.connection.ProxiedPlayer;
 import net.md_5.bungee.api.connection.Server;
@@ -92,12 +95,12 @@ public class ConnectListener implements Listener {
     }

     private final FastLoginBungee plugin;
-    private final RateLimiter rateLimiter;
+    private final AntiBotService antiBotService;
     private final Property[] emptyProperties = {};

-    public ConnectListener(FastLoginBungee plugin, RateLimiter rateLimiter) {
+    public ConnectListener(FastLoginBungee plugin, AntiBotService antiBotService) {
         this.plugin = plugin;
-        this.rateLimiter = rateLimiter;
+        this.antiBotService = antiBotService;
     }

     @EventHandler
@@ -107,17 +110,28 @@ public class ConnectListener implements Listener {
             return;
         }

-        if (!rateLimiter.tryAcquire()) {
-            plugin.getLog().warn("Simple Anti-Bot join limit - Ignoring {}", connection);
-            return;
-        }
-
+        InetSocketAddress address = preLoginEvent.getConnection().getAddress();
         String username = connection.getName();
+
         plugin.getLog().info("Incoming login request for {} from {}", username, connection.getSocketAddress());

-        preLoginEvent.registerIntent(plugin);
-        Runnable asyncPremiumCheck = new AsyncPremiumCheck(plugin, preLoginEvent, connection, username);
-        plugin.getScheduler().runAsync(asyncPremiumCheck);
+        Action action = antiBotService.onIncomingConnection(address, username);
+        switch (action) {
+            case Ignore:
+                // just ignore
+                return;
+            case Block:
+                String message = plugin.getCore().getMessage("kick-antibot");
+                preLoginEvent.setCancelReason(TextComponent.fromLegacyText(message));
+                preLoginEvent.setCancelled(true);
+                break;
+            case Continue:
+            default:
+                preLoginEvent.registerIntent(plugin);
+                Runnable asyncPremiumCheck = new AsyncPremiumCheck(plugin, preLoginEvent, connection, username);
+                plugin.getScheduler().runAsync(asyncPremiumCheck);
+                break;
+        }
     }

     @EventHandler(priority = EventPriority.LOWEST)
@@ -140,7 +154,7 @@ public class ConnectListener implements Listener {
             StoredProfile playerProfile = session.getProfile();
             playerProfile.setId(verifiedUUID);

-            // bungeecord will do this automatically so override it on disabled option
+            // BungeeCord will do this automatically so override it on disabled option
             if (uniqueIdSetter != null) {
                 InitialHandler initialHandler = (InitialHandler) connection;

diff --git a/core/src/main/java/com/github/games647/fastlogin/core/mojang/MojangApiConnector.java b/core/src/main/java/com/github/games647/fastlogin/core/antibot/AntiBotService.java
similarity index 57%
rename from core/src/main/java/com/github/games647/fastlogin/core/mojang/MojangApiConnector.java
rename to core/src/main/java/com/github/games647/fastlogin/core/antibot/AntiBotService.java
index 4321d6f..2848335 100644
--- a/core/src/main/java/com/github/games647/fastlogin/core/mojang/MojangApiConnector.java
+++ b/core/src/main/java/com/github/games647/fastlogin/core/antibot/AntiBotService.java
@@ -23,3 +23,40 @@
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  * SOFTWARE.
  */
+package com.github.games647.fastlogin.core.antibot;
+
+import java.net.InetSocketAddress;
+
+import org.slf4j.Logger;
+
+public class AntiBotService {
+
+    private final Logger logger;
+
+    private final RateLimiter rateLimiter;
+    private final Action limitReachedAction;
+
+    public AntiBotService(Logger logger, RateLimiter rateLimiter, Action limitReachedAction) {
+        this.logger = logger;
+
+        this.rateLimiter = rateLimiter;
+        this.limitReachedAction = limitReachedAction;
+    }
+
+    public Action onIncomingConnection(InetSocketAddress clientAddress, String username) {
+        if (!rateLimiter.tryAcquire()) {
+            logger.warn("Anti-Bot join limit - Ignoring {}", clientAddress);
+            return limitReachedAction;
+        }
+
+        return Action.Continue;
+    }
+
+    public enum Action {
+        Ignore,
+
+        Block,
+
+        Continue;
+    }
+}
diff --git a/core/src/main/java/com/github/games647/fastlogin/core/mojang/ProxyAgnosticMojangResolver.java b/core/src/main/java/com/github/games647/fastlogin/core/antibot/ProxyAgnosticMojangResolver.java
similarity index 100%
rename from core/src/main/java/com/github/games647/fastlogin/core/mojang/ProxyAgnosticMojangResolver.java
rename to core/src/main/java/com/github/games647/fastlogin/core/antibot/ProxyAgnosticMojangResolver.java
diff --git a/core/src/main/java/com/github/games647/fastlogin/core/RateLimiter.java b/core/src/main/java/com/github/games647/fastlogin/core/antibot/RateLimiter.java
similarity index 96%
rename from core/src/main/java/com/github/games647/fastlogin/core/RateLimiter.java
rename to core/src/main/java/com/github/games647/fastlogin/core/antibot/RateLimiter.java
index 3ec2f75..b791ca5 100644
--- a/core/src/main/java/com/github/games647/fastlogin/core/RateLimiter.java
+++ b/core/src/main/java/com/github/games647/fastlogin/core/antibot/RateLimiter.java
@@ -23,7 +23,7 @@
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  * SOFTWARE.
  */
-package com.github.games647.fastlogin.core;
+package com.github.games647.fastlogin.core.antibot;

 @FunctionalInterface
 public interface RateLimiter {
diff --git a/core/src/main/java/com/github/games647/fastlogin/core/TickingRateLimiter.java b/core/src/main/java/com/github/games647/fastlogin/core/antibot/TickingRateLimiter.java
similarity index 98%
rename from core/src/main/java/com/github/games647/fastlogin/core/TickingRateLimiter.java
rename to core/src/main/java/com/github/games647/fastlogin/core/antibot/TickingRateLimiter.java
index 6064229..ced7da1 100644
--- a/core/src/main/java/com/github/games647/fastlogin/core/TickingRateLimiter.java
+++ b/core/src/main/java/com/github/games647/fastlogin/core/antibot/TickingRateLimiter.java
@@ -23,7 +23,7 @@
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  * SOFTWARE.
  */
-package com.github.games647.fastlogin.core;
+package com.github.games647.fastlogin.core.antibot;

 import com.google.common.base.Ticker;

diff --git a/core/src/main/java/com/github/games647/fastlogin/core/mojang/UUIDTypeAdapter.java b/core/src/main/java/com/github/games647/fastlogin/core/mojang/UUIDTypeAdapter.java
deleted file mode 100644
index 4321d6f..0000000
--- a/core/src/main/java/com/github/games647/fastlogin/core/mojang/UUIDTypeAdapter.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * SPDX-License-Identifier: MIT
- *
- * The MIT License (MIT)
- *
- * Copyright (c) 2015-2022 games647 and contributors
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
diff --git a/core/src/main/java/com/github/games647/fastlogin/core/shared/FastLoginCore.java b/core/src/main/java/com/github/games647/fastlogin/core/shared/FastLoginCore.java
index 9e2a49a..474a8e2 100644
--- a/core/src/main/java/com/github/games647/fastlogin/core/shared/FastLoginCore.java
+++ b/core/src/main/java/com/github/games647/fastlogin/core/shared/FastLoginCore.java
@@ -28,8 +28,10 @@ package com.github.games647.fastlogin.core.shared;
 import com.github.games647.craftapi.resolver.MojangResolver;
 import com.github.games647.craftapi.resolver.http.RotatingProxySelector;
 import com.github.games647.fastlogin.core.CommonUtil;
-import com.github.games647.fastlogin.core.RateLimiter;
-import com.github.games647.fastlogin.core.TickingRateLimiter;
+import com.github.games647.fastlogin.core.antibot.AntiBotService;
+import com.github.games647.fastlogin.core.antibot.AntiBotService.Action;
+import com.github.games647.fastlogin.core.antibot.RateLimiter;
+import com.github.games647.fastlogin.core.antibot.TickingRateLimiter;
 import com.github.games647.fastlogin.core.hooks.AuthPlugin;
 import com.github.games647.fastlogin.core.hooks.DefaultPasswordGenerator;
 import com.github.games647.fastlogin.core.hooks.PasswordGenerator;
@@ -88,7 +90,7 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {

     private Configuration config;
     private SQLStorage storage;
-    private RateLimiter rateLimiter;
+    private AntiBotService antiBot;
     private PasswordGenerator<P> passwordGenerator = new DefaultPasswordGenerator<>();
     private AuthPlugin<P> authPlugin;

@@ -122,7 +124,7 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
         // Initialize the resolver based on the config parameter
         this.resolver = this.config.getBoolean("useProxyAgnosticResolver", false) ? new ProxyAgnosticMojangResolver() : new MojangResolver();

-        rateLimiter = createRateLimiter(config.getSection("anti-bot"));
+        antiBot = createAntiBotService(config.getSection("anti-bot"));
         Set<Proxy> proxies = config.getStringList("proxies")
                 .stream()
                 .map(HostAndPort::fromString)
@@ -144,20 +146,34 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
         resolver.setOutgoingAddresses(addresses);
     }

-    private RateLimiter createRateLimiter(Configuration botSection) {
-        boolean enabled = botSection.getBoolean("enabled", true);
-        if (!enabled) {
+    private AntiBotService createAntiBotService(Configuration botSection) {
+        RateLimiter rateLimiter;
+        if (botSection.getBoolean("enabled", true)) {
+            int maxCon = botSection.getInt("connections", 200);
+            long expireTime = botSection.getLong("expire", 5) * 60 * 1_000L;
+            if (expireTime > MAX_EXPIRE_RATE) {
+                expireTime = MAX_EXPIRE_RATE;
+            }
+
+            rateLimiter = new TickingRateLimiter(Ticker.systemTicker(), maxCon, expireTime);
+        } else {
             // no-op rate limiter
-            return () -> true;
+            rateLimiter = () -> true;
         }

-        int maxCon = botSection.getInt("connections", 200);
-        long expireTime = botSection.getLong("expire", 5) * 60 * 1_000L;
-        if (expireTime > MAX_EXPIRE_RATE) {
-            expireTime = MAX_EXPIRE_RATE;
+        Action action = Action.Ignore;
+        switch (botSection.getString("action", "ignore")) {
+            case "ignore":
+                action = Action.Ignore;
+                break;
+            case "block":
+                action = Action.Block;
+                break;
+            default:
+                plugin.getLog().warn("Invalid anti bot action - defaulting to ignore");
         }

-        return new TickingRateLimiter(Ticker.systemTicker(), maxCon, expireTime);
+        return new AntiBotService(plugin.getLog(), rateLimiter, action);
     }

     private Configuration loadFile(String fileName) throws IOException {
@@ -285,8 +301,8 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
         return authPlugin;
     }

-    public RateLimiter getRateLimiter() {
-        return rateLimiter;
+    public AntiBotService getAntiBot() {
+        return antiBot;
     }

     public void setAuthPluginHook(AuthPlugin<P> authPlugin) {
diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml
index 1b0905d..3a2ab7a 100644
--- a/core/src/main/resources/config.yml
+++ b/core/src/main/resources/config.yml
@@ -20,6 +20,9 @@ anti-bot:
   connections: 600
   # Amount of minutes after the first connection got inserted will expire and made available
   expire: 10
+  # Action - Which action should be performed when the bucket is full (too many connections)
+  # Allowed values are: 'ignore' (FastLogin drops handling the player) or 'block' (block this incoming connection)
+  action: 'ignore'

 # Request a premium login without forcing the player to type a command
 #
diff --git a/core/src/main/resources/messages.yml b/core/src/main/resources/messages.yml
index 8b315ba..33cadaa 100644
--- a/core/src/main/resources/messages.yml
+++ b/core/src/main/resources/messages.yml
@@ -48,6 +48,9 @@ not-premium-other: '&4Player is not in the premium list'
 # Admin wanted to change the premium of a user that isn't known to the plugin
 player-unknown: '&4Player not in the database'

+# Player kicked from anti bot feature
+kick-antibot: '&4Please wait a moment!'
+
 # ========= Bukkit/Spigot ================

 # The user skipped the authentication, because it was a premium player
diff --git a/core/src/test/java/com/github/games647/fastlogin/core/TickingRateLimiterTest.java b/core/src/test/java/com/github/games647/fastlogin/core/TickingRateLimiterTest.java
index 2c04b6e..3f753cd 100644
--- a/core/src/test/java/com/github/games647/fastlogin/core/TickingRateLimiterTest.java
+++ b/core/src/test/java/com/github/games647/fastlogin/core/TickingRateLimiterTest.java
@@ -25,6 +25,8 @@
  */
 package com.github.games647.fastlogin.core;

+import com.github.games647.fastlogin.core.antibot.TickingRateLimiter;
+
 import java.time.Duration;
 import java.util.concurrent.TimeUnit;

diff --git a/velocity/src/main/java/com/github/games647/fastlogin/velocity/listener/ConnectListener.java b/velocity/src/main/java/com/github/games647/fastlogin/velocity/listener/ConnectListener.java
index 2b51bd2..33c8c31 100644
--- a/velocity/src/main/java/com/github/games647/fastlogin/velocity/listener/ConnectListener.java
+++ b/velocity/src/main/java/com/github/games647/fastlogin/velocity/listener/ConnectListener.java
@@ -27,7 +27,8 @@ package com.github.games647.fastlogin.velocity.listener;

 import com.github.games647.craftapi.UUIDAdapter;
 import com.github.games647.fastlogin.core.StoredProfile;
-import com.github.games647.fastlogin.core.antibot.RateLimiter;
+import com.github.games647.fastlogin.core.antibot.AntiBotService;
+import com.github.games647.fastlogin.core.antibot.AntiBotService.Action;
 import com.github.games647.fastlogin.core.shared.LoginSession;
 import com.github.games647.fastlogin.velocity.FastLoginVelocity;
 import com.github.games647.fastlogin.velocity.VelocityLoginSession;
@@ -37,6 +38,7 @@ import com.velocitypowered.api.event.Continuation;
 import com.velocitypowered.api.event.Subscribe;
 import com.velocitypowered.api.event.connection.DisconnectEvent;
 import com.velocitypowered.api.event.connection.PreLoginEvent;
+import com.velocitypowered.api.event.connection.PreLoginEvent.PreLoginComponentResult;
 import com.velocitypowered.api.event.player.GameProfileRequestEvent;
 import com.velocitypowered.api.event.player.ServerConnectedEvent;
 import com.velocitypowered.api.proxy.InboundConnection;
@@ -44,19 +46,23 @@ import com.velocitypowered.api.proxy.Player;
 import com.velocitypowered.api.proxy.server.RegisteredServer;
 import com.velocitypowered.api.util.GameProfile;

+import java.net.InetSocketAddress;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;

+import net.kyori.adventure.text.TextComponent;
+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+
 public class ConnectListener {

     private final FastLoginVelocity plugin;
-    private final RateLimiter rateLimiter;
+    private final AntiBotService antiBotService;

-    public ConnectListener(FastLoginVelocity plugin, RateLimiter rateLimiter) {
+    public ConnectListener(FastLoginVelocity plugin, AntiBotService antiBotService) {
         this.plugin = plugin;
-        this.rateLimiter = rateLimiter;
+        this.antiBotService = antiBotService;
     }

     @Subscribe
@@ -66,16 +72,28 @@ public class ConnectListener {
         }

         InboundConnection connection = preLoginEvent.getConnection();
-        if (!rateLimiter.tryAcquire()) {
-            plugin.getLog().warn("Simple Anti-Bot join limit - Ignoring {}", connection);
-            return;
-        }
-
         String username = preLoginEvent.getUsername();
-        plugin.getLog().info("Incoming login request for {} from {}", username, connection.getRemoteAddress());
-
-        Runnable asyncPremiumCheck = new AsyncPremiumCheck(plugin, connection, username, continuation, preLoginEvent);
-        plugin.getScheduler().runAsync(asyncPremiumCheck);
+        InetSocketAddress address = connection.getRemoteAddress();
+        plugin.getLog().info("Incoming login request for {} from {}", username, address);
+
+        Action action = antiBotService.onIncomingConnection(address, username);
+        switch (action) {
+            case Ignore:
+                // just ignore
+                return;
+            case Block:
+                String message = plugin.getCore().getMessage("kick-antibot");
+                TextComponent messageParsed = LegacyComponentSerializer.legacyAmpersand().deserialize(message);
+
+                PreLoginComponentResult reason = PreLoginComponentResult.denied(messageParsed);
+                preLoginEvent.setResult(reason);
+                break;
+            case Continue:
+            default:
+                Runnable asyncPremiumCheck = new AsyncPremiumCheck(plugin, connection, username, continuation, preLoginEvent);
+                plugin.getScheduler().runAsync(asyncPremiumCheck);
+                break;
+        }
     }

     @Subscribe
2022-06-18 18:09:31 +02:00
10785b32ac Fix anti bot reading the correct values 2022-06-18 18:08:40 +02:00
ae8c040d3e Merge pull request #814 from games647/dependabot/maven/org.geysermc.floodgate-api-2.2.0-SNAPSHOT
Bump api from 2.0-SNAPSHOT to 2.2.0-SNAPSHOT
2022-06-18 18:04:15 +02:00
b221a3f294 Merge pull request #822 from games647/dependabot/github_actions/actions/cache-3.0.4
Bump actions/cache from 3.0.1 to 3.0.4
2022-06-18 16:35:24 +02:00
fb32f0b386 Bump requried Java version to 17 2022-06-18 16:15:26 +02:00
752cb1c3a7 Bump actions/cache from 3.0.1 to 3.0.4
Bumps [actions/cache](https://github.com/actions/cache) from 3.0.1 to 3.0.4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.0.1...v3.0.4)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-18 14:10:22 +00:00
f5ead50441 Fix typo in pull request template 2022-06-18 16:09:44 +02:00
b878ec34c8 Fix Java workflow setup 2022-06-18 16:09:44 +02:00
3d72e77345 Fix Java version name 2022-06-18 16:09:44 +02:00
18bd778b7e General minor code clean up and typo fixes 2022-06-18 16:09:44 +02:00
a9e4c90970 Merge pull request #799 from games647/dependabot/maven/org.mariadb.jdbc-mariadb-java-client-3.0.5
Bump mariadb-java-client from 3.0.4 to 3.0.5
2022-06-15 13:15:15 +02:00
c92f94d2e8 Bump api from 2.0-SNAPSHOT to 2.2.0-SNAPSHOT
Bumps api from 2.0-SNAPSHOT to 2.2.0-SNAPSHOT.

---
updated-dependencies:
- dependency-name: org.geysermc.floodgate:api
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-13 07:03:18 +00:00
41890679ed Bump mariadb-java-client from 3.0.4 to 3.0.5
Bumps [mariadb-java-client](https://github.com/mariadb-corporation/mariadb-connector-j) from 3.0.4 to 3.0.5.
- [Release notes](https://github.com/mariadb-corporation/mariadb-connector-j/releases)
- [Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mariadb-corporation/mariadb-connector-j/compare/3.0.4...3.0.5)

---
updated-dependencies:
- dependency-name: org.mariadb.jdbc:mariadb-java-client
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-30 07:03:27 +00:00
103 changed files with 2446 additions and 749 deletions

View File

@ -14,22 +14,3 @@ updates:
directory: "/"
schedule:
interval: weekly
ignore:
- dependency-name: com.google.code.gson:gson
versions:
- "> 2.2.4"
- dependency-name: com.google.guava
- dependency-name: me.clip:placeholderapi
versions:
- "> 2.10.8, < 2.11"
- dependency-name: net.md-5:bungeecord-config
versions:
- "> 1.12-SNAPSHOT"
- dependency-name: de.xxschrandxx.bca:BungeeCordAuthenticator
versions:
- 0.0.3
- dependency-name: com.zaxxer:HikariCP
versions:
- 4.0.0
- 4.0.2
- 4.0.3

View File

@ -2,7 +2,7 @@
[//]: # (If your work is in progress, please consider making a draft pull request.)
### Summary of your change
[//]: # (Example: motiviation, enhancement)
[//]: # (Example: motivation, enhancement)
### Related issue
[//]: # (Reference it using '#NUMBER'. Ex: Fixes/Related #...)

View File

@ -40,8 +40,7 @@ jobs:
uses: actions/setup-java@v3
with:
distribution: 'temurin'
# Use Java 16+, because it's minimum required version by Geyser
java-version: 17
java-version: 18
cache: 'maven'
# Initializes the CodeQL tools for scanning.
@ -51,7 +50,7 @@ jobs:
languages: ${{ matrix.language }}
# Cache build process too like in the maven config
- uses: actions/cache@v3.0.1
- uses: actions/cache@v3.0.4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}

View File

@ -31,8 +31,7 @@ jobs:
uses: actions/setup-java@v3
with:
distribution: 'temurin'
# Use Java 16+, because it's minimum required version by Geyser
java-version: 17
java-version: 18
cache: 'maven'
# Build and test (included in package)
@ -40,4 +39,4 @@ jobs:
# Run non-interactive, package (with compile+test),
# ignore snapshot updates, because they are likely to have breaking changes, enforce checksums to validate
# possible errors in dependencies
run: mvn test --batch-mode --no-snapshot-updates --strict-checksums --file pom.xml
run: mvn test --batch-mode --threads 2.0C --no-snapshot-updates --strict-checksums --file pom.xml

View File

@ -35,7 +35,7 @@
* Automatically register accounts if they are not in the auth plugin database but in the FastLogin database
* Update BungeeAuth dependency and use the new API. Please update your plugin if you still use the old one.
* Remove deprecated API methods from the last version
* Finally update the IP column on every login
* Finally, update the IP column on every login
* No duplicate session login
* Fix timestamp parsing in newer versions of SQLite
* Fix Spigot console command invocation sends result to in game players
@ -82,7 +82,7 @@
* Fix player entry is not saved if namechangecheck is enabled
* Fix skin applies for third-party plugins
* Switch to mcapi.ca for uuid lookups
* Fix BungeeCord not setting an premium uuid
* Fix BungeeCord not setting a premium uuid
* Fix setting skin on Cauldron
* Fix saving on name change
@ -148,7 +148,7 @@
### 1.2
* Fix race condition in BungeeCord
* Fix dead lock in xAuth
* Fix deadlock in xAuth
* Added API methods for plugins to set their own password generator
* Added API methods for plugins to set their own auth plugin hook
=> Added support for AdvancedLogin
@ -182,7 +182,7 @@
* Added a forwardSkin config option
* Added premium UUID support
* Updated to the newest changes of Spigot
* Removes the need of an Bukkit auth plugin if you use a bungeecord one
* Removes the need of a Bukkit auth plugin if you use a bungeecord one
* Optimize performance and thread-safety
* Fixed BungeeCord support
* Changed config option auto-login to auto-register to clarify the usage

View File

@ -19,7 +19,7 @@ So they don't need to enter passwords. This is also called auto login (auto-logi
* No client modifications needed
* Good performance by using async operations
* Locale messages
* Support for Bedrock players proxied through FloodGate
* Support for Bedrock players proxies through FloodGate
## Issues
@ -60,11 +60,11 @@ Possible values: `Premium`, `Cracked`, `Unknown`
## Requirements
* Java 8+
* Java 17+
* Server software in offlinemode:
* Spigot (or a fork e.g. Paper) 1.8.8+
* Protocol plugin:
* [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/) or
* [ProtocolLib 5.0+](https://www.spigotmc.org/resources/protocollib.1997/) or
* [ProtocolSupport](https://www.spigotmc.org/resources/protocolsupport.7201/)
* Latest BungeeCord (or a fork e.g. Waterfall)
* An auth plugin.
@ -78,7 +78,6 @@ Possible values: `Premium`, `Cracked`, `Unknown`
* [CrazyLogin](https://dev.bukkit.org/bukkit-plugins/crazylogin/)
* [LoginSecurity](https://dev.bukkit.org/bukkit-plugins/loginsecurity/)
* [LogIt](https://github.com/games647/LogIt)
* [SodionAuth (2.0+)](https://github.com/MohistMC/SodionAuth)
* [UltraAuth](https://dev.bukkit.org/bukkit-plugins/ultraauth-aa/)
* [UserLogin](https://www.spigotmc.org/resources/userlogin.80669/)
* [xAuth](https://dev.bukkit.org/bukkit-plugins/xauth/)
@ -87,7 +86,6 @@ Possible values: `Premium`, `Cracked`, `Unknown`
* [BungeeAuth](https://www.spigotmc.org/resources/bungeeauth.493/)
* [BungeeAuthenticator](https://www.spigotmc.org/resources/bungeecordauthenticator.87669/)
* [SodionAuth (2.0+)](https://github.com/MohistMC/SodionAuth)
## Network requests

View File

@ -29,10 +29,14 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<properties>
<nettyVersion>4.1.77.Final</nettyVersion>
</properties>
<parent>
<groupId>com.github.games647</groupId>
<artifactId>fastlogin</artifactId>
<version>1.11-SNAPSHOT</version>
<version>1.12-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@ -45,7 +49,6 @@
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.3.0</version>
<configuration>
@ -118,10 +121,7 @@
<!-- ProtocolLib -->
<repository>
<id>dmulloy2-repo</id>
<url>https://repo.dmulloy2.net/nexus/repository/public/</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
<url>https://repo.dmulloy2.net/repository/public/</url>
</repository>
<!-- AuthMe Reloaded, xAuth and LoginSecurity -->
@ -164,7 +164,7 @@
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.18-R0.1-SNAPSHOT</version>
<version>1.19-R0.1-SNAPSHOT</version>
<scope>provided</scope>
<!-- Use our own newer api version -->
<exclusions>
@ -182,11 +182,24 @@
<version>1.0.7</version>
</dependency>
<dependency>
<groupId>com.mojang</groupId>
<artifactId>datafixerupper</artifactId>
<version>5.0.28</version>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--Library for listening and sending Minecraft packets-->
<dependency>
<groupId>com.comphenix.protocol</groupId>
<artifactId>ProtocolLib</artifactId>
<version>4.8.0</version>
<version>5.0.0-SNAPSHOT</version>
<scope>provided</scope>
<exclusions>
<exclusion>
@ -201,7 +214,7 @@
<groupId>com.github.ProtocolSupport</groupId>
<artifactId>ProtocolSupport</artifactId>
<!--4.29.dev after commit about API improvements-->
<version>3a80c661fe</version>
<version>master-66b494a8dd-1</version>
<scope>provided</scope>
<exclusions>
<exclusion>
@ -215,7 +228,7 @@
<dependency>
<groupId>org.geysermc.floodgate</groupId>
<artifactId>api</artifactId>
<version>2.0-SNAPSHOT</version>
<version>2.2.0-SNAPSHOT</version>
<scope>provided</scope>
<exclusions>
<exclusion>
@ -255,7 +268,7 @@
<dependency>
<groupId>me.clip</groupId>
<artifactId>placeholderapi</artifactId>
<version>2.11.1</version>
<version>2.11.2</version>
<scope>provided</scope>
<optional>true</optional>
<exclusions>
@ -270,7 +283,7 @@
<dependency>
<groupId>fr.xephi</groupId>
<artifactId>authme</artifactId>
<version>5.4.0</version>
<version>5.6.0-beta2</version>
<scope>provided</scope>
<optional>true</optional>
<exclusions>
@ -351,5 +364,34 @@
<scope>system</scope>
<systemPath>${project.basedir}/lib/UltraAuth v2.1.2.jar</systemPath>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.71</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport</artifactId>
<version>${nettyVersion}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec</artifactId>
<version>${nettyVersion}</version>
<scope>test</scope>
</dependency>
<!-- Provided by the spigot, required for testing ProtocolLib -->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -26,11 +26,14 @@
package com.github.games647.fastlogin.bukkit;
import com.github.games647.craftapi.model.skin.SkinProperty;
import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.LoginSession;
import java.util.Optional;
import javax.annotation.Nullable;
/**
* Represents a client connecting to the server.
*
@ -42,30 +45,33 @@ public class BukkitLoginSession extends LoginSession {
private final byte[] verifyToken;
private final ClientPublicKey clientPublicKey;
private boolean verified;
private SkinProperty skinProperty;
public BukkitLoginSession(String username, byte[] verifyToken, boolean registered
, StoredProfile profile) {
public BukkitLoginSession(String username, byte[] verifyToken, ClientPublicKey publicKey, boolean registered,
StoredProfile profile) {
super(username, registered, profile);
this.clientPublicKey = publicKey;
this.verifyToken = verifyToken.clone();
}
//available for BungeeCord
public BukkitLoginSession(String username, boolean registered) {
this(username, EMPTY_ARRAY, registered, null);
this(username, EMPTY_ARRAY, null, registered, null);
}
//cracked player
public BukkitLoginSession(String username, StoredProfile profile) {
this(username, EMPTY_ARRAY, false, profile);
this(username, EMPTY_ARRAY, null, false, profile);
}
//ProtocolSupport
public BukkitLoginSession(String username, boolean registered, StoredProfile profile) {
this(username, EMPTY_ARRAY, registered, profile);
this(username, EMPTY_ARRAY, null, registered, profile);
}
/**
@ -79,6 +85,11 @@ public class BukkitLoginSession extends LoginSession {
return verifyToken.clone();
}
@Nullable
public ClientPublicKey getClientPublicKey() {
return clientPublicKey;
}
/**
* @return premium skin if available
*/

View File

@ -33,8 +33,11 @@ import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
@ -61,7 +64,7 @@ public class BungeeManager {
private final FastLoginBukkit plugin;
private boolean enabled;
private final Set<UUID> firedJoinEvents = new HashSet<>();
private final Collection<UUID> firedJoinEvents = new HashSet<>();
public BungeeManager(FastLoginBukkit plugin) {
this.plugin = plugin;
@ -87,33 +90,66 @@ public class BungeeManager {
}
public void initialize() {
try {
enabled = detectProxy();
} catch (Exception ex) {
plugin.getLog().warn("Cannot check proxy support. Fallback to non-proxy mode", ex);
}
enabled = detectProxy();
if (enabled) {
proxyIds = loadBungeeCordIds();
registerPluginChannels();
plugin.getLog().info("Found enabled proxy configuration");
plugin.getLog().info("Remember to follow the proxy guide to complete your setup");
} else {
plugin.getLog().warn("Disabling Minecraft proxy configuration. Assuming direct connections from now on.");
}
}
private boolean isProxySupported(String className, String fieldName) {
private boolean isProxySupported(String className, String fieldName)
throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
return Class.forName(className).getDeclaredField(fieldName).getBoolean(null);
}
private boolean isVelocityEnabled()
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException, ClassNotFoundException,
NoSuchMethodException, InvocationTargetException {
try {
return Class.forName(className).getDeclaredField(fieldName).getBoolean(null);
} catch (ClassNotFoundException notFoundEx) {
//ignore server has no proxy support
} catch (NoSuchFieldException | IllegalAccessException noSuchFieldException) {
plugin.getLog().warn("Cannot access proxy field", noSuchFieldException);
Class<?> globalConfig = Class.forName("io.papermc.paper.configuration.GlobalConfiguration");
Object global = globalConfig.getDeclaredMethod("get").invoke(null);
Object proxiesConfiguration = global.getClass().getDeclaredField("proxies").get(global);
Field velocitySectionField = proxiesConfiguration.getClass().getDeclaredField("velocity");
Object velocityConfig = velocitySectionField.get(proxiesConfiguration);
return velocityConfig.getClass().getDeclaredField("enabled").getBoolean(velocityConfig);
} catch (ClassNotFoundException classNotFoundException) {
// try again using the older Paper configuration, because the old class file still exists in newer versions
if (isProxySupported("com.destroystokyo.paper.PaperConfig", "velocitySupport")) {
return true;
}
}
return false;
}
private boolean detectProxy() {
return isProxySupported("org.spigotmc.SpigotConfig", "bungee")
|| isProxySupported("com.destroystokyo.paper.PaperConfig", "velocitySupport");
try {
if (isProxySupported("org.spigotmc.SpigotConfig", "bungee")) {
return true;
}
} catch (ClassNotFoundException classNotFoundException) {
// leave stacktrace for class not found out
plugin.getLog().warn("Cannot check for BungeeCord support: {}", classNotFoundException.getMessage());
} catch (NoSuchFieldException | IllegalAccessException ex) {
plugin.getLog().warn("Cannot check for BungeeCord support", ex);
}
try {
return isVelocityEnabled();
} catch (ClassNotFoundException classNotFoundException) {
plugin.getLog().warn("Cannot check for Velocity support in Paper: {}", classNotFoundException.getMessage());
} catch (NoSuchFieldException | IllegalAccessException | NoSuchMethodException | InvocationTargetException ex) {
plugin.getLog().warn("Cannot check for Velocity support in Paper", ex);
}
return false;
}
private void registerPluginChannels() {
@ -147,9 +183,7 @@ public class BungeeManager {
Files.deleteIfExists(legacyFile);
try (Stream<String> lines = Files.lines(proxiesFile)) {
return lines.map(String::trim)
.map(UUID::fromString)
.collect(toSet());
return lines.map(String::trim).map(UUID::fromString).collect(toSet());
}
} catch (IOException ex) {
plugin.getLog().error("Failed to read proxies", ex);
@ -176,7 +210,7 @@ public class BungeeManager {
/**
* Check if the event fired including with the task delay. This necessary to restore the order of processing the
* BungeeCord messages after the PlayerJoinEvent fires including the delay.
*
* <p>
* If the join event fired, the delay exceeded, but it ran earlier and couldn't find the recently started login
* session. If not fired, we can start a new force login task. This will still match the requirement that we wait
* a certain time after the player join event fired.

View File

@ -25,7 +25,6 @@
*/
package com.github.games647.fastlogin.bukkit;
import com.destroystokyo.paper.event.player.PlayerHandshakeEvent;
import com.github.games647.fastlogin.bukkit.command.CrackedCommand;
import com.github.games647.fastlogin.bukkit.command.PremiumCommand;
import com.github.games647.fastlogin.bukkit.listener.ConnectionListener;
@ -56,8 +55,6 @@ import java.util.concurrent.ConcurrentMap;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.geysermc.floodgate.api.FloodgateApi;
@ -107,15 +104,6 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
bungeeManager = new BungeeManager(this);
bungeeManager.initialize();
// getServer().getPluginManager().registerEvents(new Listener() {
//
// @EventHandler
// void onHandshake(PlayerHandshakeEvent handshakeEvent) {
// handshakeEvent.setCancelled(false);
// handshakeEvent.setSocketAddressHostname("192.168.0.1");
// }
// }, this);
PluginManager pluginManager = getServer().getPluginManager();
if (bungeeManager.isEnabled()) {
markInitialized();
@ -126,23 +114,12 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
}
if (pluginManager.isPluginEnabled("ProtocolSupport")) {
pluginManager.registerEvents(new ProtocolSupportListener(this, core.getRateLimiter()), this);
pluginManager.registerEvents(new ProtocolSupportListener(this, core.getAntiBot()), this);
} else if (pluginManager.isPluginEnabled("ProtocolLib")) {
ProtocolLibListener.register(this, core.getRateLimiter());
ProtocolLibListener.register(this, core.getAntiBot(), core.getConfig().getBoolean("verifyClientKeys"));
if (isPluginInstalled("floodgate")) {
if (getConfig().getBoolean("floodgatePrefixWorkaround")){
ManualNameChange.register(this, floodgateService);
logger.info("Floodgate prefix injection workaround has been enabled.");
logger.info("If you have problems joining the server, try disabling it in the configuration.");
} else {
logger.warn("We have detected that you are running FastLogin alongside Floodgate and ProtocolLib.");
logger.warn("Currently there is an issue with FastLogin that prevents Floodgate name prefixes from showing up "
+ "when it is together used with ProtocolLib.");
logger.warn("If you would like to use Floodgate name prefixes, you can enable an experimental workaround by changing "
+ "the value 'floodgatePrefixWorkaround' to true in config.yml.");
logger.warn("For more information visit https://github.com/games647/FastLogin/issues/493");
}
printFloodgateWarning();
}
//if server is using paper - we need to set the skin at pre login anyway, so no need for this listener
@ -178,6 +155,21 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
dependencyWarnings();
}
private void printFloodgateWarning() {
if (getConfig().getBoolean("floodgatePrefixWorkaround")) {
ManualNameChange.register(this, floodgateService);
logger.info("Floodgate prefix injection workaround has been enabled.");
logger.info("If you have problems joining the server, try disabling it in the configuration.");
} else {
logger.warn("We have detected that you are running FastLogin alongside Floodgate and ProtocolLib.");
logger.warn("Currently there is an issue with FastLogin that prevents Floodgate name prefixes from "
+ "showing up when it is together used with ProtocolLib.");
logger.warn("If you would like to use Floodgate name prefixes, you can enable an experimental "
+ "workaround by changing the value 'floodgatePrefixWorkaround' to true in config.yml.");
logger.warn("For more information visit https://github.com/games647/FastLogin/issues/493");
}
}
private boolean initializeFloodgate() {
if (getServer().getPluginManager().getPlugin("Geyser-Spigot") != null) {
geyserService = new GeyserService(GeyserImpl.getInstance(), core);
@ -214,6 +206,10 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
logger.error("Failed to unregister placeholder", exception);
}
}
// if (isPluginInstalled("ProtocolLib")) {
// ProtocolLibrary.getProtocolManager().getAsynchronousManager().unregisterAsyncHandlers(this);
// }
}
public FastLoginCore<Player, CommandSender, FastLoginBukkit> getCore() {
@ -341,6 +337,6 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
+ "Floodgate 2.0 from https://ci.opencollab.dev/job/GeyserMC/job/Floodgate/job/dev%252F2.0/");
logger.warn("Don't forget to update Geyser to a supported version as well from "
+ "https://ci.opencollab.dev/job/GeyserMC/job/Geyser/job/floodgate-2.0/");
}
}
}
}

View File

@ -25,6 +25,9 @@
*/
package com.github.games647.fastlogin.bukkit;
import java.util.Collections;
import java.util.List;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.OfflinePlayer;
@ -40,6 +43,16 @@ public class PremiumPlaceholder extends PlaceholderExpansion {
this.plugin = plugin;
}
@Override
public boolean persist() {
return true;
}
@Override
public @NotNull List<String> getPlaceholders() {
return Collections.singletonList(PLACEHOLDER_VARIABLE);
}
@Override
public String onRequest(OfflinePlayer player, @NotNull String identifier) {
// player is null if offline

View File

@ -42,9 +42,10 @@ public class CrackedCommand extends ToggleCommand {
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
String[] args) {
if (args.length == 0) {
onCrackedSelf(sender, command, args);
onCrackedSelf(sender);
} else {
onCrackedOther(sender, command, args);
}
@ -52,7 +53,7 @@ public class CrackedCommand extends ToggleCommand {
return true;
}
private void onCrackedSelf(CommandSender sender, Command cmd, String[] args) {
private void onCrackedSelf(CommandSender sender) {
if (isConsole(sender)) {
return;
}
@ -61,25 +62,20 @@ public class CrackedCommand extends ToggleCommand {
return;
}
if (plugin.getBungeeManager().isEnabled()) {
sendBungeeActivateMessage(sender, sender.getName(), false);
plugin.getCore().sendLocaleMessage("wait-on-proxy", sender);
} else {
//todo: load async if
StoredProfile profile = plugin.getCore().getStorage().loadProfile(sender.getName());
if (profile.isPremium()) {
plugin.getCore().sendLocaleMessage("remove-premium", sender);
// todo: load async if
StoredProfile profile = plugin.getCore().getStorage().loadProfile(sender.getName());
if (profile.isPremium()) {
plugin.getCore().sendLocaleMessage("remove-premium", sender);
profile.setPremium(false);
profile.setId(null);
plugin.getScheduler().runAsync(() -> {
plugin.getCore().getStorage().save(profile);
plugin.getServer().getPluginManager().callEvent(
new BukkitFastLoginPremiumToggleEvent(profile, PremiumToggleReason.COMMAND_OTHER));
});
} else {
plugin.getCore().sendLocaleMessage("not-premium", sender);
}
profile.setPremium(false);
profile.setId(null);
plugin.getScheduler().runAsync(() -> {
plugin.getCore().getStorage().save(profile);
plugin.getServer().getPluginManager().callEvent(
new BukkitFastLoginPremiumToggleEvent(profile, PremiumToggleReason.COMMAND_OTHER));
});
} else {
plugin.getCore().sendLocaleMessage("not-premium", sender);
}
}

View File

@ -49,9 +49,10 @@ public class PremiumCommand extends ToggleCommand {
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
String[] args) {
if (args.length == 0) {
onPremiumSelf(sender, command, args);
onPremiumSelf(sender);
} else {
onPremiumOther(sender, command, args);
}
@ -59,7 +60,7 @@ public class PremiumCommand extends ToggleCommand {
return true;
}
private void onPremiumSelf(CommandSender sender, Command cmd, String[] args) {
private void onPremiumSelf(CommandSender sender) {
if (isConsole(sender)) {
return;
}

View File

@ -81,7 +81,7 @@ public abstract class ToggleCommand implements CommandExecutor {
plugin.getBungeeManager().sendPluginMessage((PluginMessageRecipient) invoker, message);
} else {
Optional<? extends Player> optPlayer = Bukkit.getServer().getOnlinePlayers().stream().findFirst();
if (optPlayer.isEmpty()) {
if (!optPlayer.isPresent()) {
plugin.getLog().info("No player online to send a plugin message to the proxy");
return;
}

View File

@ -28,6 +28,7 @@ package com.github.games647.fastlogin.bukkit.event;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.LoginSession;
import com.github.games647.fastlogin.core.shared.event.FastLoginAutoLoginEvent;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
@ -35,7 +36,7 @@ import org.jetbrains.annotations.NotNull;
public class BukkitFastLoginAutoLoginEvent extends Event implements FastLoginAutoLoginEvent, Cancellable {
private static final HandlerList handlers = new HandlerList();
private static final HandlerList HANDLERS = new HandlerList();
private final LoginSession session;
private final StoredProfile profile;
private boolean cancelled;
@ -69,10 +70,10 @@ public class BukkitFastLoginAutoLoginEvent extends Event implements FastLoginAut
@Override
public @NotNull HandlerList getHandlers() {
return handlers;
return HANDLERS;
}
public static HandlerList getHandlerList() {
return handlers;
return HANDLERS;
}
}

View File

@ -28,13 +28,14 @@ package com.github.games647.fastlogin.bukkit.event;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.LoginSource;
import com.github.games647.fastlogin.core.shared.event.FastLoginPreLoginEvent;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
public class BukkitFastLoginPreLoginEvent extends Event implements FastLoginPreLoginEvent {
private static final HandlerList handlers = new HandlerList();
private static final HandlerList HANDLERS = new HandlerList();
private final String username;
private final LoginSource source;
private final StoredProfile profile;
@ -64,10 +65,10 @@ public class BukkitFastLoginPreLoginEvent extends Event implements FastLoginPreL
@Override
public @NotNull HandlerList getHandlers() {
return handlers;
return HANDLERS;
}
public static HandlerList getHandlerList() {
return handlers;
return HANDLERS;
}
}

View File

@ -27,13 +27,14 @@ package com.github.games647.fastlogin.bukkit.event;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.event.FastLoginPremiumToggleEvent;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
public class BukkitFastLoginPremiumToggleEvent extends Event implements FastLoginPremiumToggleEvent {
private static final HandlerList handlers = new HandlerList();
private static final HandlerList HANDLERS = new HandlerList();
private final StoredProfile profile;
private final PremiumToggleReason reason;
@ -55,10 +56,10 @@ public class BukkitFastLoginPremiumToggleEvent extends Event implements FastLogi
@Override
public @NotNull HandlerList getHandlers() {
return handlers;
return HANDLERS;
}
public static HandlerList getHandlerList() {
return handlers;
return HANDLERS;
}
}

View File

@ -43,13 +43,13 @@ import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
/**
* GitHub: https://github.com/Xephi/AuthMeReloaded/
* GitHub: <a href="https://github.com/Xephi/AuthMeReloaded/">...</a>
* <p>
* Project page:
* <p>
* Bukkit: https://dev.bukkit.org/bukkit-plugins/authme-reloaded/
* Bukkit: <a href="https://dev.bukkit.org/bukkit-plugins/authme-reloaded/">...</a>
* <p>
* Spigot: https://www.spigotmc.org/resources/authme-reloaded.6269/
* Spigot: <a href="https://www.spigotmc.org/resources/authme-reloaded.6269/">...</a>
*/
public class AuthMeHook implements AuthPlugin<Player>, Listener {

View File

@ -25,7 +25,8 @@
*/
package com.github.games647.fastlogin.bukkit.hook;
import com.comphenix.protocol.reflect.FieldUtils;
import com.comphenix.protocol.reflect.accessors.Accessors;
import com.comphenix.protocol.reflect.accessors.FieldAccessor;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
@ -43,11 +44,11 @@ import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
/**
* GitHub: https://github.com/ST-DDT/CrazyLogin
* GitHub: <a href="https://github.com/ST-DDT/CrazyLogin">...</a>
* <p>
* Project page:
* <p>
* Bukkit: https://dev.bukkit.org/server-mods/crazylogin/
* Bukkit: <a href="https://dev.bukkit.org/server-mods/crazylogin/">...</a>
*/
public class CrazyLoginHook implements AuthPlugin<Player> {
@ -134,15 +135,8 @@ public class CrazyLoginHook implements AuthPlugin<Player> {
return false;
}
private PlayerListener getListener() {
PlayerListener listener;
try {
listener = (PlayerListener) FieldUtils.readField(crazyLoginPlugin, "playerListener", true);
} catch (IllegalAccessException ex) {
plugin.getLog().error("Failed to get the listener instance for auto login", ex);
listener = null;
}
return listener;
protected PlayerListener getListener() {
FieldAccessor accessor = Accessors.getFieldAccessor(crazyLoginPlugin.getClass(), PlayerListener.class, true);
return (PlayerListener) accessor.get(crazyLoginPlugin);
}
}

View File

@ -38,7 +38,7 @@ import java.time.Instant;
import org.bukkit.entity.Player;
/**
* GitHub: https://github.com/XziomekX/LogIt
* GitHub: <a href="https://github.com/XziomekX/LogIt">...</a>
* <p>
* Project page:
* <p>

View File

@ -36,13 +36,13 @@ import com.lenis0012.bukkit.loginsecurity.session.action.RegisterAction;
import org.bukkit.entity.Player;
/**
* GitHub: https://github.com/lenis0012/LoginSecurity-2
* GitHub: <a href="https://github.com/lenis0012/LoginSecurity-2">...</a>
* <p>
* Project page:
* <p>
* Bukkit: https://dev.bukkit.org/bukkit-plugins/loginsecurity/
* Bukkit: <a href="https://dev.bukkit.org/bukkit-plugins/loginsecurity/">...</a>
* <p>
* Spigot: https://www.spigotmc.org/resources/loginsecurity.19362/
* Spigot: <a href="https://www.spigotmc.org/resources/loginsecurity.19362/">...</a>
*/
public class LoginSecurityHook implements AuthPlugin<Player> {

View File

@ -39,9 +39,9 @@ import ultraauth.managers.PlayerManager;
/**
* Project page:
* <p>
* Bukkit: https://dev.bukkit.org/bukkit-plugins/ultraauth-aa/
* Bukkit: <a href="https://dev.bukkit.org/bukkit-plugins/ultraauth-aa/">...</a>
* <p>
* Spigot: https://www.spigotmc.org/resources/ultraauth.17044/
* Spigot: <a href="https://www.spigotmc.org/resources/ultraauth.17044/">...</a>
*/
public class UltraAuthHook implements AuthPlugin<Player> {

View File

@ -38,18 +38,18 @@ import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
/**
* GitHub: https://github.com/LycanDevelopment/xAuth/
* GitHub: <a href="https://github.com/LycanDevelopment/xAuth/">...</a>
* <p>
* Project page:
* <p>
* Bukkit: https://dev.bukkit.org/bukkit-plugins/xauth/
* Bukkit: <a href="https://dev.bukkit.org/bukkit-plugins/xauth/">...</a>
*/
public class xAuthHook implements AuthPlugin<Player> {
public class XAuthHook implements AuthPlugin<Player> {
private final xAuth xAuthPlugin = xAuth.getPlugin();
private final FastLoginBukkit plugin;
public xAuthHook(FastLoginBukkit plugin) {
public XAuthHook(FastLoginBukkit plugin) {
this.plugin = plugin;
}

View File

@ -97,8 +97,8 @@ public class ConnectionListener implements Listener {
String sessionId = plugin.getSessionId(player.spigot().getRawAddress());
plugin.getLog().info("No on-going login session for player: {} with ID {}. ", player, sessionId);
plugin.getLog().info("Setups using Minecraft proxies will start delayed " +
"when the command from the proxy is received");
plugin.getLog().info("Setups using Minecraft proxies will start delayed "
+ "when the command from the proxy is received");
} else {
Runnable forceLoginTask = new ForceLoginTask(plugin.getCore(), player, session);
Bukkit.getScheduler().runTaskAsynchronously(plugin, forceLoginTask);

View File

@ -29,6 +29,7 @@ import com.destroystokyo.paper.profile.ProfileProperty;
import com.github.games647.craftapi.model.skin.Textures;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;

View File

@ -25,34 +25,69 @@
*/
package com.github.games647.fastlogin.bukkit.listener.protocollib;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.google.common.io.Resources;
import com.google.common.primitives.Longs;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
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.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.time.Instant;
import java.util.Arrays;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.util.Random;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import lombok.val;
/**
* Encryption and decryption minecraft util for connection between servers
* and paid Minecraft account clients.
*
* @see net.minecraft.server.MinecraftEncryption
*/
public class EncryptionUtil {
final class EncryptionUtil {
public static final int VERIFY_TOKEN_LENGTH = 4;
public static final String KEY_PAIR_ALGORITHM = "RSA";
private static final int RSA_LENGTH = 1_024;
private static final PublicKey MOJANG_SESSION_KEY;
private static final int LINE_LENGTH = 76;
private static final Encoder KEY_ENCODER = Base64.getMimeEncoder(
LINE_LENGTH, "\n".getBytes(StandardCharsets.UTF_8)
);
static {
try {
MOJANG_SESSION_KEY = loadMojangSessionKey();
} catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException ex) {
throw new RuntimeException("Failed to load Mojang session key", ex);
}
}
private EncryptionUtil() {
// utility
throw new RuntimeException("No instantiation of utility classes allowed");
}
/**
@ -61,11 +96,10 @@ public class EncryptionUtil {
* @return The RSA key pair.
*/
public static KeyPair generateKeyPair() {
// KeyPair b()
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_PAIR_ALGORITHM);
keyPairGenerator.initialize(1_024);
keyPairGenerator.initialize(RSA_LENGTH);
return keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException nosuchalgorithmexception) {
// Should be existing in every vm
@ -78,10 +112,9 @@ public class EncryptionUtil {
* in a login session.
*
* @param random random generator
* @return an error with 4 bytes long
* @return a token with 4 bytes long
*/
public static byte[] generateVerifyToken(Random random) {
// extracted from LoginListener
byte[] token = new byte[VERIFY_TOKEN_LENGTH];
random.nextBytes(token);
return token;
@ -90,68 +123,91 @@ public class EncryptionUtil {
/**
* Generate the server id based on client and server data.
*
* @param sessionId session for the current login attempt
* @param serverId session for the current login attempt
* @param sharedSecret shared secret between the client and the server
* @param publicKey public key of the server
* @param publicKey public key of the server
* @return the server id formatted as a hexadecimal string.
*/
public static String getServerIdHashString(String sessionId, SecretKey sharedSecret, PublicKey publicKey) {
// found in LoginListener
try {
byte[] serverHash = getServerIdHash(sessionId, publicKey, sharedSecret);
return (new BigInteger(serverHash)).toString(16);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return "";
public static String getServerIdHashString(String serverId, SecretKey sharedSecret, PublicKey publicKey) {
byte[] serverHash = getServerIdHash(serverId, publicKey, sharedSecret);
return (new BigInteger(serverHash)).toString(16);
}
/**
* Decrypts the content and extracts the key spec.
*
* @param privateKey private server key
* @param sharedKey the encrypted shared key
* @param sharedKey the encrypted shared key
* @return shared secret key
* @throws GeneralSecurityException if it fails to decrypt the data
*/
public static SecretKey decryptSharedKey(PrivateKey privateKey, byte[] sharedKey) throws GeneralSecurityException {
// SecretKey a(PrivateKey var0, byte[] var1)
public static SecretKey decryptSharedKey(PrivateKey privateKey, byte[] sharedKey)
throws NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException,
BadPaddingException, InvalidKeyException {
return new SecretKeySpec(decrypt(privateKey, sharedKey), "AES");
}
public static byte[] decrypt(PrivateKey key, byte[] data) throws GeneralSecurityException {
// b(Key var0, byte[] var1)
Cipher cipher = Cipher.getInstance(key.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, key);
return decrypt(cipher, data);
public static boolean verifyClientKey(ClientPublicKey clientKey, Instant verifyTimestamp)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
if (clientKey.isExpired(verifyTimestamp)) {
return false;
}
Signature verifier = Signature.getInstance("SHA1withRSA");
// key of the signer
verifier.initVerify(MOJANG_SESSION_KEY);
verifier.update(toSignable(clientKey).getBytes(StandardCharsets.US_ASCII));
return verifier.verify(clientKey.signature());
}
/**
* Decrypted the given data using the cipher.
*
* @param cipher decryption cypher initialized with the private key
* @param data the encrypted data
* @return clear text data
* @throws GeneralSecurityException if it fails to decrypt the data
*/
private static byte[] decrypt(Cipher cipher, byte[] data) throws GeneralSecurityException {
// inlined: byte[] a(int var0, Key var1, byte[] var2), Cipher a(int var0, String var1, Key
// var2)
public static boolean verifyNonce(byte[] expected, PrivateKey decryptionKey, byte[] encryptedNonce)
throws NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException,
BadPaddingException, InvalidKeyException {
byte[] decryptedNonce = decrypt(decryptionKey, encryptedNonce);
return Arrays.equals(expected, decryptedNonce);
}
public static boolean verifySignedNonce(byte[] nonce, PublicKey clientKey, long signatureSalt, byte[] signature)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
Signature verifier = Signature.getInstance("SHA256withRSA");
// key of the signer
verifier.initVerify(clientKey);
verifier.update(nonce);
verifier.update(Longs.toByteArray(signatureSalt));
return verifier.verify(signature);
}
private static PublicKey loadMojangSessionKey()
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
val keyUrl = FastLoginBukkit.class.getClassLoader().getResource("yggdrasil_session_pubkey.der");
val keyData = Resources.toByteArray(keyUrl);
val keySpec = new X509EncodedKeySpec(keyData);
return KeyFactory.getInstance("RSA").generatePublic(keySpec);
}
private static String toSignable(ClientPublicKey clientPublicKey) {
long expiry = clientPublicKey.expiry().toEpochMilli();
String encoded = KEY_ENCODER.encodeToString(clientPublicKey.key().getEncoded());
return expiry + "-----BEGIN RSA PUBLIC KEY-----\n" + encoded + "\n-----END RSA PUBLIC KEY-----\n";
}
private static byte[] decrypt(PrivateKey key, byte[] data)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException,
IllegalBlockSizeException, BadPaddingException {
Cipher cipher = Cipher.getInstance(key.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, key);
return cipher.doFinal(data);
}
private static byte[] getServerIdHash(
String sessionId, PublicKey publicKey, SecretKey sharedSecret)
throws NoSuchAlgorithmException {
// byte[] a(String var0, PublicKey var1, SecretKey var2)
MessageDigest digest = MessageDigest.getInstance("SHA-1");
private static byte[] getServerIdHash(String sessionId, PublicKey publicKey, SecretKey sharedSecret) {
@SuppressWarnings("deprecation")
Hasher hasher = Hashing.sha1().newHasher();
// inlined from byte[] a(String var0, byte[]... var1)
digest.update(sessionId.getBytes(StandardCharsets.ISO_8859_1));
digest.update(sharedSecret.getEncoded());
digest.update(publicKey.getEncoded());
hasher.putBytes(sessionId.getBytes(StandardCharsets.ISO_8859_1));
hasher.putBytes(sharedSecret.getEncoded());
hasher.putBytes(publicKey.getEncoded());
return digest.digest();
return hasher.hash().asBytes();
}
}

View File

@ -32,19 +32,19 @@ import com.comphenix.protocol.wrappers.WrappedGameProfile;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.hooks.bedrock.FloodgateService;
import java.util.UUID;
import org.geysermc.floodgate.api.FloodgateApi;
import static com.comphenix.protocol.PacketType.Login.Client.START;
import com.comphenix.protocol.ProtocolLibrary;
/**
* Manually inject Floodgate player name prefixes.
* <br>
* This is used as a workaround, because Floodgate fails to inject
* the prefixes when it's used together with ProtocolLib and FastLogin.
* <br>
* For more information visit: https://github.com/games647/FastLogin/issues/493
* For more information visit: <a href="https://github.com/games647/FastLogin/issues/493">...</a>
*/
public class ManualNameChange extends PacketAdapter {
@ -61,25 +61,41 @@ public class ManualNameChange extends PacketAdapter {
public static void register(FastLoginBukkit plugin, FloodgateService floodgate) {
// they will be created with a static builder, because otherwise it will throw a NoClassDefFoundError
ProtocolLibrary.getProtocolManager()
.getAsynchronousManager()
.registerAsyncHandler(new ManualNameChange(plugin, floodgate))
.start();
// ProtocolLibrary.getProtocolManager()
// .getAsynchronousManager()
// .registerAsyncHandler(new ManualNameChange(plugin, floodgate))
// .start();
}
@Override
public void onPacketReceiving(PacketEvent packetEvent) {
PacketContainer packet = packetEvent.getPacket();
WrappedGameProfile originalProfile = packet.getGameProfiles().read(0);
String username = readUsername(packet);
if (floodgate.getBedrockPlayer(originalProfile.getName()) == null) {
if (floodgate.getBedrockPlayer(username) == null) {
//not a Floodgate player, no need to add a prefix
return;
}
packet.setMeta("original_name", originalProfile.getName());
String prefixedName = FloodgateApi.getInstance().getPlayerPrefix() + originalProfile.getName();
WrappedGameProfile updatedProfile = originalProfile.withName(prefixedName);
packet.getGameProfiles().write(0, updatedProfile);
packet.setMeta("original_name", username);
String prefixedName = FloodgateApi.getInstance().getPlayerPrefix() + username;
setUsername(packet, prefixedName);
}
private void setUsername(PacketContainer packet, String name) {
if (packet.getGameProfiles().size() > 0) {
WrappedGameProfile updatedProfile = new WrappedGameProfile(UUID.randomUUID(), name);
packet.getGameProfiles().write(0, updatedProfile);
} else {
packet.getStrings().write(0, name);
}
}
private String readUsername(PacketContainer packet) {
if (packet.getGameProfiles().size() > 0) {
return packet.getGameProfiles().read(0).getName();
} else {
return packet.getStrings().read(0);
}
}
}

View File

@ -25,11 +25,11 @@
*/
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.bukkit.event.BukkitFastLoginPreLoginEvent;
import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.JoinManagement;
import com.github.games647.fastlogin.core.shared.event.FastLoginPreLoginEvent;
@ -45,7 +45,9 @@ public class NameCheckTask extends JoinManagement<Player, CommandSender, Protoco
private final FastLoginBukkit plugin;
private final PacketEvent packetEvent;
private final PublicKey publicKey;
private final ClientPublicKey clientKey;
private final PublicKey serverKey;
private final Random random;
@ -53,12 +55,13 @@ public class NameCheckTask extends JoinManagement<Player, CommandSender, Protoco
private final String username;
public NameCheckTask(FastLoginBukkit plugin, Random random, Player player, PacketEvent packetEvent,
String username, PublicKey publicKey) {
String username, ClientPublicKey clientKey, PublicKey serverKey) {
super(plugin.getCore(), plugin.getCore().getAuthPluginHook(), plugin.getBedrockService());
this.plugin = plugin;
this.packetEvent = packetEvent;
this.publicKey = publicKey;
this.clientKey = clientKey;
this.serverKey = serverKey;
this.random = random;
this.player = player;
this.username = username;
@ -66,11 +69,11 @@ public class NameCheckTask extends JoinManagement<Player, CommandSender, Protoco
@Override
public void run() {
try {
super.onLogin(username, new ProtocolLibLoginSource(player, random, publicKey));
} finally {
ProtocolLibrary.getProtocolManager().getAsynchronousManager().signalPacketTransmission(packetEvent);
}
// try {
super.onLogin(username, new ProtocolLibLoginSource(player, random, serverKey, clientKey));
// } finally {
// ProtocolLibrary.getProtocolManager().getAsynchronousManager().signalPacketTransmission(packetEvent);
// }
}
@Override
@ -84,8 +87,8 @@ public class NameCheckTask extends JoinManagement<Player, CommandSender, Protoco
//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) {
public void requestPremiumLogin(ProtocolLibLoginSource source, StoredProfile profile,
String username, boolean registered) {
try {
source.enableOnlinemode();
} catch (Exception ex) {
@ -97,13 +100,14 @@ public class NameCheckTask extends JoinManagement<Player, CommandSender, Protoco
core.getPendingLogin().put(ip + username, new Object());
byte[] verify = source.getVerifyToken();
ClientPublicKey clientKey = source.getClientKey();
BukkitLoginSession playerSession = new BukkitLoginSession(username, verify, registered, profile);
BukkitLoginSession playerSession = new BukkitLoginSession(username, verify, clientKey, registered, profile);
plugin.putSession(player.getAddress(), playerSession);
//cancel only if the player has a paid account otherwise login as normal offline player
synchronized (packetEvent.getAsyncMarker().getProcessingLock()) {
packetEvent.setCancelled(true);
}
// synchronized (packetEvent.getAsyncMarker().getProcessingLock()) {
packetEvent.setCancelled(true);
// }
}
@Override

View File

@ -30,13 +30,36 @@ 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.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.utility.MinecraftVersion;
import com.comphenix.protocol.wrappers.BukkitConverters;
import com.comphenix.protocol.wrappers.WrappedGameProfile;
import com.comphenix.protocol.wrappers.WrappedProfilePublicKey.WrappedProfileKeyData;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.RateLimiter;
import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;
import com.github.games647.fastlogin.core.antibot.AntiBotService;
import com.github.games647.fastlogin.core.antibot.AntiBotService.Action;
import com.mojang.datafixers.util.Either;
import java.net.InetSocketAddress;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.SignatureException;
import java.time.Instant;
import java.util.Objects;
import java.util.Optional;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import lombok.var;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Nullable;
import static com.comphenix.protocol.PacketType.Login.Client.ENCRYPTION_BEGIN;
import static com.comphenix.protocol.PacketType.Login.Client.START;
@ -50,30 +73,52 @@ public class ProtocolLibListener extends PacketAdapter {
//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();
private final RateLimiter rateLimiter;
private final AntiBotService antiBotService;
public ProtocolLibListener(FastLoginBukkit plugin, RateLimiter rateLimiter) {
private final boolean verifyClientKeys;
@Nullable
private PacketContainer lastStartPacket;
public ProtocolLibListener(FastLoginBukkit plugin, AntiBotService antiBotService, boolean verifyClientKeys) {
//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());
.types(START, ENCRYPTION_BEGIN));
this.plugin = plugin;
this.rateLimiter = rateLimiter;
this.antiBotService = antiBotService;
this.verifyClientKeys = verifyClientKeys;
}
public static void register(FastLoginBukkit plugin, RateLimiter rateLimiter) {
public static void register(FastLoginBukkit plugin, AntiBotService antiBotService, boolean verifyClientKeys) {
// they will be created with a static builder, because otherwise it will throw a NoClassDefFoundError
// TODO: make synchronous processing, but do web or database requests async
ProtocolLibrary.getProtocolManager()
.getAsynchronousManager()
.registerAsyncHandler(new ProtocolLibListener(plugin, rateLimiter))
.start();
.addPacketListener(new ProtocolLibListener(plugin, antiBotService, verifyClientKeys));
}
@Override
public void onPacketReceiving(PacketEvent packetEvent) {
PacketContainer packet = packetEvent.getPacket();
plugin.getLog().info("New packet {} from {}; Cancellation: {}, Meta: {}",
packetEvent.getPacketType(), packetEvent.getPlayer(), packetEvent.isCancelled(),
packet.getMeta(SOURCE_META_KEY)
);
if (packetEvent.getPacketType() == START) {
if (lastStartPacket != null) {
plugin.getLog().info("Start-packet equality (Last/New): {}/{}, {}",
lastStartPacket.getHandle().hashCode(), packet.getHandle().hashCode(),
Objects.equals(lastStartPacket.getHandle(), packet.getHandle())
);
plugin.getLog().info("Content: {}, {}", lastStartPacket.getHandle(), packet.getHandle());
}
lastStartPacket = packet;
}
if (packetEvent.isCancelled()
|| plugin.getCore().getAuthPluginHook() == null
|| !plugin.isServerFullyStarted()) {
@ -88,18 +133,32 @@ public class ProtocolLibListener extends PacketAdapter {
Player sender = packetEvent.getPlayer();
PacketType packetType = packetEvent.getPacketType();
if (packetType == START) {
if (!rateLimiter.tryAcquire()) {
plugin.getLog().warn("Simple Anti-Bot join limit - Ignoring {}", sender);
return;
}
// PacketContainer packet = packet;
onLogin(packetEvent, sender);
InetSocketAddress address = sender.getAddress();
String username = getUsername(packet);
Action action = antiBotService.onIncomingConnection(address, username);
switch (action) {
case Ignore:
// just ignore
return;
case Block:
String message = plugin.getCore().getMessage("kick-antibot");
sender.kickPlayer(message);
break;
case Continue:
default:
//player.getName() won't work at this state
onLoginStart(packetEvent, sender, username);
break;
}
} else {
onEncryptionBegin(packetEvent, sender);
}
}
private Boolean isFastLoginPacket(PacketEvent packetEvent) {
private boolean isFastLoginPacket(PacketEvent packetEvent) {
return packetEvent.getPacket().getMeta(SOURCE_META_KEY)
.map(val -> val.equals(plugin.getName()))
.orElse(false);
@ -108,32 +167,122 @@ public class ProtocolLibListener extends PacketAdapter {
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);
plugin.getScheduler().runAsync(verifyTask);
BukkitLoginSession session = plugin.getSession(sender.getAddress());
if (session == null) {
plugin.getLog().warn("Profile {} tried to send encryption response at invalid state", sender.getAddress());
sender.kickPlayer(plugin.getCore().getMessage("invalid-request"));
} else {
byte[] expectedVerifyToken = session.getVerifyToken();
if (verifyNonce(sender, packetEvent.getPacket(), session.getClientPublicKey(), expectedVerifyToken)) {
// packetEvent.getAsyncMarker().incrementProcessingDelay();
Runnable verifyTask = new VerifyResponseTask(
plugin, packetEvent, sender, session, sharedSecret, keyPair
);
verifyTask.run();
// plugin.getScheduler().runAsync(verifyTask);
} else {
sender.kickPlayer(plugin.getCore().getMessage("invalid-verify-token"));
}
}
}
private void onLogin(PacketEvent packetEvent, Player player) {
private boolean verifyNonce(Player sender, PacketContainer packet,
ClientPublicKey clientPublicKey, byte[] expectedToken) {
try {
if (MinecraftVersion.atOrAbove(new MinecraftVersion(1, 19, 0))) {
Either<byte[], ?> either = packet.getSpecificModifier(Either.class).read(0);
if (clientPublicKey == null) {
Optional<byte[]> left = either.left();
if (!left.isPresent()) {
plugin.getLog().error("No verify token sent if requested without player signed key {}", sender);
return false;
}
return EncryptionUtil.verifyNonce(expectedToken, keyPair.getPrivate(), left.get());
} else {
Optional<?> optSignatureData = either.right();
if (!optSignatureData.isPresent()) {
plugin.getLog().error("No signature given to sent player signing key {}", sender);
return false;
}
Object signatureData = optSignatureData.get();
long salt = FuzzyReflection.getFieldValue(signatureData, Long.TYPE, true);
byte[] signature = FuzzyReflection.getFieldValue(signatureData, byte[].class, true);
PublicKey publicKey = clientPublicKey.key();
return EncryptionUtil.verifySignedNonce(expectedToken, publicKey, salt, signature);
}
} else {
byte[] nonce = packet.getByteArrays().read(1);
return EncryptionUtil.verifyNonce(expectedToken, keyPair.getPrivate(), nonce);
}
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException | NoSuchPaddingException
| IllegalBlockSizeException | BadPaddingException signatureEx) {
plugin.getLog().error("Invalid signature from player {}", sender, signatureEx);
return false;
}
}
private void onLoginStart(PacketEvent packetEvent, Player player, String username) {
//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.removeSession(player.getAddress());
//player.getName() won't work at this state
PacketContainer packet = packetEvent.getPacket();
String username = packet.getGameProfiles().read(0).getName();
if (packetEvent.getPacket().getMeta("original_name").isPresent()) {
//username has been injected by ManualNameChange.java
username = (String) packetEvent.getPacket().getMeta("original_name").get();
}
PacketContainer packet = packetEvent.getPacket();
var profileKey = packet.getOptionals(BukkitConverters.getWrappedPublicKeyDataConverter())
.optionRead(0);
var clientKey = profileKey.flatMap(opt -> opt).flatMap(this::verifyPublicKey);
if (verifyClientKeys && !clientKey.isPresent()) {
// missing or incorrect
// expired always not allowed
player.kickPlayer(plugin.getCore().getMessage("invalid-public-key"));
plugin.getLog().warn("Invalid public key from player {}", username);
return;
}
plugin.getLog().trace("GameProfile {} with {} connecting", sessionKey, username);
packetEvent.getAsyncMarker().incrementProcessingDelay();
Runnable nameCheckTask = new NameCheckTask(plugin, random, player, packetEvent, username, keyPair.getPublic());
plugin.getScheduler().runAsync(nameCheckTask);
// packetEvent.getAsyncMarker().incrementProcessingDelay();
Runnable nameCheckTask = new NameCheckTask(
plugin, random, player, packetEvent, username, clientKey.orElse(null), keyPair.getPublic()
);
// plugin.getScheduler().runAsync(nameCheckTask);
nameCheckTask.run();
}
private Optional<ClientPublicKey> verifyPublicKey(WrappedProfileKeyData profileKey) {
Instant expires = profileKey.getExpireTime();
PublicKey key = profileKey.getKey();
byte[] signature = profileKey.getSignature();
ClientPublicKey clientKey = ClientPublicKey.of(expires, key, signature);
try {
if (EncryptionUtil.verifyClientKey(clientKey, Instant.now())) {
return Optional.of(clientKey);
}
} catch (SignatureException | InvalidKeyException | NoSuchAlgorithmException ex) {
return Optional.empty();
}
return Optional.empty();
}
private String getUsername(PacketContainer packet) {
WrappedGameProfile profile = packet.getGameProfiles().readSafely(0);
if (profile == null) {
return packet.getStrings().read(0);
}
//player.getName() won't work at this state
return profile.getName();
}
}

View File

@ -30,9 +30,9 @@ import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.reflect.StructureModifier;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;
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;
@ -48,19 +48,22 @@ class ProtocolLibLoginSource implements LoginSource {
private final Player player;
private final Random random;
private final ClientPublicKey clientKey;
private final PublicKey publicKey;
private final String serverId = "";
private byte[] verifyToken;
public ProtocolLibLoginSource(Player player, Random random, PublicKey publicKey) {
ProtocolLibLoginSource(Player player, Random random, PublicKey serverPublicKey, ClientPublicKey clientKey) {
this.player = player;
this.random = random;
this.publicKey = publicKey;
this.publicKey = serverPublicKey;
this.clientKey = clientKey;
}
@Override
public void enableOnlinemode() throws InvocationTargetException {
public void enableOnlinemode() {
verifyToken = EncryptionUtil.generateVerifyToken(random);
/*
@ -88,7 +91,7 @@ class ProtocolLibLoginSource implements LoginSource {
}
@Override
public void kick(String message) throws InvocationTargetException {
public void kick(String message) {
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
PacketContainer kickPacket = new PacketContainer(DISCONNECT);
@ -109,6 +112,10 @@ class ProtocolLibLoginSource implements LoginSource {
return player.getAddress();
}
public ClientPublicKey getClientKey() {
return clientKey;
}
public String getServerId() {
return serverId;
}
@ -119,11 +126,11 @@ class ProtocolLibLoginSource implements LoginSource {
@Override
public String toString() {
return this.getClass().getSimpleName() + '{' +
"player=" + player +
", random=" + random +
", serverId='" + serverId + '\'' +
", verifyToken=" + Arrays.toString(verifyToken) +
'}';
return this.getClass().getSimpleName() + '{'
+ "player=" + player
+ ", random=" + random
+ ", serverId='" + serverId + '\''
+ ", verifyToken=" + Arrays.toString(verifyToken)
+ '}';
}
}

View File

@ -25,15 +25,12 @@
*/
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.Textures;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
@ -41,13 +38,8 @@ import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerLoginEvent.Result;
import java.lang.reflect.InvocationTargetException;
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) {
@ -77,16 +69,6 @@ public class SkinApplyListener implements Listener {
WrappedGameProfile gameProfile = WrappedGameProfile.fromPlayer(player);
WrappedSignedProperty skin = WrappedSignedProperty.fromValues(Textures.KEY, skinData, signature);
try {
gameProfile.getProperties().put(Textures.KEY, skin);
} catch (ClassCastException castException) {
//Cauldron, MCPC, Thermos, ...
Object map = GET_PROPERTIES.invoke(gameProfile.getHandle());
try {
MethodUtils.invokeMethod(map, "put", new Object[]{Textures.KEY, skin.getHandle()});
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) {
plugin.getLog().error("Error setting premium skin of: {}", player, ex);
}
}
gameProfile.getProperties().put(Textures.KEY, skin);
}
}

View File

@ -28,26 +28,28 @@ 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.injector.temporary.TemporaryPlayerFactory;
import com.comphenix.protocol.reflect.EquivalentConverter;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.reflect.accessors.Accessors;
import com.comphenix.protocol.reflect.accessors.FieldAccessor;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.utility.MinecraftVersion;
import com.comphenix.protocol.wrappers.BukkitConverters;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import com.comphenix.protocol.wrappers.WrappedGameProfile;
import com.comphenix.protocol.wrappers.WrappedProfilePublicKey.WrappedProfileKeyData;
import com.github.games647.craftapi.model.auth.Verification;
import com.github.games647.craftapi.model.skin.SkinProperty;
import com.github.games647.craftapi.resolver.AbstractResolver;
import com.github.games647.craftapi.resolver.MojangResolver;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyPair;
@ -59,6 +61,7 @@ import java.util.UUID;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import lombok.val;
import org.bukkit.entity.Player;
import static com.comphenix.protocol.PacketType.Login.Client.START;
@ -70,7 +73,9 @@ public class VerifyResponseTask implements Runnable {
private static final Class<?> ENCRYPTION_CLASS;
static {
ENCRYPTION_CLASS = MinecraftReflection.getMinecraftClass("util." + ENCRYPTION_CLASS_NAME, ENCRYPTION_CLASS_NAME);
ENCRYPTION_CLASS = MinecraftReflection.getMinecraftClass(
"util." + ENCRYPTION_CLASS_NAME, ENCRYPTION_CLASS_NAME
);
}
private final FastLoginBukkit plugin;
@ -79,16 +84,20 @@ public class VerifyResponseTask implements Runnable {
private final Player player;
private final BukkitLoginSession session;
private final byte[] sharedSecret;
private static Method encryptMethod;
private static Method cipherMethod;
public VerifyResponseTask(FastLoginBukkit plugin, PacketEvent packetEvent, Player player,
public VerifyResponseTask(FastLoginBukkit plugin, PacketEvent packetEvent,
Player player, BukkitLoginSession session,
byte[] sharedSecret, KeyPair keyPair) {
this.plugin = plugin;
this.packetEvent = packetEvent;
this.player = player;
this.session = session;
this.sharedSecret = Arrays.copyOf(sharedSecret, sharedSecret.length);
this.serverKey = keyPair;
}
@ -96,21 +105,14 @@ public class VerifyResponseTask implements Runnable {
@Override
public void run() {
try {
BukkitLoginSession session = plugin.getSession(player.getAddress());
if (session == null) {
disconnect("invalid-request",
"GameProfile {0} tried to send encryption response at invalid state",
player.getAddress());
} else {
verifyResponse(session);
}
verifyResponse(session);
} finally {
//this is a fake packet; it shouldn't be sent to the server
synchronized (packetEvent.getAsyncMarker().getProcessingLock()) {
packetEvent.setCancelled(true);
}
// synchronized (packetEvent.getAsyncMarker().getProcessingLock()) {
packetEvent.setCancelled(true);
// }
ProtocolLibrary.getProtocolManager().getAsynchronousManager().signalPacketTransmission(packetEvent);
// ProtocolLibrary.getProtocolManager().getAsynchronousManager().signalPacketTransmission(packetEvent);
}
}
@ -126,7 +128,7 @@ public class VerifyResponseTask implements Runnable {
}
try {
if (!checkVerifyToken(session) || !enableEncryption(loginKey)) {
if (!enableEncryption(loginKey)) {
return;
}
} catch (Exception ex) {
@ -143,71 +145,67 @@ public class VerifyResponseTask implements Runnable {
InetAddress address = socketAddress.getAddress();
Optional<Verification> response = resolver.hasJoined(requestedUsername, serverId, address);
if (response.isPresent()) {
Verification verification = response.get();
plugin.getLog().info("Profile {} has a verified premium account", requestedUsername);
String realUsername = verification.getName();
if (realUsername == null) {
disconnect("invalid-session", "Username field null for {}", requestedUsername);
return;
}
SkinProperty[] properties = verification.getProperties();
if (properties.length > 0) {
session.setSkinProperty(properties[0]);
}
session.setVerifiedUsername(realUsername);
session.setUuid(verification.getId());
session.setVerified(true);
setPremiumUUID(session.getUuid());
receiveFakeStartPacket(realUsername);
encryptConnection(session, requestedUsername, response.get());
} else {
//user tried to fake an authentication
disconnect("invalid-session", "GameProfile {} ({}) tried to log in with an invalid session. ServerId: {}", session.getRequestUsername(), socketAddress, serverId);
disconnect(
"invalid-session",
"GameProfile {} ({}) tried to log in with an invalid session. ServerId: {}",
session.getRequestUsername(), socketAddress, serverId
);
}
} catch (IOException ioEx) {
disconnect("error-kick", "Failed to connect to session server", ioEx);
}
}
private void encryptConnection(BukkitLoginSession session, String requestedUsername, Verification verification) {
plugin.getLog().info("Profile {} has a verified premium account", requestedUsername);
String realUsername = verification.getName();
if (realUsername == null) {
disconnect("invalid-session", "Username field null for {}", requestedUsername);
return;
}
SkinProperty[] properties = verification.getProperties();
if (properties.length > 0) {
session.setSkinProperty(properties[0]);
}
session.setVerifiedUsername(realUsername);
session.setUuid(verification.getId());
session.setVerified(true);
setPremiumUUID(session.getUuid());
receiveFakeStartPacket(realUsername, session.getClientPublicKey());
}
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);
Class<?> managerClass = networkManager.getClass();
FieldAccessor accessor = Accessors.getFieldAccessorOrNull(managerClass, "spoofedUUID", UUID.class);
accessor.set(networkManager, premiumUUID);
} catch (Exception exc) {
plugin.getLog().error("Error setting premium uuid of {}", player, exc);
}
}
}
private boolean checkVerifyToken(BukkitLoginSession session) 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(serverKey.getPrivate(), responseVerify))) {
//check if the verify-token are equal to the server sent one
disconnect("invalid-verify-token",
"GameProfile {0} ({1}) tried to login with an invalid verify token. Server: {2} Client: {3}",
session.getRequestUsername(), packetEvent.getPlayer().getAddress(), requestVerify, responseVerify);
return false;
}
return true;
}
//try to get the networkManager from ProtocolLib
private Object getNetworkManager() throws IllegalAccessException, ClassNotFoundException {
private Object getNetworkManager() throws 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);
Class<?> rawInjectorClass = rawInjector.getClass();
FieldAccessor accessor = Accessors.getFieldAccessorOrNull(rawInjectorClass, "networkManager", Object.class);
return accessor.get(rawInjector);
}
private boolean enableEncryption(SecretKey loginKey) throws IllegalArgumentException {
@ -219,15 +217,15 @@ public class VerifyResponseTask implements Runnable {
try {
// Try to get the old (pre MC 1.16.4) encryption method
encryptMethod = FuzzyReflection.fromClass(networkManagerClass)
.getMethodByParameters("a", SecretKey.class);
.getMethodByParameters("a", SecretKey.class);
} catch (IllegalArgumentException exception) {
// Get the new encryption method
encryptMethod = FuzzyReflection.fromClass(networkManagerClass)
.getMethodByParameters("a", Cipher.class, Cipher.class);
.getMethodByParameters("a", Cipher.class, Cipher.class);
// Get the needed Cipher helper method (used to generate ciphers from login key)
cipherMethod = FuzzyReflection.fromClass(ENCRYPTION_CLASS)
.getMethodByParameters("a", int.class, Key.class);
.getMethodByParameters("a", int.class, Key.class);
}
}
@ -262,33 +260,36 @@ public class VerifyResponseTask implements Runnable {
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);
}
//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");
}
//fake a new login packet in order to let the server handle all the other stuff
private void receiveFakeStartPacket(String username) {
private void receiveFakeStartPacket(String username, ClientPublicKey clientKey) {
//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
startPacket.setMeta(ProtocolLibListener.SOURCE_META_KEY, plugin.getName());
ProtocolLibrary.getProtocolManager().recieveClientPacket(player, startPacket, true);
} 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"));
if (MinecraftVersion.atOrAbove(new MinecraftVersion(1, 19, 0))) {
startPacket.getStrings().write(0, username);
EquivalentConverter<WrappedProfileKeyData> converter = BukkitConverters.getWrappedPublicKeyDataConverter();
val wrappedKey = Optional.ofNullable(clientKey).map(key ->
new WrappedProfileKeyData(clientKey.expiry(), clientKey.key(), clientKey.signature())
);
startPacket.getOptionals(converter).write(0, wrappedKey);
} else {
//uuid is ignored by the packet definition
WrappedGameProfile fakeProfile = new WrappedGameProfile(UUID.randomUUID(), username);
startPacket.getGameProfiles().write(0, fakeProfile);
}
//we don't want to handle our own packets so ignore filters
startPacket.setMeta(ProtocolLibListener.SOURCE_META_KEY, plugin.getName());
ProtocolLibrary.getProtocolManager().receiveClientPacket(player, startPacket, true);
plugin.getLog().info("Sending new fake login start packet to {}-{}", player, username);
}
}

View File

@ -23,3 +23,22 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.games647.fastlogin.bukkit.listener.protocollib.packet;
import java.security.PublicKey;
import java.time.Instant;
import lombok.Value;
import lombok.experimental.Accessors;
@Accessors(fluent = true)
@Value(staticConstructor = "of")
public class ClientPublicKey {
Instant expiry;
PublicKey key;
byte[] signature;
public boolean isExpired(Instant verifyTimestamp) {
return !verifyTimestamp.isBefore(expiry);
}
}

View File

@ -60,8 +60,8 @@ public class ProtocolLoginSource implements LoginSource {
@Override
public String toString() {
return this.getClass().getSimpleName() + '{' +
"loginStartEvent=" + loginStartEvent +
'}';
return this.getClass().getSimpleName() + '{'
+ "loginStartEvent=" + loginStartEvent
+ '}';
}
}

View File

@ -29,8 +29,9 @@ import com.github.games647.craftapi.UUIDAdapter;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.event.BukkitFastLoginPreLoginEvent;
import com.github.games647.fastlogin.core.RateLimiter;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.antibot.AntiBotService;
import com.github.games647.fastlogin.core.antibot.AntiBotService.Action;
import com.github.games647.fastlogin.core.shared.JoinManagement;
import com.github.games647.fastlogin.core.shared.event.FastLoginPreLoginEvent;
@ -49,13 +50,13 @@ public class ProtocolSupportListener extends JoinManagement<Player, CommandSende
implements Listener {
private final FastLoginBukkit plugin;
private final RateLimiter rateLimiter;
private final AntiBotService antiBotService;
public ProtocolSupportListener(FastLoginBukkit plugin, RateLimiter rateLimiter) {
public ProtocolSupportListener(FastLoginBukkit plugin, AntiBotService antiBotService) {
super(plugin.getCore(), plugin.getCore().getAuthPluginHook(), plugin.getBedrockService());
this.plugin = plugin;
this.rateLimiter = rateLimiter;
this.antiBotService = antiBotService;
}
@EventHandler
@ -64,19 +65,28 @@ public class ProtocolSupportListener extends JoinManagement<Player, CommandSende
return;
}
if (!rateLimiter.tryAcquire()) {
plugin.getLog().warn("Simple Anti-Bot join limit - Ignoring {}", loginStartEvent.getConnection());
return;
}
String username = loginStartEvent.getConnection().getProfile().getName();
InetSocketAddress address = loginStartEvent.getConnection().getRawAddress();
plugin.getLog().info("Incoming login request for {} from {}", username, address);
//remove old data every time on a new login in order to keep the session only for one person
plugin.removeSession(address);
Action action = antiBotService.onIncomingConnection(address, username);
switch (action) {
case Ignore:
// just ignore
return;
case Block:
String message = plugin.getCore().getMessage("kick-antibot");
loginStartEvent.denyLogin(message);
break;
case Continue:
default:
//remove old data every time on a new login in order to keep the session only for one person
plugin.removeSession(address);
ProtocolLoginSource source = new ProtocolLoginSource(loginStartEvent);
super.onLogin(username, source);
ProtocolLoginSource source = new ProtocolLoginSource(loginStartEvent);
super.onLogin(username, source);
break;
}
}
@EventHandler
@ -103,15 +113,16 @@ public class ProtocolSupportListener extends JoinManagement<Player, CommandSende
}
@Override
public FastLoginPreLoginEvent callFastLoginPreLoginEvent(String username, ProtocolLoginSource source, StoredProfile profile) {
public FastLoginPreLoginEvent callFastLoginPreLoginEvent(String username, ProtocolLoginSource source,
StoredProfile profile) {
BukkitFastLoginPreLoginEvent event = new BukkitFastLoginPreLoginEvent(username, source, profile);
plugin.getServer().getPluginManager().callEvent(event);
return event;
}
@Override
public void requestPremiumLogin(ProtocolLoginSource source, StoredProfile profile, String username
, boolean registered) {
public void requestPremiumLogin(ProtocolLoginSource source, StoredProfile profile, String username,
boolean registered) {
source.enableOnlinemode();
String ip = source.getAddress().getAddress().getHostAddress();

View File

@ -31,7 +31,7 @@ import com.github.games647.fastlogin.bukkit.hook.CrazyLoginHook;
import com.github.games647.fastlogin.bukkit.hook.LogItHook;
import com.github.games647.fastlogin.bukkit.hook.LoginSecurityHook;
import com.github.games647.fastlogin.bukkit.hook.UltraAuthHook;
import com.github.games647.fastlogin.bukkit.hook.xAuthHook;
import com.github.games647.fastlogin.bukkit.hook.XAuthHook;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import java.lang.reflect.Constructor;
@ -94,8 +94,8 @@ public class DelayedAuthHook implements Runnable {
private AuthPlugin<Player> getAuthHook() {
try {
List<Class<? extends AuthPlugin<Player>>> hooks = Arrays.asList(AuthMeHook.class,
CrazyLoginHook.class, LogItHook.class, LoginSecurityHook.class, UltraAuthHook.class,
xAuthHook.class);
CrazyLoginHook.class, LogItHook.class, LoginSecurityHook.class, UltraAuthHook.class,
XAuthHook.class);
for (Class<? extends AuthPlugin<Player>> clazz : hooks) {
String pluginName = clazz.getSimpleName();
@ -113,7 +113,7 @@ public class DelayedAuthHook implements Runnable {
return null;
}
private AuthPlugin<Player> newInstance(Class<? extends AuthPlugin<Player>> clazz)
protected AuthPlugin<Player> newInstance(Class<? extends AuthPlugin<Player>> clazz)
throws ReflectiveOperationException {
try {
Constructor<? extends AuthPlugin<Player>> cons = clazz.getDeclaredConstructor(FastLoginBukkit.class);

View File

@ -25,6 +25,11 @@
*/
package com.github.games647.fastlogin.bukkit.task;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.github.games647.fastlogin.core.shared.FloodgateManagement;
import java.net.InetSocketAddress;
import java.util.UUID;
@ -33,14 +38,10 @@ import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.geysermc.floodgate.api.player.FloodgatePlayer;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.github.games647.fastlogin.core.shared.FloodgateManagement;
public class FloodgateAuthTask extends FloodgateManagement<Player, CommandSender, BukkitLoginSession, FastLoginBukkit> {
public FloodgateAuthTask(FastLoginCore<Player, CommandSender, FastLoginBukkit> core, Player player, FloodgatePlayer floodgatePlayer) {
public FloodgateAuthTask(FastLoginCore<Player, CommandSender, FastLoginBukkit> core, Player player,
FloodgatePlayer floodgatePlayer) {
super(core, player, floodgatePlayer);
}

Binary file not shown.

View File

@ -0,0 +1,50 @@
/*
* SPDX-License-Identifier: MIT
*
* The MIT License (MIT)
*
* Copyright (c) 2015-2022 games647 and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.games647.fastlogin.bukkit;
import com.github.games647.fastlogin.core.CommonUtil;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.chat.ComponentSerializer;
import lombok.val;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class FastLoginBukkitTest {
@Test
void testRGB() {
val message = "&x00002a00002b&lText";
val msg = CommonUtil.translateColorCodes(message);
assertEquals(msg, "§x00002a00002b§lText");
val components = TextComponent.fromLegacyText(msg);
val expected = "{\"bold\":true,\"color\":\"#00a00b\",\"text\":\"Text\"}";
assertEquals(ComponentSerializer.toString(components), expected);
}
}

View File

@ -0,0 +1,45 @@
/*
* SPDX-License-Identifier: MIT
*
* The MIT License (MIT)
*
* Copyright (c) 2015-2022 games647 and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.games647.fastlogin.bukkit;
import com.comphenix.protocol.reflect.accessors.Accessors;
import com.comphenix.protocol.reflect.accessors.FieldAccessor;
import fr.xephi.authme.api.v3.AuthMeApi;
import fr.xephi.authme.process.Management;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
class ReflectionTest {
@Test
void testAuthMeManagementField() {
FieldAccessor accessor = Accessors.getFieldAccessor(AuthMeApi.class, Management.class, true);
assertNotNull(accessor.getField());
}
}

View File

@ -0,0 +1,46 @@
/*
* SPDX-License-Identifier: MIT
*
* The MIT License (MIT)
*
* Copyright (c) 2015-2022 games647 and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.games647.fastlogin.bukkit.hook;
import com.comphenix.protocol.reflect.accessors.Accessors;
import com.comphenix.protocol.reflect.accessors.FieldAccessor;
import de.st_ddt.crazylogin.CrazyLogin;
import de.st_ddt.crazylogin.listener.PlayerListener;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
class CrazyLoginHookTest {
@Test
void testPlayerListener() {
FieldAccessor accessor = Accessors.getFieldAccessor(CrazyLogin.class, PlayerListener.class, true);
assertNotNull(accessor.getField());
}
}

View File

@ -0,0 +1,50 @@
/*
* SPDX-License-Identifier: MIT
*
* The MIT License (MIT)
*
* Copyright (c) 2015-2022 games647 and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.games647.fastlogin.bukkit.listener.protocollib;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.util.Base64;
import lombok.val;
public class Base64Adapter extends TypeAdapter<byte[]> {
@Override
public void write(JsonWriter out, byte[] value) throws IOException {
val encoded = Base64.getEncoder().encodeToString(value);
out.value(encoded);
}
@Override
public byte[] read(JsonReader in) throws IOException {
String encoded = in.nextString();
return Base64.getDecoder().decode(encoded);
}
}

View File

@ -25,22 +25,256 @@
*/
package com.github.games647.fastlogin.bukkit.listener.protocollib;
import java.security.SecureRandom;
import com.github.games647.fastlogin.bukkit.listener.protocollib.SignatureTestData.SignatureData;
import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;
import com.google.common.hash.Hashing;
import org.junit.Test;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.ThreadLocalRandom;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
public class EncryptionUtilTest {
import lombok.val;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;
class EncryptionUtilTest {
@Test
public void testVerifyToken() {
SecureRandom random = new SecureRandom();
void testVerifyToken() {
val random = ThreadLocalRandom.current();
byte[] token = EncryptionUtil.generateVerifyToken(random);
assertThat(token, notNullValue());
assertThat(token.length, is(4));
assertAll(
() -> assertNotNull(token),
() -> assertEquals(token.length, 4)
);
}
@Test
void testServerKey() {
KeyPair keyPair = EncryptionUtil.generateKeyPair();
Key privateKey = keyPair.getPrivate();
assertEquals(privateKey.getAlgorithm(), "RSA");
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
assertEquals(publicKey.getAlgorithm(), "RSA");
// clients accept larger values than the standard vanilla server, but we shouldn't crash them
assertAll(
() -> assertTrue(publicKey.getModulus().bitLength() >= 1024),
() -> assertTrue(publicKey.getModulus().bitLength() < 8192)
);
}
@Test
void testExpiredClientKey() throws Exception {
val clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
// Client expires at the exact second mentioned, so use it for verification
val expiredTimestamp = clientKey.expiry();
assertFalse(EncryptionUtil.verifyClientKey(clientKey, expiredTimestamp));
}
@ParameterizedTest
@ValueSource(strings = {
// expiration date changed should make the signature invalid while still being not expired
"client_keys/invalid_wrong_expiration.json",
// changed public key no longer corresponding to the signature
"client_keys/invalid_wrong_key.json",
// signature modified no longer corresponding to key and expiration date
"client_keys/invalid_wrong_signature.json"
})
void testInvalidClientKey(String clientKeySource) throws Exception {
val clientKey = ResourceLoader.loadClientKey(clientKeySource);
Instant expireTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS);
assertFalse(EncryptionUtil.verifyClientKey(clientKey, expireTimestamp));
}
@Test
void testValidClientKey() throws Exception {
val clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
val verificationTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS);
assertTrue(EncryptionUtil.verifyClientKey(clientKey, verificationTimestamp));
}
@Test
void testDecryptSharedSecret() throws Exception {
KeyPair serverPair = EncryptionUtil.generateKeyPair();
val serverPK = serverPair.getPublic();
SecretKey secretKey = generateSharedKey();
byte[] encryptedSecret = encrypt(serverPK, secretKey.getEncoded());
SecretKey decryptSharedKey = EncryptionUtil.decryptSharedKey(serverPair.getPrivate(), encryptedSecret);
assertEquals(decryptSharedKey, secretKey);
}
private static byte[] encrypt(PublicKey receiverKey, byte... message)
throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
IllegalBlockSizeException, BadPaddingException {
val encryptCipher = Cipher.getInstance(receiverKey.getAlgorithm());
encryptCipher.init(Cipher.ENCRYPT_MODE, receiverKey);
return encryptCipher.doFinal(message);
}
private static SecretKeySpec generateSharedKey() {
// according to wiki.vg 16 bytes long
byte[] sharedKey = new byte[16];
ThreadLocalRandom.current().nextBytes(sharedKey);
// shared key is to be used for the AES/CFB8 stream cipher to encrypt the traffic
// therefore the encryption/decryption has to be AES
return new SecretKeySpec(sharedKey, "AES");
}
@Test
void testServerIdHash() throws Exception {
val serverId = "";
val sharedSecret = generateSharedKey();
val serverPK = ResourceLoader.loadClientKey("client_keys/valid_public_key.json").key();
String sessionHash = getServerHash(serverId, sharedSecret, serverPK);
assertEquals(EncryptionUtil.getServerIdHashString(serverId, sharedSecret, serverPK), sessionHash);
}
private static String getServerHash(CharSequence serverId, SecretKey sharedSecret, PublicKey serverPK) {
// https://wiki.vg/Protocol_Encryption#Client
// sha1 := Sha1()
// sha1.update(ASCII encoding of the server id string from Encryption Request)
// sha1.update(shared secret)
// sha1.update(server's encoded public key from Encryption Request)
// hash := sha1.hexdigest() # String of hex characters
@SuppressWarnings("deprecation")
val hasher = Hashing.sha1().newHasher();
hasher.putString(serverId, StandardCharsets.US_ASCII);
hasher.putBytes(sharedSecret.getEncoded());
hasher.putBytes(serverPK.getEncoded());
// It works by treating the sha1 output bytes as one large integer in two's complement and then printing the
// integer in base 16, placing a minus sign if the interpreted number is negative.
// reference:
// https://github.com/SpigotMC/BungeeCord/blob/ff5727c5ef9c0b56ad35f9816ae6bd660b622cf0/proxy/src/main/java/net/md_5/bungee/connection/InitialHandler.java#L456
return new BigInteger(hasher.hash().asBytes()).toString(16);
}
@Test
void testServerIdHashWrongSecret() throws Exception {
val serverId = "";
val sharedSecret = generateSharedKey();
val serverPK = ResourceLoader.loadClientKey("client_keys/valid_public_key.json").key();
String sessionHash = getServerHash(serverId, sharedSecret, serverPK);
assertNotEquals(EncryptionUtil.getServerIdHashString("", generateSharedKey(), serverPK), sessionHash);
}
@Test
void testServerIdHashWrongServerKey() {
val serverId = "";
val sharedSecret = generateSharedKey();
val serverPK = EncryptionUtil.generateKeyPair().getPublic();
String sessionHash = getServerHash(serverId, sharedSecret, serverPK);
val wrongPK = EncryptionUtil.generateKeyPair().getPublic();
assertNotEquals(EncryptionUtil.getServerIdHashString("", sharedSecret, wrongPK), sessionHash);
}
@Test
void testValidSignedNonce() throws Exception {
ClientPublicKey clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
SignatureTestData testData = SignatureTestData.fromResource("signature/valid_signature.json");
assertTrue(verifySignedNonce(testData, clientKey));
}
@ParameterizedTest
@ValueSource(strings = {
"signature/incorrect_nonce.json",
"signature/incorrect_salt.json",
"signature/incorrect_signature.json",
})
void testIncorrectNonce(String signatureSource) throws Exception {
ClientPublicKey clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
SignatureTestData testData = SignatureTestData.fromResource(signatureSource);
assertFalse(verifySignedNonce(testData, clientKey));
}
@Test
void testWrongPublicKeySigned() throws Exception {
// load a different public key
ClientPublicKey clientKey = ResourceLoader.loadClientKey("client_keys/invalid_wrong_key.json");
SignatureTestData testData = SignatureTestData.fromResource("signature/valid_signature.json");
assertFalse(verifySignedNonce(testData, clientKey));
}
private static boolean verifySignedNonce(SignatureTestData testData, ClientPublicKey clientKey)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
PublicKey clientPublicKey = clientKey.key();
byte[] nonce = testData.getNonce();
SignatureData signature = testData.getSignature();
long salt = signature.getSalt();
return EncryptionUtil.verifySignedNonce(nonce, clientPublicKey, salt, signature.getSignature());
}
@Test
void testNonce() throws Exception {
byte[] expected = {1, 2, 3, 4};
val serverKey = EncryptionUtil.generateKeyPair();
val encryptedNonce = encrypt(serverKey.getPublic(), expected);
assertTrue(EncryptionUtil.verifyNonce(expected, serverKey.getPrivate(), encryptedNonce));
}
@Test
void testNonceIncorrect() throws Exception {
byte[] expected = {1, 2, 3, 4};
val serverKey = EncryptionUtil.generateKeyPair();
// flipped first character
val encryptedNonce = encrypt(serverKey.getPublic(), new byte[]{0, 2, 3, 4});
assertFalse(EncryptionUtil.verifyNonce(expected, serverKey.getPrivate(), encryptedNonce));
}
@Test
void testNonceFailedDecryption() throws Exception {
byte[] expected = {1, 2, 3, 4};
val serverKey = EncryptionUtil.generateKeyPair();
// generate a new keypair that is different
val encryptedNonce = encrypt(EncryptionUtil.generateKeyPair().getPublic(), expected);
assertThrows(GeneralSecurityException.class,
() -> EncryptionUtil.verifyNonce(expected, serverKey.getPrivate(), encryptedNonce)
);
}
@Test
void testNonceIncorrectEmpty() {
byte[] expected = {1, 2, 3, 4};
val serverKey = EncryptionUtil.generateKeyPair();
byte[] encryptedNonce = {};
assertThrows(GeneralSecurityException.class,
() -> EncryptionUtil.verifyNonce(expected, serverKey.getPrivate(), encryptedNonce)
);
}
}

View File

@ -0,0 +1,97 @@
/*
* SPDX-License-Identifier: MIT
*
* The MIT License (MIT)
*
* Copyright (c) 2015-2022 games647 and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.games647.fastlogin.bukkit.listener.protocollib;
import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;
import com.google.common.io.Resources;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.time.Instant;
import java.util.Base64;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
public class ResourceLoader {
public static RSAPrivateKey parsePrivateKey(String keySpec)
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
try (
Reader reader = new StringReader(keySpec);
PemReader pemReader = new PemReader(reader)
) {
PemObject pemObject = pemReader.readPemObject();
byte[] content = pemObject.getContent();
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(content);
KeyFactory factory = KeyFactory.getInstance("RSA");
return (RSAPrivateKey) factory.generatePrivate(privateKeySpec);
}
}
protected static ClientPublicKey loadClientKey(String path)
throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
URL keyUrl = Resources.getResource(path);
String lines = Resources.toString(keyUrl, StandardCharsets.US_ASCII);
JsonObject object = new Gson().fromJson(lines, JsonObject.class);
Instant expires = Instant.parse(object.getAsJsonPrimitive("expires_at").getAsString());
String key = object.getAsJsonPrimitive("key").getAsString();
RSAPublicKey publicKey = parsePublicKey(key);
byte[] signature = Base64.getDecoder().decode(object.getAsJsonPrimitive("signature").getAsString());
return ClientPublicKey.of(expires, publicKey, signature);
}
private static RSAPublicKey parsePublicKey(String keySpec)
throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
try (
Reader reader = new StringReader(keySpec);
PemReader pemReader = new PemReader(reader)
) {
PemObject pemObject = pemReader.readPemObject();
byte[] content = pemObject.getContent();
X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(content);
KeyFactory factory = KeyFactory.getInstance("RSA");
return (RSAPublicKey) factory.generatePublic(pubKeySpec);
}
}
}

View File

@ -0,0 +1,74 @@
/*
* SPDX-License-Identifier: MIT
*
* The MIT License (MIT)
*
* Copyright (c) 2015-2022 games647 and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.games647.fastlogin.bukkit.listener.protocollib;
import com.google.common.io.Resources;
import com.google.gson.Gson;
import com.google.gson.annotations.JsonAdapter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import lombok.val;
public class SignatureTestData {
public static SignatureTestData fromResource(String resourceName) throws IOException {
val keyUrl = Resources.getResource(resourceName);
val encodedSignature = Resources.toString(keyUrl, StandardCharsets.US_ASCII);
return new Gson().fromJson(encodedSignature, SignatureTestData.class);
}
@JsonAdapter(Base64Adapter.class)
private byte[] nonce;
private SignatureData signature;
public byte[] getNonce() {
return nonce;
}
public SignatureData getSignature() {
return signature;
}
public static class SignatureData {
private long salt;
@JsonAdapter(Base64Adapter.class)
private byte[] signature;
public long getSalt() {
return salt;
}
public byte[] getSignature() {
return signature;
}
}
}

View File

@ -0,0 +1,68 @@
/*
* SPDX-License-Identifier: MIT
*
* The MIT License (MIT)
*
* Copyright (c) 2015-2022 games647 and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.games647.fastlogin.bukkit.listener.protocollib;
import com.comphenix.protocol.injector.packet.PacketRegistry;
import com.comphenix.protocol.reflect.accessors.Accessors;
import com.comphenix.protocol.reflect.accessors.FieldAccessor;
import com.comphenix.protocol.utility.MinecraftReflection;
import java.util.Optional;
import org.bukkit.Bukkit;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import static org.mockito.ArgumentMatchers.any;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.mockStatic;
class VerifyResponseTaskTest {
private static final String NETTY_INJECTOR_CLASS =
"com.comphenix.protocol.injector.netty.channel.NettyChannelInjector";
@Test
void getNetworkManagerReflection() throws ClassNotFoundException {
try (
MockedStatic<Bukkit> bukkitMock = mockStatic(Bukkit.class);
MockedStatic<MinecraftReflection> reflectionMock = mockStatic(MinecraftReflection.class);
MockedStatic<PacketRegistry> registryMock = mockStatic(PacketRegistry.class)
) {
bukkitMock.when(Bukkit::getVersion).thenReturn("git-Bukkit-18fbb24 (MC: 1.17)");
reflectionMock.when(MinecraftReflection::getMinecraftPackage).thenReturn("xyz");
reflectionMock.when(MinecraftReflection::getEnumProtocolClass).thenReturn(Object.class);
registryMock.when(() -> PacketRegistry.tryGetPacketClass(any())).thenReturn(Optional.empty());
Class<?> injectorClass = Class.forName(NETTY_INJECTOR_CLASS);
FieldAccessor accessor = Accessors.getFieldAccessorOrNull(injectorClass, "networkManager", Object.class);
assertNotNull(accessor.getField());
}
}
}

View File

@ -0,0 +1,61 @@
/*
* SPDX-License-Identifier: MIT
*
* The MIT License (MIT)
*
* Copyright (c) 2015-2022 games647 and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.games647.fastlogin.bukkit.task;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import lombok.val;
import org.bukkit.entity.Player;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
class DelayedAuthHookTest {
@Test
void createNewReflectiveInstance() throws ReflectiveOperationException {
val authHook = new DelayedAuthHook(null);
assertNotNull(authHook.newInstance(DummyHook.class));
}
public static class DummyHook implements AuthPlugin<Player> {
@Override
public boolean forceLogin(Player player) {
return false;
}
@Override
public boolean forceRegister(Player player, String password) {
return false;
}
@Override
public boolean isRegistered(String playerName) throws Exception {
return false;
}
}
}

View File

@ -0,0 +1,53 @@
# Integration tests for authentication
## Description
Projects require integration tests in order to check against errors that could only occur if connected to other
components. However, they are heavier in terms of performance and require a more complex setup. Unit tests often make
use of fake, mock, stubs, etc. implementations to test the unit in isolation and thus could hide issues across
boundaries of a unit. Nevertheless, both are not replacement for each other.
## Usage in this project
The authentication system is a core component, so it requires some kind of testing. Here we are going to
spin up a Spigot server and test with the supported authentication schemes against the implementation of MCProtocolLib.
### Goals
* OS platform independent
* Reproducible, but not fixed to a specific image hash
* This is a dev container, so fixing it to feature/major version is enough instead of a version fixed by hash
* Improve container spin up
* E.g. Remove/Reduce world generation
### Note on automation
The simplest solution it to use the official Mojang session and authentication servers. However, this would require
a spare Minecraft account. Mocking the auth servers would be a solution to avoid this.
## Related
Interest blog article about integration tests and why they are necessary.
https://software.rajivprab.com/2019/04/28/rethinking-software-testing-perspectives-from-the-world-of-hardware/
## Issues
### Slow startup
Tried a lot of optimizations like only loading a single world without the nether or the end. However, there the startup
is still slow. If you have any ideas on how to tune the startup parameters of the Minecraft server or the JVM
itself to reduce the startup time, please suggest it.
### Checkpoint
An idea to optimize the time is to use CRIU (checkpoint and restore). So to save the process at a certain stage and
restore all data multiple times. This could cause a lot of issues like open files have to be present. However, the
impact is significant and since it runs inside the container all files, pids (pid=1) should be matching. Potential
checkpoint locations are:
* Direct before loading the plugins
* Likely before binding the port to prevent issues
* After loading the libraries
Nevertheless, the current state requires to run it with root and the Java support is currently still in progress.

View File

@ -0,0 +1,27 @@
# About
This contains test resources for the unit tests. The files are extracted from the Minecraft directory with slight
modifications. The files are found in `$MINECRAFT_HOME$/profilekeys/`, where `$MINECRAFT_HOME$` represents the
OS-dependent minecraft folder.
**Notable the files in this folder do not contain the private key information. It should be explicitly
stripped before including it.**
## Minecraft folder
* Windows: `%appdata%\.minecraft`
* Linux: `/home/username/.minecraft`
* Mac: `~/Library/Application Support/minecraft`
## Directory structure
* `valid_public_key.json`: Extracted from the actual file
* `invalid_wrong_expiration.json`: Changed the expiration date
* `invalid_wrong_key.json`: Modified public key while keeping the RSA structure valid
* `invalid_wrong_signature.json`: Changed a character in the public key signature
## File content
* `expires_at`: Expiration date
* `key`: Public key from the original file out of `public_key.key`
* `signature`: Mojang signed signature of this public key

View File

@ -0,0 +1,5 @@
{
"expires_at": "2022-06-20T07:31:47.318722344Z",
"key": "-----BEGIN RSA PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApd3ZxDhcRWWru1XEBke6uYqmbnS2Oxyk\nOMj+QDKrkwUqhVJYciyXGsMx46Mgr/KIoGCcokP5OtIxc6+69/ZLqJ9PvM81kLIxAqyvfBMKMGjP\n376LgxTF1FeDpbe5zXaNRxfmnvQhS5YTLbzgk36qWVjqxJMG4VLVmh7RV5zWsb7XlckZb2zRHM2Y\nMHbEC+ggX+l6zQyfG1KK0MH5k+O6b0xD0rv1wm24sLOesTXH6RZG8cNE3ofdnavxjFodTOnra6w1\naiVcoUTdEPSS86wQwq9j0YCcAKOwMXsqbk9NhpujrdyJ94dev+ELwkNS7P0pPrcfiyFTQeJCZTXz\nJB36MwIDAQAB\n-----END RSA PUBLIC KEY-----\n",
"signature": "lfRXK4zL213wBKg760eiPV7yvnLZ6a6v9Iohmw78yxIzqXO3tfrC5Z+P2LGiO1BdI4xckx8yz4ktn82zX97+r2zktBw0As7g71H/FjInpoZ76j3gMUaiFNrQJ0vKCCI7xsjonemroWAVDCAqlvdyqwUu/Fnz85+WoR2kCQ721vwy6IjWA3xhq8XrWjkI/AlBmoS/kVqnvjjjc9vocdddJXbUYzCse/hWWIbsFeBXyiGCd3v7apgtXwQfM++tt87fq7444zQskiYb14oQP8/uNwqZWQ9jAs00i1BZ0MNM6+TZYGHOfS6rbHZ1bcX34VZdcCwpapK/Z2HBRIgDN4QOcgJkyq1GcjvlM2wjfhN8gXTsmbF9Ee+5Y6a4ONRkxRZK2sT8oAXdm0OlTEGB0P0+WRRFOQ/PnRqbI7lvANao2METT2EUHHrtqFMe53kqCHdzy5qyuHxdCEa6l/gSR08fybx9DdRRmhOlhSPGxhgwqyi1fEMrN4CsSKNrv5u+sMqhspA05b3DQJeLDX+UV5ujRHwm0A49NF+h1ZYlrcefz5IMUUisOOw6HiLc/YGLD2jHwSePGdfMwMnrIxbxjCta7/7A91aaN7eYm16KW9erCOwAfJmBSQC6Pbmg5f7rd7rAKVOPxglq7nayXmd+BK53Mal5tltMSgd/0iY6SEtGSEU="
}

View File

@ -0,0 +1,5 @@
{
"expires_at": "2022-06-20T08:31:47.318722344Z",
"key": "-----BEGIN RSA PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoOv23jt2QPyab6bPRBwH2ggmzQU3I+xmDpi3X5ZB5Em/4uzyZqNVLJc0gShpk0XsdoB28Nq1bPxczOTBxuXi3rg5ax5gL+iymDSU27DLM8s/33lOofzGPJUEQGKlFm0QelDKZ/q5Y/9inHE3hEJKf7h0tnmGahXFmZSF/nRz9GHnfSYpjtDr9bsZOzQuLhHXT5E4ksNRTFW41h0MlZ1qOhO+NiiVgk7LmgVYiV7RRbgO8U6RaCEqg5n28Ewo6QtzB+DF4NTDeu3E9BLH5G0npdUrVNhdRUWCFDmH6n9hqSIz2J7o6GvWqEvp0h9e/3qtLsoS60hnQXunrcWcPaEIYQIDAQAB\n-----END RSA PUBLIC KEY-----\n",
"signature": "lfRXK4zL213wBKg760eiPV7yvnLZ6a6v9Iohmw78yxIzqXO3tfrC5Z+P2LGiO1BdI4xckx8yz4ktn82zX97+r2zktBw0As7g71H/FjInpoZ76j3gMUaiFNrQJ0vKCCI7xsjonemroWAVDCAqlvdyqwUu/Fnz85+WoR2kCQ721vwy6IjWA3xhq8XrWjkI/AlBmoS/kVqnvjjjc9vocdddJXbUYzCse/hWWIbsFeBXyiGCd3v7apgtXwQfM++tt87fq7444zQskiYb14oQP8/uNwqZWQ9jAs00i1BZ0MNM6+TZYGHOfS6rbHZ1bcX34VZdcCwpapK/Z2HBRIgDN4QOcgJkyq1GcjvlM2wjfhN8gXTsmbF9Ee+5Y6a4ONRkxRZK2sT8oAXdm0OlTEGB0P0+WRRFOQ/PnRqbI7lvANao2METT2EUHHrtqFMe53kqCHdzy5qyuHxdCEa6l/gSR08fybx9DdRRmhOlhSPGxhgwqyi1fEMrN4CsSKNrv5u+sMqhspA05b3DQJeLDX+UV5ujRHwm0A49NF+h1ZYlrcefz5IMUUisOOw6HiLc/YGLD2jHwSePGdfMwMnrIxbxjCta7/7A91aaN7eYm16KW9erCOwAfJmBSQC6Pbmg5f7rd7rAKVOPxglq7nayXmd+BK53Mal5tltMSgd/0iY6SEtGSEU="
}

View File

@ -0,0 +1,5 @@
{
"expires_at": "2022-06-20T08:31:47.318722344Z",
"key": "-----BEGIN RSA PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApd3ZxDhcRWWru1XEBke6uYqmbnS2Oxyk\nOMj+QDKrkwUqhVJYciyXGsMx46Mgr/KIoGCcokP5OtIxc6+69/ZLqJ9PvM81kLIxAqyvfBMKMGjP\n376LgxTF1FeDpbe5zXaNRxfmnvQhS5YTLbzgk36qWVjqxJMG4VLVmh7RV5zWsb7XlckZb2zRHM2Y\nMHbEC+ggX+l6zQyfG1KK0MH5k+O6b0xD0rv1wm24sLOesTXH6RZG8cNE3ofdnavxjFodTOnra6w1\naiVcoUTdEPSS86wQwq9j0YCcAKOwMXsqbk9NhpujrdyJ94dev+ELwkNS7P0pPrcfiyFTQeJCZTXz\nJB36MwIDAQAB\n-----END RSA PUBLIC KEY-----\n",
"signature": "lfRxK4zL213wBKg760eiPV7yvnLZ6a6v9Iohmw78yxIzqXO3tfrC5Z+P2LGiO1BdI4xckx8yz4ktn82zX97+r2zktBw0As7g71H/FjInpoZ76j3gMUaiFNrQJ0vKCCI7xsjonemroWAVDCAqlvdyqwUu/Fnz85+WoR2kCQ721vwy6IjWA3xhq8XrWjkI/AlBmoS/kVqnvjjjc9vocdddJXbUYzCse/hWWIbsFeBXyiGCd3v7apgtXwQfM++tt87fq7444zQskiYb14oQP8/uNwqZWQ9jAs00i1BZ0MNM6+TZYGHOfS6rbHZ1bcX34VZdcCwpapK/Z2HBRIgDN4QOcgJkyq1GcjvlM2wjfhN8gXTsmbF9Ee+5Y6a4ONRkxRZK2sT8oAXdm0OlTEGB0P0+WRRFOQ/PnRqbI7lvANao2METT2EUHHrtqFMe53kqCHdzy5qyuHxdCEa6l/gSR08fybx9DdRRmhOlhSPGxhgwqyi1fEMrN4CsSKNrv5u+sMqhspA05b3DQJeLDX+UV5ujRHwm0A49NF+h1ZYlrcefz5IMUUisOOw6HiLc/YGLD2jHwSePGdfMwMnrIxbxjCta7/7A91aaN7eYm16KW9erCOwAfJmBSQC6Pbmg5f7rd7rAKVOPxglq7nayXmd+BK53Mal5tltMSgd/0iY6SEtGSEU="
}

View File

@ -0,0 +1,5 @@
{
"expires_at": "2022-06-20T08:31:47.318722344Z",
"key": "-----BEGIN RSA PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApd3ZxDhcRWWru1XEBke6uYqmbnS2Oxyk\nOMj+QDKrkwUqhVJYciyXGsMx46Mgr/KIoGCcokP5OtIxc6+69/ZLqJ9PvM81kLIxAqyvfBMKMGjP\n376LgxTF1FeDpbe5zXaNRxfmnvQhS5YTLbzgk36qWVjqxJMG4VLVmh7RV5zWsb7XlckZb2zRHM2Y\nMHbEC+ggX+l6zQyfG1KK0MH5k+O6b0xD0rv1wm24sLOesTXH6RZG8cNE3ofdnavxjFodTOnra6w1\naiVcoUTdEPSS86wQwq9j0YCcAKOwMXsqbk9NhpujrdyJ94dev+ELwkNS7P0pPrcfiyFTQeJCZTXz\nJB36MwIDAQAB\n-----END RSA PUBLIC KEY-----\n",
"signature": "lfRXK4zL213wBKg760eiPV7yvnLZ6a6v9Iohmw78yxIzqXO3tfrC5Z+P2LGiO1BdI4xckx8yz4ktn82zX97+r2zktBw0As7g71H/FjInpoZ76j3gMUaiFNrQJ0vKCCI7xsjonemroWAVDCAqlvdyqwUu/Fnz85+WoR2kCQ721vwy6IjWA3xhq8XrWjkI/AlBmoS/kVqnvjjjc9vocdddJXbUYzCse/hWWIbsFeBXyiGCd3v7apgtXwQfM++tt87fq7444zQskiYb14oQP8/uNwqZWQ9jAs00i1BZ0MNM6+TZYGHOfS6rbHZ1bcX34VZdcCwpapK/Z2HBRIgDN4QOcgJkyq1GcjvlM2wjfhN8gXTsmbF9Ee+5Y6a4ONRkxRZK2sT8oAXdm0OlTEGB0P0+WRRFOQ/PnRqbI7lvANao2METT2EUHHrtqFMe53kqCHdzy5qyuHxdCEa6l/gSR08fybx9DdRRmhOlhSPGxhgwqyi1fEMrN4CsSKNrv5u+sMqhspA05b3DQJeLDX+UV5ujRHwm0A49NF+h1ZYlrcefz5IMUUisOOw6HiLc/YGLD2jHwSePGdfMwMnrIxbxjCta7/7A91aaN7eYm16KW9erCOwAfJmBSQC6Pbmg5f7rd7rAKVOPxglq7nayXmd+BK53Mal5tltMSgd/0iY6SEtGSEU="
}

View File

@ -0,0 +1,16 @@
# About
This contains test resources for the unit tests. Files in this folder include pre-made cryptographic signatures.
## Directory structure
* `valid_signature.json`: Extracted using packet extract from an actual authentication
* `incorrect_nonce.json`: Different nonce token simulating that the server expected a different token than signed
* `incorrect_salt.json`: Salt sent is different to the content signed by the signature (changed salt field)
* `incorrect_signature.json`: Changed signature
## File content
* `nonce`: Server generated nonce token
* `salt`: Client generated random token that will be signed
* `signature`: Nonce and salt signed using the client key from `valid_public_key.json`

View File

@ -0,0 +1,7 @@
{
"nonce": "galNig\u003d\u003d",
"signature": {
"signature": "JlXAUtIGDjxUOnF5vkg/NUEN2wlzXcqADyYIw2WRTb5hgKwIgxyUPO5v/2M7xU3hxz2Zf0iYHM97h8qNMGQ43cLgfVH9VWZ1kGMuOby2LNSb6nDaMzm0b02ftThaWOWj9kJXbR8fN7qdpB+28t2CTW5ILT+2AZYI/Sn8gFFR+LvJJt1ENMfEj2ZIIkHecpNBuKyLz1aDCZ5BEASSLfAqHEAA3dpHV1DIgzfpO6xwo7bVFDtcBEeusl/Nc3KyGyT8sDFTsZWgitgz53xNKrZUK8Q2BaJfP0zrGAX36rpYURJSVD0AtI1ic9s5aG+OFUC1YhLXb/1cDv37ZjHcdV2ppw\u003d\u003d",
"salt": -2985008842905108412
}
}

View File

@ -0,0 +1,7 @@
{
"nonce": "GalNig\u003d\u003d",
"signature": {
"signature": "JlXAUtIGDjxUOnF5vkg/NUEN2wlzXcqADyYIw2WRTb5hgKwIgxyUPO5v/2M7xU3hxz2Zf0iYHM97h8qNMGQ43cLgfVH9VWZ1kGMuOby2LNSb6nDaMzm0b02ftThaWOWj9kJXbR8fN7qdpB+28t2CTW5ILT+2AZYI/Sn8gFFR+LvJJt1ENMfEj2ZIIkHecpNBuKyLz1aDCZ5BEASSLfAqHEAA3dpHV1DIgzfpO6xwo7bVFDtcBEeusl/Nc3KyGyT8sDFTsZWgitgz53xNKrZUK8Q2BaJfP0zrGAX36rpYURJSVD0AtI1ic9s5aG+OFUC1YhLXb/1cDv37ZjHcdV2ppw\u003d\u003d",
"salt": -1985008842905108412
}
}

View File

@ -0,0 +1,7 @@
{
"nonce": "GalNig\u003d\u003d",
"signature": {
"signature": "jlXAUtIGDjxUOnF5vkg/NUEN2wlzXcqADyYIw2WRTb5hgKwIgxyUPO5v/2M7xU3hxz2Zf0iYHM97h8qNMGQ43cLgfVH9VWZ1kGMuOby2LNSb6nDaMzm0b02ftThaWOWj9kJXbR8fN7qdpB+28t2CTW5ILT+2AZYI/Sn8gFFR+LvJJt1ENMfEj2ZIIkHecpNBuKyLz1aDCZ5BEASSLfAqHEAA3dpHV1DIgzfpO6xwo7bVFDtcBEeusl/Nc3KyGyT8sDFTsZWgitgz53xNKrZUK8Q2BaJfP0zrGAX36rpYURJSVD0AtI1ic9s5aG+OFUC1YhLXb/1cDv37ZjHcdV2ppw\u003d\u003d",
"salt": -2985008842905108412
}
}

View File

@ -0,0 +1,7 @@
{
"nonce": "GalNig\u003d\u003d",
"signature": {
"signature": "JlXAUtIGDjxUOnF5vkg/NUEN2wlzXcqADyYIw2WRTb5hgKwIgxyUPO5v/2M7xU3hxz2Zf0iYHM97h8qNMGQ43cLgfVH9VWZ1kGMuOby2LNSb6nDaMzm0b02ftThaWOWj9kJXbR8fN7qdpB+28t2CTW5ILT+2AZYI/Sn8gFFR+LvJJt1ENMfEj2ZIIkHecpNBuKyLz1aDCZ5BEASSLfAqHEAA3dpHV1DIgzfpO6xwo7bVFDtcBEeusl/Nc3KyGyT8sDFTsZWgitgz53xNKrZUK8Q2BaJfP0zrGAX36rpYURJSVD0AtI1ic9s5aG+OFUC1YhLXb/1cDv37ZjHcdV2ppw\u003d\u003d",
"salt": -2985008842905108412
}
}

View File

@ -32,7 +32,7 @@
<parent>
<groupId>com.github.games647</groupId>
<artifactId>fastlogin</artifactId>
<version>1.11-SNAPSHOT</version>
<version>1.12-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@ -46,10 +46,11 @@
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<minimizeJar>true</minimizeJar>
<createDependencyReducedPom>false</createDependencyReducedPom>
<shadedArtifactAttached>false</shadedArtifactAttached>
<artifactSet>
@ -137,6 +138,30 @@
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
<exclusion>
<groupId>mysql</groupId>
<artifactId>*</artifactId>
</exclusion>
<exclusion>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-native</artifactId>
</exclusion>
<exclusion>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-query</artifactId>
</exclusion>
<exclusion>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-slf4j</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.maven</groupId>
<artifactId>*</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.maven.resolver</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
@ -192,7 +217,7 @@
<dependency>
<groupId>de.xxschrandxx.bca</groupId>
<artifactId>BungeeCordAuthenticator</artifactId>
<version>0.0.2</version>
<version>0.0.3</version>
<scope>provided</scope>
<exclusions>
<exclusion>

View File

@ -59,10 +59,10 @@ public class BungeeLoginSession extends LoginSession {
@Override
public synchronized String toString() {
return this.getClass().getSimpleName() + '{' +
"alreadySaved=" + alreadySaved +
", alreadyLogged=" + alreadyLogged +
", registered=" + registered +
"} " + super.toString();
return this.getClass().getSimpleName() + '{'
+ "alreadySaved=" + alreadySaved
+ ", alreadyLogged=" + alreadyLogged
+ ", registered=" + registered
+ "} " + super.toString();
}
}

View File

@ -72,8 +72,8 @@ public class BungeeLoginSource implements LoginSource {
@Override
public String toString() {
return this.getClass().getSimpleName() + '{' +
"connection=" + connection +
'}';
return this.getClass().getSimpleName() + '{'
+ "connection=" + connection
+ '}';
}
}

View File

@ -57,6 +57,7 @@ import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.connection.Server;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.api.plugin.PluginManager;
import net.md_5.bungee.api.scheduler.GroupedThreadFactory;
@ -100,7 +101,7 @@ public class FastLoginBungee extends Plugin implements PlatformPlugin<CommandSen
//events
PluginManager pluginManager = getProxy().getPluginManager();
ConnectListener connectListener = new ConnectListener(this, core.getRateLimiter());
Listener connectListener = new ConnectListener(this, core.getAntiBot());
pluginManager.registerListener(this, connectListener);
pluginManager.registerListener(this, new PluginMessageListener(this));

View File

@ -28,6 +28,7 @@ package com.github.games647.fastlogin.bungee.event;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.LoginSession;
import com.github.games647.fastlogin.core.shared.event.FastLoginAutoLoginEvent;
import net.md_5.bungee.api.plugin.Cancellable;
import net.md_5.bungee.api.plugin.Event;

View File

@ -28,6 +28,7 @@ package com.github.games647.fastlogin.bungee.event;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.LoginSource;
import com.github.games647.fastlogin.core.shared.event.FastLoginPreLoginEvent;
import net.md_5.bungee.api.plugin.Event;
public class BungeeFastLoginPreLoginEvent extends Event implements FastLoginPreLoginEvent {

View File

@ -34,11 +34,13 @@ import me.vik1395.BungeeAuthAPI.RequestHandler;
import net.md_5.bungee.api.connection.ProxiedPlayer;
/**
* GitHub: https://github.com/vik1395/BungeeAuth-Minecraft
* GitHub:
* <a href="https://github.com/vik1395/BungeeAuth-Minecraft">...</a>
*
* Project page:
*
* Spigot: https://www.spigotmc.org/resources/bungeeauth.493/
* Spigot:
* <a href="https://www.spigotmc.org/resources/bungeeauth.493/">...</a>
*/
public class BungeeAuthHook implements AuthPlugin<ProxiedPlayer> {

View File

@ -38,11 +38,11 @@ import net.md_5.bungee.api.connection.ProxiedPlayer;
/**
* GitHub:
* https://github.com/xXSchrandXx/SpigotPlugins/tree/master/BungeeCordAuthenticator
* <a href="https://github.com/xXSchrandXx/SpigotPlugins/tree/master/BungeeCordAuthenticator">...</a>
* <p>
* Project page:
* <p>
* Spigot: https://www.spigotmc.org/resources/bungeecordauthenticator.87669/
* Spigot: <a href="https://www.spigotmc.org/resources/bungeecordauthenticator.87669/">...</a>
*/
public class BungeeCordAuthenticatorBungeeHook implements AuthPlugin<ProxiedPlayer> {

View File

@ -31,8 +31,9 @@ import com.github.games647.fastlogin.bungee.FastLoginBungee;
import com.github.games647.fastlogin.bungee.task.AsyncPremiumCheck;
import com.github.games647.fastlogin.bungee.task.FloodgateAuthTask;
import com.github.games647.fastlogin.bungee.task.ForceLoginTask;
import com.github.games647.fastlogin.core.RateLimiter;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.antibot.AntiBotService;
import com.github.games647.fastlogin.core.antibot.AntiBotService.Action;
import com.github.games647.fastlogin.core.hooks.bedrock.FloodgateService;
import com.github.games647.fastlogin.core.shared.LoginSession;
import com.google.common.base.Throwables;
@ -41,8 +42,10 @@ import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Field;
import java.net.InetSocketAddress;
import java.util.UUID;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.connection.Server;
@ -68,7 +71,7 @@ import org.slf4j.LoggerFactory;
public class ConnectListener implements Listener {
private static final String UUID_FIELD_NAME = "uniqueId";
private static final MethodHandle uniqueIdSetter;
protected static final MethodHandle UNIQUE_ID_SETTER;
static {
MethodHandle setHandle = null;
@ -84,20 +87,20 @@ public class ConnectListener implements Listener {
Logger logger = LoggerFactory.getLogger(ConnectListener.class);
logger.error(
"Cannot find Bungee initial handler; Disabling premium UUID and skin won't work.",
reflectiveOperationException
reflectiveOperationException
);
}
uniqueIdSetter = setHandle;
UNIQUE_ID_SETTER = setHandle;
}
private final FastLoginBungee plugin;
private final RateLimiter rateLimiter;
private final AntiBotService antiBotService;
private final Property[] emptyProperties = {};
public ConnectListener(FastLoginBungee plugin, RateLimiter rateLimiter) {
public ConnectListener(FastLoginBungee plugin, AntiBotService antiBotService) {
this.plugin = plugin;
this.rateLimiter = rateLimiter;
this.antiBotService = antiBotService;
}
@EventHandler
@ -107,17 +110,28 @@ public class ConnectListener implements Listener {
return;
}
if (!rateLimiter.tryAcquire()) {
plugin.getLog().warn("Simple Anti-Bot join limit - Ignoring {}", connection);
return;
}
InetSocketAddress address = preLoginEvent.getConnection().getAddress();
String username = connection.getName();
plugin.getLog().info("Incoming login request for {} from {}", username, connection.getSocketAddress());
preLoginEvent.registerIntent(plugin);
Runnable asyncPremiumCheck = new AsyncPremiumCheck(plugin, preLoginEvent, connection, username);
plugin.getScheduler().runAsync(asyncPremiumCheck);
Action action = antiBotService.onIncomingConnection(address, username);
switch (action) {
case Ignore:
// just ignore
return;
case Block:
String message = plugin.getCore().getMessage("kick-antibot");
preLoginEvent.setCancelReason(TextComponent.fromLegacyText(message));
preLoginEvent.setCancelled(true);
break;
case Continue:
default:
preLoginEvent.registerIntent(plugin);
Runnable asyncPremiumCheck = new AsyncPremiumCheck(plugin, preLoginEvent, connection, username);
plugin.getScheduler().runAsync(asyncPremiumCheck);
break;
}
}
@EventHandler(priority = EventPriority.LOWEST)
@ -140,8 +154,8 @@ public class ConnectListener implements Listener {
StoredProfile playerProfile = session.getProfile();
playerProfile.setId(verifiedUUID);
// bungeecord will do this automatically so override it on disabled option
if (uniqueIdSetter != null) {
// BungeeCord will do this automatically so override it on disabled option
if (UNIQUE_ID_SETTER != null) {
InitialHandler initialHandler = (InitialHandler) connection;
if (!plugin.getCore().getConfig().get("premiumUuid", true)) {
@ -157,7 +171,7 @@ public class ConnectListener implements Listener {
}
}
private void setOfflineId(InitialHandler connection, String username) {
protected void setOfflineId(InitialHandler connection, String username) {
try {
UUID oldPremiumId = connection.getUniqueId();
UUID offlineUUID = UUIDAdapter.generateOfflineId(username);
@ -165,7 +179,7 @@ public class ConnectListener implements Listener {
// BungeeCord only allows setting the UUID in PreLogin events and before requesting online mode
// However if online mode is requested, it will override previous values
// So we have to do it with reflection
uniqueIdSetter.invokeExact(connection, offlineUUID);
UNIQUE_ID_SETTER.invokeExact(connection, offlineUUID);
String format = "Overridden UUID from {} to {} (based of {}) on {}";
plugin.getLog().info(format, oldPremiumId, offlineUUID, username, connection);

View File

@ -29,8 +29,8 @@ import com.github.games647.fastlogin.bungee.FastLoginBungee;
import com.github.games647.fastlogin.bungee.event.BungeeFastLoginPremiumToggleEvent;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.github.games647.fastlogin.core.shared.event.FastLoginPremiumToggleEvent.PremiumToggleReason;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.chat.TextComponent;
@ -73,8 +73,8 @@ public class AsyncToggleMessage implements Runnable {
playerProfile.setPremium(false);
playerProfile.setId(null);
core.getStorage().save(playerProfile);
PremiumToggleReason reason = (!isPlayerSender || !sender.getName().equalsIgnoreCase(playerProfile.getName())) ?
PremiumToggleReason.COMMAND_OTHER : PremiumToggleReason.COMMAND_SELF;
PremiumToggleReason reason = (!isPlayerSender || !sender.getName().equalsIgnoreCase(playerProfile.getName()))
? PremiumToggleReason.COMMAND_OTHER : PremiumToggleReason.COMMAND_SELF;
core.getPlugin().getProxy().getPluginManager().callEvent(
new BungeeFastLoginPremiumToggleEvent(playerProfile, reason));
sendMessage("remove-premium");
@ -89,8 +89,8 @@ public class AsyncToggleMessage implements Runnable {
playerProfile.setPremium(true);
core.getStorage().save(playerProfile);
PremiumToggleReason reason = (!isPlayerSender || !sender.getName().equalsIgnoreCase(playerProfile.getName())) ?
PremiumToggleReason.COMMAND_OTHER : PremiumToggleReason.COMMAND_SELF;
PremiumToggleReason reason = (!isPlayerSender || !sender.getName().equalsIgnoreCase(playerProfile.getName()))
? PremiumToggleReason.COMMAND_OTHER : PremiumToggleReason.COMMAND_SELF;
core.getPlugin().getProxy().getPluginManager().callEvent(
new BungeeFastLoginPremiumToggleEvent(playerProfile, reason));
sendMessage("add-premium");

View File

@ -25,19 +25,19 @@
*/
package com.github.games647.fastlogin.bungee.task;
import com.github.games647.fastlogin.bungee.BungeeLoginSession;
import com.github.games647.fastlogin.bungee.FastLoginBungee;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.github.games647.fastlogin.core.shared.FloodgateManagement;
import java.net.InetSocketAddress;
import java.util.UUID;
import org.geysermc.floodgate.api.player.FloodgatePlayer;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.connection.Server;
import com.github.games647.fastlogin.bungee.BungeeLoginSession;
import com.github.games647.fastlogin.bungee.FastLoginBungee;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.github.games647.fastlogin.core.shared.FloodgateManagement;
import org.geysermc.floodgate.api.player.FloodgatePlayer;
public class FloodgateAuthTask
extends FloodgateManagement<ProxiedPlayer, CommandSender, BungeeLoginSession, FastLoginBungee> {

View File

@ -0,0 +1,58 @@
/*
* SPDX-License-Identifier: MIT
*
* The MIT License (MIT)
*
* Copyright (c) 2015-2022 games647 and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.games647.fastlogin.bungee.listener;
import java.lang.reflect.Field;
import java.util.UUID;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.conf.Configuration;
import net.md_5.bungee.connection.InitialHandler;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
class ConnectListenerTest {
@Test
void testUUIDSetter() throws Throwable {
BungeeCord proxyMock = mock(BungeeCord.class);
BungeeCord.setInstance(proxyMock);
Configuration configMock = mock(Configuration.class);
Field configField = proxyMock.getClass().getField("config");
configField.setAccessible(true);
configField.set(proxyMock, configMock);
InitialHandler handler = new InitialHandler(proxyMock, null);
UUID expectedUUID = UUID.randomUUID();
ConnectListener.UNIQUE_ID_SETTER.invokeExact(handler, expectedUUID);
assertEquals(expectedUUID, handler.getUniqueId());
}
}

224
checkstyle.xml Normal file
View File

@ -0,0 +1,224 @@
<?xml version="1.0"?>
<!--
SPDX-License-Identifier: MIT
The MIT License (MIT)
Copyright (c) 2015-2022 games647 and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<!--
Checkstyle configuration that checks the sun coding conventions from:
- the Java Language Specification at
https://docs.oracle.com/javase/specs/jls/se11/html/index.html
- the Sun Code Conventions at https://www.oracle.com/java/technologies/javase/codeconventions-contents.html
- the Javadoc guidelines at
https://www.oracle.com/technical-resources/articles/java/javadoc-tool.html
- the JDK Api documentation https://docs.oracle.com/en/java/javase/11/
- some best practices
Checkstyle is very configurable. Be sure to read the documentation at
https://checkstyle.org (or in your downloaded distribution).
Most Checks are configurable, be sure to consult the documentation.
To completely disable a check, just comment it out or delete it from the file.
To suppress certain violations please review suppression filters.
Finally, it is worth reading the documentation.
-->
<module name="Checker">
<!--
If you set the basedir property below, then all reported file
names will be relative to the specified directory. See
https://checkstyle.org/config.html#Checker
<property name="basedir" value="${basedir}"/>
-->
<property name="severity" value="error"/>
<property name="fileExtensions" value="java, properties, xml"/>
<!-- Excludes all 'module-info.java' files -->
<!-- See https://checkstyle.org/config_filefilters.html -->
<module name="BeforeExecutionExclusionFileFilter">
<property name="fileNamePattern" value="module\-info\.java$"/>
</module>
<!-- https://checkstyle.org/config_filters.html#SuppressionFilter -->
<module name="SuppressionFilter">
<property name="file" value="${org.checkstyle.sun.suppressionfilter.config}"
default="checkstyle-suppressions.xml" />
<property name="optional" value="true"/>
</module>
<!-- Checks that a package-info.java file exists for each package. -->
<!-- See https://checkstyle.org/config_javadoc.html#JavadocPackage -->
<!--<module name="JavadocPackage"/>-->
<!-- Checks whether files end with a new line. -->
<!-- See https://checkstyle.org/config_misc.html#NewlineAtEndOfFile -->
<module name="NewlineAtEndOfFile"/>
<!-- Checks that property files contain the same keys. -->
<!-- See https://checkstyle.org/config_misc.html#Translation -->
<module name="Translation"/>
<!-- Checks for Size Violations. -->
<!-- See https://checkstyle.org/config_sizes.html -->
<module name="FileLength"/>
<module name="LineLength">
<property name="max" value="120"/>
<property name="fileExtensions" value="java"/>
</module>
<!-- Checks for whitespace -->
<!-- See https://checkstyle.org/config_whitespace.html -->
<module name="FileTabCharacter"/>
<!-- Miscellaneous other checks. -->
<!-- See https://checkstyle.org/config_misc.html -->
<module name="RegexpSingleline">
<property name="format" value="\s+$"/>
<property name="minimum" value="0"/>
<property name="maximum" value="0"/>
<property name="message" value="Line has trailing spaces."/>
</module>
<!-- Checks for Headers -->
<!-- See https://checkstyle.org/config_header.html -->
<!-- <module name="Header"> -->
<!-- <property name="headerFile" value="${checkstyle.header.file}"/> -->
<!-- <property name="fileExtensions" value="java"/> -->
<!-- </module> -->
<module name="TreeWalker">
<!-- Checks for Javadoc comments. -->
<!-- See https://checkstyle.org/config_javadoc.html -->
<module name="InvalidJavadocPosition"/>
<module name="JavadocMethod"/>
<!--<module name="JavadocType"/>-->
<!--<module name="JavadocVariable"/>-->
<!--<module name="JavadocStyle"/>-->
<!--<module name="MissingJavadocMethod"/>-->
<!-- Checks for Naming Conventions. -->
<!-- See https://checkstyle.org/config_naming.html -->
<module name="ConstantName"/>
<module name="LocalFinalVariableName"/>
<module name="LocalVariableName"/>
<module name="MemberName"/>
<module name="MethodName"/>
<module name="PackageName"/>
<module name="ParameterName"/>
<module name="StaticVariableName"/>
<module name="TypeName"/>
<!-- Checks for imports -->
<!-- See https://checkstyle.org/config_imports.html -->
<module name="AvoidStarImport"/>
<module name="IllegalImport"/> <!-- defaults to sun.* packages -->
<module name="RedundantImport"/>
<module name="UnusedImports">
<property name="processJavadoc" value="false"/>
</module>
<!-- Checks for Size Violations. -->
<!-- See https://checkstyle.org/config_sizes.html -->
<module name="MethodLength"/>
<module name="ParameterNumber"/>
<!-- Checks for whitespace -->
<!-- See https://checkstyle.org/config_whitespace.html -->
<module name="EmptyForIteratorPad"/>
<module name="GenericWhitespace"/>
<module name="MethodParamPad"/>
<module name="NoWhitespaceAfter"/>
<module name="NoWhitespaceBefore"/>
<module name="OperatorWrap"/>
<module name="ParenPad"/>
<module name="TypecastParenPad"/>
<module name="WhitespaceAfter"/>
<module name="WhitespaceAround"/>
<!-- Modifier Checks -->
<!-- See https://checkstyle.org/config_modifier.html -->
<module name="ModifierOrder"/>
<module name="RedundantModifier"/>
<!-- Checks for blocks. You know, those {}'s -->
<!-- See https://checkstyle.org/config_blocks.html -->
<module name="AvoidNestedBlocks"/>
<module name="EmptyBlock"/>
<module name="LeftCurly"/>
<module name="NeedBraces"/>
<module name="RightCurly"/>
<!-- Checks for common coding problems -->
<!-- See https://checkstyle.org/config_coding.html -->
<module name="EmptyStatement"/>
<module name="EqualsHashCode"/>
<module name="IllegalInstantiation"/>
<module name="InnerAssignment"/>
<!--<module name="MagicNumber"/>-->
<module name="MissingSwitchDefault"/>
<module name="MultipleVariableDeclarations"/>
<module name="SimplifyBooleanExpression"/>
<module name="SimplifyBooleanReturn"/>
<!-- Checks for class design -->
<!-- See https://checkstyle.org/config_design.html -->
<!--<module name="DesignForExtension"/>-->
<module name="FinalClass"/>
<module name="HideUtilityClassConstructor"/>
<module name="InterfaceIsType"/>
<!-- Miscellaneous other checks. -->
<!-- See https://checkstyle.org/config_misc.html -->
<module name="ArrayTypeStyle"/>
<!--<module name="FinalParameters"/>-->
<!-- <module name="TodoComment"/>-->
<module name="UpperEll"/>
<!-- https://checkstyle.org/config_filters.html#SuppressionXpathFilter -->
<module name="SuppressionXpathFilter">
<property name="file" value="${org.checkstyle.sun.suppressionxpathfilter.config}"
default="checkstyle-xpath-suppressions.xml" />
<property name="optional" value="true"/>
</module>
</module>
</module>

View File

@ -32,7 +32,7 @@
<parent>
<groupId>com.github.games647</groupId>
<artifactId>fastlogin</artifactId>
<version>1.11-SNAPSHOT</version>
<version>1.12-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@ -52,9 +52,6 @@
<repository>
<id>codemc-repo</id>
<url>https://repo.codemc.io/repository/maven-public/</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<!-- Floodgate -->
<repository>
@ -95,7 +92,7 @@
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-config</artifactId>
<version>1.16-R0.4</version>
<version>1.19-R0.1-20220702.004052-16</version>
<exclusions>
<exclusion>
<groupId>*</groupId>

View File

@ -58,16 +58,18 @@ public class AsyncScheduler {
}
public CompletableFuture<Void> runAsync(Runnable task) {
return CompletableFuture.runAsync(() -> {
currentlyRunning.incrementAndGet();
try {
task.run();
} finally {
currentlyRunning.getAndDecrement();
}
}, processingPool).exceptionally(error -> {
return CompletableFuture.runAsync(() -> process(task), processingPool).exceptionally(error -> {
logger.warn("Error occurred on thread pool", error);
return null;
});
}
private void process(Runnable task) {
currentlyRunning.incrementAndGet();
try {
task.run();
} finally {
currentlyRunning.getAndDecrement();
}
}
}

View File

@ -25,10 +25,10 @@
*/
package com.github.games647.fastlogin.core;
import com.github.games647.craftapi.cache.SafeCacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.CacheBuilder;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
@ -37,13 +37,13 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.jul.JDK14LoggerAdapter;
public class CommonUtil {
public final class CommonUtil {
private static final char COLOR_CHAR = '&';
private static final char TRANSLATED_CHAR = '§';
public static <K, V> ConcurrentMap<K, V> buildCache(int expireAfterWrite, int maxSize) {
SafeCacheBuilder<Object, Object> builder = SafeCacheBuilder.newBuilder();
CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
if (expireAfterWrite > 0) {
builder.expireAfterWrite(expireAfterWrite, TimeUnit.MINUTES);
@ -53,15 +53,13 @@ public class CommonUtil {
builder.maximumSize(maxSize);
}
return builder.build(CacheLoader.from(() -> {
throw new UnsupportedOperationException();
}));
return builder.<K, V>build().asMap();
}
public static String translateColorCodes(String rawMessage) {
char[] chars = rawMessage.toCharArray();
for (int i = 0; i < chars.length - 1; i++) {
if (chars[i] == COLOR_CHAR && "0123456789AaBbCcDdEeFfKkLlMmNnOoRr".indexOf(chars[i + 1]) > -1) {
if (chars[i] == COLOR_CHAR && "x0123456789AaBbCcDdEeFfKkLlMmNnOoRr".indexOf(chars[i + 1]) > -1) {
chars[i] = TRANSLATED_CHAR;
chars[i + 1] = Character.toLowerCase(chars[i + 1]);
}
@ -74,7 +72,7 @@ public class CommonUtil {
* This creates a SLF4J logger. In the process it initializes the SLF4J service provider. This method looks
* for the provider in the plugin jar instead of in the server jar when creating a Logger. The provider is only
* initialized once, so this method should be called early.
*
* <p>
* The provider is bound to the service class `SLF4JServiceProvider`. Relocating this class makes it available
* for exclusive own usage. Other dependencies will use the relocated service too, and therefore will find the
* initialized provider.
@ -93,12 +91,9 @@ public class CommonUtil {
LoggerFactory.getLogger(parent.getName()).info("Initialize logging service");
try {
parent.setLevel(Level.ALL);
Class<JDK14LoggerAdapter> adapterClass = JDK14LoggerAdapter.class;
Constructor<JDK14LoggerAdapter> cons = adapterClass.getDeclaredConstructor(java.util.logging.Logger.class);
cons.setAccessible(true);
return cons.newInstance(parent);
} catch (ReflectiveOperationException reflectEx) {
return createJDKLogger(parent);
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException reflectEx) {
parent.log(Level.WARNING, "Cannot create slf4j logging adapter", reflectEx);
parent.log(Level.WARNING, "Creating logger instance manually...");
return LoggerFactory.getLogger(parent.getName());
@ -108,7 +103,15 @@ public class CommonUtil {
}
}
protected static JDK14LoggerAdapter createJDKLogger(java.util.logging.Logger parent)
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Class<JDK14LoggerAdapter> adapterClass = JDK14LoggerAdapter.class;
Constructor<JDK14LoggerAdapter> cons = adapterClass.getDeclaredConstructor(java.util.logging.Logger.class);
cons.setAccessible(true);
return cons.newInstance(parent);
}
private CommonUtil() {
//Utility class
throw new RuntimeException("No instantiation of utility classes allowed");
}
}

View File

@ -114,11 +114,21 @@ public class StoredProfile extends Profile {
@Override
public synchronized boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof StoredProfile that)) return false;
if (!super.equals(o)) return false;
if (this == o) {
return true;
}
if (!(o instanceof StoredProfile)) {
return false;
}
StoredProfile that = (StoredProfile) o;
if (!super.equals(o)) {
return false;
}
return rowId == that.rowId && premium == that.premium
&& Objects.equals(lastIp, that.lastIp) && lastLogin.equals(that.lastLogin);
&& Objects.equals(lastIp, that.lastIp) && lastLogin.equals(that.lastLogin);
}
@Override
@ -128,11 +138,11 @@ public class StoredProfile extends Profile {
@Override
public synchronized String toString() {
return this.getClass().getSimpleName() + '{' +
"rowId=" + rowId +
", premium=" + premium +
", lastIp='" + lastIp + '\'' +
", lastLogin=" + lastLogin +
"} " + super.toString();
return this.getClass().getSimpleName() + '{'
+ "rowId=" + rowId
+ ", premium=" + premium
+ ", lastIp='" + lastIp + '\''
+ ", lastLogin=" + lastLogin
+ "} " + super.toString();
}
}

View File

@ -0,0 +1,62 @@
/*
* SPDX-License-Identifier: MIT
*
* The MIT License (MIT)
*
* Copyright (c) 2015-2022 games647 and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.games647.fastlogin.core.antibot;
import java.net.InetSocketAddress;
import org.slf4j.Logger;
public class AntiBotService {
private final Logger logger;
private final RateLimiter rateLimiter;
private final Action limitReachedAction;
public AntiBotService(Logger logger, RateLimiter rateLimiter, Action limitReachedAction) {
this.logger = logger;
this.rateLimiter = rateLimiter;
this.limitReachedAction = limitReachedAction;
}
public Action onIncomingConnection(InetSocketAddress clientAddress, String username) {
if (!rateLimiter.tryAcquire()) {
logger.warn("Anti-Bot join limit - Ignoring {}", clientAddress);
return limitReachedAction;
}
return Action.Continue;
}
public enum Action {
Ignore,
Block,
Continue;
}
}

View File

@ -36,42 +36,46 @@ import java.util.Optional;
/**
* An extension to {@link MojangResolver} which allows connection using transparent reverse proxies.
* The significant difference is that unlike MojangResolver from the CraftAPI implementation, which sends the "ip" parameter
* when the hostIp parameter is an IPv4 address, but skips it for IPv6, this implementation leaves out the "ip" parameter
* also for IPv4, effectively enabling transparent proxies to work.
* The significant difference is that unlike MojangResolver from the CraftAPI implementation, which sends the
* "ip" parameter when the hostIp parameter is an IPv4 address, but skips it for IPv6, this implementation leaves out
* the "ip" parameter also for IPv4, effectively enabling transparent proxies to work.
*
* @author games647, Enginecrafter77
*/
public class ProxyAgnosticMojangResolver extends MojangResolver {
/**
* A formatting string containing an URL used to call the {@code hasJoined} method on mojang session servers.
*
* Formatting parameters:
* 1. The username of the player in question
* 2. The serverId of this server
*/
public static final String MOJANG_SESSIONSERVER_HASJOINED_CALL_URLFMT = "https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s";
@Override
public Optional<Verification> hasJoined(String username, String serverHash, InetAddress hostIp) throws IOException
{
String url = String.format(MOJANG_SESSIONSERVER_HASJOINED_CALL_URLFMT, username, serverHash);
private static final String HOST = "sessionserver.mojang.com";
HttpURLConnection conn = this.getConnection(url);
int responseCode = conn.getResponseCode();
/**
* A formatting string containing a URL used to call the {@code hasJoined} method on mojang session servers.
* <p>
* Formatting parameters:
* 1. The username of the player in question
* 2. The serverId of this server
*/
public static final String ENDPOINT = "https://" + HOST + "/session/minecraft/hasJoined?username=%s&serverId=%s";
Verification verification = null;
@Override
public Optional<Verification> hasJoined(String username, String serverHash, InetAddress hostIp)
throws IOException {
String url = String.format(ENDPOINT, username, serverHash);
// Mojang session servers send HTTP 204 (NO CONTENT) when the authentication seems invalid
// If that's not our case, the authentication is valid, and so we can parse the response.
if(responseCode != HttpURLConnection.HTTP_NO_CONTENT)
verification = this.parseRequest(conn, this::parseVerification);
HttpURLConnection conn = this.getConnection(url);
int responseCode = conn.getResponseCode();
return Optional.ofNullable(verification);
}
Verification verification = null;
// Functional implementation of InputStreamAction, used in hasJoined method in parseRequest call
protected Verification parseVerification(InputStream input) throws IOException
{
return this.readJson(input, Verification.class);
}
// Mojang session servers send HTTP 204 (NO CONTENT) when the authentication seems invalid
// If that's not our case, the authentication is valid, and so we can parse the response.
if (responseCode != HttpURLConnection.HTTP_NO_CONTENT) {
verification = this.parseRequest(conn, this::parseVerification);
}
return Optional.ofNullable(verification);
}
// Functional implementation of InputStreamAction, used in hasJoined method in parseRequest call
protected Verification parseVerification(InputStream input) throws IOException {
return this.readJson(input, Verification.class);
}
}

View File

@ -23,7 +23,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.games647.fastlogin.core;
package com.github.games647.fastlogin.core.antibot;
@FunctionalInterface
public interface RateLimiter {

View File

@ -23,7 +23,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.games647.fastlogin.core;
package com.github.games647.fastlogin.core.antibot;
import com.google.common.base.Ticker;
@ -113,7 +113,7 @@ public class TickingRateLimiter implements RateLimiter {
private final long expireTime;
private int count;
public TimeRecord(long firstMinuteRecord, long expireTime) {
TimeRecord(long firstMinuteRecord, long expireTime) {
this.firstMinuteRecord = firstMinuteRecord;
this.expireTime = expireTime;
this.count = 1;

View File

@ -74,12 +74,12 @@ public abstract class BedrockService<B> {
"Could not check whether Bedrock Player {}'s name conflicts a premium Java player's name.",
username);
kickPlayer(source, username, "Could not check if your name conflicts an existing " +
"premium Java account's name. This is usually a serverside error.");
kickPlayer(source, username, "Could not check if your name conflicts an existing "
+ "premium Java account's name. This is usually a serverside error.");
} catch (RateLimitException rateLimitException) {
core.getPlugin().getLog().warn("Mojang API rate limit hit");
kickPlayer(source, username, "Could not check if your name conflicts an existing premium " +
"Java account's name. Try again in a few minutes");
kickPlayer(source, username, "Could not check if your name conflicts an existing premium "
+ "Java account's name. Try again in a few minutes");
}
if (premiumUUID.isPresent()) {

View File

@ -54,19 +54,22 @@ public class FloodgateService extends BedrockService<FloodgatePlayer> {
* <li>autoLoginFloodgate
* <li>autoRegisterFloodgate
* </ul>
* </p>
*
* @param key the key of the entry in config.yml
* @return <b>true</b> if the entry's value is "true", "false", or "linked"
*/
public boolean isValidFloodgateConfigString(String key) {
String value = core.getConfig().get(key).toString().toLowerCase(Locale.ENGLISH);
if (!value.equals("true") && !value.equals("linked") && !value.equals("false") && !value.equals("no-conflict")) {
core.getPlugin().getLog().error("Invalid value detected for {} in FastLogin/config.yml.", key);
return false;
switch (value) {
case "true":
case "linked":
case "false":
case "no-conflict":
return true;
default:
core.getPlugin().getLog().error("Invalid value detected for {} in FastLogin/config.yml.", key);
return false;
}
return true;
}
@Override
@ -83,11 +86,11 @@ public class FloodgateService extends BedrockService<FloodgatePlayer> {
if ("false".equals(allowConflict)
|| "linked".equals(allowConflict) && !isLinked) {
super.checkNameConflict(username, source);
super.checkNameConflict(username, source);
} else {
core.getPlugin().getLog().info("Skipping name conflict checking for player {}", username);
}
//Floodgate users don't need Java specific checks
return true;
}
@ -98,7 +101,7 @@ public class FloodgateService extends BedrockService<FloodgatePlayer> {
* username can be found
* <br>
* <i>Falls back to non-prefixed name checks, if ProtocolLib is installed</i>
*
*
* @param prefixedUsername the name of the player with the prefix appended
* @return FloodgatePlayer if found, null otherwise
*/

View File

@ -79,10 +79,10 @@ public class ChangePremiumMessage implements ChannelMessage {
@Override
public String toString() {
return this.getClass().getSimpleName() + '{' +
"playerName='" + playerName + '\'' +
", shouldEnable=" + willEnable +
", isSourceInvoker=" + isSourceInvoker +
'}';
return this.getClass().getSimpleName() + '{'
+ "playerName='" + playerName + '\''
+ ", shouldEnable=" + willEnable
+ ", isSourceInvoker=" + isSourceInvoker
+ '}';
}
}

View File

@ -92,11 +92,11 @@ public class LoginActionMessage implements ChannelMessage {
@Override
public String toString() {
return this.getClass().getSimpleName() + '{' +
"type='" + type + '\'' +
", playerName='" + playerName + '\'' +
", proxyId=" + proxyId +
'}';
return this.getClass().getSimpleName() + '{'
+ "type='" + type + '\''
+ ", playerName='" + playerName + '\''
+ ", proxyId=" + proxyId
+ '}';
}
public enum Type {

View File

@ -28,8 +28,10 @@ package com.github.games647.fastlogin.core.shared;
import com.github.games647.craftapi.resolver.MojangResolver;
import com.github.games647.craftapi.resolver.http.RotatingProxySelector;
import com.github.games647.fastlogin.core.CommonUtil;
import com.github.games647.fastlogin.core.RateLimiter;
import com.github.games647.fastlogin.core.TickingRateLimiter;
import com.github.games647.fastlogin.core.antibot.AntiBotService;
import com.github.games647.fastlogin.core.antibot.AntiBotService.Action;
import com.github.games647.fastlogin.core.antibot.RateLimiter;
import com.github.games647.fastlogin.core.antibot.TickingRateLimiter;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import com.github.games647.fastlogin.core.hooks.DefaultPasswordGenerator;
import com.github.games647.fastlogin.core.hooks.PasswordGenerator;
@ -88,7 +90,7 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
private Configuration config;
private SQLStorage storage;
private RateLimiter rateLimiter;
private AntiBotService antiBot;
private PasswordGenerator<P> passwordGenerator = new DefaultPasswordGenerator<>();
private AuthPlugin<P> authPlugin;
@ -120,9 +122,10 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
}
// Initialize the resolver based on the config parameter
this.resolver = this.config.getBoolean("useProxyAgnosticResolver", false) ? new ProxyAgnosticMojangResolver() : new MojangResolver();
this.resolver = this.config.getBoolean("useProxyAgnosticResolver", false)
? new ProxyAgnosticMojangResolver() : new MojangResolver();
rateLimiter = createRateLimiter(config.getSection("anti-bot"));
antiBot = createAntiBotService(config.getSection("anti-bot"));
Set<Proxy> proxies = config.getStringList("proxies")
.stream()
.map(HostAndPort::fromString)
@ -144,20 +147,34 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
resolver.setOutgoingAddresses(addresses);
}
private RateLimiter createRateLimiter(Configuration botSection) {
boolean enabled = botSection.getBoolean("enabled", true);
if (!enabled) {
private AntiBotService createAntiBotService(Configuration botSection) {
RateLimiter rateLimiter;
if (botSection.getBoolean("enabled", true)) {
int maxCon = botSection.getInt("connections", 200);
long expireTime = botSection.getLong("expire", 5) * 60 * 1_000L;
if (expireTime > MAX_EXPIRE_RATE) {
expireTime = MAX_EXPIRE_RATE;
}
rateLimiter = new TickingRateLimiter(Ticker.systemTicker(), maxCon, expireTime);
} else {
// no-op rate limiter
return () -> true;
rateLimiter = () -> true;
}
int maxCon = botSection.getInt("anti-bot.connections", 200);
long expireTime = botSection.getLong("anti-bot.expire", 5) * 60 * 1_000L;
if (expireTime > MAX_EXPIRE_RATE) {
expireTime = MAX_EXPIRE_RATE;
Action action = Action.Ignore;
switch (botSection.getString("action", "ignore")) {
case "ignore":
action = Action.Ignore;
break;
case "block":
action = Action.Block;
break;
default:
plugin.getLog().warn("Invalid anti bot action - defaulting to ignore");
}
return new TickingRateLimiter(Ticker.systemTicker(), maxCon, expireTime);
return new AntiBotService(plugin.getLog(), rateLimiter, action);
}
private Configuration loadFile(String fileName) throws IOException {
@ -175,7 +192,7 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
config = configProvider.load(reader, defaults);
}
// explicitly add keys here, because Configuration.getKeys doesn't return the keys from the default configuration
// explicitly add keys here, because Configuration.getKeys doesn't return the keys from the default config
for (String key : defaults.getKeys()) {
config.set(key, config.get(key));
}
@ -228,9 +245,13 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
boolean useSSL = config.get("useSSL", false);
if (useSSL) {
databaseConfig.addDataSourceProperty("allowPublicKeyRetrieval", config.getBoolean("allowPublicKeyRetrieval", false));
databaseConfig.addDataSourceProperty("serverRSAPublicKeyFile", config.getString("ServerRSAPublicKeyFile"));
databaseConfig.addDataSourceProperty("sslMode", config.getString("sslMode", "Required"));
boolean publicKeyRetrieval = config.getBoolean("allowPublicKeyRetrieval", false);
String rsaPublicKeyFile = config.getString("ServerRSAPublicKeyFile");
String sslMode = config.getString("sslMode", "Required");
databaseConfig.addDataSourceProperty("allowPublicKeyRetrieval", publicKeyRetrieval);
databaseConfig.addDataSourceProperty("serverRSAPublicKeyFile", rsaPublicKeyFile);
databaseConfig.addDataSourceProperty("sslMode", sslMode);
}
databaseConfig.setUsername(config.get("username", ""));
@ -254,8 +275,8 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
} catch (ClassNotFoundException notFoundEx) {
Logger log = plugin.getLog();
log.warn("This driver {} is not supported on this platform", className);
log.warn("Please choose either MySQL (Spigot, BungeeCord), SQLite (Spigot, Sponge) or " +
"MariaDB (Sponge, Velocity)", notFoundEx);
log.warn("Please choose either MySQL (Spigot, BungeeCord), SQLite (Spigot, Sponge) or "
+ "MariaDB (Sponge, Velocity)", notFoundEx);
}
return false;
@ -285,8 +306,8 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
return authPlugin;
}
public RateLimiter getRateLimiter() {
return rateLimiter;
public AntiBotService getAntiBot() {
return antiBot;
}
public void setAuthPluginHook(AuthPlugin<P> authPlugin) {

View File

@ -25,10 +25,10 @@
*/
package com.github.games647.fastlogin.core.shared;
import com.github.games647.fastlogin.core.storage.SQLStorage;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import com.github.games647.fastlogin.core.shared.event.FastLoginAutoLoginEvent;
import com.github.games647.fastlogin.core.storage.SQLStorage;
public abstract class ForceLoginManagement<P extends C, C, L extends LoginSession, T extends PlatformPlugin<C>>
implements Runnable {

View File

@ -93,7 +93,7 @@ public abstract class JoinManagement<P extends C, C, S extends LoginSource> {
premiumUUID = core.getResolver().findProfile(username);
}
if (premiumUUID.isEmpty()
if (!premiumUUID.isPresent()
|| (!checkNameChange(source, username, premiumUUID.get())
&& !checkPremiumName(source, username, profile))) {
//nothing detected the player as premium -> start a cracked session
@ -106,9 +106,9 @@ public abstract class JoinManagement<P extends C, C, S extends LoginSource> {
}
}
} catch (RateLimitException rateLimitEx) {
core.getPlugin().getLog().error("Mojang's rate limit reached for {}. The public IPv4 address of this" +
" server issued more than 600 Name -> UUID requests within 10 minutes. After those 10" +
" minutes we can make requests again.", username);
core.getPlugin().getLog().error("Mojang's rate limit reached for {}. The public IPv4 address of this"
+ " server issued more than 600 Name -> UUID requests within 10 minutes. After those 10"
+ " minutes we can make requests again.", username);
} catch (Exception ex) {
core.getPlugin().getLog().error("Failed to check premium state for {}", username, ex);
core.getPlugin().getLog().error("Failed to check premium state of {}", username, ex);

View File

@ -26,8 +26,8 @@
package com.github.games647.fastlogin.core.shared;
import com.github.games647.fastlogin.core.StoredProfile;
import com.google.common.base.MoreObjects;
import java.util.StringJoiner;
import java.util.UUID;
public abstract class LoginSession {
@ -52,7 +52,7 @@ public abstract class LoginSession {
return requestUsername;
}
public String getUsername() {
public synchronized String getUsername() {
return username;
}
@ -90,13 +90,13 @@ public abstract class LoginSession {
}
@Override
public synchronized String toString() {
return MoreObjects.toStringHelper(this)
.add("profile", profile)
.add("requestUsername", requestUsername)
.add("username", username)
.add("uuid", uuid)
.add("registered", registered)
public String toString() {
return new StringJoiner(", ", LoginSession.class.getSimpleName() + "[", "]")
.add("profile=" + profile)
.add("requestUsername='" + requestUsername + "'")
.add("username='" + username + "'")
.add("uuid=" + uuid)
.add("registered=" + registered)
.toString();
}
}

View File

@ -59,7 +59,7 @@ public interface PlatformPlugin<C> {
default ThreadFactory getThreadFactory() {
return new ThreadFactoryBuilder()
.setNameFormat(getName() + " Pool Thread #%1$d")
// Hikari create daemons by default. We could daemon threads for our own scheduler too
// Hikari create daemons by default. We could use daemon threads for our own scheduler too
// because we safely shut down
.setDaemon(true)
.build();

View File

@ -31,7 +31,7 @@ import com.zaxxer.hikari.HikariConfig;
public class MySQLStorage extends SQLStorage {
public MySQLStorage(FastLoginCore<?, ?, ?> core, String driver, String host, int port, String database,
HikariConfig config,boolean useSSL) {
HikariConfig config, boolean useSSL) {
super(core,
buildJDBCUrl(driver, host, port, database),
setParams(config, useSSL));

View File

@ -20,6 +20,9 @@ anti-bot:
connections: 600
# Amount of minutes after the first connection got inserted will expire and made available
expire: 10
# Action - Which action should be performed when the bucket is full (too many connections)
# Allowed values are: 'ignore' (FastLogin drops handling the player) or 'block' (block this incoming connection)
action: 'ignore'
# Request a premium login without forcing the player to type a command
#
@ -269,6 +272,15 @@ autoRegisterFloodgate: false
# Enabling this might lead to people gaining unauthorized access to other's accounts!
floodgatePrefixWorkaround: false
# This option resembles the vanilla configuration option 'enforce-secure-profile' in the 'server.properties' file.
# It verifies if the incoming cryptographic key in the login request from the player is signed by Mojang. This key
# is necessary for servers where you or other in-game players want to verify that a chat message sent and signed by
# this player is not modified by any third-party. Modifications by your server would also invalidate the message.
#
# This feature is only relevant if you use the plugin in ProtocolLib mode and use 1.19+.
# This also the case if you don't have any proxies in use.
verifyClientKeys: false
# Database configuration
# Recommended is the use of MariaDB (a better version of MySQL)

View File

@ -48,6 +48,9 @@ not-premium-other: '&4Player is not in the premium list'
# Admin wanted to change the premium of a user that isn't known to the plugin
player-unknown: '&4Player not in the database'
# Player kicked from anti bot feature
kick-antibot: '&4Please wait a moment!'
# ========= Bukkit/Spigot ================
# The user skipped the authentication, because it was a premium player
@ -77,7 +80,7 @@ error-kick: '&4Error occurred'
# The server sends a verify-token within the premium authentication request. If this doesn't match on response,
# it could be another client sending malicious packets
invalid-verify-token: '&4Invalid token'
invalid-verify-token: '&4Invalid nonce token. Please verify you are using the correct server address'
# The client sent no request join server request to the mojang servers which would proof that it's owner of that
# account. Only modified clients would do this.
@ -93,6 +96,9 @@ not-started: '&cServer is not fully started yet. Please retry'
premium-warning: '&c&lWARNING: &6This command should&l only&6 be invoked if you are the owner of this paid Minecraft account
Type &a/premium&6 again to confirm'
# Invalid client public key that will be used in the future to send authenticated chat messages from clients
invalid-public-key: '&cInvalid client public key. Please try to restart your game.'
# ========= Bungee/Waterfall only ================================

View File

@ -23,3 +23,16 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.games647.fastlogin.core;
import java.util.logging.Logger;
import org.junit.jupiter.api.Test;
class CommonUtilTest {
@Test
void createJDKLogger() throws Exception {
CommonUtil.createJDKLogger(Logger.getAnonymousLogger());
}
}

View File

@ -25,132 +25,80 @@
*/
package com.github.games647.fastlogin.core;
import com.github.games647.fastlogin.core.antibot.RateLimiter;
import com.github.games647.fastlogin.core.antibot.TickingRateLimiter;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class TickingRateLimiterTest {
private static final long THRESHOLD_MILLI = 10;
class TickingRateLimiterTest {
/**
* Always expired
*/
@Test
public void allowExpire() throws InterruptedException {
@ParameterizedTest
@ValueSource(longs = {5_000_000L, -5_000_000L})
void allowExpire(long initial) {
int size = 3;
FakeTicker ticker = new FakeTicker(5_000_000L);
FakeTicker ticker = new FakeTicker(initial);
// run twice the size to fill it first and then test it
TickingRateLimiter rateLimiter = new TickingRateLimiter(ticker, size, 0);
RateLimiter rateLimiter = new TickingRateLimiter(ticker, size, 0);
for (int i = 0; i < size; i++) {
assertTrue("Filling up", rateLimiter.tryAcquire());
assertTrue(rateLimiter.tryAcquire(), "Filling up");
}
for (int i = 0; i < size; i++) {
ticker.add(Duration.ofSeconds(1));
assertTrue("Should be expired", rateLimiter.tryAcquire());
}
}
@Test
public void allowExpireNegative() throws InterruptedException {
int size = 3;
FakeTicker ticker = new FakeTicker(-5_000_000L);
// run twice the size to fill it first and then test it
TickingRateLimiter rateLimiter = new TickingRateLimiter(ticker, size, 0);
for (int i = 0; i < size; i++) {
assertTrue("Filling up", rateLimiter.tryAcquire());
}
for (int i = 0; i < size; i++) {
ticker.add(Duration.ofSeconds(1));
assertTrue("Should be expired", rateLimiter.tryAcquire());
assertTrue(rateLimiter.tryAcquire(), "Should be expired");
}
}
/**
* Too many requests
*/
@Test
public void shouldBlock() {
@ParameterizedTest
@ValueSource(longs = {5_000_000L, -5_000_000L})
void shouldBlock(long initial) {
int size = 3;
FakeTicker ticker = new FakeTicker(5_000_000L);
FakeTicker ticker = new FakeTicker(initial);
// fill the size
TickingRateLimiter rateLimiter = new TickingRateLimiter(ticker, size, TimeUnit.SECONDS.toMillis(30));
RateLimiter rateLimiter = new TickingRateLimiter(ticker, size, TimeUnit.SECONDS.toMillis(30));
for (int i = 0; i < size; i++) {
assertTrue("Filling up", rateLimiter.tryAcquire());
assertTrue(rateLimiter.tryAcquire(), "Filling up");
}
assertFalse("Should be full and no entry should be expired", rateLimiter.tryAcquire());
assertFalse(rateLimiter.tryAcquire(), "Should be full and no entry should be expired");
}
/**
* Too many requests
*/
@Test
public void shouldBlockNegative() {
int size = 3;
FakeTicker ticker = new FakeTicker(-5_000_000L);
// fill the size
TickingRateLimiter rateLimiter = new TickingRateLimiter(ticker, size, TimeUnit.SECONDS.toMillis(30));
for (int i = 0; i < size; i++) {
assertTrue("Filling up", rateLimiter.tryAcquire());
}
assertFalse("Should be full and no entry should be expired", rateLimiter.tryAcquire());
}
/**
* Blocked attempts shouldn't replace existing ones.
*/
@Test
public void blockedNotAdded() throws InterruptedException {
FakeTicker ticker = new FakeTicker(5_000_000L);
@ParameterizedTest
@ValueSource(longs = {5_000_000L, -5_000_000L})
void blockedNotAdded(long initial) {
FakeTicker ticker = new FakeTicker(initial);
// fill the size - 100ms should be reasonable high
TickingRateLimiter rateLimiter = new TickingRateLimiter(ticker, 1, 100);
assertTrue("Filling up", rateLimiter.tryAcquire());
RateLimiter rateLimiter = new TickingRateLimiter(ticker, 1, 100);
assertTrue(rateLimiter.tryAcquire(), "Filling up");
ticker.add(Duration.ofMillis(50));
// still is full - should fail
assertFalse("Expired too early", rateLimiter.tryAcquire());
assertFalse(rateLimiter.tryAcquire(), "Expired too early");
// wait the remaining time and add a threshold, because
ticker.add(Duration.ofMillis(50));
assertTrue("Request not released", rateLimiter.tryAcquire());
}
/**
* Blocked attempts shouldn't replace existing ones.
*/
@Test
public void blockedNotAddedNegative() throws InterruptedException {
FakeTicker ticker = new FakeTicker(-5_000_000L);
// fill the size - 100ms should be reasonable high
TickingRateLimiter rateLimiter = new TickingRateLimiter(ticker, 1, 100);
assertTrue("Filling up", rateLimiter.tryAcquire());
ticker.add(Duration.ofMillis(50));
// still is full - should fail
assertFalse("Expired too early", rateLimiter.tryAcquire());
// wait the remaining time and add a threshold, because
ticker.add(Duration.ofMillis(50));
assertTrue("Request not released", rateLimiter.tryAcquire());
assertTrue(rateLimiter.tryAcquire(), "Request not released");
}
}

74
pom.xml
View File

@ -35,7 +35,7 @@
<packaging>pom</packaging>
<name>FastLogin</name>
<version>1.11-SNAPSHOT</version>
<version>1.12-SNAPSHOT</version>
<url>https://www.spigotmc.org/resources/fastlogin.14153/</url>
<description>
@ -48,11 +48,14 @@
<!-- Set default for non-git clones -->
<git.commit.id>Unknown</git.commit.id>
<java.version>17</java.version>
<!-- Verify Java 8 compatibility while compiling with a newer toolchain
(i.e. check for unavailable methods) -->
<java.version>8</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<maven.compiler.release>${java.version}</maven.compiler.release>
<floodgate.version>2.0-SNAPSHOT</floodgate.version>
<floodgate.version>2.2.0-SNAPSHOT</floodgate.version>
<geyser.version>2.0.0-SNAPSHOT</geyser.version>
</properties>
@ -125,11 +128,54 @@
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<configLocation>checkstyle.xml</configLocation>
<consoleOutput>true</consoleOutput>
<failsOnError>true</failsOnError>
<linkXRef>false</linkXRef>
</configuration>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>10.3.1</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>validate</id>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Require newer versions for Junit5 support -->
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins>
<resources>
<resource>
<!-- Certificate should not be filtered, as it would make it invalid -->
<directory>src/main/resources</directory>
<includes>
<include>yggdrasil_session_pubkey.der</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<excludes>
<exclude>yggdrasil_session_pubkey.der</exclude>
</excludes>
<!--Replace variables-->
<filtering>true</filtering>
</resource>
@ -137,10 +183,26 @@
</build>
<dependencies>
<!-- Use lombok to use some newer Java syntax features in Java 8 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<!-- Require inline to support static mocks -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>4.6.1</version>
<scope>test</scope>
</dependency>
</dependencies>

View File

@ -32,7 +32,7 @@
<parent>
<groupId>com.github.games647</groupId>
<artifactId>fastlogin</artifactId>
<version>1.11-SNAPSHOT</version>
<version>1.12-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@ -59,10 +59,11 @@
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<minimizeJar>true</minimizeJar>
<createDependencyReducedPom>false</createDependencyReducedPom>
<shadedArtifactAttached>false</shadedArtifactAttached>
<relocations>
@ -96,6 +97,12 @@
<exclude>META-INF/MANIFEST.MF</exclude>
</excludes>
</filter>
<filter>
<artifact>org.mariadb.jdbc:*</artifact>
<includes>
<include>**</include>
</includes>
</filter>
</filters>
</configuration>
<executions>
@ -113,7 +120,7 @@
<repositories>
<repository>
<id>velocity</id>
<url>https://nexus.velocitypowered.com/repository/maven-public/</url>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
</repositories>
@ -129,7 +136,7 @@
<dependency>
<groupId>com.velocitypowered</groupId>
<artifactId>velocity-api</artifactId>
<version>3.1.0</version>
<version>3.1.1</version>
<scope>provided</scope>
</dependency>
@ -137,7 +144,7 @@
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>3.0.4</version>
<version>3.0.6</version>
</dependency>
</dependencies>
</project>

View File

@ -46,6 +46,7 @@ import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.plugin.annotation.DataDirectory;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.messages.ChannelRegistrar;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import com.velocitypowered.api.proxy.server.RegisteredServer;
@ -73,7 +74,7 @@ public class FastLoginVelocity implements PlatformPlugin<CommandSource> {
private final Path dataDirectory;
private final Logger logger;
private final ConcurrentMap<InetSocketAddress, VelocityLoginSession> session = new MapMaker().weakKeys().makeMap();
private static final String PROXY_ID_fILE = "proxyId.txt";
private static final String PROXY_ID_FILE = "proxyId.txt";
private FastLoginCore<Player, CommandSource, FastLoginVelocity> core;
private AsyncScheduler scheduler;
@ -96,10 +97,12 @@ public class FastLoginVelocity implements PlatformPlugin<CommandSource> {
return;
}
server.getEventManager().register(this, new ConnectListener(this, core.getRateLimiter()));
server.getEventManager().register(this, new ConnectListener(this, core.getAntiBot()));
server.getEventManager().register(this, new PluginMessageListener(this));
server.getChannelRegistrar().register(MinecraftChannelIdentifier.create(getName(), ChangePremiumMessage.CHANGE_CHANNEL));
server.getChannelRegistrar().register(MinecraftChannelIdentifier.create(getName(), SuccessMessage.SUCCESS_CHANNEL));
ChannelRegistrar channelRegistry = server.getChannelRegistrar();
channelRegistry.register(MinecraftChannelIdentifier.create(getName(), ChangePremiumMessage.CHANGE_CHANNEL));
channelRegistry.register(MinecraftChannelIdentifier.create(getName(), SuccessMessage.SUCCESS_CHANNEL));
}
@Subscribe
@ -167,7 +170,7 @@ public class FastLoginVelocity implements PlatformPlugin<CommandSource> {
}
private void loadOrGenerateProxyId() {
Path idFile = dataDirectory.resolve(PROXY_ID_fILE);
Path idFile = dataDirectory.resolve(PROXY_ID_FILE);
boolean shouldGenerate = false;
if (Files.exists(idFile)) {
@ -182,7 +185,8 @@ public class FastLoginVelocity implements PlatformPlugin<CommandSource> {
logger.error("Unable to load proxy id from '{}'", idFile.toAbsolutePath());
logger.error("Detailed exception:", e);
} catch (IllegalArgumentException e) {
logger.error("'{}' contains an invalid uuid! FastLogin will not work without a valid id.", idFile.toAbsolutePath());
Path filePath = idFile.toAbsolutePath();
logger.error("'{}' contains an invalid uuid! FastLogin will not work without a valid id.", filePath);
}
} else {
shouldGenerate = true;

View File

@ -58,10 +58,10 @@ public class VelocityLoginSession extends LoginSession {
@Override
public synchronized String toString() {
return this.getClass().getSimpleName() + '{' +
"alreadySaved=" + alreadySaved +
", alreadyLogged=" + alreadyLogged +
", registered=" + registered +
"} " + super.toString();
return this.getClass().getSimpleName() + '{'
+ "alreadySaved=" + alreadySaved
+ ", alreadyLogged=" + alreadyLogged
+ ", registered=" + registered
+ "} " + super.toString();
}
}

View File

@ -33,7 +33,7 @@ import com.velocitypowered.api.event.ResultedEvent;
import java.util.Objects;
public class VelocityFastLoginAutoLoginEvent
implements FastLoginAutoLoginEvent, ResultedEvent<ResultedEvent.GenericResult> {
implements FastLoginAutoLoginEvent, ResultedEvent<ResultedEvent.GenericResult> {
private final LoginSession session;
private final StoredProfile profile;
@ -67,11 +67,11 @@ public class VelocityFastLoginAutoLoginEvent
@Override
public GenericResult getResult() {
return cancelled ? GenericResult.denied(): GenericResult.allowed();
return cancelled ? GenericResult.denied() : GenericResult.allowed();
}
@Override
public void setResult(GenericResult result) {
cancelled = Objects.requireNonNull(result) != GenericResult.allowed();
cancelled = Objects.requireNonNull(result) != GenericResult.allowed();
}
}

View File

@ -26,8 +26,9 @@
package com.github.games647.fastlogin.velocity.listener;
import com.github.games647.craftapi.UUIDAdapter;
import com.github.games647.fastlogin.core.RateLimiter;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.antibot.AntiBotService;
import com.github.games647.fastlogin.core.antibot.AntiBotService.Action;
import com.github.games647.fastlogin.core.shared.LoginSession;
import com.github.games647.fastlogin.velocity.FastLoginVelocity;
import com.github.games647.fastlogin.velocity.VelocityLoginSession;
@ -37,26 +38,33 @@ import com.velocitypowered.api.event.Continuation;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.event.connection.PreLoginEvent;
import com.velocitypowered.api.event.connection.PreLoginEvent.PreLoginComponentResult;
import com.velocitypowered.api.event.player.GameProfileRequestEvent;
import com.velocitypowered.api.event.player.ServerConnectedEvent;
import com.velocitypowered.api.proxy.InboundConnection;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.api.util.GameProfile.Property;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
public class ConnectListener {
private final FastLoginVelocity plugin;
private final RateLimiter rateLimiter;
private final AntiBotService antiBotService;
public ConnectListener(FastLoginVelocity plugin, RateLimiter rateLimiter) {
public ConnectListener(FastLoginVelocity plugin, AntiBotService antiBotService) {
this.plugin = plugin;
this.rateLimiter = rateLimiter;
this.antiBotService = antiBotService;
}
@Subscribe
@ -66,16 +74,30 @@ public class ConnectListener {
}
InboundConnection connection = preLoginEvent.getConnection();
if (!rateLimiter.tryAcquire()) {
plugin.getLog().warn("Simple Anti-Bot join limit - Ignoring {}", connection);
return;
}
String username = preLoginEvent.getUsername();
plugin.getLog().info("Incoming login request for {} from {}", username, connection.getRemoteAddress());
InetSocketAddress address = connection.getRemoteAddress();
plugin.getLog().info("Incoming login request for {} from {}", username, address);
Runnable asyncPremiumCheck = new AsyncPremiumCheck(plugin, connection, username, continuation, preLoginEvent);
plugin.getScheduler().runAsync(asyncPremiumCheck);
Action action = antiBotService.onIncomingConnection(address, username);
switch (action) {
case Ignore:
// just ignore
return;
case Block:
String message = plugin.getCore().getMessage("kick-antibot");
TextComponent messageParsed = LegacyComponentSerializer.legacyAmpersand().deserialize(message);
PreLoginComponentResult reason = PreLoginComponentResult.denied(messageParsed);
preLoginEvent.setResult(reason);
break;
case Continue:
default:
Runnable asyncPremiumCheck = new AsyncPremiumCheck(
plugin, connection, username, continuation, preLoginEvent
);
plugin.getScheduler().runAsync(asyncPremiumCheck);
break;
}
}
@Subscribe
@ -98,16 +120,18 @@ public class ConnectListener {
}
if (!plugin.getCore().getConfig().get("forwardSkin", true)) {
event.setGameProfile(event.getGameProfile().withProperties(removeSkin(event.getGameProfile().getProperties())));
List<Property> newProp = removeSkin(event.getGameProfile().getProperties());
event.setGameProfile(event.getGameProfile().withProperties(newProp));
}
}
}
private List<GameProfile.Property> removeSkin(List<GameProfile.Property> oldProperties) {
List<GameProfile.Property> newProperties = new ArrayList<>(oldProperties.size() - 1);
private List<GameProfile.Property> removeSkin(Collection<Property> oldProperties) {
List<GameProfile.Property> newProperties = new ArrayList<>(oldProperties.size());
for (GameProfile.Property property : oldProperties) {
if (!"textures".equals(property.getName()))
if (!"textures".equals(property.getName())) {
newProperties.add(property);
}
}
return newProperties;

View File

@ -55,8 +55,9 @@ public class PluginMessageListener {
public PluginMessageListener(FastLoginVelocity plugin) {
this.plugin = plugin;
this.successChannel = MinecraftChannelIdentifier.create(plugin.getName(), SuccessMessage.SUCCESS_CHANNEL).getId();
this.changeChannel = MinecraftChannelIdentifier.create(plugin.getName(), ChangePremiumMessage.CHANGE_CHANNEL).getId();
String prefix = plugin.getName();
this.successChannel = MinecraftChannelIdentifier.create(prefix, SuccessMessage.SUCCESS_CHANNEL).getId();
this.changeChannel = MinecraftChannelIdentifier.create(prefix, ChangePremiumMessage.CHANGE_CHANNEL).getId();
}
@Subscribe
@ -95,8 +96,9 @@ public class PluginMessageListener {
String playerName = changeMessage.getPlayerName();
boolean isSourceInvoker = changeMessage.isSourceInvoker();
if (changeMessage.shouldEnable()) {
if (playerName.equals(forPlayer.getUsername()) && plugin.getCore().getConfig().get("premium-warning", true)
&& !core.getPendingConfirms().contains(forPlayer.getUniqueId())) {
Boolean premiumWarning = plugin.getCore().getConfig().get("premium-warning", true);
if (playerName.equals(forPlayer.getUsername()) && premiumWarning
&& !core.getPendingConfirms().contains(forPlayer.getUniqueId())) {
String message = core.getMessage("premium-warning");
forPlayer.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(message));
core.getPendingConfirms().add(forPlayer.getUniqueId());
@ -114,7 +116,7 @@ public class PluginMessageListener {
}
private void onSuccessMessage(Player forPlayer) {
if (forPlayer.isOnlineMode()){
if (forPlayer.isOnlineMode()) {
//bukkit module successfully received and force logged in the user
//update only on success to prevent corrupt data
VelocityLoginSession loginSession = plugin.getSession().get(forPlayer.getRemoteAddress());

Some files were not shown because too many files have changed in this diff Show More