Migrate to Java 8 (sponsored contribution)

Java >8 includes many helpful features and API additions. Current
phoronix benchmarks indicated a very similar performance. Furthermore,
the fact that Java 18 even works with 1.8.8 contributed to the
decision to move to Java 17 like vanilla Java in 1.19.

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

Nevertheless, many server owners were frustrated about this decision.
Thanks to financial contribution, we revised this decision until Java 8
is end of life or no longer used actively used according to bstats.org
This commit is contained in:
games647
2022-07-22 13:26:52 +02:00
13 changed files with 92 additions and 66 deletions

View File

@ -50,7 +50,7 @@ import java.time.Instant;
import java.util.Arrays;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.util.random.RandomGenerator;
import java.util.Random;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
@ -59,6 +59,8 @@ import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import lombok.val;
/**
* Encryption and decryption minecraft util for connection between servers
* and paid Minecraft account clients.
@ -85,7 +87,7 @@ final class EncryptionUtil {
}
private EncryptionUtil() {
// utility
throw new RuntimeException("No instantiation of utility classes allowed");
}
/**
@ -112,7 +114,7 @@ final class EncryptionUtil {
* @param random random generator
* @return a token with 4 bytes long
*/
public static byte[] generateVerifyToken(RandomGenerator random) {
public static byte[] generateVerifyToken(Random random) {
byte[] token = new byte[VERIFY_TOKEN_LENGTH];
random.nextBytes(token);
return token;
@ -177,9 +179,9 @@ final class EncryptionUtil {
private static PublicKey loadMojangSessionKey()
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
var keyUrl = FastLoginBukkit.class.getClassLoader().getResource("yggdrasil_session_pubkey.der");
var keyData = Resources.toByteArray(keyUrl);
var keySpec = new X509EncodedKeySpec(keyData);
val keyUrl = FastLoginBukkit.class.getClassLoader().getResource("yggdrasil_session_pubkey.der");
val keyData = Resources.toByteArray(keyUrl);
val keySpec = new X509EncodedKeySpec(keyData);
return KeyFactory.getInstance("RSA").generatePublic(keySpec);
}

View File

@ -56,6 +56,7 @@ import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import lombok.var;
import org.bukkit.entity.Player;
import static com.comphenix.protocol.PacketType.Login.Client.ENCRYPTION_BEGIN;
@ -171,7 +172,7 @@ public class ProtocolLibListener extends PacketAdapter {
Either<byte[], ?> either = packet.getSpecificModifier(Either.class).read(0);
if (clientPublicKey == null) {
Optional<byte[]> left = either.left();
if (left.isEmpty()) {
if (!left.isPresent()) {
plugin.getLog().error("No verify token sent if requested without player signed key {}", sender);
return false;
}
@ -179,7 +180,7 @@ public class ProtocolLibListener extends PacketAdapter {
return EncryptionUtil.verifyNonce(expectedToken, keyPair.getPrivate(), left.get());
} else {
Optional<?> optSignatureData = either.right();
if (optSignatureData.isEmpty()) {
if (!optSignatureData.isPresent()) {
plugin.getLog().error("No signature given to sent player signing key {}", sender);
return false;
}
@ -219,7 +220,7 @@ public class ProtocolLibListener extends PacketAdapter {
.optionRead(0);
var clientKey = profileKey.flatMap(opt -> opt).flatMap(this::verifyPublicKey);
if (verifyClientKeys && clientKey.isEmpty()) {
if (verifyClientKeys && !clientKey.isPresent()) {
// missing or incorrect
// expired always not allowed
player.kickPlayer(plugin.getCore().getMessage("invalid-public-key"));
@ -240,7 +241,7 @@ public class ProtocolLibListener extends PacketAdapter {
Instant expires = profileKey.getExpireTime();
PublicKey key = profileKey.getKey();
byte[] signature = profileKey.getSignature();
ClientPublicKey clientKey = new ClientPublicKey(expires, key, signature);
ClientPublicKey clientKey = ClientPublicKey.of(expires, key, signature);
try {
if (EncryptionUtil.verifyClientKey(clientKey, Instant.now())) {
return Optional.of(clientKey);

View File

@ -60,6 +60,7 @@ import java.util.UUID;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import lombok.val;
import org.bukkit.entity.Player;
import static com.comphenix.protocol.PacketType.Login.Client.START;
@ -268,7 +269,7 @@ public class VerifyResponseTask implements Runnable {
startPacket.getStrings().write(0, username);
EquivalentConverter<WrappedProfileKeyData> converter = BukkitConverters.getWrappedPublicKeyDataConverter();
var wrappedKey = Optional.ofNullable(clientKey).map(key ->
val wrappedKey = Optional.ofNullable(clientKey).map(key ->
new WrappedProfileKeyData(clientKey.expiry(), clientKey.key(), clientKey.signature())
);

View File

@ -28,7 +28,15 @@ 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) {
import lombok.Value;
import lombok.experimental.Accessors;
@Accessors(fluent = true)
@Value(staticConstructor = "of")
public class ClientPublicKey {
Instant expiry;
PublicKey key;
byte[] signature;
public boolean isExpired(Instant verifyTimestamp) {
return !verifyTimestamp.isBefore(expiry);

View File

@ -30,6 +30,7 @@ import com.github.games647.fastlogin.core.CommonUtil;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.chat.ComponentSerializer;
import lombok.val;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -38,13 +39,12 @@ class FastLoginBukkitTest {
@Test
void testRGB() {
var message = "&x00002a00002b&lText";
var msg = CommonUtil.translateColorCodes(message);
val message = "&x00002a00002b&lText";
val msg = CommonUtil.translateColorCodes(message);
assertEquals(msg, "§x00002a00002b§lText");
var components = TextComponent.fromLegacyText(msg);
var expected = """
{"bold":true,"color":"#00a00b","text":"Text"}""";
val components = TextComponent.fromLegacyText(msg);
val expected = "{\"bold\":true,\"color\":\"#00a00b\",\"text\":\"Text\"}";
assertEquals(ComponentSerializer.toString(components), expected);
}
}

View File

@ -32,11 +32,13 @@ import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.util.Base64;
import lombok.val;
public class Base64Adapter extends TypeAdapter<byte[]> {
@Override
public void write(JsonWriter out, byte[] value) throws IOException {
var encoded = Base64.getEncoder().encodeToString(value);
val encoded = Base64.getEncoder().encodeToString(value);
out.value(encoded);
}

View File

@ -50,6 +50,7 @@ import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import lombok.val;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
@ -60,7 +61,7 @@ class EncryptionUtilTest {
@Test
void testVerifyToken() {
var random = ThreadLocalRandom.current();
val random = ThreadLocalRandom.current();
byte[] token = EncryptionUtil.generateVerifyToken(random);
assertAll(
@ -88,10 +89,10 @@ class EncryptionUtilTest {
@Test
void testExpiredClientKey() throws Exception {
var clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
val clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
// Client expires at the exact second mentioned, so use it for verification
var expiredTimestamp = clientKey.expiry();
val expiredTimestamp = clientKey.expiry();
assertFalse(EncryptionUtil.verifyClientKey(clientKey, expiredTimestamp));
}
@ -105,7 +106,7 @@ class EncryptionUtilTest {
"client_keys/invalid_wrong_signature.json"
})
void testInvalidClientKey(String clientKeySource) throws Exception {
var clientKey = ResourceLoader.loadClientKey(clientKeySource);
val clientKey = ResourceLoader.loadClientKey(clientKeySource);
Instant expireTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS);
assertFalse(EncryptionUtil.verifyClientKey(clientKey, expireTimestamp));
@ -113,8 +114,8 @@ class EncryptionUtilTest {
@Test
void testValidClientKey() throws Exception {
var clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
var verificationTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS);
val clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
val verificationTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS);
assertTrue(EncryptionUtil.verifyClientKey(clientKey, verificationTimestamp));
}
@ -122,7 +123,7 @@ class EncryptionUtilTest {
@Test
void testDecryptSharedSecret() throws Exception {
KeyPair serverPair = EncryptionUtil.generateKeyPair();
var serverPK = serverPair.getPublic();
val serverPK = serverPair.getPublic();
SecretKey secretKey = generateSharedKey();
byte[] encryptedSecret = encrypt(serverPK, secretKey.getEncoded());
@ -134,7 +135,7 @@ class EncryptionUtilTest {
private static byte[] encrypt(PublicKey receiverKey, byte... message)
throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
IllegalBlockSizeException, BadPaddingException {
var encryptCipher = Cipher.getInstance(receiverKey.getAlgorithm());
val encryptCipher = Cipher.getInstance(receiverKey.getAlgorithm());
encryptCipher.init(Cipher.ENCRYPT_MODE, receiverKey);
return encryptCipher.doFinal(message);
}
@ -150,9 +151,9 @@ class EncryptionUtilTest {
@Test
void testServerIdHash() throws Exception {
var serverId = "";
var sharedSecret = generateSharedKey();
var serverPK = ResourceLoader.loadClientKey("client_keys/valid_public_key.json").key();
val serverId = "";
val sharedSecret = generateSharedKey();
val serverPK = ResourceLoader.loadClientKey("client_keys/valid_public_key.json").key();
String sessionHash = getServerHash(serverId, sharedSecret, serverPK);
assertEquals(EncryptionUtil.getServerIdHashString(serverId, sharedSecret, serverPK), sessionHash);
@ -166,7 +167,7 @@ class EncryptionUtilTest {
// sha1.update(server's encoded public key from Encryption Request)
// hash := sha1.hexdigest() # String of hex characters
@SuppressWarnings("deprecation")
var hasher = Hashing.sha1().newHasher();
val hasher = Hashing.sha1().newHasher();
hasher.putString(serverId, StandardCharsets.US_ASCII);
hasher.putBytes(sharedSecret.getEncoded());
hasher.putBytes(serverPK.getEncoded());
@ -179,9 +180,9 @@ class EncryptionUtilTest {
@Test
void testServerIdHashWrongSecret() throws Exception {
var serverId = "";
var sharedSecret = generateSharedKey();
var serverPK = ResourceLoader.loadClientKey("client_keys/valid_public_key.json").key();
val serverId = "";
val sharedSecret = generateSharedKey();
val serverPK = ResourceLoader.loadClientKey("client_keys/valid_public_key.json").key();
String sessionHash = getServerHash(serverId, sharedSecret, serverPK);
assertNotEquals(EncryptionUtil.getServerIdHashString("", generateSharedKey(), serverPK), sessionHash);
@ -189,12 +190,12 @@ class EncryptionUtilTest {
@Test
void testServerIdHashWrongServerKey() {
var serverId = "";
var sharedSecret = generateSharedKey();
var serverPK = EncryptionUtil.generateKeyPair().getPublic();
val serverId = "";
val sharedSecret = generateSharedKey();
val serverPK = EncryptionUtil.generateKeyPair().getPublic();
String sessionHash = getServerHash(serverId, sharedSecret, serverPK);
var wrongPK = EncryptionUtil.generateKeyPair().getPublic();
val wrongPK = EncryptionUtil.generateKeyPair().getPublic();
assertNotEquals(EncryptionUtil.getServerIdHashString("", sharedSecret, wrongPK), sessionHash);
}
@ -238,8 +239,8 @@ class EncryptionUtilTest {
@Test
void testNonce() throws Exception {
byte[] expected = {1, 2, 3, 4};
var serverKey = EncryptionUtil.generateKeyPair();
var encryptedNonce = encrypt(serverKey.getPublic(), expected);
val serverKey = EncryptionUtil.generateKeyPair();
val encryptedNonce = encrypt(serverKey.getPublic(), expected);
assertTrue(EncryptionUtil.verifyNonce(expected, serverKey.getPrivate(), encryptedNonce));
}
@ -247,19 +248,19 @@ class EncryptionUtilTest {
@Test
void testNonceIncorrect() throws Exception {
byte[] expected = {1, 2, 3, 4};
var serverKey = EncryptionUtil.generateKeyPair();
val serverKey = EncryptionUtil.generateKeyPair();
// flipped first character
var encryptedNonce = encrypt(serverKey.getPublic(), new byte[]{0, 2, 3, 4});
val encryptedNonce = encrypt(serverKey.getPublic(), new byte[]{0, 2, 3, 4});
assertFalse(EncryptionUtil.verifyNonce(expected, serverKey.getPrivate(), encryptedNonce));
}
@Test
void testNonceFailedDecryption() throws Exception {
byte[] expected = {1, 2, 3, 4};
var serverKey = EncryptionUtil.generateKeyPair();
val serverKey = EncryptionUtil.generateKeyPair();
// generate a new keypair that is different
var encryptedNonce = encrypt(EncryptionUtil.generateKeyPair().getPublic(), expected);
val encryptedNonce = encrypt(EncryptionUtil.generateKeyPair().getPublic(), expected);
assertThrows(GeneralSecurityException.class,
() -> EncryptionUtil.verifyNonce(expected, serverKey.getPrivate(), encryptedNonce)
@ -269,7 +270,7 @@ class EncryptionUtilTest {
@Test
void testNonceIncorrectEmpty() {
byte[] expected = {1, 2, 3, 4};
var serverKey = EncryptionUtil.generateKeyPair();
val serverKey = EncryptionUtil.generateKeyPair();
byte[] encryptedNonce = {};
assertThrows(GeneralSecurityException.class,

View File

@ -33,6 +33,7 @@ import com.google.gson.JsonObject;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
@ -57,26 +58,26 @@ public class ResourceLoader {
) {
PemObject pemObject = pemReader.readPemObject();
byte[] content = pemObject.getContent();
var privateKeySpec = new PKCS8EncodedKeySpec(content);
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(content);
var factory = KeyFactory.getInstance("RSA");
KeyFactory factory = KeyFactory.getInstance("RSA");
return (RSAPrivateKey) factory.generatePrivate(privateKeySpec);
}
}
protected static ClientPublicKey loadClientKey(String path)
throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
var keyUrl = Resources.getResource(path);
URL keyUrl = Resources.getResource(path);
var lines = Resources.toString(keyUrl, StandardCharsets.US_ASCII);
var object = new Gson().fromJson(lines, JsonObject.class);
String lines = Resources.toString(keyUrl, StandardCharsets.US_ASCII);
JsonObject object = new Gson().fromJson(lines, JsonObject.class);
Instant expires = Instant.parse(object.getAsJsonPrimitive("expires_at").getAsString());
String key = object.getAsJsonPrimitive("key").getAsString();
RSAPublicKey publicKey = parsePublicKey(key);
byte[] signature = Base64.getDecoder().decode(object.getAsJsonPrimitive("signature").getAsString());
return new ClientPublicKey(expires, publicKey, signature);
return ClientPublicKey.of(expires, publicKey, signature);
}
private static RSAPublicKey parsePublicKey(String keySpec)
@ -87,9 +88,9 @@ public class ResourceLoader {
) {
PemObject pemObject = pemReader.readPemObject();
byte[] content = pemObject.getContent();
var pubKeySpec = new X509EncodedKeySpec(content);
X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(content);
var factory = KeyFactory.getInstance("RSA");
KeyFactory factory = KeyFactory.getInstance("RSA");
return (RSAPublicKey) factory.generatePublic(pubKeySpec);
}
}

View File

@ -32,11 +32,13 @@ import com.google.gson.annotations.JsonAdapter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import lombok.val;
public class SignatureTestData {
public static SignatureTestData fromResource(String resourceName) throws IOException {
var keyUrl = Resources.getResource(resourceName);
var encodedSignature = Resources.toString(keyUrl, StandardCharsets.US_ASCII);
val keyUrl = Resources.getResource(resourceName);
val encodedSignature = Resources.toString(keyUrl, StandardCharsets.US_ASCII);
return new Gson().fromJson(encodedSignature, SignatureTestData.class);
}

View File

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

View File

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

View File

@ -118,10 +118,11 @@ public class StoredProfile extends Profile {
return true;
}
if (!(o instanceof StoredProfile that)) {
if (!(o instanceof StoredProfile)) {
return false;
}
StoredProfile that = (StoredProfile) o;
if (!super.equals(o)) {
return false;
}

View File

@ -48,7 +48,7 @@
<!-- Set default for non-git clones -->
<git.commit.id>Unknown</git.commit.id>
<java.version>17</java.version>
<java.version>8</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
@ -189,5 +189,12 @@
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>