From 265a35080b6990e6f25aaf5959e83ace73cc3064 Mon Sep 17 00:00:00 2001 From: ChomeNS <95471003+ChomeNS@users.noreply.github.com> Date: Tue, 25 Mar 2025 10:10:23 +0700 Subject: [PATCH] feat: chomens mod integration (completely unused) i will probably make it a server instead idk, right now i just wanted to make the chipmunkmod core refill silent --- build-number.txt | 2 +- .../java/me/chayapak1/chomens_bot/Bot.java | 2 + .../chomens_bot/chomeNSMod/Packet.java | 8 + .../chomens_bot/chomeNSMod/Types.java | 52 +++ .../ClientboundCoreOutputPacket.java | 34 ++ .../ClientboundSuccessfulHandshakePacket.java | 22 ++ .../ServerboundHandshakePacket.java | 21 ++ .../ServerboundRunCoreCommandPacket.java | 33 ++ .../plugins/ChomeNSModIntegrationPlugin.java | 313 ++++++++++++++++++ 9 files changed, 486 insertions(+), 1 deletion(-) create mode 100644 src/main/java/me/chayapak1/chomens_bot/chomeNSMod/Packet.java create mode 100644 src/main/java/me/chayapak1/chomens_bot/chomeNSMod/Types.java create mode 100644 src/main/java/me/chayapak1/chomens_bot/chomeNSMod/clientboundPackets/ClientboundCoreOutputPacket.java create mode 100644 src/main/java/me/chayapak1/chomens_bot/chomeNSMod/clientboundPackets/ClientboundSuccessfulHandshakePacket.java create mode 100644 src/main/java/me/chayapak1/chomens_bot/chomeNSMod/serverboundPackets/ServerboundHandshakePacket.java create mode 100644 src/main/java/me/chayapak1/chomens_bot/chomeNSMod/serverboundPackets/ServerboundRunCoreCommandPacket.java create mode 100644 src/main/java/me/chayapak1/chomens_bot/plugins/ChomeNSModIntegrationPlugin.java diff --git a/build-number.txt b/build-number.txt index edaa2d42..fc358d09 100644 --- a/build-number.txt +++ b/build-number.txt @@ -1 +1 @@ -2131 \ No newline at end of file +2160 \ No newline at end of file diff --git a/src/main/java/me/chayapak1/chomens_bot/Bot.java b/src/main/java/me/chayapak1/chomens_bot/Bot.java index ce702f02..4e7abbb7 100644 --- a/src/main/java/me/chayapak1/chomens_bot/Bot.java +++ b/src/main/java/me/chayapak1/chomens_bot/Bot.java @@ -107,6 +107,7 @@ public class Bot extends SessionAdapter { public PacketSnifferPlugin packetSniffer; public VoiceChatPlugin voiceChat; public TeamJoinerPlugin teamJoiner; + public ChomeNSModIntegrationPlugin chomeNSMod; public AuthPlugin auth; public ScreensharePlugin screenshare; public FormatCheckerPlugin formatChecker; @@ -159,6 +160,7 @@ public class Bot extends SessionAdapter { this.packetSniffer = new PacketSnifferPlugin(this); this.voiceChat = new VoiceChatPlugin(this); this.teamJoiner = new TeamJoinerPlugin(this); + this.chomeNSMod = new ChomeNSModIntegrationPlugin(this); this.auth = new AuthPlugin(this); // this.screenshare = new ScreensharePlugin(this); this.formatChecker = new FormatCheckerPlugin(this); diff --git a/src/main/java/me/chayapak1/chomens_bot/chomeNSMod/Packet.java b/src/main/java/me/chayapak1/chomens_bot/chomeNSMod/Packet.java new file mode 100644 index 00000000..9fceae72 --- /dev/null +++ b/src/main/java/me/chayapak1/chomens_bot/chomeNSMod/Packet.java @@ -0,0 +1,8 @@ +package me.chayapak1.chomens_bot.chomeNSMod; + +import io.netty.buffer.ByteBuf; + +public interface Packet { + int getId (); + void serialize (ByteBuf buf); +} diff --git a/src/main/java/me/chayapak1/chomens_bot/chomeNSMod/Types.java b/src/main/java/me/chayapak1/chomens_bot/chomeNSMod/Types.java new file mode 100644 index 00000000..a52d486a --- /dev/null +++ b/src/main/java/me/chayapak1/chomens_bot/chomeNSMod/Types.java @@ -0,0 +1,52 @@ +package me.chayapak1.chomens_bot.chomeNSMod; + +import io.netty.buffer.ByteBuf; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +public class Types { + public static UUID readUUID (ByteBuf buf) { + final long mostSignificantBits = buf.readLong(); + final long leastSignificantBits = buf.readLong(); + + return new UUID(mostSignificantBits, leastSignificantBits); + } + + public static void writeUUID (ByteBuf buf, UUID uuid) { + buf.writeLong(uuid.getMostSignificantBits()); + buf.writeLong(uuid.getLeastSignificantBits()); + } + + public static void writeString (ByteBuf buf, String string) { + buf.writeInt(string.length()); + buf.writeBytes(string.getBytes(StandardCharsets.UTF_8)); + } + + public static String readString (ByteBuf buf) { + final int length = buf.readInt(); + + final byte[] bytes = new byte[length]; + buf.readBytes(bytes); + + return new String(bytes, StandardCharsets.UTF_8); + } + + public static Component readComponent (ByteBuf buf) { + final String stringJSON = readString(buf); + + try { + return GsonComponentSerializer.gson().deserialize(stringJSON); + } catch (Exception e) { + return null; + } + } + + public static void writeComponent (ByteBuf buf, Component component) { + final String stringJSON = GsonComponentSerializer.gson().serialize(component); + + writeString(buf, stringJSON); + } +} diff --git a/src/main/java/me/chayapak1/chomens_bot/chomeNSMod/clientboundPackets/ClientboundCoreOutputPacket.java b/src/main/java/me/chayapak1/chomens_bot/chomeNSMod/clientboundPackets/ClientboundCoreOutputPacket.java new file mode 100644 index 00000000..3b690529 --- /dev/null +++ b/src/main/java/me/chayapak1/chomens_bot/chomeNSMod/clientboundPackets/ClientboundCoreOutputPacket.java @@ -0,0 +1,34 @@ +package me.chayapak1.chomens_bot.chomeNSMod.clientboundPackets; + +import io.netty.buffer.ByteBuf; +import me.chayapak1.chomens_bot.chomeNSMod.Packet; +import me.chayapak1.chomens_bot.chomeNSMod.Types; +import net.kyori.adventure.text.Component; + +import java.util.UUID; + +public class ClientboundCoreOutputPacket implements Packet { + public final UUID runID; + public final Component output; + + public ClientboundCoreOutputPacket (UUID runID, Component output) { + this.runID = runID; + this.output = output; + } + + public ClientboundCoreOutputPacket (ByteBuf buf) { + this.runID = Types.readUUID(buf); + this.output = Types.readComponent(buf); + } + + @Override + public int getId () { + return 1; + } + + @Override + public void serialize (ByteBuf buf) { + Types.writeUUID(buf, this.runID); + Types.writeComponent(buf, this.output); + } +} diff --git a/src/main/java/me/chayapak1/chomens_bot/chomeNSMod/clientboundPackets/ClientboundSuccessfulHandshakePacket.java b/src/main/java/me/chayapak1/chomens_bot/chomeNSMod/clientboundPackets/ClientboundSuccessfulHandshakePacket.java new file mode 100644 index 00000000..cb223542 --- /dev/null +++ b/src/main/java/me/chayapak1/chomens_bot/chomeNSMod/clientboundPackets/ClientboundSuccessfulHandshakePacket.java @@ -0,0 +1,22 @@ +package me.chayapak1.chomens_bot.chomeNSMod.clientboundPackets; + +import io.netty.buffer.ByteBuf; +import me.chayapak1.chomens_bot.chomeNSMod.Packet; + +public class ClientboundSuccessfulHandshakePacket implements Packet { + public ClientboundSuccessfulHandshakePacket () { + } + + public ClientboundSuccessfulHandshakePacket (ByteBuf buf) { + } + + @Override + public int getId() { + return 0; + } + + @Override + public void serialize (ByteBuf buf) { + + } +} diff --git a/src/main/java/me/chayapak1/chomens_bot/chomeNSMod/serverboundPackets/ServerboundHandshakePacket.java b/src/main/java/me/chayapak1/chomens_bot/chomeNSMod/serverboundPackets/ServerboundHandshakePacket.java new file mode 100644 index 00000000..1bf04a99 --- /dev/null +++ b/src/main/java/me/chayapak1/chomens_bot/chomeNSMod/serverboundPackets/ServerboundHandshakePacket.java @@ -0,0 +1,21 @@ +package me.chayapak1.chomens_bot.chomeNSMod.serverboundPackets; + +import io.netty.buffer.ByteBuf; +import me.chayapak1.chomens_bot.chomeNSMod.Packet; + +public class ServerboundHandshakePacket implements Packet { + public ServerboundHandshakePacket () { + } + + public ServerboundHandshakePacket (ByteBuf buf) { + } + + @Override + public int getId () { + return 0; + } + + @Override + public void serialize (ByteBuf buf) { + } +} diff --git a/src/main/java/me/chayapak1/chomens_bot/chomeNSMod/serverboundPackets/ServerboundRunCoreCommandPacket.java b/src/main/java/me/chayapak1/chomens_bot/chomeNSMod/serverboundPackets/ServerboundRunCoreCommandPacket.java new file mode 100644 index 00000000..d798ec70 --- /dev/null +++ b/src/main/java/me/chayapak1/chomens_bot/chomeNSMod/serverboundPackets/ServerboundRunCoreCommandPacket.java @@ -0,0 +1,33 @@ +package me.chayapak1.chomens_bot.chomeNSMod.serverboundPackets; + +import io.netty.buffer.ByteBuf; +import me.chayapak1.chomens_bot.chomeNSMod.Packet; +import me.chayapak1.chomens_bot.chomeNSMod.Types; + +import java.util.UUID; + +public class ServerboundRunCoreCommandPacket implements Packet { + public final UUID runID; + public final String command; + + public ServerboundRunCoreCommandPacket (UUID runID, String command) { + this.runID = runID; + this.command = command; + } + + public ServerboundRunCoreCommandPacket (ByteBuf buf) { + this.runID = Types.readUUID(buf); + this.command = Types.readString(buf); + } + + @Override + public int getId () { + return 1; + } + + @Override + public void serialize (ByteBuf buf) { + Types.writeUUID(buf, this.runID); + Types.writeString(buf, this.command); + } +} diff --git a/src/main/java/me/chayapak1/chomens_bot/plugins/ChomeNSModIntegrationPlugin.java b/src/main/java/me/chayapak1/chomens_bot/plugins/ChomeNSModIntegrationPlugin.java new file mode 100644 index 00000000..28e8d935 --- /dev/null +++ b/src/main/java/me/chayapak1/chomens_bot/plugins/ChomeNSModIntegrationPlugin.java @@ -0,0 +1,313 @@ +package me.chayapak1.chomens_bot.plugins; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import me.chayapak1.chomens_bot.Bot; +import me.chayapak1.chomens_bot.Configuration; +import me.chayapak1.chomens_bot.chomeNSMod.Packet; +import me.chayapak1.chomens_bot.chomeNSMod.Types; +import me.chayapak1.chomens_bot.chomeNSMod.clientboundPackets.ClientboundCoreOutputPacket; +import me.chayapak1.chomens_bot.chomeNSMod.clientboundPackets.ClientboundSuccessfulHandshakePacket; +import me.chayapak1.chomens_bot.chomeNSMod.serverboundPackets.ServerboundHandshakePacket; +import me.chayapak1.chomens_bot.chomeNSMod.serverboundPackets.ServerboundRunCoreCommandPacket; +import me.chayapak1.chomens_bot.data.player.PlayerEntry; +import me.chayapak1.chomens_bot.util.LoggerUtilities; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import org.apache.commons.lang3.tuple.Pair; + +import javax.crypto.Cipher; +import java.io.BufferedWriter; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +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.security.spec.X509EncodedKeySpec; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; + +// This is inspired from the ChomeNS Bot Proxy which is in the JavaScript version of ChomeNS Bot. +// Right now it is not really used anywhere, so you'll see duplicate codes from AuthPlugin +public class ChomeNSModIntegrationPlugin implements ChatPlugin.Listener, PlayersPlugin.Listener { + private static final String ID = "chomens_mod"; + + public static final List> SERVERBOUND_PACKETS = new ArrayList<>(); + + static { + SERVERBOUND_PACKETS.add(ServerboundHandshakePacket.class); + SERVERBOUND_PACKETS.add(ServerboundRunCoreCommandPacket.class); + } + + private static PrivateKey PRIVATE_KEY; + + private static final Map CLIENT_PUBLIC_KEYS = new HashMap<>(); + private static final Path CLIENT_PUBLIC_KEYS_PATH = Path.of("client_public_keys"); + + 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)); + + // lol this is so messy + if (Files.isDirectory(CLIENT_PUBLIC_KEYS_PATH)) { + try (final Stream files = Files.list(CLIENT_PUBLIC_KEYS_PATH)) { + for (Path path : files.toList()) { + try { + final String publicKeyString = new String(Files.readAllBytes(path)) + .replace(BEGIN_PUBLIC_KEY + "\n", "") + .replace("\n" + END_PUBLIC_KEY, "") + .replace("\n", "") + .trim(); + + final byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyString); + final KeyFactory clientKeyFactory = KeyFactory.getInstance("RSA"); + + String username = path.getFileName().toString(); + + if (username.contains(".")) username = username.substring(0, username.lastIndexOf(".")); + + CLIENT_PUBLIC_KEYS.put( + username, + clientKeyFactory.generatePublic(new X509EncodedKeySpec(publicKeyBytes)) + ); + } catch (Exception ignored) {} + } + } + } + } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException | IllegalArgumentException e) { + LoggerUtilities.error(e); + } + } + + private final Bot bot; + + private final List listeners = new ArrayList<>(); + + public final List connectedPlayers = new ArrayList<>(); + + public ChomeNSModIntegrationPlugin (Bot bot) { + this.bot = bot; + + bot.chat.addListener(this); + bot.players.addListener(this); + } + + public byte[] decrypt (byte[] data) throws Exception { + final Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.DECRYPT_MODE, PRIVATE_KEY); + + return cipher.doFinal(data); + } + + public String encrypt (String player, byte[] data) throws Exception { + final PublicKey publicKey = CLIENT_PUBLIC_KEYS.get(player); + + if (publicKey == null) return null; + + final Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + + final byte[] encryptedBytes = cipher.doFinal(data); + + return Base64.getEncoder().encodeToString(encryptedBytes); + } + + public void send (PlayerEntry target, Packet packet) { + if (!connectedPlayers.contains(target)) return; + + final ByteBuf buf = Unpooled.buffer(); + + buf.writeInt(packet.getId()); + packet.serialize(buf); + + final byte[] bytes = new byte[buf.readableBytes()]; + buf.readBytes(bytes); + + try { + final String encrypted = encrypt(target.profile.getName(), bytes); + + final Component component = Component + .text(ID) + .append(Component.text(encrypted)); + + bot.chat.tellraw(component, target.profile.getId()); + } catch (Exception ignored) {} + } + + private Pair deserialize (byte[] data) { + final ByteBuf buf = Unpooled.wrappedBuffer(data); + + final UUID uuid = Types.readUUID(buf); + + final PlayerEntry player = bot.players.getEntry(uuid); + + if (player == null) return null; + + final int id = buf.readInt(); + + final Class packetClass = SERVERBOUND_PACKETS.get(id); + + if (packetClass == null) return null; + + try { + return Pair.of(player, packetClass.getDeclaredConstructor(ByteBuf.class).newInstance(buf)); + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) { + return null; + } + } + + @Override + public boolean systemMessageReceived (Component component, String string, String ansi) { + if (!(component instanceof TextComponent textComponent)) return true; + + final String id = textComponent.content(); + + if (!id.equals(ID)) return true; + + if (component.children().size() != 1) return true; + + if (!(component.children().getFirst() instanceof TextComponent dataComponent)) return true; + + final String data = dataComponent.content(); + + try { + final byte[] decrypted = decrypt(Base64.getDecoder().decode(data)); + + final Pair deserialized = deserialize(decrypted); + + if (deserialized == null) return false; + + final PlayerEntry player = deserialized.getKey(); + final Packet packet = deserialized.getValue(); + + handlePacket(player, packet); + +// isAuthenticating = false; +// +// bot.logger.log( +// LogType.AUTH, +// Component +// .text("Player has been verified") +// .color(NamedTextColor.GREEN) +// ); +// +// final PlayerEntry target = bot.players.getEntry(bot.config.ownerName); +// +// if (target == null) return false; // sad :( +// +// bot.chat.tellraw( +// Component +// .text("You have been verified") +// .color(NamedTextColor.GREEN), +// target.profile.getId() +// ); + } catch (Exception ignored) {} + + return false; + } + + private void handlePacket (PlayerEntry player, Packet packet) { + if (packet instanceof ServerboundHandshakePacket t_packet) handlePacket(player, t_packet); + else if (packet instanceof ServerboundRunCoreCommandPacket t_packet) handlePacket(player, t_packet); + + for (Listener listener : listeners) listener.packetReceived(player, packet); + } + + private void handlePacket (PlayerEntry player, ServerboundHandshakePacket ignoredPacket) { + connectedPlayers.remove(player); + + connectedPlayers.add(player); + + send(player, new ClientboundSuccessfulHandshakePacket()); + } + + private void handlePacket (PlayerEntry player, ServerboundRunCoreCommandPacket packet) { + final CompletableFuture future = bot.core.runTracked(packet.command); + + if (future == null) { + send( + player, + new ClientboundCoreOutputPacket( + packet.runID, + Component.empty() + ) + ); + + return; + } + + future.thenApply(output -> { + send( + player, + new ClientboundCoreOutputPacket( + packet.runID, + output + ) + ); + + return null; + }); + } + + @Override + public void playerLeft (PlayerEntry target) { + if (!connectedPlayers.contains(target)) return; + + connectedPlayers.remove(target); + } + + public interface Listener { + default void packetReceived (PlayerEntry player, Packet packet) {} + } +}