diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 9204a3b..af44bdd 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -3,4 +3,5 @@ dependencies { implementation("com.zaxxer:HikariCP:3.4.5") compileOnly("com.destroystokyo:paperspigot:1.12.2") implementation("com.github.cryptomorin:XSeries:7.8.0") + implementation("com.warrenstrange:googleauth:1.4.0") } \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/account/AccountRepository.java b/core/src/main/java/zone/themcgamer/core/account/AccountRepository.java index 6fc4ad9..d43f0c2 100644 --- a/core/src/main/java/zone/themcgamer/core/account/AccountRepository.java +++ b/core/src/main/java/zone/themcgamer/core/account/AccountRepository.java @@ -117,6 +117,7 @@ public class AccountRepository extends MySQLRepository { } Bukkit.getPluginManager().callEvent(new AccountPreLoadEvent(uuid, name, ipAddress)); int finalAccountId = accountId; + query+= AccountManager.MINI_ACCOUNTS.parallelStream().map(miniAccount -> miniAccount.getQuery(finalAccountId, uuid, name, ipAddress, encryptedIpAddress)).collect(Collectors.joining()); if (!query.trim().isEmpty()) { log.info("Executing mini account tasks (" + AccountManager.MINI_ACCOUNTS.size() + ") for " + name); diff --git a/core/src/main/java/zone/themcgamer/core/account/MiniAccount.java b/core/src/main/java/zone/themcgamer/core/account/MiniAccount.java index 514e182..16bbdd4 100644 --- a/core/src/main/java/zone/themcgamer/core/account/MiniAccount.java +++ b/core/src/main/java/zone/themcgamer/core/account/MiniAccount.java @@ -2,6 +2,7 @@ package zone.themcgamer.core.account; import lombok.Getter; import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.core.account.Account; import zone.themcgamer.core.module.Module; import java.sql.ResultSet; diff --git a/core/src/main/java/zone/themcgamer/core/chat/ChatManager.java b/core/src/main/java/zone/themcgamer/core/chat/ChatManager.java index ada042b..9ba7ef1 100644 --- a/core/src/main/java/zone/themcgamer/core/chat/ChatManager.java +++ b/core/src/main/java/zone/themcgamer/core/chat/ChatManager.java @@ -8,6 +8,7 @@ import net.md_5.bungee.api.chat.TextComponent; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.plugin.java.JavaPlugin; import zone.themcgamer.core.account.Account; @@ -65,8 +66,10 @@ public class ChatManager extends Module { registerCommand(new EmotesCommand(this)); } - @EventHandler + @EventHandler(priority = EventPriority.HIGHEST) private void onChat(AsyncPlayerChatEvent event) { + if (event.isCancelled()) + return; Player player = event.getPlayer(); String message = event.getMessage(); diff --git a/core/src/main/java/zone/themcgamer/core/module/Module.java b/core/src/main/java/zone/themcgamer/core/module/Module.java index ac581e6..3eceafb 100644 --- a/core/src/main/java/zone/themcgamer/core/module/Module.java +++ b/core/src/main/java/zone/themcgamer/core/module/Module.java @@ -38,6 +38,10 @@ public abstract class Module implements Listener { public void onEnable() {} // Called when the module is enabled public void onDisable() {} // Called when the module is disabled + public String getName() { + return info.name(); + } + public void registerCommand(Object command) { CommandManager commandManager = getModule(CommandManager.class); if (commandManager == null) @@ -50,7 +54,7 @@ public abstract class Module implements Listener { * @param message the message to log */ public void log(String message) { - System.out.println(info.name() + " » " + message); + Bukkit.getLogger().info("§b" + info.name() + " §8» §7" + message); } /** diff --git a/core/src/main/java/zone/themcgamer/core/plugin/MGZPlugin.java b/core/src/main/java/zone/themcgamer/core/plugin/MGZPlugin.java index 06e4f5b..46b2957 100644 --- a/core/src/main/java/zone/themcgamer/core/plugin/MGZPlugin.java +++ b/core/src/main/java/zone/themcgamer/core/plugin/MGZPlugin.java @@ -20,6 +20,7 @@ import zone.themcgamer.core.plugin.command.PluginsCommand; import zone.themcgamer.core.server.ServerManager; import zone.themcgamer.core.task.TaskManager; import zone.themcgamer.core.traveler.ServerTraveler; +import zone.themcgamer.core.twoFactor.TwoFactorAuthentication; import zone.themcgamer.core.update.ServerUpdater; import zone.themcgamer.data.jedis.JedisController; import zone.themcgamer.data.jedis.data.server.MinecraftServer; @@ -168,6 +169,7 @@ public abstract class MGZPlugin extends JavaPlugin { commandManager.registerCommand(this, new PluginsCommand()); new CooldownHandler(this); + AccountManager.addMiniAccount(new TwoFactorAuthentication(this, mySQLController)); nametagManager = new NametagManager(this); accountManager = new AccountManager(this, mySQLController, nametagManager); diff --git a/core/src/main/java/zone/themcgamer/core/twoFactor/TwoFactorAuthentication.java b/core/src/main/java/zone/themcgamer/core/twoFactor/TwoFactorAuthentication.java new file mode 100644 index 0000000..fd517f4 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/twoFactor/TwoFactorAuthentication.java @@ -0,0 +1,207 @@ +package zone.themcgamer.core.twoFactor; + +import com.cryptomorin.xseries.XSound; +import com.warrenstrange.googleauth.GoogleAuthenticator; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.FoodLevelChangeEvent; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.player.*; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.common.EnumUtils; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.account.MiniAccount; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.module.ModuleInfo; +import zone.themcgamer.data.Rank; +import zone.themcgamer.data.mysql.MySQLController; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * @author Braydon + */ +@ModuleInfo(name = "Two Factor") +public class TwoFactorAuthentication extends MiniAccount { + private final TwoFactorRepository repository; + private final GoogleAuthenticator googleAuthenticator = new GoogleAuthenticator(); + private final List authenticating = new ArrayList<>(); + + public TwoFactorAuthentication(JavaPlugin plugin, MySQLController mySQLController) { + super(plugin); + repository = new TwoFactorRepository(mySQLController.getDataSource()); + } + + @Override + public TwoFactorClient getAccount(int accountId, UUID uuid, String name, String ip, String encryptedIp) { + return new TwoFactorClient(); + } + + @Override + public String getQuery(int accountId, UUID uuid, String name, String ip, String encryptedIp) { + return "SELECT dbKey, value FROM `twoFactor` WHERE `accountId` = '" + accountId + "';"; + } + + @Override + public void loadAccount(int accountId, UUID uuid, String name, String ip, String encryptedIp, ResultSet resultSet) throws SQLException { + Optional optionalTwoFactorClient = lookup(uuid); + if (optionalTwoFactorClient.isEmpty()) + return; + TwoFactorClient twoFactorClient = optionalTwoFactorClient.get(); + while (resultSet.next()) { + TwoFactorDBKey databaseKey = EnumUtils.fromString(TwoFactorDBKey.class, resultSet.getString("dbKey")); + if (databaseKey == null) + continue; + if (databaseKey == TwoFactorDBKey.SECRET_KEY) + twoFactorClient.setSecretKey(resultSet.getString("value")); + else twoFactorClient.setLastAuthentication(resultSet.getLong("value")); + } + } + + @EventHandler(priority = EventPriority.LOWEST) + private void onLogin(PlayerLoginEvent event) { + UUID uuid = event.getPlayer().getUniqueId(); + Optional optionalAccount = AccountManager.fromCache(uuid); + if (optionalAccount.isEmpty()) + return; + Account account = optionalAccount.get(); + if (!account.hasRank(Rank.HELPER)) + return; + Optional optionalTwoFactorClient = lookup(uuid); + if (optionalTwoFactorClient.isEmpty()) + return; + // If the player hasn't setup 2fa, the player hasn't authenticated in 24 hours, or their ip has changed, make them authenticate + if (optionalTwoFactorClient.get().requiresAuthentication() || !account.getLastEncryptedIpAddress().equals(account.getEncryptedIpAddress())) + authenticating.add(uuid); + } + + @EventHandler(priority = EventPriority.HIGHEST) + private void onJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + Optional optionalAccount = AccountManager.fromCache(player.getUniqueId()); + if (optionalAccount.isEmpty()) + return; + Account account = optionalAccount.get(); + if (!account.hasRank(Rank.HELPER)) + return; + if (!authenticating.contains(player.getUniqueId())) { + player.sendMessage(Style.main(getName(), "§aAuthenticated!")); + return; + } + Optional optionalTwoFactorClient = lookup(player.getUniqueId()); + if (optionalTwoFactorClient.isEmpty()) + return; + TwoFactorClient twoFactorClient = optionalTwoFactorClient.get(); + Bukkit.getScheduler().scheduleSyncDelayedTask(getPlugin(), () -> { + if (twoFactorClient.getSecretKey() == null) { + String secretKey = googleAuthenticator.createCredentials().getKey(); + twoFactorClient.setSecretKey(secretKey); + + // TODO: 2/20/21 generate a qr map and give it to the player so they can scan it on their twoFactor app + player.sendMessage(Style.main(getName(), "Hey §b" + player.getName() + "§7, you have not setup your two factor authentication yet!")); + player.sendMessage(Style.main(getName(), "To begin, open your authentication app of choice and insert the secret key")); + player.sendMessage(Style.main(getName(), "§f" + secretKey)); + player.sendMessage(Style.main(getName(), "Once done, type the 6 digit code provided by your authentication app into the chat")); + return; + } + player.sendMessage(Style.main(getName(), "§cYou need to re-authenticate!")); + player.sendMessage(Style.main(getName(), "Type the 6 digit code provided by your authentication app into the chat")); + }, 1L); + } + + @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) + private void onChat(AsyncPlayerChatEvent event) { + Player player = event.getPlayer(); + if (!authenticating.contains(player.getUniqueId())) + return; + Optional optionalAccount = AccountManager.fromCache(player.getUniqueId()); + if (optionalAccount.isEmpty()) + return; + Optional optionalTwoFactorClient = lookup(player.getUniqueId()); + if (optionalTwoFactorClient.isEmpty()) + return; + event.setCancelled(true); + String message = event.getMessage().replaceAll(" ", ""); + int code; + try { + code = Integer.parseInt(message); + } catch (NumberFormatException ex) { + player.sendMessage(Style.main(getName(), "§cInvalid authentication code!")); + return; + } + TwoFactorClient twoFactorClient = optionalTwoFactorClient.get(); + String secretKey = twoFactorClient.getSecretKey(); + if (!googleAuthenticator.authorize(secretKey, code)) + player.sendMessage(Style.main(getName(), "§cInvalid authentication code!")); + else { + twoFactorClient.setLastAuthentication(System.currentTimeMillis()); + repository.authenticate(optionalAccount.get().getId(), secretKey, twoFactorClient.getLastAuthentication()); + authenticating.remove(player.getUniqueId()); + player.playSound(player.getEyeLocation(), XSound.ENTITY_PLAYER_LEVELUP.parseSound(), 0.9f, 1f); + player.sendMessage(Style.main(getName(), "§aAuthenticated!")); + } + } + + @EventHandler(priority = EventPriority.LOWEST) + private void onCommand(PlayerCommandPreprocessEvent event) { + cancelEvent(event); + } + + @EventHandler(priority = EventPriority.LOWEST) + private void onMove(PlayerMoveEvent event) { + Location from = event.getFrom(); + Location to = event.getTo(); + if (from.getBlockX() != to.getBlockX() || from.getBlockZ() != to.getBlockZ()) + cancelEvent(event); + } + + @EventHandler(priority = EventPriority.LOWEST) + private void onInventoryClick(InventoryClickEvent event) { + cancelEvent((Player) event.getWhoClicked(), event); + } + + @EventHandler(priority = EventPriority.LOWEST) + private void onFoodLevelChange(FoodLevelChangeEvent event) { + cancelEvent(event); + } + + @EventHandler(priority = EventPriority.LOWEST) + private void onDamage(EntityDamageEvent event) { + Entity entity = event.getEntity(); + if (entity instanceof Player) + cancelEvent((Player) entity, event); + } + + @EventHandler(priority = EventPriority.LOWEST) + private void onHeldItemChange(PlayerItemHeldEvent event) { + cancelEvent(event); + } + + @EventHandler + private void onQuit(PlayerQuitEvent event) { + authenticating.remove(event.getPlayer().getUniqueId()); + } + + private void cancelEvent(Cancellable cancellable) { + if (!(cancellable instanceof PlayerEvent)) + return; + cancelEvent(((PlayerEvent) cancellable).getPlayer(), cancellable); + } + + private void cancelEvent(Player player, Cancellable cancellable) { + if (authenticating.contains(player.getUniqueId())) + cancellable.setCancelled(true); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/twoFactor/TwoFactorClient.java b/core/src/main/java/zone/themcgamer/core/twoFactor/TwoFactorClient.java new file mode 100644 index 0000000..777a0f0 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/twoFactor/TwoFactorClient.java @@ -0,0 +1,20 @@ +package zone.themcgamer.core.twoFactor; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.concurrent.TimeUnit; + +/** + * @author Braydon + */ +@NoArgsConstructor @Setter @Getter +public class TwoFactorClient { + private String secretKey; + private long lastAuthentication; + + public boolean requiresAuthentication() { + return secretKey == null || (System.currentTimeMillis() - lastAuthentication) >= TimeUnit.DAYS.toMillis(1L); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/twoFactor/TwoFactorDBKey.java b/core/src/main/java/zone/themcgamer/core/twoFactor/TwoFactorDBKey.java new file mode 100644 index 0000000..8f0422b --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/twoFactor/TwoFactorDBKey.java @@ -0,0 +1,8 @@ +package zone.themcgamer.core.twoFactor; + +/** + * @author Braydon + */ +public enum TwoFactorDBKey { + SECRET_KEY, LAST_AUTHENTICATION +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/twoFactor/TwoFactorRepository.java b/core/src/main/java/zone/themcgamer/core/twoFactor/TwoFactorRepository.java new file mode 100644 index 0000000..2a10bef --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/twoFactor/TwoFactorRepository.java @@ -0,0 +1,39 @@ +package zone.themcgamer.core.twoFactor; + +import com.zaxxer.hikari.HikariDataSource; +import zone.themcgamer.data.mysql.data.column.Column; +import zone.themcgamer.data.mysql.data.column.impl.IntegerColumn; +import zone.themcgamer.data.mysql.data.column.impl.LongColumn; +import zone.themcgamer.data.mysql.data.column.impl.StringColumn; +import zone.themcgamer.data.mysql.repository.MySQLRepository; + +import java.util.concurrent.CompletableFuture; + +/** + * @author Braydon + */ +public class TwoFactorRepository extends MySQLRepository { + private static final String INSERT_TWO_FACTOR = "INSERT INTO `twoFactor` " + + "(`accountId`, `dbKey`, `value`) VALUES " + + "(?, ?, ?) ON DUPLICATE KEY UPDATE `value`=?"; + + public TwoFactorRepository(HikariDataSource dataSource) { + super(dataSource); + } + + public void authenticate(int accountId, String secretKey, long lastAuthentication) { + StringColumn secretKeyColumn = new StringColumn("value", secretKey); + LongColumn lastAuthenticationColumn = new LongColumn("value", lastAuthentication); + CompletableFuture.runAsync(() -> { + for (TwoFactorDBKey dbKey : TwoFactorDBKey.values()) { + Column valueColumn = dbKey == TwoFactorDBKey.SECRET_KEY ? secretKeyColumn : lastAuthenticationColumn; + executeInsert(INSERT_TWO_FACTOR, new Column[] { + new IntegerColumn("accountId", accountId), + new StringColumn("dbKey", dbKey.name()), + valueColumn, + valueColumn + }); + } + }); + } +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/mysql/MySQLController.java b/serverdata/src/main/java/zone/themcgamer/data/mysql/MySQLController.java index bfe0f30..eb5016d 100644 --- a/serverdata/src/main/java/zone/themcgamer/data/mysql/MySQLController.java +++ b/serverdata/src/main/java/zone/themcgamer/data/mysql/MySQLController.java @@ -45,6 +45,12 @@ public class MySQLController { new LongColumn("lastLogin", false) }, new String[] { "id" }), + new Table("twoFactor", new Column[] { + new IntegerColumn("accountId", false, false), + new StringColumn("dbKey", 50, false), + new StringColumn("value", 50, false) + }, new String[] { "accountId", "dbKey" }), + new Table("punishments", new Column[] { new IntegerColumn("id", true, false), new StringColumn("targetIp", 255, false),