diff --git a/README.md b/README.md index d5847c54..64a28e09 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Possible values: `Premium`, `Cracked`, `Unknown` * Server software in offlinemode: * Spigot (or a fork e.g. Paper) 1.8.8+ * Protocol plugin: - * [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/) or + * [ProtocolLib 5.0+](https://www.spigotmc.org/resources/protocollib.1997/) or * [ProtocolSupport](https://www.spigotmc.org/resources/protocolsupport.7201/) * Latest BungeeCord (or a fork e.g. Waterfall) * An auth plugin. 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 53189ea8..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 @@ -47,6 +47,7 @@ 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; @@ -142,9 +143,9 @@ class EncryptionUtil { return new SecretKeySpec(decrypt(privateKey, sharedKey), "AES"); } - public static boolean verifyClientKey(ClientPublicKey clientKey, Instant verifyTimstamp) + public static boolean verifyClientKey(ClientPublicKey clientKey, Instant verifyTimestamp) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { - if (clientKey.isExpired(verifyTimstamp)) { + if (clientKey.isExpired(verifyTimestamp)) { return false; } @@ -155,6 +156,13 @@ class EncryptionUtil { return verifier.verify(clientKey.signature()); } + 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"); diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java index ebcbed6e..d3acfbe1 100644 --- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/ProtocolLibListener.java @@ -31,6 +31,7 @@ import com.comphenix.protocol.events.PacketAdapter; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.utility.MinecraftVersion; import com.comphenix.protocol.wrappers.BukkitConverters; import com.comphenix.protocol.wrappers.WrappedGameProfile; import com.comphenix.protocol.wrappers.WrappedProfilePublicKey.WrappedProfileKeyData; @@ -51,6 +52,10 @@ import java.security.SignatureException; import java.time.Instant; import java.util.Optional; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; + import org.bukkit.entity.Player; import static com.comphenix.protocol.PacketType.Login.Client.ENCRYPTION_BEGIN; @@ -145,32 +150,52 @@ 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 { - if (session.getClientPublicKey() == null) { - // we cannot verify the signature if no public was provided - plugin.getLog().error("No public key provided for signed nonce {}", sender); + 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")); - return; } + } + } - Either either = packetEvent.getPacket().getSpecificModifier(Either.class).read(0); - Object signatureData = either.right().get(); - long salt = FuzzyReflection.getFieldValue(signatureData, Long.TYPE, true); - byte[] signature = FuzzyReflection.getFieldValue(signatureData, byte[].class, true); + 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; + } - PublicKey publicKey = session.getClientPublicKey().key(); - try { - if (EncryptionUtil.verifySignedNonce(session.getVerifyToken(), publicKey, salt, signature)) { - packetEvent.getAsyncMarker().incrementProcessingDelay(); - Runnable verifyTask = new VerifyResponseTask(plugin, packetEvent, sender, session, sharedSecret, keyPair); - plugin.getScheduler().runAsync(verifyTask); + return EncryptionUtil.verifyNonce(expectedToken, keyPair.getPrivate(), left.get()); } else { - sender.kickPlayer(plugin.getCore().getMessage("invalid-verify-token")); - plugin.getLog().error("Invalid signature from player {}", sender); + 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); } - } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException signatureEx) { - sender.kickPlayer(plugin.getCore().getMessage("invalid-verify-token")); - plugin.getLog().error("Invalid signature from player {}", sender, signatureEx); + } 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; } } @@ -188,10 +213,10 @@ public class ProtocolLibListener extends PacketAdapter { PacketContainer packet = packetEvent.getPacket(); var profileKey = packet.getOptionals(BukkitConverters.getWrappedPublicKeyDataConverter()) - .read(0); + .optionRead(0); - var clientKey = profileKey.flatMap(this::verifyPublicKey); - if (verifyClientKeys && !clientKey.isPresent()) { + 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")); 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 34579b30..d8a4f5b2 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 @@ -28,29 +28,19 @@ package com.github.games647.fastlogin.bukkit.listener.protocollib; import com.github.games647.fastlogin.bukkit.listener.protocollib.SignatureTestData.SignatureData; import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey; import com.google.common.hash.Hashing; -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.math.BigInteger; import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; import java.security.InvalidKeyException; import java.security.Key; -import java.security.KeyFactory; import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; -import java.security.interfaces.RSAPrivateKey; +import java.security.SignatureException; import java.security.interfaces.RSAPublicKey; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.Base64; import java.util.concurrent.ThreadLocalRandom; import javax.crypto.BadPaddingException; @@ -60,9 +50,6 @@ import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; -import org.bouncycastle.util.io.pem.PemObject; -import org.bouncycastle.util.io.pem.PemReader; -import org.jetbrains.annotations.NotNull; import org.junit.Test; import static org.hamcrest.CoreMatchers.is; @@ -100,7 +87,7 @@ public class EncryptionUtilTest { @Test public void testExpiredClientKey() throws Exception { - var clientKey = loadClientKey("client_keys/valid_public_key.json"); + 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(); @@ -111,7 +98,7 @@ public class EncryptionUtilTest { public void testInvalidChangedExpiration() throws Exception { // expiration date changed should make the signature invalid // expiration should still be valid - var clientKey = loadClientKey("client_keys/invalid_wrong_expiration.json"); + 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)); @@ -120,7 +107,7 @@ public class EncryptionUtilTest { @Test public void testInvalidChangedKey() throws Exception { // changed public key no longer corresponding to the signature - var clientKey = loadClientKey("client_keys/invalid_wrong_key.json"); + 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)); @@ -129,7 +116,7 @@ public class EncryptionUtilTest { @Test public void testInvalidChangedSignature() throws Exception { // signature modified no longer corresponding to key and expiration date - var clientKey = loadClientKey("client_keys/invalid_wrong_signature.json"); + 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)); @@ -137,7 +124,7 @@ public class EncryptionUtilTest { @Test public void testValidClientKey() throws Exception { - var clientKey = loadClientKey("client_keys/valid_public_key.json"); + 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)); @@ -155,7 +142,7 @@ public class EncryptionUtilTest { assertThat(decryptSharedKey, is(secretKey)); } - private byte[] encrypt(PublicKey receiverKey, byte[] message) + private static byte[] encrypt(PublicKey receiverKey, byte[] message) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { var encryptCipher = Cipher.getInstance(receiverKey.getAlgorithm()); @@ -163,7 +150,7 @@ public class EncryptionUtilTest { return encryptCipher.doFinal(message); } - private SecretKeySpec generateSharedKey() { + private static SecretKeySpec generateSharedKey() { // according to wiki.vg 16 bytes long byte[] sharedKey = new byte[16]; ThreadLocalRandom.current().nextBytes(sharedKey); @@ -176,14 +163,13 @@ public class EncryptionUtilTest { public void testServerIdHash() throws Exception { var serverId = ""; var sharedSecret = generateSharedKey(); - var serverPK = loadClientKey("client_keys/valid_public_key.json").key(); + 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)); } - @NotNull - private String getServerHash(String serverId, SecretKey sharedSecret, PublicKey serverPK) { + private static String getServerHash(String 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) @@ -204,7 +190,7 @@ public class EncryptionUtilTest { public void testServerIdHashWrongSecret() throws Exception { var serverId = ""; var sharedSecret = generateSharedKey(); - var serverPK = loadClientKey("client_keys/valid_public_key.json").key(); + var serverPK = ResourceLoader.loadClientKey("client_keys/valid_public_key.json").key(); String sessionHash = getServerHash(serverId, sharedSecret, serverPK); assertThat(EncryptionUtil.getServerIdHashString("", generateSharedKey(), serverPK), not(sessionHash)); @@ -223,116 +209,88 @@ public class EncryptionUtilTest { @Test public void testValidSignedNonce() throws Exception { - ClientPublicKey clientKey = loadClientKey("client_keys/valid_public_key.json"); - PublicKey clientPublicKey = clientKey.key(); - - SignatureTestData testData = loadSignatureResource("signature/valid_signature.json"); - byte[] nonce = testData.getNonce(); - SignatureData signature = testData.getSignature(); - long salt = signature.getSalt(); - assertThat(EncryptionUtil.verifySignedNonce(nonce, clientPublicKey, salt, signature.getSignature()), is(true)); + 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 = loadClientKey("client_keys/valid_public_key.json"); - PublicKey clientPublicKey = clientKey.key(); - - SignatureTestData testData = loadSignatureResource("signature/incorrect_nonce.json"); - byte[] nonce = testData.getNonce(); - SignatureData signature = testData.getSignature(); - long salt = signature.getSalt(); - assertThat(EncryptionUtil.verifySignedNonce(nonce, clientPublicKey, salt, signature.getSignature()), is(false)); + 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 = loadClientKey("client_keys/valid_public_key.json"); - PublicKey clientPublicKey = clientKey.key(); - - SignatureTestData testData = loadSignatureResource("signature/incorrect_salt.json"); - byte[] nonce = testData.getNonce(); - SignatureData signature = testData.getSignature(); - long salt = signature.getSalt(); - assertThat(EncryptionUtil.verifySignedNonce(nonce, clientPublicKey, salt, signature.getSignature()), is(false)); + 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 = loadClientKey("client_keys/valid_public_key.json"); - PublicKey clientPublicKey = clientKey.key(); - - SignatureTestData testData = loadSignatureResource("signature/incorrect_signature.json"); - byte[] nonce = testData.getNonce(); - SignatureData signature = testData.getSignature(); - long salt = signature.getSalt(); - assertThat(EncryptionUtil.verifySignedNonce(nonce, clientPublicKey, salt, signature.getSignature()), is(false)); + 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 = loadClientKey("client_keys/invalid_wrong_key.json"); + 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(); - SignatureTestData testData = loadSignatureResource("signature/valid_signature.json"); byte[] nonce = testData.getNonce(); SignatureData signature = testData.getSignature(); long salt = signature.getSalt(); - assertThat(EncryptionUtil.verifySignedNonce(nonce, clientPublicKey, salt, signature.getSignature()), is(false)); + return EncryptionUtil.verifySignedNonce(nonce, clientPublicKey, salt, signature.getSignature()); } - private SignatureTestData loadSignatureResource(String resourceName) throws IOException { - var keyUrl = Resources.getResource(resourceName); - var encodedSignature = Resources.toString(keyUrl, StandardCharsets.US_ASCII); + @Test + public void testNonce() throws Exception { + byte[] expected = {1, 2, 3, 4}; + var serverKey = EncryptionUtil.generateKeyPair(); + var encryptedNonce = encrypt(serverKey.getPublic(), expected); - return new Gson().fromJson(encodedSignature, SignatureTestData.class); + assertThat(EncryptionUtil.verifyNonce(expected, serverKey.getPrivate(), encryptedNonce), is(true)); } - private RSAPrivateKey parsePrivateKey(String keySpec) - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { - try ( - Reader reader = new StringReader(keySpec); - PemReader pemReader = new PemReader(reader) - ) { - PemObject pemObject = pemReader.readPemObject(); - byte[] content = pemObject.getContent(); - var privateKeySpec = new PKCS8EncodedKeySpec(content); + @Test + public void testNonceIncorrect() throws Exception { + byte[] expected = {1, 2, 3, 4}; + var serverKey = EncryptionUtil.generateKeyPair(); - var factory = KeyFactory.getInstance("RSA"); - return (RSAPrivateKey) factory.generatePrivate(privateKeySpec); - } + // flipped first character + var encryptedNonce = encrypt(serverKey.getPublic(), new byte[]{0, 2, 3 , 4}); + + assertThat(EncryptionUtil.verifyNonce(expected, serverKey.getPrivate(), encryptedNonce), is(false)); } - private ClientPublicKey loadClientKey(String path) - throws NoSuchAlgorithmException, IOException, InvalidKeySpecException { - var keyUrl = Resources.getResource(path); + @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); - 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); + EncryptionUtil.verifyNonce(expected, serverKey.getPrivate(), encryptedNonce); } - private 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); + @Test(expected = GeneralSecurityException.class) + public void testNonceIncorrectEmpty() throws Exception { + byte[] expected = {1, 2, 3, 4}; + var serverKey = EncryptionUtil.generateKeyPair(); + byte[] encryptedNonce = {}; - var factory = KeyFactory.getInstance("RSA"); - return (RSAPublicKey) factory.generatePublic(pubKeySpec); - } + 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 index 8ea85f6e..ee62e242 100644 --- a/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/SignatureTestData.java +++ b/bukkit/src/test/java/com/github/games647/fastlogin/bukkit/listener/protocollib/SignatureTestData.java @@ -25,10 +25,22 @@ */ 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; diff --git a/core/src/main/java/com/github/games647/fastlogin/core/AsyncScheduler.java b/core/src/main/java/com/github/games647/fastlogin/core/AsyncScheduler.java index d5ebe1a0..4c01bc19 100644 --- a/core/src/main/java/com/github/games647/fastlogin/core/AsyncScheduler.java +++ b/core/src/main/java/com/github/games647/fastlogin/core/AsyncScheduler.java @@ -58,16 +58,18 @@ public class AsyncScheduler { } public CompletableFuture runAsync(Runnable task) { - return CompletableFuture.runAsync(() -> { - currentlyRunning.incrementAndGet(); - try { - task.run(); - } finally { - currentlyRunning.getAndDecrement(); - } - }, processingPool).exceptionally(error -> { + return CompletableFuture.runAsync(() -> process(task), processingPool).exceptionally(error -> { logger.warn("Error occurred on thread pool", error); return null; }); } + + private void process(Runnable task) { + currentlyRunning.incrementAndGet(); + try { + task.run(); + } finally { + currentlyRunning.getAndDecrement(); + } + } }