Make client public key verification optional

This commit is contained in:
games647
2022-07-06 16:49:26 +02:00
parent 7c8de84a34
commit 0b0a46a18a
7 changed files with 55 additions and 28 deletions

View File

@ -32,6 +32,8 @@ import com.github.games647.fastlogin.core.shared.LoginSession;
import java.util.Optional; import java.util.Optional;
import javax.annotation.Nullable;
/** /**
* Represents a client connecting to the server. * Represents a client connecting to the server.
* *
@ -83,6 +85,7 @@ public class BukkitLoginSession extends LoginSession {
return verifyToken.clone(); return verifyToken.clone();
} }
@Nullable
public ClientPublicKey getClientPublicKey() { public ClientPublicKey getClientPublicKey() {
return clientPublicKey; return clientPublicKey;
} }

View File

@ -117,7 +117,7 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
if (pluginManager.isPluginEnabled("ProtocolSupport")) { if (pluginManager.isPluginEnabled("ProtocolSupport")) {
pluginManager.registerEvents(new ProtocolSupportListener(this, core.getAntiBot()), this); pluginManager.registerEvents(new ProtocolSupportListener(this, core.getAntiBot()), this);
} else if (pluginManager.isPluginEnabled("ProtocolLib")) { } else if (pluginManager.isPluginEnabled("ProtocolLib")) {
ProtocolLibListener.register(this, core.getAntiBot()); ProtocolLibListener.register(this, core.getAntiBot(), core.getConfig().getBoolean("verifyClientKeys"));
if (isPluginInstalled("floodgate")) { if (isPluginInstalled("floodgate")) {
if (getConfig().getBoolean("floodgatePrefixWorkaround")){ if (getConfig().getBoolean("floodgatePrefixWorkaround")){

View File

@ -27,8 +27,6 @@ package com.github.games647.fastlogin.bukkit.listener.protocollib;
import com.comphenix.protocol.ProtocolLibrary; import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.wrappers.BukkitConverters;
import com.comphenix.protocol.wrappers.WrappedProfilePublicKey.WrappedProfileKeyData;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession; import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit; import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.event.BukkitFastLoginPreLoginEvent; import com.github.games647.fastlogin.bukkit.event.BukkitFastLoginPreLoginEvent;
@ -38,7 +36,6 @@ import com.github.games647.fastlogin.core.shared.JoinManagement;
import com.github.games647.fastlogin.core.shared.event.FastLoginPreLoginEvent; import com.github.games647.fastlogin.core.shared.event.FastLoginPreLoginEvent;
import java.security.PublicKey; import java.security.PublicKey;
import java.util.Optional;
import java.util.Random; import java.util.Random;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
@ -49,6 +46,8 @@ public class NameCheckTask extends JoinManagement<Player, CommandSender, Protoco
private final FastLoginBukkit plugin; private final FastLoginBukkit plugin;
private final PacketEvent packetEvent; private final PacketEvent packetEvent;
private final ClientPublicKey clientKey;
private final PublicKey serverKey; private final PublicKey serverKey;
private final Random random; private final Random random;
@ -57,11 +56,12 @@ public class NameCheckTask extends JoinManagement<Player, CommandSender, Protoco
private final String username; private final String username;
public NameCheckTask(FastLoginBukkit plugin, Random random, Player player, PacketEvent packetEvent, public NameCheckTask(FastLoginBukkit plugin, Random random, Player player, PacketEvent packetEvent,
String username, PublicKey serverKey) { String username, ClientPublicKey clientKey, PublicKey serverKey) {
super(plugin.getCore(), plugin.getCore().getAuthPluginHook(), plugin.getBedrockService()); super(plugin.getCore(), plugin.getCore().getAuthPluginHook(), plugin.getBedrockService());
this.plugin = plugin; this.plugin = plugin;
this.packetEvent = packetEvent; this.packetEvent = packetEvent;
this.clientKey = clientKey;
this.serverKey = serverKey; this.serverKey = serverKey;
this.random = random; this.random = random;
this.player = player; this.player = player;
@ -71,10 +71,7 @@ public class NameCheckTask extends JoinManagement<Player, CommandSender, Protoco
@Override @Override
public void run() { public void run() {
try { try {
Optional<WrappedProfileKeyData> clientKey = packetEvent.getPacket() super.onLogin(username, new ProtocolLibLoginSource(player, random, serverKey, clientKey));
.getOptionals(BukkitConverters.getWrappedPublicKeyDataConverter()).read(0);
super.onLogin(username, new ProtocolLibLoginSource(player, random, serverKey, clientKey.orElse(null)));
} finally { } finally {
ProtocolLibrary.getProtocolManager().getAsynchronousManager().signalPacketTransmission(packetEvent); ProtocolLibrary.getProtocolManager().getAsynchronousManager().signalPacketTransmission(packetEvent);
} }
@ -104,8 +101,7 @@ public class NameCheckTask extends JoinManagement<Player, CommandSender, Protoco
core.getPendingLogin().put(ip + username, new Object()); core.getPendingLogin().put(ip + username, new Object());
byte[] verify = source.getVerifyToken(); byte[] verify = source.getVerifyToken();
WrappedProfileKeyData key = source.getClientPublicKey(); ClientPublicKey clientKey = source.getClientKey();
ClientPublicKey clientKey = new ClientPublicKey(key.getExpireTime(), key.getKey(), key.getSignature());
BukkitLoginSession playerSession = new BukkitLoginSession(username, verify, clientKey, registered, profile); BukkitLoginSession playerSession = new BukkitLoginSession(username, verify, clientKey, registered, profile);
plugin.putSession(player.getAddress(), playerSession); plugin.putSession(player.getAddress(), playerSession);

View File

@ -49,6 +49,7 @@ import java.security.PublicKey;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.SignatureException; import java.security.SignatureException;
import java.time.Instant; import java.time.Instant;
import java.util.Optional;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@ -66,7 +67,9 @@ public class ProtocolLibListener extends PacketAdapter {
private final KeyPair keyPair = EncryptionUtil.generateKeyPair(); private final KeyPair keyPair = EncryptionUtil.generateKeyPair();
private final AntiBotService antiBotService; private final AntiBotService antiBotService;
public ProtocolLibListener(FastLoginBukkit plugin, AntiBotService antiBotService) { private final boolean verifyClientKeys;
public ProtocolLibListener(FastLoginBukkit plugin, AntiBotService antiBotService, boolean verifyClientKeys) {
//run async in order to not block the server, because we are making api calls to Mojang //run async in order to not block the server, because we are making api calls to Mojang
super(params() super(params()
.plugin(plugin) .plugin(plugin)
@ -75,14 +78,15 @@ public class ProtocolLibListener extends PacketAdapter {
this.plugin = plugin; this.plugin = plugin;
this.antiBotService = antiBotService; this.antiBotService = antiBotService;
this.verifyClientKeys = verifyClientKeys;
} }
public static void register(FastLoginBukkit plugin, AntiBotService antiBotService) { public static void register(FastLoginBukkit plugin, AntiBotService antiBotService, boolean verifyClientKeys) {
// they will be created with a static builder, because otherwise it will throw a NoClassDefFoundError // they will be created with a static builder, because otherwise it will throw a NoClassDefFoundError
// TODO: make synchronous processing, but do web or database requests async // TODO: make synchronous processing, but do web or database requests async
ProtocolLibrary.getProtocolManager() ProtocolLibrary.getProtocolManager()
.getAsynchronousManager() .getAsynchronousManager()
.registerAsyncHandler(new ProtocolLibListener(plugin, antiBotService)) .registerAsyncHandler(new ProtocolLibListener(plugin, antiBotService, verifyClientKeys))
.start(); .start();
} }
@ -141,6 +145,13 @@ 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) {
// we cannot verify the signature if no public was provided
plugin.getLog().error("No public key provided for signed nonce {}", sender);
sender.kickPlayer(plugin.getCore().getMessage("invalid-verify-token"));
return;
}
Either<byte[], ?> either = packetEvent.getPacket().getSpecificModifier(Either.class).read(0); Either<byte[], ?> either = packetEvent.getPacket().getSpecificModifier(Either.class).read(0);
Object signatureData = either.right().get(); Object signatureData = either.right().get();
long salt = FuzzyReflection.getFieldValue(signatureData, Long.TYPE, true); long salt = FuzzyReflection.getFieldValue(signatureData, Long.TYPE, true);
@ -176,9 +187,13 @@ public class ProtocolLibListener extends PacketAdapter {
} }
PacketContainer packet = packetEvent.getPacket(); PacketContainer packet = packetEvent.getPacket();
WrappedProfileKeyData profileKey = packet.getOptionals(BukkitConverters.getWrappedPublicKeyDataConverter()) var profileKey = packet.getOptionals(BukkitConverters.getWrappedPublicKeyDataConverter())
.read(0).orElse(null); .read(0);
if (profileKey != null && !verifyPublicKey(profileKey)) {
var clientKey = profileKey.flatMap(this::verifyPublicKey);
if (verifyClientKeys && !clientKey.isPresent()) {
// missing or incorrect
// expired always not allowed
player.kickPlayer(plugin.getCore().getMessage("invalid-public-key")); player.kickPlayer(plugin.getCore().getMessage("invalid-public-key"));
plugin.getLog().warn("Invalid public key from player {}", username); plugin.getLog().warn("Invalid public key from player {}", username);
return; return;
@ -187,20 +202,24 @@ public class ProtocolLibListener extends PacketAdapter {
plugin.getLog().trace("GameProfile {} with {} connecting", sessionKey, username); plugin.getLog().trace("GameProfile {} with {} connecting", sessionKey, username);
packetEvent.getAsyncMarker().incrementProcessingDelay(); packetEvent.getAsyncMarker().incrementProcessingDelay();
Runnable nameCheckTask = new NameCheckTask(plugin, random, player, packetEvent, username, keyPair.getPublic()); Runnable nameCheckTask = new NameCheckTask(plugin, random, player, packetEvent, username, clientKey.orElse(null), keyPair.getPublic());
plugin.getScheduler().runAsync(nameCheckTask); plugin.getScheduler().runAsync(nameCheckTask);
} }
private boolean verifyPublicKey(WrappedProfileKeyData profileKey) { private Optional<ClientPublicKey> verifyPublicKey(WrappedProfileKeyData profileKey) {
Instant expires = profileKey.getExpireTime(); Instant expires = profileKey.getExpireTime();
PublicKey key = profileKey.getKey(); PublicKey key = profileKey.getKey();
byte[] signature = profileKey.getSignature(); byte[] signature = profileKey.getSignature();
ClientPublicKey clientKey = new ClientPublicKey(expires, key, signature); ClientPublicKey clientKey = new ClientPublicKey(expires, key, signature);
try { try {
return EncryptionUtil.verifyClientKey(clientKey, Instant.now()); if (EncryptionUtil.verifyClientKey(clientKey, Instant.now())) {
return Optional.of(clientKey);
}
} catch (SignatureException | InvalidKeyException | NoSuchAlgorithmException ex) { } catch (SignatureException | InvalidKeyException | NoSuchAlgorithmException ex) {
return false; return Optional.empty();
} }
return Optional.empty();
} }
private String getUsername(PacketContainer packet) { private String getUsername(PacketContainer packet) {

View File

@ -30,7 +30,7 @@ import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.reflect.StructureModifier; import com.comphenix.protocol.reflect.StructureModifier;
import com.comphenix.protocol.wrappers.WrappedChatComponent; import com.comphenix.protocol.wrappers.WrappedChatComponent;
import com.comphenix.protocol.wrappers.WrappedProfilePublicKey.WrappedProfileKeyData; import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;
import com.github.games647.fastlogin.core.shared.LoginSource; import com.github.games647.fastlogin.core.shared.LoginSource;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
@ -49,17 +49,17 @@ class ProtocolLibLoginSource implements LoginSource {
private final Random random; private final Random random;
private final WrappedProfileKeyData clientPublicKey; private final ClientPublicKey clientKey;
private final PublicKey publicKey; private final PublicKey publicKey;
private final String serverId = ""; private final String serverId = "";
private byte[] verifyToken; private byte[] verifyToken;
public ProtocolLibLoginSource(Player player, Random random, PublicKey serverPublicKey, WrappedProfileKeyData clientPublicKey) { public ProtocolLibLoginSource(Player player, Random random, PublicKey serverPublicKey, ClientPublicKey clientKey) {
this.player = player; this.player = player;
this.random = random; this.random = random;
this.publicKey = serverPublicKey; this.publicKey = serverPublicKey;
this.clientPublicKey = clientPublicKey; this.clientKey = clientKey;
} }
@Override @Override
@ -112,8 +112,8 @@ class ProtocolLibLoginSource implements LoginSource {
return player.getAddress(); return player.getAddress();
} }
public WrappedProfileKeyData getClientPublicKey() { public ClientPublicKey getClientKey() {
return clientPublicKey; return clientKey;
} }
public String getServerId() { public String getServerId() {

View File

@ -263,7 +263,7 @@ public class VerifyResponseTask implements Runnable {
EquivalentConverter<WrappedProfileKeyData> converter = BukkitConverters.getWrappedPublicKeyDataConverter(); EquivalentConverter<WrappedProfileKeyData> converter = BukkitConverters.getWrappedPublicKeyDataConverter();
var key = new WrappedProfileKeyData(clientKey.expiry(), clientKey.key(), sharedSecret); var key = new WrappedProfileKeyData(clientKey.expiry(), clientKey.key(), sharedSecret);
startPacket.getOptionals(converter).write(0, Optional.of(key)); startPacket.getOptionals(converter).write(0, Optional.ofNullable(key));
} else { } else {
//uuid is ignored by the packet definition //uuid is ignored by the packet definition
WrappedGameProfile fakeProfile = new WrappedGameProfile(UUID.randomUUID(), username); WrappedGameProfile fakeProfile = new WrappedGameProfile(UUID.randomUUID(), username);

View File

@ -272,6 +272,15 @@ autoRegisterFloodgate: false
# Enabling this might lead to people gaining unauthorized access to other's accounts! # Enabling this might lead to people gaining unauthorized access to other's accounts!
floodgatePrefixWorkaround: false floodgatePrefixWorkaround: false
# This option resembles the vanilla configuration option 'enforce-secure-profile' in the 'server.properties' file.
# It verifies if the incoming cryptographic key in the login request from the player is signed by Mojang. This key
# is necessary for servers where you or other in-game players want to verify that a chat message sent and signed by
# this player is not modified by any third-party. Modifications by your server would also invalidate the message.
#
# This feature is only relevant if you use the plugin in ProtocolLib mode. This also the case if you don't have any
# proxies in use.
verifyClientKeys: true
# Database configuration # Database configuration
# Recommended is the use of MariaDB (a better version of MySQL) # Recommended is the use of MariaDB (a better version of MySQL)