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 +205,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.
- *
+ *
* 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.
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 3121cbca..8b5dad60 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
@@ -117,7 +117,7 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin getPlaceholders() {
+ return List.of(PLACEHOLDER_VARIABLE);
+ }
+
@Override
public String onRequest(OfflinePlayer player, @NotNull String identifier) {
// player is null if offline
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 a6ac9b7b..61533387 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 26b6d312..9f1ef982 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 8c61330b..6412c6d8 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 6b2edfca..5bf6df99 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 146efb07..0904826d 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 6003c02c..45d5a5f4 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,26 +28,28 @@ 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: https://github.com/Xephi/AuthMeReloaded/
+ * GitHub: ...
*
* Project page:
*
- * Bukkit: https://dev.bukkit.org/bukkit-plugins/authme-reloaded/
+ * Bukkit: ...
*
- * Spigot: https://www.spigotmc.org/resources/authme-reloaded.6269/
+ * Spigot: ...
*/
public class AuthMeHook implements AuthPlugin, Listener {
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/CrazyLoginHook.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/CrazyLoginHook.java
index bb4d1804..f0680293 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/CrazyLoginHook.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/CrazyLoginHook.java
@@ -43,11 +43,11 @@ import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
/**
- * GitHub: https://github.com/ST-DDT/CrazyLogin
+ * GitHub: ...
*
* Project page:
*
- * Bukkit: https://dev.bukkit.org/server-mods/crazylogin/
+ * Bukkit: ...
*/
public class CrazyLoginHook implements AuthPlugin {
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/LogItHook.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/LogItHook.java
index cccfffd3..3868a62a 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/LogItHook.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/LogItHook.java
@@ -38,7 +38,7 @@ import java.time.Instant;
import org.bukkit.entity.Player;
/**
- * GitHub: https://github.com/XziomekX/LogIt
+ * GitHub: ...
*
* Project page:
*
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/LoginSecurityHook.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/LoginSecurityHook.java
index 5467e434..f4b134a9 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/LoginSecurityHook.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/LoginSecurityHook.java
@@ -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: ...
*
* Project page:
*
- * Bukkit: https://dev.bukkit.org/bukkit-plugins/loginsecurity/
+ * Bukkit: ...
*
- * Spigot: https://www.spigotmc.org/resources/loginsecurity.19362/
+ * Spigot: ...
*/
public class LoginSecurityHook implements AuthPlugin {
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/UltraAuthHook.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/UltraAuthHook.java
index 08a23d99..9b687a24 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/UltraAuthHook.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/UltraAuthHook.java
@@ -39,9 +39,9 @@ import ultraauth.managers.PlayerManager;
/**
* Project page:
*
- * Bukkit: https://dev.bukkit.org/bukkit-plugins/ultraauth-aa/
+ * Bukkit: ...
*
- * Spigot: https://www.spigotmc.org/resources/ultraauth.17044/
+ * Spigot: ...
*/
public class UltraAuthHook implements AuthPlugin {
diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/xAuthHook.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/xAuthHook.java
index 58280bb4..9f6cab6c 100644
--- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/xAuthHook.java
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/hook/xAuthHook.java
@@ -38,11 +38,11 @@ import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
/**
- * GitHub: https://github.com/LycanDevelopment/xAuth/
+ * GitHub: ...
*
* Project page:
*
- * Bukkit: https://dev.bukkit.org/bukkit-plugins/xauth/
+ * Bukkit: ...
*/
public class xAuthHook implements AuthPlugin {
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 e444ed92..44ff6ea0 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/EncryptionUtil.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionUtil.java
index 1197514b..844b92c6 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,32 +25,63 @@
*/
package com.github.games647.fastlogin.bukkit.listener.protocollib;
+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.util.Random;
+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.RandomGenerator;
+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;
/**
* Encryption and decryption minecraft util for connection between servers
* and paid Minecraft account clients.
- *
- * @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
}
@@ -61,11 +92,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
@@ -80,8 +110,7 @@ public class EncryptionUtil {
* @param random random generator
* @return an error with 4 bytes long
*/
- public static byte[] generateVerifyToken(Random random) {
- // extracted from LoginListener
+ public static byte[] generateVerifyToken(RandomGenerator random) {
byte[] token = new byte[VERIFY_TOKEN_LENGTH];
random.nextBytes(token);
return token;
@@ -90,68 +119,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(mojangSessionKey);
+ 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[] exptected, PrivateKey decryptionKey, byte[] encryptedNonce)
+ throws NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException,
+ BadPaddingException, InvalidKeyException {
+ byte[] decryptedNonce = decrypt(decryptionKey, encryptedNonce);
+ return Arrays.equals(exptected, 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 {
+ 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.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) {
+ 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();
}
}
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 78a8c474..2a36e88a 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,15 +37,13 @@ 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.
*
* This is used as a workaround, because Floodgate fails to inject
* the prefixes when it's used together with ProtocolLib and FastLogin.
*
- * For more information visit: https://github.com/games647/FastLogin/issues/493
+ * For more information visit: ...
*/
public class ManualNameChange extends PacketAdapter {
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 f0a40830..a135116f 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
@@ -30,6 +30,7 @@ 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;
@@ -41,11 +42,13 @@ import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
public class NameCheckTask extends JoinManagement
- implements Runnable {
+ implements Runnable {
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 +56,13 @@ public class NameCheckTask extends JoinManagement val.equals(plugin.getName()))
.orElse(false);
@@ -130,13 +150,56 @@ 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);
+ 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);
+ plugin.getScheduler().runAsync(verifyTask);
+ } else {
+ sender.kickPlayer(plugin.getCore().getMessage("invalid-verify-token"));
+ }
}
}
- private void onLogin(PacketEvent packetEvent, Player player, String username) {
+ private boolean verifyNonce(Player sender, PacketContainer packet,
+ ClientPublicKey clientPublicKey, byte[] expectedToken) {
+ try {
+ if (MinecraftVersion.atOrAbove(new MinecraftVersion(1, 19, 0))) {
+ Either either = packet.getSpecificModifier(Either.class).read(0);
+ if (clientPublicKey == null) {
+ Optional left = either.left();
+ if (left.isEmpty()) {
+ 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.isEmpty()) {
+ 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();
@@ -148,10 +211,49 @@ public class ProtocolLibListener extends PacketAdapter {
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.isEmpty()) {
+ // 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());
+ Runnable nameCheckTask = new NameCheckTask(plugin, random, player, packetEvent, username, clientKey.orElse(null), keyPair.getPublic());
plugin.getScheduler().runAsync(nameCheckTask);
}
+
+ private Optional verifyPublicKey(WrappedProfileKeyData profileKey) {
+ Instant expires = profileKey.getExpireTime();
+ PublicKey key = profileKey.getKey();
+ byte[] signature = profileKey.getSignature();
+ ClientPublicKey clientKey = new ClientPublicKey(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();
+ }
}
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 dd191f1c..71b4c3ba 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,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) {
+ public 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;
}
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 7d43835d..3d4a807b 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/listener/protocollib/VerifyResponseTask.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java
index 54d525c2..c99a3cdf 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,26 +28,27 @@ 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.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.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;
@@ -123,7 +124,7 @@ public class VerifyResponseTask implements Runnable {
}
try {
- if (!checkVerifyToken(session) || !enableEncryption(loginKey)) {
+ if (!enableEncryption(loginKey)) {
return;
}
} catch (Exception ex) {
@@ -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) {
@@ -183,23 +184,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);
@@ -262,33 +246,32 @@ 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 converter = BukkitConverters.getWrappedPublicKeyDataConverter();
+ var key = new WrappedProfileKeyData(clientKey.expiry(), clientKey.key(), sharedSecret);
+ startPacket.getOptionals(converter).write(0, Optional.ofNullable(key));
+ } 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);
}
}
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 00000000..e375e294
--- /dev/null
+++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/packet/ClientPublicKey.java
@@ -0,0 +1,36 @@
+/*
+ * 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.security.PublicKey;
+import java.time.Instant;
+
+public record ClientPublicKey(Instant expiry, PublicKey key, byte[] signature) {
+
+ public boolean isExpired(Instant verifyTimestamp) {
+ return !verifyTimestamp.isBefore(expiry);
+ }
+}
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 acb35976..ef9fed0a 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 {
public FloodgateAuthTask(FastLoginCore core, Player player, FloodgatePlayer floodgatePlayer) {
diff --git a/bukkit/src/main/resources/yggdrasil_session_pubkey.der b/bukkit/src/main/resources/yggdrasil_session_pubkey.der
new file mode 100644
index 00000000..9c79a3aa
Binary files /dev/null and b/bukkit/src/main/resources/yggdrasil_session_pubkey.der differ
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 00000000..c2a2d1a4
--- /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 {
+
+ @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 7a097f1d..3a8adbfb 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,22 +25,273 @@
*/
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 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 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 org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+
public class EncryptionUtilTest {
@Test
public void testVerifyToken() {
- SecureRandom random = new SecureRandom();
+ var random = ThreadLocalRandom.current();
byte[] token = EncryptionUtil.generateVerifyToken(random);
assertThat(token, notNullValue());
assertThat(token.length, is(4));
}
+
+ @Test
+ public void testServerKey() {
+ KeyPair keyPair = EncryptionUtil.generateKeyPair();
+
+ Key privateKey = keyPair.getPrivate();
+ assertThat(privateKey.getAlgorithm(), is("RSA"));
+
+ RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
+ assertThat(publicKey.getAlgorithm(), is("RSA"));
+
+ // clients accept larger values than the standard vanilla server, but we shouldn't crash them
+ assertTrue(publicKey.getModulus().bitLength() >= 1024);
+ assertTrue(publicKey.getModulus().bitLength() < 8192);
+ }
+
+ @Test
+ public void testExpiredClientKey() throws Exception {
+ var 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();
+ assertThat(EncryptionUtil.verifyClientKey(clientKey, expiredTimestamp), is(false));
+ }
+
+ @Test
+ public void testInvalidChangedExpiration() throws Exception {
+ // expiration date changed should make the signature invalid
+ // expiration should still be valid
+ var clientKey = ResourceLoader.loadClientKey("client_keys/invalid_wrong_expiration.json");
+ Instant expireTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS);
+
+ assertThat(EncryptionUtil.verifyClientKey(clientKey, expireTimestamp), is(false));
+ }
+
+ @Test
+ public void testInvalidChangedKey() throws Exception {
+ // changed public key no longer corresponding to the signature
+ var clientKey = ResourceLoader.loadClientKey("client_keys/invalid_wrong_key.json");
+ Instant expireTimestamp = clientKey.expiry().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 = ResourceLoader.loadClientKey("client_keys/invalid_wrong_signature.json");
+ Instant expireTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS);
+
+ assertThat(EncryptionUtil.verifyClientKey(clientKey, expireTimestamp), is(false));
+ }
+
+ @Test
+ public void testValidClientKey() throws Exception {
+ var clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
+ var verificationTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS);
+
+ assertThat(EncryptionUtil.verifyClientKey(clientKey, verificationTimestamp), is(true));
+ }
+
+ @Test
+ public void testDecryptSharedSecret() throws Exception {
+ KeyPair serverPair = EncryptionUtil.generateKeyPair();
+ var serverPK = serverPair.getPublic();
+
+ SecretKey secretKey = generateSharedKey();
+ byte[] encryptedSecret = encrypt(serverPK, secretKey.getEncoded());
+
+ SecretKey decryptSharedKey = EncryptionUtil.decryptSharedKey(serverPair.getPrivate(), encryptedSecret);
+ assertThat(decryptSharedKey, is(secretKey));
+ }
+
+ private static byte[] encrypt(PublicKey receiverKey, byte... message)
+ throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
+ IllegalBlockSizeException, BadPaddingException {
+ var 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
+ public void testServerIdHash() throws Exception {
+ var serverId = "";
+ var sharedSecret = generateSharedKey();
+ var serverPK = ResourceLoader.loadClientKey("client_keys/valid_public_key.json").key();
+
+ String sessionHash = getServerHash(serverId, sharedSecret, serverPK);
+ assertThat(EncryptionUtil.getServerIdHashString(serverId, sharedSecret, serverPK), is(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")
+ var 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
+ public void testServerIdHashWrongSecret() throws Exception {
+ var serverId = "";
+ var sharedSecret = generateSharedKey();
+ var serverPK = ResourceLoader.loadClientKey("client_keys/valid_public_key.json").key();
+
+ String sessionHash = getServerHash(serverId, sharedSecret, serverPK);
+ assertThat(EncryptionUtil.getServerIdHashString("", generateSharedKey(), serverPK), not(sessionHash));
+ }
+
+ @Test
+ public void testServerIdHashWrongServerKey() {
+ var serverId = "";
+ var sharedSecret = generateSharedKey();
+ var serverPK = EncryptionUtil.generateKeyPair().getPublic();
+
+ String sessionHash = getServerHash(serverId, sharedSecret, serverPK);
+ var wrongPK = EncryptionUtil.generateKeyPair().getPublic();
+ assertThat(EncryptionUtil.getServerIdHashString("", sharedSecret, wrongPK), not(sessionHash));
+ }
+
+ @Test
+ public void testValidSignedNonce() throws Exception {
+ ClientPublicKey clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
+ SignatureTestData testData = SignatureTestData.fromResource("signature/valid_signature.json");
+ assertThat(verifySignedNonce(testData, clientKey), is(true));
+ }
+
+ @Test
+ public void testIncorrectNonce() throws Exception {
+ ClientPublicKey clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
+ SignatureTestData testData = SignatureTestData.fromResource("signature/incorrect_nonce.json");
+ assertThat(verifySignedNonce(testData, clientKey), is(false));
+ }
+
+ @Test
+ public void testIncorrectSalt() throws Exception {
+ // client generated
+ ClientPublicKey clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
+ SignatureTestData testData = SignatureTestData.fromResource("signature/incorrect_salt.json");
+ assertThat(verifySignedNonce(testData, clientKey), is(false));
+ }
+
+ @Test
+ public void testIncorrectSignature() throws Exception {
+ // client generated
+ ClientPublicKey clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
+ SignatureTestData testData = SignatureTestData.fromResource("signature/incorrect_signature.json");
+ assertThat(verifySignedNonce(testData, clientKey), is(false));
+ }
+
+ @Test
+ public 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");
+ assertThat(verifySignedNonce(testData, clientKey), is(false));
+ }
+
+ 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
+ public void testNonce() throws Exception {
+ byte[] expected = {1, 2, 3, 4};
+ var serverKey = EncryptionUtil.generateKeyPair();
+ var encryptedNonce = encrypt(serverKey.getPublic(), expected);
+
+ assertThat(EncryptionUtil.verifyNonce(expected, serverKey.getPrivate(), encryptedNonce), is(true));
+ }
+
+ @Test
+ public void testNonceIncorrect() throws Exception {
+ byte[] expected = {1, 2, 3, 4};
+ var serverKey = EncryptionUtil.generateKeyPair();
+
+ // flipped first character
+ var encryptedNonce = encrypt(serverKey.getPublic(), new byte[]{0, 2, 3 , 4});
+
+ assertThat(EncryptionUtil.verifyNonce(expected, serverKey.getPrivate(), encryptedNonce), is(false));
+ }
+
+ @Test(expected = GeneralSecurityException.class)
+ public void testNonceFailedDecryption() throws Exception {
+ byte[] expected = {1, 2, 3, 4};
+ var serverKey = EncryptionUtil.generateKeyPair();
+ // generate a new keypair that iss different
+ var encryptedNonce = encrypt(EncryptionUtil.generateKeyPair().getPublic(), expected);
+
+ EncryptionUtil.verifyNonce(expected, serverKey.getPrivate(), encryptedNonce);
+ }
+
+ @Test(expected = GeneralSecurityException.class)
+ public void testNonceIncorrectEmpty() throws Exception {
+ byte[] expected = {1, 2, 3, 4};
+ var serverKey = EncryptionUtil.generateKeyPair();
+ byte[] encryptedNonce = {};
+
+ assertThat(EncryptionUtil.verifyNonce(expected, serverKey.getPrivate(), encryptedNonce), is(false));
+ }
}
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
new file mode 100644
index 00000000..4361fe9b
--- /dev/null
+++ b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ResourceLoader.java
@@ -0,0 +1,96 @@
+/*
+ * 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.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();
+ var privateKeySpec = new PKCS8EncodedKeySpec(content);
+
+ var factory = KeyFactory.getInstance("RSA");
+ return (RSAPrivateKey) factory.generatePrivate(privateKeySpec);
+ }
+ }
+
+ protected static ClientPublicKey loadClientKey(String path)
+ throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
+ var keyUrl = Resources.getResource(path);
+
+ var lines = Resources.toString(keyUrl, StandardCharsets.US_ASCII);
+ var 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 new ClientPublicKey(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();
+ var pubKeySpec = new X509EncodedKeySpec(content);
+
+ var 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
new file mode 100644
index 00000000..ee62e242
--- /dev/null
+++ b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/SignatureTestData.java
@@ -0,0 +1,72 @@
+/*
+ * 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;
+
+public class SignatureTestData {
+
+ public static SignatureTestData fromResource(String resourceName) throws IOException {
+ var keyUrl = Resources.getResource(resourceName);
+ var 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;
+ }
+ }
+}
diff --git a/bukkit/src/test/java/integration/README.md b/bukkit/src/test/java/integration/README.md
new file mode 100644
index 00000000..d4efc537
--- /dev/null
+++ b/bukkit/src/test/java/integration/README.md
@@ -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.
+
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 00000000..78882233
--- /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 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
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 00000000..ea509fec
--- /dev/null
+++ b/bukkit/src/test/resources/client_keys/invalid_wrong_expiration.json
@@ -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="
+}
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 00000000..98ea33f5
--- /dev/null
+++ b/bukkit/src/test/resources/client_keys/invalid_wrong_key.json
@@ -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="
+}
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 00000000..2b80c2c9
--- /dev/null
+++ b/bukkit/src/test/resources/client_keys/invalid_wrong_signature.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/client_keys/valid_public_key.json b/bukkit/src/test/resources/client_keys/valid_public_key.json
new file mode 100644
index 00000000..8943e87b
--- /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 00000000..401142fa
--- /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 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`
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 00000000..b88c0f5d
--- /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 00000000..8edffb62
--- /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 00000000..ba6bac53
--- /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 00000000..7f4f4ad5
--- /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
+ }
+}
diff --git a/bungee/pom.xml b/bungee/pom.xml
index ee0d6d89..b7645231 100644
--- a/bungee/pom.xml
+++ b/bungee/pom.xml
@@ -32,7 +32,7 @@
com.github.games647
fastlogin
- 1.11-SNAPSHOT
+ 1.12-SNAPSHOT
../pom.xml
@@ -137,6 +137,42 @@
org.slf4j
slf4j-api