From 3b200d8a2bcfdc11c3d156dceaa1771dd999a7ef Mon Sep 17 00:00:00 2001 From: ChomeNS <95471003+ChomeNS@users.noreply.github.com> Date: Sun, 23 Mar 2025 08:41:12 +0700 Subject: [PATCH] feat: re-add AuthPlugin --- build-number.txt | 2 +- .../chayapak1/chomens_bot/Configuration.java | 3 +- .../java/me/chayapak1/chomens_bot/Main.java | 1 + .../chomens_bot/plugins/AuthPlugin.java | 209 +++++++++++++++--- src/main/resources/default-config.yml | 4 +- 5 files changed, 188 insertions(+), 31 deletions(-) diff --git a/build-number.txt b/build-number.txt index 74fae4f4..e9ad4b45 100644 --- a/build-number.txt +++ b/build-number.txt @@ -1 +1 @@ -2075 \ No newline at end of file +2094 \ No newline at end of file diff --git a/src/main/java/me/chayapak1/chomens_bot/Configuration.java b/src/main/java/me/chayapak1/chomens_bot/Configuration.java index 1daa57e4..338e26d0 100644 --- a/src/main/java/me/chayapak1/chomens_bot/Configuration.java +++ b/src/main/java/me/chayapak1/chomens_bot/Configuration.java @@ -52,8 +52,7 @@ public class Configuration { public static class OwnerAuthentication { public boolean enabled = false; - public String muteReason = ""; - public Map ips = new HashMap<>(); + public int timeout = 10 * 1000; } public static class Backup { diff --git a/src/main/java/me/chayapak1/chomens_bot/Main.java b/src/main/java/me/chayapak1/chomens_bot/Main.java index efb9a3cd..d54ddb0f 100644 --- a/src/main/java/me/chayapak1/chomens_bot/Main.java +++ b/src/main/java/me/chayapak1/chomens_bot/Main.java @@ -133,6 +133,7 @@ public class Main { // initialize plugins console = new ConsolePlugin(config); LoggerPlugin.init(); + AuthPlugin.init(config); if (config.database.enabled) database = new DatabasePlugin(config); if (config.discord.enabled) discord = new DiscordPlugin(config); if (config.irc.enabled) new IRCPlugin(config); diff --git a/src/main/java/me/chayapak1/chomens_bot/plugins/AuthPlugin.java b/src/main/java/me/chayapak1/chomens_bot/plugins/AuthPlugin.java index bf93e995..239f5884 100644 --- a/src/main/java/me/chayapak1/chomens_bot/plugins/AuthPlugin.java +++ b/src/main/java/me/chayapak1/chomens_bot/plugins/AuthPlugin.java @@ -1,59 +1,218 @@ package me.chayapak1.chomens_bot.plugins; import me.chayapak1.chomens_bot.Bot; +import me.chayapak1.chomens_bot.Configuration; import me.chayapak1.chomens_bot.data.logging.LogType; import me.chayapak1.chomens_bot.data.player.PlayerEntry; +import me.chayapak1.chomens_bot.util.LoggerUtilities; +import me.chayapak1.chomens_bot.util.UUIDUtilities; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.NamedTextColor; +import org.geysermc.mcprotocollib.network.event.session.DisconnectedEvent; -import java.util.concurrent.CompletableFuture; +import javax.crypto.Cipher; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.*; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; import java.util.concurrent.TimeUnit; public class AuthPlugin extends PlayersPlugin.Listener { + private static final String ID = "chomens_bot_verify"; + + private static PrivateKey PRIVATE_KEY; + private static final Path PRIVATE_KEY_PATH = Path.of("private.key"); + private static final Path PUBLIC_KEY_PATH = Path.of("public.key"); + + private static final String BEGIN_PRIVATE_KEY = "-----BEGIN CHOMENS BOT PRIVATE KEY-----"; + private static final String END_PRIVATE_KEY = "-----END CHOMENS BOT PRIVATE KEY-----"; + + private static final String BEGIN_PUBLIC_KEY = "-----BEGIN CHOMENS BOT PUBLIC KEY-----"; + private static final String END_PUBLIC_KEY = "-----END CHOMENS BOT PUBLIC KEY-----"; + + public static void init (Configuration config) { + if (!config.ownerAuthentication.enabled) return; + + try { + // let's only check for the private key here + if (!Files.exists(PRIVATE_KEY_PATH)) { + final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + + final KeyPair pair = keyGen.generateKeyPair(); + + // write the keys + // (note: no newline split is intentional) + final String encodedPrivateKey = + BEGIN_PRIVATE_KEY + "\n" + + Base64.getEncoder().encodeToString(pair.getPrivate().getEncoded()) + + "\n" + END_PRIVATE_KEY; + final String encodedPublicKey = + BEGIN_PUBLIC_KEY + "\n" + + Base64.getEncoder().encodeToString(pair.getPublic().getEncoded()) + + "\n" + END_PUBLIC_KEY; + + final BufferedWriter privateKeyWriter = Files.newBufferedWriter(PRIVATE_KEY_PATH); + privateKeyWriter.write(encodedPrivateKey); + privateKeyWriter.close(); + + final BufferedWriter publicKeyWriter = Files.newBufferedWriter(PUBLIC_KEY_PATH); + publicKeyWriter.write(encodedPublicKey); + publicKeyWriter.close(); + } + + // is this a good way to remove the things? + final String privateKeyString = new String(Files.readAllBytes(PRIVATE_KEY_PATH)) + .replace(BEGIN_PRIVATE_KEY + "\n", "") + .replace("\n" + END_PRIVATE_KEY, "") + .replace("\n", "") + .trim(); + + final byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyString); + final KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + + PRIVATE_KEY = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes)); + } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException | IllegalArgumentException e) { + LoggerUtilities.error(e); + } + } + private final Bot bot; - private final String ownerIpForServer; + public boolean isAuthenticating = false; + public long startTime; public AuthPlugin (Bot bot) { this.bot = bot; - this.ownerIpForServer = bot.config.ownerAuthentication.ips.get(bot.getServerString(true)); + if (!bot.config.ownerAuthentication.enabled) return; - if (!bot.config.ownerAuthentication.enabled || ownerIpForServer == null) return; + bot.executor.scheduleAtFixedRate(() -> { + if (!isAuthenticating || !bot.config.ownerAuthentication.enabled) return; + + timeoutCheck(); + sendAuthRequestMessage(); + }, 500, 500, TimeUnit.MILLISECONDS); + + bot.addListener(new Bot.Listener() { + @Override + public void disconnected (DisconnectedEvent event) { + AuthPlugin.this.disconnected(); + } + }); + + bot.chat.addListener(new ChatPlugin.Listener() { + @Override + public boolean systemMessageReceived (Component component, String string, String ansi) { + return AuthPlugin.this.systemMessageReceived(component); + } + }); bot.players.addListener(this); } + private String decrypt (byte[] data) throws Exception { + final Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.DECRYPT_MODE, PRIVATE_KEY); + + final byte[] decryptedBytes = cipher.doFinal(data); + + return new String(decryptedBytes); + } + + private void timeoutCheck () { + if (System.currentTimeMillis() - startTime < bot.config.ownerAuthentication.timeout) return; + + final PlayerEntry target = bot.players.getEntry(bot.config.ownerName); + + if (target == null) return; + + bot.filterManager.add(target, "Authentication timed out"); + } + + private void sendAuthRequestMessage () { + final PlayerEntry target = bot.players.getEntry(bot.config.ownerName); + + if (target == null) return; + + final Component component = Component + .text(ID) + .append(Component.text(UUIDUtilities.selector(bot.profile.getId()))); + + bot.chat.tellraw(component, target.profile.getId()); + } + @Override - public void playerJoined(PlayerEntry target) { + public void playerJoined (PlayerEntry target) { if (!target.profile.getName().equals(bot.config.ownerName) || !bot.options.useCore) return; - final CompletableFuture future = bot.players.getPlayerIP(target, true); + startTime = System.currentTimeMillis(); + isAuthenticating = true; + } - future.completeOnTimeout("", 10, TimeUnit.SECONDS); + private boolean systemMessageReceived (Component component) { + if (!bot.config.ownerAuthentication.enabled) return true; + + if (!(component instanceof TextComponent textComponent)) return true; + + final String id = textComponent.content(); + + if (!id.equals(ID)) return true; + + if (!isAuthenticating) return false; + + if (component.children().size() != 1) return true; + + if (!(component.children().getFirst() instanceof TextComponent dataComponent)) return true; + + final String data = dataComponent.content(); + + try { + final String decrypted = decrypt(Base64.getDecoder().decode(data)); + + // what should i use here? should it be the ID? + // or does it even matter? + if (!decrypted.equals(ID)) return false; + + isAuthenticating = false; - future.thenApply(ip -> { bot.logger.log( LogType.AUTH, - Component.translatable( - "Authenticating with user IP %s and configured owner IP %s", - Component.text(ip), - Component.text(ownerIpForServer) - ) + Component + .text("Player has been verified") + .color(NamedTextColor.GREEN) ); - if (ip.equals(ownerIpForServer)) { - bot.chat.tellraw( - Component - .text("You have been verified") - .color(NamedTextColor.GREEN), - target.profile.getId() - ); - } else { - bot.filterManager.doAll(target, bot.config.ownerAuthentication.muteReason); - } + final PlayerEntry target = bot.players.getEntry(bot.config.ownerName); - return ip; - }); + if (target == null) return false; // sad :( + + bot.chat.tellraw( + Component + .text("You have been verified") + .color(NamedTextColor.GREEN), + target.profile.getId() + ); + } catch (Exception e) { + bot.logger.error(e); + } + + return false; + } + + @Override + public void playerLeft (PlayerEntry target) { + if (!target.profile.getName().equals(bot.config.ownerName)) return; + + isAuthenticating = false; + } + + private void disconnected () { + isAuthenticating = false; } } diff --git a/src/main/resources/default-config.yml b/src/main/resources/default-config.yml index ad9b44e3..4ec33c67 100644 --- a/src/main/resources/default-config.yml +++ b/src/main/resources/default-config.yml @@ -77,9 +77,7 @@ imposterFormatChecker: ownerAuthentication: enabled: false - muteReason: '' - ips: - localhost:25565: '127.0.0.1' + timeout: 10000 # 10 seconds - 10 * 1000 # if this is enabled when someone does a clear chat command the bot # will tellraw `{player} cleared the chat`