forked from TuxCoding/FastLogin
Restore compatibility with older Minecraft versions
This commit is contained in:
@ -64,7 +64,7 @@ Possible values: `Premium`, `Cracked`, `Unknown`
|
|||||||
* Server software in offlinemode:
|
* Server software in offlinemode:
|
||||||
* Spigot (or a fork e.g. Paper) 1.8.8+
|
* Spigot (or a fork e.g. Paper) 1.8.8+
|
||||||
* Protocol plugin:
|
* 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/)
|
* [ProtocolSupport](https://www.spigotmc.org/resources/protocolsupport.7201/)
|
||||||
* Latest BungeeCord (or a fork e.g. Waterfall)
|
* Latest BungeeCord (or a fork e.g. Waterfall)
|
||||||
* An auth plugin.
|
* An auth plugin.
|
||||||
|
@ -47,6 +47,7 @@ import java.security.SignatureException;
|
|||||||
import java.security.spec.InvalidKeySpecException;
|
import java.security.spec.InvalidKeySpecException;
|
||||||
import java.security.spec.X509EncodedKeySpec;
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Base64.Encoder;
|
import java.util.Base64.Encoder;
|
||||||
import java.util.random.RandomGenerator;
|
import java.util.random.RandomGenerator;
|
||||||
@ -142,9 +143,9 @@ class EncryptionUtil {
|
|||||||
return new SecretKeySpec(decrypt(privateKey, sharedKey), "AES");
|
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 {
|
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
||||||
if (clientKey.isExpired(verifyTimstamp)) {
|
if (clientKey.isExpired(verifyTimestamp)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,6 +156,13 @@ class EncryptionUtil {
|
|||||||
return verifier.verify(clientKey.signature());
|
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)
|
public static boolean verifySignedNonce(byte[] nonce, PublicKey clientKey, long signatureSalt, byte[] signature)
|
||||||
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
||||||
Signature verifier = Signature.getInstance("SHA256withRSA");
|
Signature verifier = Signature.getInstance("SHA256withRSA");
|
||||||
|
@ -31,6 +31,7 @@ import com.comphenix.protocol.events.PacketAdapter;
|
|||||||
import com.comphenix.protocol.events.PacketContainer;
|
import com.comphenix.protocol.events.PacketContainer;
|
||||||
import com.comphenix.protocol.events.PacketEvent;
|
import com.comphenix.protocol.events.PacketEvent;
|
||||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||||
|
import com.comphenix.protocol.utility.MinecraftVersion;
|
||||||
import com.comphenix.protocol.wrappers.BukkitConverters;
|
import com.comphenix.protocol.wrappers.BukkitConverters;
|
||||||
import com.comphenix.protocol.wrappers.WrappedGameProfile;
|
import com.comphenix.protocol.wrappers.WrappedGameProfile;
|
||||||
import com.comphenix.protocol.wrappers.WrappedProfilePublicKey.WrappedProfileKeyData;
|
import com.comphenix.protocol.wrappers.WrappedProfilePublicKey.WrappedProfileKeyData;
|
||||||
@ -51,6 +52,10 @@ import java.security.SignatureException;
|
|||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import javax.crypto.BadPaddingException;
|
||||||
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
import static com.comphenix.protocol.PacketType.Login.Client.ENCRYPTION_BEGIN;
|
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());
|
plugin.getLog().warn("GameProfile {} tried to send encryption response at invalid state", sender.getAddress());
|
||||||
sender.kickPlayer(plugin.getCore().getMessage("invalid-request"));
|
sender.kickPlayer(plugin.getCore().getMessage("invalid-request"));
|
||||||
} else {
|
} else {
|
||||||
if (session.getClientPublicKey() == null) {
|
byte[] expectedVerifyToken = session.getVerifyToken();
|
||||||
// we cannot verify the signature if no public was provided
|
if (verifyNonce(sender, packetEvent.getPacket(), session.getClientPublicKey(), expectedVerifyToken)) {
|
||||||
plugin.getLog().error("No public key provided for signed nonce {}", sender);
|
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"));
|
sender.kickPlayer(plugin.getCore().getMessage("invalid-verify-token"));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Either<byte[], ?> either = packetEvent.getPacket().getSpecificModifier(Either.class).read(0);
|
private boolean verifyNonce(Player sender, PacketContainer packet,
|
||||||
Object signatureData = either.right().get();
|
ClientPublicKey clientPublicKey, byte[] expectedToken) {
|
||||||
long salt = FuzzyReflection.getFieldValue(signatureData, Long.TYPE, true);
|
try {
|
||||||
byte[] signature = FuzzyReflection.getFieldValue(signatureData, byte[].class, true);
|
if (MinecraftVersion.atOrAbove(new MinecraftVersion(1, 19, 0))) {
|
||||||
|
Either<byte[], ?> either = packet.getSpecificModifier(Either.class).read(0);
|
||||||
|
if (clientPublicKey == null) {
|
||||||
|
Optional<byte[]> left = either.left();
|
||||||
|
if (left.isEmpty()) {
|
||||||
|
plugin.getLog().error("No verify token sent if requested without player signed key {}", sender);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
PublicKey publicKey = session.getClientPublicKey().key();
|
return EncryptionUtil.verifyNonce(expectedToken, keyPair.getPrivate(), left.get());
|
||||||
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);
|
|
||||||
} else {
|
} else {
|
||||||
sender.kickPlayer(plugin.getCore().getMessage("invalid-verify-token"));
|
Optional<?> optSignatureData = either.right();
|
||||||
plugin.getLog().error("Invalid signature from player {}", sender);
|
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) {
|
} else {
|
||||||
sender.kickPlayer(plugin.getCore().getMessage("invalid-verify-token"));
|
byte[] nonce = packet.getByteArrays().read(1);
|
||||||
plugin.getLog().error("Invalid signature from player {}", sender, signatureEx);
|
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();
|
PacketContainer packet = packetEvent.getPacket();
|
||||||
var profileKey = packet.getOptionals(BukkitConverters.getWrappedPublicKeyDataConverter())
|
var profileKey = packet.getOptionals(BukkitConverters.getWrappedPublicKeyDataConverter())
|
||||||
.read(0);
|
.optionRead(0);
|
||||||
|
|
||||||
var clientKey = profileKey.flatMap(this::verifyPublicKey);
|
var clientKey = profileKey.flatMap(opt -> opt).flatMap(this::verifyPublicKey);
|
||||||
if (verifyClientKeys && !clientKey.isPresent()) {
|
if (verifyClientKeys && clientKey.isEmpty()) {
|
||||||
// missing or incorrect
|
// missing or incorrect
|
||||||
// expired always not allowed
|
// expired always not allowed
|
||||||
player.kickPlayer(plugin.getCore().getMessage("invalid-public-key"));
|
player.kickPlayer(plugin.getCore().getMessage("invalid-public-key"));
|
||||||
|
@ -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.SignatureTestData.SignatureData;
|
||||||
import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;
|
import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;
|
||||||
import com.google.common.hash.Hashing;
|
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.math.BigInteger;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
import java.security.KeyFactory;
|
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.security.interfaces.RSAPrivateKey;
|
import java.security.SignatureException;
|
||||||
import java.security.interfaces.RSAPublicKey;
|
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.Instant;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
import javax.crypto.BadPaddingException;
|
import javax.crypto.BadPaddingException;
|
||||||
@ -60,9 +50,6 @@ import javax.crypto.NoSuchPaddingException;
|
|||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
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 org.junit.Test;
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.is;
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
@ -100,7 +87,7 @@ public class EncryptionUtilTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testExpiredClientKey() throws Exception {
|
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
|
// Client expires at the exact second mentioned, so use it for verification
|
||||||
var expiredTimestamp = clientKey.expiry();
|
var expiredTimestamp = clientKey.expiry();
|
||||||
@ -111,7 +98,7 @@ public class EncryptionUtilTest {
|
|||||||
public void testInvalidChangedExpiration() throws Exception {
|
public void testInvalidChangedExpiration() throws Exception {
|
||||||
// expiration date changed should make the signature invalid
|
// expiration date changed should make the signature invalid
|
||||||
// expiration should still be valid
|
// 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);
|
Instant expireTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS);
|
||||||
|
|
||||||
assertThat(EncryptionUtil.verifyClientKey(clientKey, expireTimestamp), is(false));
|
assertThat(EncryptionUtil.verifyClientKey(clientKey, expireTimestamp), is(false));
|
||||||
@ -120,7 +107,7 @@ public class EncryptionUtilTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testInvalidChangedKey() throws Exception {
|
public void testInvalidChangedKey() throws Exception {
|
||||||
// changed public key no longer corresponding to the signature
|
// 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);
|
Instant expireTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS);
|
||||||
|
|
||||||
assertThat(EncryptionUtil.verifyClientKey(clientKey, expireTimestamp), is(false));
|
assertThat(EncryptionUtil.verifyClientKey(clientKey, expireTimestamp), is(false));
|
||||||
@ -129,7 +116,7 @@ public class EncryptionUtilTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testInvalidChangedSignature() throws Exception {
|
public void testInvalidChangedSignature() throws Exception {
|
||||||
// signature modified no longer corresponding to key and expiration date
|
// 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);
|
Instant expireTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS);
|
||||||
|
|
||||||
assertThat(EncryptionUtil.verifyClientKey(clientKey, expireTimestamp), is(false));
|
assertThat(EncryptionUtil.verifyClientKey(clientKey, expireTimestamp), is(false));
|
||||||
@ -137,7 +124,7 @@ public class EncryptionUtilTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testValidClientKey() throws Exception {
|
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);
|
var verificationTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS);
|
||||||
|
|
||||||
assertThat(EncryptionUtil.verifyClientKey(clientKey, verificationTimestamp), is(true));
|
assertThat(EncryptionUtil.verifyClientKey(clientKey, verificationTimestamp), is(true));
|
||||||
@ -155,7 +142,7 @@ public class EncryptionUtilTest {
|
|||||||
assertThat(decryptSharedKey, is(secretKey));
|
assertThat(decryptSharedKey, is(secretKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] encrypt(PublicKey receiverKey, byte[] message)
|
private static byte[] encrypt(PublicKey receiverKey, byte[] message)
|
||||||
throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
|
throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
|
||||||
IllegalBlockSizeException, BadPaddingException {
|
IllegalBlockSizeException, BadPaddingException {
|
||||||
var encryptCipher = Cipher.getInstance(receiverKey.getAlgorithm());
|
var encryptCipher = Cipher.getInstance(receiverKey.getAlgorithm());
|
||||||
@ -163,7 +150,7 @@ public class EncryptionUtilTest {
|
|||||||
return encryptCipher.doFinal(message);
|
return encryptCipher.doFinal(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private SecretKeySpec generateSharedKey() {
|
private static SecretKeySpec generateSharedKey() {
|
||||||
// according to wiki.vg 16 bytes long
|
// according to wiki.vg 16 bytes long
|
||||||
byte[] sharedKey = new byte[16];
|
byte[] sharedKey = new byte[16];
|
||||||
ThreadLocalRandom.current().nextBytes(sharedKey);
|
ThreadLocalRandom.current().nextBytes(sharedKey);
|
||||||
@ -176,14 +163,13 @@ public class EncryptionUtilTest {
|
|||||||
public void testServerIdHash() throws Exception {
|
public void testServerIdHash() throws Exception {
|
||||||
var serverId = "";
|
var serverId = "";
|
||||||
var sharedSecret = generateSharedKey();
|
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);
|
String sessionHash = getServerHash(serverId, sharedSecret, serverPK);
|
||||||
assertThat(EncryptionUtil.getServerIdHashString(serverId, sharedSecret, serverPK), is(sessionHash));
|
assertThat(EncryptionUtil.getServerIdHashString(serverId, sharedSecret, serverPK), is(sessionHash));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
private static String getServerHash(String serverId, SecretKey sharedSecret, PublicKey serverPK) {
|
||||||
private String getServerHash(String serverId, SecretKey sharedSecret, PublicKey serverPK) {
|
|
||||||
// https://wiki.vg/Protocol_Encryption#Client
|
// https://wiki.vg/Protocol_Encryption#Client
|
||||||
// sha1 := Sha1()
|
// sha1 := Sha1()
|
||||||
// sha1.update(ASCII encoding of the server id string from Encryption Request)
|
// sha1.update(ASCII encoding of the server id string from Encryption Request)
|
||||||
@ -204,7 +190,7 @@ public class EncryptionUtilTest {
|
|||||||
public void testServerIdHashWrongSecret() throws Exception {
|
public void testServerIdHashWrongSecret() throws Exception {
|
||||||
var serverId = "";
|
var serverId = "";
|
||||||
var sharedSecret = generateSharedKey();
|
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);
|
String sessionHash = getServerHash(serverId, sharedSecret, serverPK);
|
||||||
assertThat(EncryptionUtil.getServerIdHashString("", generateSharedKey(), serverPK), not(sessionHash));
|
assertThat(EncryptionUtil.getServerIdHashString("", generateSharedKey(), serverPK), not(sessionHash));
|
||||||
@ -223,116 +209,88 @@ public class EncryptionUtilTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testValidSignedNonce() throws Exception {
|
public void testValidSignedNonce() throws Exception {
|
||||||
ClientPublicKey clientKey = loadClientKey("client_keys/valid_public_key.json");
|
ClientPublicKey clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
|
||||||
PublicKey clientPublicKey = clientKey.key();
|
SignatureTestData testData = SignatureTestData.fromResource("signature/valid_signature.json");
|
||||||
|
assertThat(verifySignedNonce(testData, clientKey), is(true));
|
||||||
SignatureTestData testData = loadSignatureResource("signature/valid_signature.json");
|
|
||||||
byte[] nonce = testData.getNonce();
|
|
||||||
SignatureData signature = testData.getSignature();
|
|
||||||
long salt = signature.getSalt();
|
|
||||||
assertThat(EncryptionUtil.verifySignedNonce(nonce, clientPublicKey, salt, signature.getSignature()), is(true));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testIncorrectNonce() throws Exception {
|
public void testIncorrectNonce() throws Exception {
|
||||||
ClientPublicKey clientKey = loadClientKey("client_keys/valid_public_key.json");
|
ClientPublicKey clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
|
||||||
PublicKey clientPublicKey = clientKey.key();
|
SignatureTestData testData = SignatureTestData.fromResource("signature/incorrect_nonce.json");
|
||||||
|
assertThat(verifySignedNonce(testData, clientKey), is(false));
|
||||||
SignatureTestData testData = loadSignatureResource("signature/incorrect_nonce.json");
|
|
||||||
byte[] nonce = testData.getNonce();
|
|
||||||
SignatureData signature = testData.getSignature();
|
|
||||||
long salt = signature.getSalt();
|
|
||||||
assertThat(EncryptionUtil.verifySignedNonce(nonce, clientPublicKey, salt, signature.getSignature()), is(false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testIncorrectSalt() throws Exception {
|
public void testIncorrectSalt() throws Exception {
|
||||||
// client generated
|
// client generated
|
||||||
ClientPublicKey clientKey = loadClientKey("client_keys/valid_public_key.json");
|
ClientPublicKey clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
|
||||||
PublicKey clientPublicKey = clientKey.key();
|
SignatureTestData testData = SignatureTestData.fromResource("signature/incorrect_salt.json");
|
||||||
|
assertThat(verifySignedNonce(testData, clientKey), is(false));
|
||||||
SignatureTestData testData = loadSignatureResource("signature/incorrect_salt.json");
|
|
||||||
byte[] nonce = testData.getNonce();
|
|
||||||
SignatureData signature = testData.getSignature();
|
|
||||||
long salt = signature.getSalt();
|
|
||||||
assertThat(EncryptionUtil.verifySignedNonce(nonce, clientPublicKey, salt, signature.getSignature()), is(false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testIncorrectSignature() throws Exception {
|
public void testIncorrectSignature() throws Exception {
|
||||||
// client generated
|
// client generated
|
||||||
ClientPublicKey clientKey = loadClientKey("client_keys/valid_public_key.json");
|
ClientPublicKey clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
|
||||||
PublicKey clientPublicKey = clientKey.key();
|
SignatureTestData testData = SignatureTestData.fromResource("signature/incorrect_signature.json");
|
||||||
|
assertThat(verifySignedNonce(testData, clientKey), is(false));
|
||||||
SignatureTestData testData = loadSignatureResource("signature/incorrect_signature.json");
|
|
||||||
byte[] nonce = testData.getNonce();
|
|
||||||
SignatureData signature = testData.getSignature();
|
|
||||||
long salt = signature.getSalt();
|
|
||||||
assertThat(EncryptionUtil.verifySignedNonce(nonce, clientPublicKey, salt, signature.getSignature()), is(false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testWrongPublicKeySigned() throws Exception {
|
public void testWrongPublicKeySigned() throws Exception {
|
||||||
// load a different public key
|
// 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();
|
PublicKey clientPublicKey = clientKey.key();
|
||||||
|
|
||||||
SignatureTestData testData = loadSignatureResource("signature/valid_signature.json");
|
|
||||||
byte[] nonce = testData.getNonce();
|
byte[] nonce = testData.getNonce();
|
||||||
SignatureData signature = testData.getSignature();
|
SignatureData signature = testData.getSignature();
|
||||||
long salt = signature.getSalt();
|
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 {
|
@Test
|
||||||
var keyUrl = Resources.getResource(resourceName);
|
public void testNonce() throws Exception {
|
||||||
var encodedSignature = Resources.toString(keyUrl, StandardCharsets.US_ASCII);
|
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)
|
@Test
|
||||||
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
|
public void testNonceIncorrect() throws Exception {
|
||||||
try (
|
byte[] expected = {1, 2, 3, 4};
|
||||||
Reader reader = new StringReader(keySpec);
|
var serverKey = EncryptionUtil.generateKeyPair();
|
||||||
PemReader pemReader = new PemReader(reader)
|
|
||||||
) {
|
|
||||||
PemObject pemObject = pemReader.readPemObject();
|
|
||||||
byte[] content = pemObject.getContent();
|
|
||||||
var privateKeySpec = new PKCS8EncodedKeySpec(content);
|
|
||||||
|
|
||||||
var factory = KeyFactory.getInstance("RSA");
|
// flipped first character
|
||||||
return (RSAPrivateKey) factory.generatePrivate(privateKeySpec);
|
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)
|
@Test(expected = GeneralSecurityException.class)
|
||||||
throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
|
public void testNonceFailedDecryption() throws Exception {
|
||||||
var keyUrl = Resources.getResource(path);
|
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);
|
EncryptionUtil.verifyNonce(expected, serverKey.getPrivate(), encryptedNonce);
|
||||||
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 RSAPublicKey parsePublicKey(String keySpec)
|
@Test(expected = GeneralSecurityException.class)
|
||||||
throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
|
public void testNonceIncorrectEmpty() throws Exception {
|
||||||
try (
|
byte[] expected = {1, 2, 3, 4};
|
||||||
Reader reader = new StringReader(keySpec);
|
var serverKey = EncryptionUtil.generateKeyPair();
|
||||||
PemReader pemReader = new PemReader(reader)
|
byte[] encryptedNonce = {};
|
||||||
) {
|
|
||||||
PemObject pemObject = pemReader.readPemObject();
|
|
||||||
byte[] content = pemObject.getContent();
|
|
||||||
var pubKeySpec = new X509EncodedKeySpec(content);
|
|
||||||
|
|
||||||
var factory = KeyFactory.getInstance("RSA");
|
assertThat(EncryptionUtil.verifyNonce(expected, serverKey.getPrivate(), encryptedNonce), is(false));
|
||||||
return (RSAPublicKey) factory.generatePublic(pubKeySpec);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -25,10 +25,22 @@
|
|||||||
*/
|
*/
|
||||||
package com.github.games647.fastlogin.bukkit.listener.protocollib;
|
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 com.google.gson.annotations.JsonAdapter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
public class SignatureTestData {
|
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)
|
@JsonAdapter(Base64Adapter.class)
|
||||||
private byte[] nonce;
|
private byte[] nonce;
|
||||||
|
|
||||||
|
@ -58,16 +58,18 @@ public class AsyncScheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<Void> runAsync(Runnable task) {
|
public CompletableFuture<Void> runAsync(Runnable task) {
|
||||||
return CompletableFuture.runAsync(() -> {
|
return CompletableFuture.runAsync(() -> process(task), processingPool).exceptionally(error -> {
|
||||||
currentlyRunning.incrementAndGet();
|
|
||||||
try {
|
|
||||||
task.run();
|
|
||||||
} finally {
|
|
||||||
currentlyRunning.getAndDecrement();
|
|
||||||
}
|
|
||||||
}, processingPool).exceptionally(error -> {
|
|
||||||
logger.warn("Error occurred on thread pool", error);
|
logger.warn("Error occurred on thread pool", error);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void process(Runnable task) {
|
||||||
|
currentlyRunning.incrementAndGet();
|
||||||
|
try {
|
||||||
|
task.run();
|
||||||
|
} finally {
|
||||||
|
currentlyRunning.getAndDecrement();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user