feat: re-add AuthPlugin

This commit is contained in:
Chayapak 2025-03-23 08:41:12 +07:00
parent d77c05389e
commit 3b200d8a2b
Signed by: ChomeNS
SSH key fingerprint: SHA256:0YoxhdyXsgbc0nfeB2N6FYE60mxMU7DS4uCUMaw2mvA
5 changed files with 188 additions and 31 deletions
build-number.txt
src/main
java/me/chayapak1/chomens_bot
resources

View file

@ -1 +1 @@
2075
2094

View file

@ -52,8 +52,7 @@ public class Configuration {
public static class OwnerAuthentication {
public boolean enabled = false;
public String muteReason = "";
public Map<String, String> ips = new HashMap<>();
public int timeout = 10 * 1000;
}
public static class Backup {

View file

@ -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);

View file

@ -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<String> 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;
}
}

View file

@ -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`