From ed45fada59d5e8721a70b2bb3c3656119d5806ca Mon Sep 17 00:00:00 2001 From: games647 Date: Thu, 28 Jul 2022 17:06:06 +0200 Subject: [PATCH] Update 1.19.1 public key verification --- .../listener/protocollib/EncryptionUtil.java | 31 ++++++++++++++----- .../protocollib/ProtocolLibListener.java | 11 +++++-- .../protocollib/packet/ClientPublicKey.java | 11 +++++++ .../protocollib/EncryptionUtilTest.java | 25 +++++++++++++-- .../client_keys/valid_public_key_19_1.json | 5 +++ 5 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 bukkit/src/test/resources/client_keys/valid_public_key_19_1.json 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 d7b3d727..69b99663 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 @@ -34,6 +34,7 @@ import com.google.common.primitives.Longs; import java.io.IOException; import java.math.BigInteger; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.KeyFactory; @@ -51,6 +52,7 @@ import java.util.Arrays; import java.util.Base64; import java.util.Base64.Encoder; import java.util.Random; +import java.util.UUID; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; @@ -77,6 +79,8 @@ final class EncryptionUtil { private static final Encoder KEY_ENCODER = Base64.getMimeEncoder( LINE_LENGTH, "\n".getBytes(StandardCharsets.UTF_8) ); + private static final int MILLISECOND_SIZE = 8; + private static final int UUID_SIZE = 2 * MILLISECOND_SIZE; static { try { @@ -146,7 +150,7 @@ final class EncryptionUtil { return new SecretKeySpec(decrypt(privateKey, sharedKey), "AES"); } - public static boolean verifyClientKey(ClientPublicKey clientKey, Instant verifyTimestamp) + public static boolean verifyClientKey(ClientPublicKey clientKey, Instant verifyTimestamp, UUID premiumId) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { if (clientKey.isExpired(verifyTimestamp)) { return false; @@ -155,10 +159,27 @@ final class EncryptionUtil { Signature verifier = Signature.getInstance("SHA1withRSA"); // key of the signer verifier.initVerify(MOJANG_SESSION_KEY); - verifier.update(toSignable(clientKey).getBytes(StandardCharsets.US_ASCII)); + verifier.update(toSignable(clientKey, premiumId)); return verifier.verify(clientKey.signature()); } + private static byte[] toSignable(ClientPublicKey clientPublicKey, UUID ownerPremiumId) { + if (ownerPremiumId == null) { + 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") + .getBytes(StandardCharsets.US_ASCII); + } + + byte[] keyData = clientPublicKey.key().getEncoded(); + return ByteBuffer.allocate(keyData.length + UUID_SIZE + MILLISECOND_SIZE) + .putLong(ownerPremiumId.getMostSignificantBits()) + .putLong(ownerPremiumId.getLeastSignificantBits()) + .putLong(clientPublicKey.expiry().toEpochMilli()) + .put(keyData) + .array(); + } + public static boolean verifyNonce(byte[] expected, PrivateKey decryptionKey, byte[] encryptedNonce) throws NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { @@ -186,12 +207,6 @@ final class EncryptionUtil { 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 { 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 6a495512..18322389 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 @@ -33,6 +33,7 @@ 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.Converters; import com.comphenix.protocol.wrappers.WrappedGameProfile; import com.github.games647.fastlogin.bukkit.BukkitLoginSession; import com.github.games647.fastlogin.bukkit.FastLoginBukkit; @@ -50,6 +51,7 @@ import java.security.SecureRandom; import java.security.SignatureException; import java.time.Instant; import java.util.Optional; +import java.util.UUID; import java.util.function.Function; import javax.crypto.BadPaddingException; @@ -215,7 +217,10 @@ public class ProtocolLibListener extends PacketAdapter { return Optional.of(ClientPublicKey.of(expires, key, signature)); }); - if (verifyClientKeys && clientKey.isPresent() && verifyPublicKey(clientKey.get())) { + // start reading from index 1, because 0 is already used by the public key + Optional sessionUUID = packet.getOptionals(Converters.passthrough(UUID.class)).readSafely(1); + if (verifyClientKeys && sessionUUID.isPresent() && clientKey.isPresent() + && verifyPublicKey(clientKey.get(), sessionUUID.get())) { // missing or incorrect // expired always not allowed player.kickPlayer(plugin.getCore().getMessage("invalid-public-key")); @@ -232,9 +237,9 @@ public class ProtocolLibListener extends PacketAdapter { plugin.getScheduler().runAsync(nameCheckTask); } - private boolean verifyPublicKey(ClientPublicKey clientKey) { + private boolean verifyPublicKey(ClientPublicKey clientKey, UUID sessionPremiumUUID) { try { - return EncryptionUtil.verifyClientKey(clientKey, Instant.now()); + return EncryptionUtil.verifyClientKey(clientKey, Instant.now(), sessionPremiumUUID); } catch (SignatureException | InvalidKeyException | NoSuchAlgorithmException ex) { return false; } 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 index cab804ef..fa60470b 100644 --- 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 @@ -27,6 +27,8 @@ package com.github.games647.fastlogin.bukkit.listener.protocollib.packet; import java.security.PublicKey; import java.time.Instant; +import java.util.Base64; +import java.util.StringJoiner; import lombok.Value; import lombok.experimental.Accessors; @@ -41,4 +43,13 @@ public class ClientPublicKey { public boolean isExpired(Instant verifyTimestamp) { return !verifyTimestamp.isBefore(expiry); } + + @Override + public String toString() { + return new StringJoiner(", ", ClientPublicKey.class.getSimpleName() + '[', "]") + .add("expiry=" + expiry) + .add("key=" + Base64.getEncoder().encodeToString(key.getEncoded())) + .add("signature=" + Base64.getEncoder().encodeToString(signature)) + .toString(); + } } 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 1bbe8642..9a6d2b39 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 @@ -41,6 +41,7 @@ import java.security.SignatureException; import java.security.interfaces.RSAPublicKey; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.UUID; import java.util.concurrent.ThreadLocalRandom; import javax.crypto.BadPaddingException; @@ -93,7 +94,7 @@ class EncryptionUtilTest { // Client expires at the exact second mentioned, so use it for verification val expiredTimestamp = clientKey.expiry(); - assertFalse(EncryptionUtil.verifyClientKey(clientKey, expiredTimestamp)); + assertFalse(EncryptionUtil.verifyClientKey(clientKey, expiredTimestamp, null)); } @ParameterizedTest @@ -109,7 +110,7 @@ class EncryptionUtilTest { val clientKey = ResourceLoader.loadClientKey(clientKeySource); Instant expireTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS); - assertFalse(EncryptionUtil.verifyClientKey(clientKey, expireTimestamp)); + assertFalse(EncryptionUtil.verifyClientKey(clientKey, expireTimestamp, null)); } @Test @@ -117,7 +118,25 @@ class EncryptionUtilTest { val clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json"); val verificationTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS); - assertTrue(EncryptionUtil.verifyClientKey(clientKey, verificationTimestamp)); + assertTrue(EncryptionUtil.verifyClientKey(clientKey, verificationTimestamp, null)); + } + + @Test + void testValid191ClientKey() throws Exception { + val clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key_19_1.json"); + val verificationTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS); + + val ownerPremiumId = UUID.fromString("0aaa2c13-922a-411b-b655-9b8c08404695"); + assertTrue(EncryptionUtil.verifyClientKey(clientKey, verificationTimestamp, ownerPremiumId)); + } + + @Test + void testIncorrect191ClientOwner() throws Exception { + val clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key_19_1.json"); + val verificationTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS); + + val ownerPremiumId = UUID.fromString("61699b2e-d327-4a01-9f1e-0ea8c3f06bc6"); + assertFalse(EncryptionUtil.verifyClientKey(clientKey, verificationTimestamp, ownerPremiumId)); } @Test diff --git a/bukkit/src/test/resources/client_keys/valid_public_key_19_1.json b/bukkit/src/test/resources/client_keys/valid_public_key_19_1.json new file mode 100644 index 00000000..28250607 --- /dev/null +++ b/bukkit/src/test/resources/client_keys/valid_public_key_19_1.json @@ -0,0 +1,5 @@ +{ + "expires_at": "2022-07-29T12:57:39.011Z", + "key": "-----BEGIN RSA PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtYOUXdid0c09/eYoseLf8qG9fKQ/G2DY9wlSyEZaFMflwZ8ZpLFYigxzfaimpT3A5cbFIdIH2W2sYl5PwsKSs128GBh/rxXUEZlLkIkS+EfxyuMp9ITclxAjCqvFgfJbZHugtB9Ofi6knCEEgjFwMDh2efdpOXkCxtHuPFfnVzDJBbHWdlCCtJesMAnA2jCT7CqCwsi7sW2QxuTarqHP/cHKiBeBIu/SngGUB6eWmvAwERW5x2D+O26w8Z5sQCND3xQ4D868RALiPNG94TyKoJV+jKi0tTUmjGGs/1ksbSGDQb5xqIH0NYKZhoZrczYPNmJX4k7g5BA5RHX8AGORaQIDAQAB\n-----END RSA PUBLIC KEY-----\n", + "signature": "Fto/GDqEMTWpNrktWSi3tnP3ZZlo8r4Jled/5PKYRvaL/zksfjB2RK2O8pZL+w5mI2VAViTVAQmSJEF2o/BCb2L0zXp3/hC9VhZj5NTVi4KbHfnfMorj7/WJP2vvMgVxIxgLb3EEQXGS2Mmo0w2ikUVauwXgLWECvVt10KAZnTAWNIvpM8NUoZ2oCCxVimYHBtlwWQ7WvowAatP4ypa7fo3xhQg8Im1hvQDsFTNp58pgnd7l3l99xLj1uYOUJM06HGZJ/Xd0kzzJz44Csh4m50Q0RP4Nq5L+fYPeUx990Z1r1lw0sSayk+vA2Qnxgbs/z6KgkxfhBg7oOlp4ykl9cLC2kA/LdV6igqsdr/KoP4GWxwTA7RgQbhMkDFdmIg1W+gh3XqwASFQK2BAN/eAfmDTf8u9BtOEF7Ehn9uPOaiFiGztyaHxXNIkVSPTG2GXMFSijnd3Ms28jHYVY/67INTnDRmN0//KzBAoTRMe1S5idai19kug4EUVIRKDziipowzCPdbD18trdQGpK0dYOrw9XQiQd4N4V3eItpyAULGiZd8KcjgKo4orqgsUfNyhLI1keig7TyJUE3FkBOfX4hlZBm7Q/Wq4hwarlc5yZIjhsuivKV/q4tcnYYPwjP7kNMRsIApWG+yHmSIo8QfZhBiPxvtWSSLZgoFgnlxfaEko=" +}