Commit Graph

19 Commits

Author SHA1 Message Date
games647
e8bb3ec7a9 Validate other encryption methods 2022-06-28 18:34:24 +02:00
games647
1d46640b42 Limit length of server keys 2022-06-28 18:34:24 +02:00
games647
a0fddd69aa Decrease necessary entropy for running tests 2022-06-28 18:34:24 +02:00
games647
11077a002d Migrate public key to record 2022-06-28 18:34:24 +02:00
games647
d9bf7267a6 Test valid server key pairs 2022-06-28 18:34:23 +02:00
games647
b041a89209 Typo fixes 2022-06-28 18:34:23 +02:00
games647
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
games647
bc7c4eea60 Use PublicKey instead of byte representation 2022-06-28 18:34:22 +02:00
games647
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
games647
9ba6e9ba90 Add missing license header 2022-06-24 16:28:18 +02:00
games647
f182adc3bc Add support for RGB colors using the color char
Examples include:
`&x00002a00002b&lText`
2022-06-24 16:21:53 +02:00
Smart123s
a856356c49 Update license headers to 02db104
Done using `mvn license:format`
2022-03-13 10:08:57 +01:00
games647
52d778afb1 Clean up 2022-01-14 12:52:46 +01:00
games647
d6a02aa538 Apply license headers to every file
(Related #494)
2021-04-05 11:04:42 +02:00
games647
f40e787b55 Delay force action until PlayerJoinEvent is fired
Fixes Paper async chunk loading -> delayed PlayerJoinEvent
(Related #331)
2020-05-09 18:49:43 +02:00
games647
6f99b6e9b4 Add explicit Junit dependency 2020-03-20 16:13:45 +01:00
games647
3beb8beaeb Migrate tests to assertThat 2018-01-27 18:47:01 +01:00
games647
bb80521ab6 Thermos supports GSON so we could share the json parsing 2017-09-22 18:17:35 +02:00
games647
bb240d3aa0 Refactor encryption implementation
* Simplify utility class and make it more independent from the vendor code
* Create only one cipher object for verification
2017-08-28 12:17:47 +02:00