This commit is contained in:
Oskar Nordling 2018-11-22 22:46:51 +01:00
parent e0406a8f65
commit 99f5a817bd
13 changed files with 485 additions and 147 deletions

View File

@ -4,7 +4,8 @@ import eu.oskar3123.spigot2fa.command.TFACommand;
import eu.oskar3123.spigot2fa.config.Config;
import eu.oskar3123.spigot2fa.config.ConfigHandler;
import eu.oskar3123.spigot2fa.handler.TFAHandler;
import eu.oskar3123.spigot2fa.listener.JoinQuitListener;
import eu.oskar3123.spigot2fa.listener.AuthListener;
import eu.oskar3123.spigot2fa.listener.MapListener;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
@ -41,7 +42,8 @@ public class Main extends JavaPlugin
private void registerListeners()
{
PluginManager pm = this.getServer().getPluginManager();
pm.registerEvents(new JoinQuitListener(this), this);
pm.registerEvents(new AuthListener(this), this);
pm.registerEvents(new MapListener(this), this);
}
}

View File

@ -2,18 +2,28 @@ package eu.oskar3123.spigot2fa.command;
import eu.oskar3123.spigot2fa.Main;
import eu.oskar3123.spigot2fa.handler.TFAHandler;
import eu.oskar3123.spigot2fa.tfa.TFA;
import eu.oskar3123.spigot2fa.util.PlayerUtils;
import org.apache.commons.lang.StringUtils;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.bukkit.util.StringUtil;
public class TFACommand implements CommandExecutor
import java.util.*;
public class TFACommand implements CommandExecutor, TabCompleter
{
private Main plugin;
private TFAHandler th;
private final Map<String, String[]> TAB_COMPLETE_MAP = new HashMap<>();
{
TAB_COMPLETE_MAP.put("", new String[]{"add", "remove", "<code>"});
TAB_COMPLETE_MAP.put(":remove", new String[]{"[player]"});
}
public TFACommand(Main plugin)
{
@ -25,29 +35,123 @@ public class TFACommand implements CommandExecutor
public boolean onCommand(CommandSender sender, Command command, String label, String[] args)
{
Player player = (Player) sender;
if (!th.isInProcess(player.getUniqueId()))
if (args.length >= 1 && args[0].equalsIgnoreCase("remove"))
{
th.startCreating(player);
return true;
}
else
{
String secret = th.getKey(player.getUniqueId());
String code = TFA.getTOTPCode(secret);
String pCode = StringUtils.join(args);
if (th.matchCode(code, pCode))
if (!player.hasPermission("2fa.remove"))
{
th.creatingSuccess(player);
player.sendMessage("Successfully activated 2FA");
player.sendMessage("You don't have permission to do that");
return true;
}
if (args.length >= 2 && player.hasPermission("2fa.remove.other"))
{
OfflinePlayer otherPlayer = PlayerUtils.getOfflinePlayer(args[1]);
if (otherPlayer == null)
{
player.sendMessage("Could not find player \"" + args[1] + "\".");
return true;
}
if (th.hasEnabled2FA(otherPlayer.getUniqueId()))
{
th.remove2FA(otherPlayer.getUniqueId());
player.sendMessage("Removed two-factor authentication for " + otherPlayer.getName() + ".");
return true;
}
else
{
player.sendMessage(otherPlayer.getName() + " does not have two-factor authentication enabled.");
return true;
}
}
if (th.hasEnabled2FA(player.getUniqueId()))
{
th.remove2FA(player.getUniqueId());
player.sendMessage("Removed two-factor authentication.");
return true;
}
else
{
th.creatingFailed(player);
player.sendMessage("That code is incorrect, aborting");
player.sendMessage("You don't have two-factor authentication enabled.");
return true;
}
}
else if (args.length >= 1 && args[0].equalsIgnoreCase("add"))
{
if (!player.hasPermission("2fa.activate"))
{
player.sendMessage("You don't have permission to do that");
return true;
}
if (th.hasEnabled2FA(player.getUniqueId()))
{
player.sendMessage("You have already enabled two-factor authentication.");
return true;
}
if (!th.isInProcess(player.getUniqueId()))
{
boolean canStart = th.startCreating(player);
if (!canStart)
{
player.sendMessage("You can't hold anything when starting the process.");
return true;
}
player.sendMessage("Scan the barcode or enter the secret manually in your 2FA app.");
player.sendMessage("Use /2fa <code> to verify the authenticator.");
return true;
}
else
{
String secret = th.getKey(player.getUniqueId());
String pCode = StringUtils.join(args);
if (th.matchCode(secret, pCode))
{
th.creatingSuccess(player);
player.sendMessage("Successfully activated two-factor authentication.");
return true;
}
else
{
th.creatingFailed(player);
player.sendMessage("That code is incorrect, aborting.");
return true;
}
}
}
else
{
if (th.isInProcess(player.getUniqueId()))
{
player.sendMessage("Confirm the activation by typing /2fa <code>.");
return true;
}
else
{
player.sendMessage("Usage: " + command.getUsage().replace("<command>", label));
return true;
}
}
}
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args)
{
if (args.length < 1)
{
return new ArrayList<>();
}
StringBuilder search = new StringBuilder();
for (int i = 0; i < args.length - 1; i++)
{
search.append(":").append(args[i]);
}
if (!TAB_COMPLETE_MAP.containsKey(search.toString()))
{
return new ArrayList<>();
}
String[] commands = TAB_COMPLETE_MAP.get(search.toString());
final List<String> completions = new ArrayList<>();
StringUtil.copyPartialMatches(args[args.length - 1], Arrays.asList(commands), completions);
Collections.sort(completions);
return completions;
}
}

View File

@ -1,6 +1,7 @@
package eu.oskar3123.spigot2fa.config;
import com.google.common.base.Charsets;
import org.bukkit.Bukkit;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
@ -55,18 +56,40 @@ public class ConfigHandler
}
public void saveConfig(Config config)
{
saveConfig(config, false);
}
public void saveConfig(Config config, boolean async)
{
if (config.fileConfig == null || config.file == null)
{
return;
}
try
if (async)
{
getConfig(config).save(config.file);
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
{
try
{
getConfig(config).save(config.file);
}
catch (IOException e)
{
plugin.getLogger().log(Level.SEVERE, "Could not save config to " + config.file, e);
}
});
}
catch (IOException ex)
else
{
plugin.getLogger().log(Level.SEVERE, "Could not save config to " + config.file, ex);
try
{
getConfig(config).save(config.file);
}
catch (IOException e)
{
plugin.getLogger().log(Level.SEVERE, "Could not save config to " + config.file, e);
}
}
}
@ -80,6 +103,7 @@ public class ConfigHandler
{
plugin.saveResource(config.name + ".yml", false);
}
reloadConfig(config);
}
}

View File

@ -12,6 +12,7 @@ import org.bukkit.inventory.meta.MapMeta;
import org.bukkit.map.MapView;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@ -27,21 +28,67 @@ public class TFAHandler
this.plugin = plugin;
}
public boolean shouldBypassCode(Player player)
{
UUID uuid = player.getUniqueId();
String ip = player.getAddress().getAddress().getHostAddress();
if (!hasEnabled2FA(uuid))
{
return true;
}
long now = new Date().getTime();
long sessionTimeInMs = plugin.configHandler.getConfig(plugin.config).getLong("2fa.sessiontime") * 60000L;
Configuration players = plugin.configHandler.getConfig(plugin.players);
long last = players.getLong(uuid.toString() + ".lastlogin");
String lastIp = players.getString(uuid.toString() + ".lastip");
return last >= now - sessionTimeInMs && ip.equalsIgnoreCase(lastIp);
}
public void setLastLogin(Player player, Long time)
{
UUID uuid = player.getUniqueId();
String ip = player.getAddress().getAddress().getHostAddress();
if (!hasEnabled2FA(uuid))
{
return;
}
long now;
if (time == null)
{
now = new Date().getTime();
}
else
{
now = time;
}
Configuration players = plugin.configHandler.getConfig(plugin.players);
players.set(uuid.toString() + ".lastlogin", now);
players.set(uuid.toString() + ".lastip", ip);
plugin.configHandler.saveConfig(plugin.players, true);
}
public String formatSecret(String secret)
{
return secret.toLowerCase().replaceAll("(.{4})(?=.{4})", "$1 ");
}
public void remove2FA(UUID uuid)
{
plugin.configHandler.getConfig(plugin.players).set(uuid.toString(), null);
plugin.configHandler.saveConfig(plugin.players, true);
}
public boolean hasEnabled2FA(UUID uuid)
{
return plugin.configHandler.getConfig(plugin.players).isString(uuid.toString());
return plugin.configHandler.getConfig(plugin.players).isString(uuid.toString() + ".secret");
}
public void creatingSuccess(Player player)
{
String secret = remove(player.getUniqueId());
plugin.configHandler.getConfig(plugin.players).set(player.getUniqueId().toString(), secret);
plugin.configHandler.saveConfig(plugin.players);
player.getInventory().setItemInMainHand(new ItemStack(Material.AIR));
plugin.configHandler.getConfig(plugin.players).set(player.getUniqueId().toString() + ".secret", secret);
plugin.configHandler.saveConfig(plugin.players, true);
}
public void creatingFailed(Player player)
@ -49,15 +96,22 @@ public class TFAHandler
remove(player.getUniqueId());
}
public boolean matchCode(String code1, String code2)
public boolean matchCode(String secret, String code)
{
return code1.equalsIgnoreCase(code2);
String[] actualCodes = TFA.getTOTPCodes(secret);
for (String actualCode : actualCodes)
{
if (actualCode.equalsIgnoreCase(code))
{
return true;
}
}
return false;
}
public boolean matchCode(Player player, String code)
{
String actualCode = TFA.getTOTPCode(getKey(player.getUniqueId()));
return matchCode(actualCode, code);
return matchCode(getKey(player.getUniqueId()), code);
}
private String remove(UUID uuid)
@ -65,33 +119,42 @@ public class TFAHandler
return isInProcess.remove(uuid);
}
public String startCreating(Player player)
public boolean startCreating(Player player)
{
String key = TFA.getRandomSecretKey();
isInProcess.put(player.getUniqueId(), key);
showQRCode(player, key);
return key;
boolean couldShow = showQRCode(player, key);
if (couldShow)
{
isInProcess.put(player.getUniqueId(), key);
}
return couldShow;
}
private void showQRCode(Player player, String secret)
private boolean showQRCode(Player player, String secret)
{
ItemStack inHand = player.getInventory().getItemInMainHand();
if (inHand != null && inHand.getType() != Material.AIR)
{
return false;
}
try
{
ItemStack map = new ItemStack(Material.FILLED_MAP);
MapView view = Bukkit.createMap(player.getWorld());
view.getRenderers().clear();
view.addRenderer(new QRMapRenderer(secret, player));
view.addRenderer(new QRMapRenderer(plugin, formatSecret(secret), player));
MapMeta mapMeta = (MapMeta) map.getItemMeta();
mapMeta.setMapId(view.getId());
map.setItemMeta(mapMeta);
player.getInventory().setItemInMainHand(map);
player.sendMap(view);
player.sendMessage("Secret Key: " + secret);
return true;
}
catch (IOException e)
{
e.printStackTrace();
player.sendMessage("Failed to generate qr code");
return false;
}
}
@ -107,7 +170,16 @@ public class TFAHandler
return isInProcess.get(uuid);
}
Configuration players = plugin.configHandler.getConfig(plugin.players);
return players.getString(uuid.toString());
return players.getString(uuid.toString() + ".secret");
}
public void stopCreating(Player player)
{
if (!isInProcess(player.getUniqueId()))
{
return;
}
isInProcess.remove(player.getUniqueId());
}
}

View File

@ -0,0 +1,105 @@
package eu.oskar3123.spigot2fa.listener;
import eu.oskar3123.spigot2fa.Main;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.scheduler.BukkitTask;
import java.util.*;
public class AuthListener implements Listener
{
private class WaitingInfo
{
private BukkitTask kickTask;
private BukkitTask teleportTask;
private WaitingInfo(BukkitTask kickTask, BukkitTask teleportTask)
{
this.kickTask = kickTask;
this.teleportTask = teleportTask;
}
private void cancel(Player player)
{
this.kickTask.cancel();
this.teleportTask.cancel();
player.removePotionEffect(PotionEffectType.BLINDNESS);
}
}
private Main plugin;
private Map<UUID, WaitingInfo> waitingForCode = new HashMap<>();
public AuthListener(Main plugin)
{
this.plugin = plugin;
}
@EventHandler
public void onJoin(PlayerJoinEvent event)
{
if (plugin.tfaHandler.shouldBypassCode(event.getPlayer()))
{
event.getPlayer().sendMessage("bypassed 2fa check");
return;
}
final Player player = event.getPlayer();
BukkitTask task = Bukkit.getScheduler().runTaskLater(plugin, () ->
{
waitingForCode.remove(player.getUniqueId());
player.kickPlayer("You did not enter the 2FA code in time.");
}, 800L);
// 800 ticks == 40 secs
final Location loginLocation = player.getLocation().clone();
BukkitTask tpTask = Bukkit.getScheduler().runTaskTimer(plugin, () ->
{
player.teleport(loginLocation);
player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, 45, 0, false, false), true);
}, 0L, 20L);
waitingForCode.put(player.getUniqueId(), new WaitingInfo(task, tpTask));
Bukkit.getScheduler().runTaskLater(plugin, () -> player.sendMessage("Please enter your 2FA code in the chat."), 1L);
}
@EventHandler
public void chat(AsyncPlayerChatEvent event)
{
if (!waitingForCode.containsKey(event.getPlayer().getUniqueId()))
{
return;
}
boolean correct = plugin.tfaHandler.matchCode(event.getPlayer(), event.getMessage());
event.setCancelled(true);
if (!correct)
{
event.getPlayer().sendMessage("Incorrect code, try again.");
}
else
{
waitingForCode.remove(event.getPlayer().getUniqueId()).cancel(event.getPlayer());
plugin.tfaHandler.setLastLogin(event.getPlayer(), null);
event.getPlayer().sendMessage("You have been authorized.");
}
}
@EventHandler
public void command(PlayerCommandPreprocessEvent event)
{
if (!waitingForCode.containsKey(event.getPlayer().getUniqueId()))
{
return;
}
event.setCancelled(true);
event.getPlayer().sendMessage("You have to authorize first.");
}
}

View File

@ -1,59 +0,0 @@
package eu.oskar3123.spigot2fa.listener;
import eu.oskar3123.spigot2fa.Main;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
public class JoinQuitListener implements Listener
{
private Main plugin;
private Set<UUID> waitingForCode = new HashSet<>();
public JoinQuitListener(Main plugin)
{
this.plugin = plugin;
}
@EventHandler
public void onJoin(PlayerJoinEvent event)
{
boolean enabled = plugin.tfaHandler.hasEnabled2FA(event.getPlayer().getUniqueId());
if (!enabled)
{
return;
}
event.getPlayer().sendMessage("Enter 2FA code");
waitingForCode.add(event.getPlayer().getUniqueId());
}
@EventHandler
public void chat(AsyncPlayerChatEvent event)
{
if (!waitingForCode.contains(event.getPlayer().getUniqueId()))
{
return;
}
boolean correct = plugin.tfaHandler.matchCode(event.getPlayer(), event.getMessage());
waitingForCode.remove(event.getPlayer().getUniqueId());
event.setCancelled(true);
if (!correct)
{
final Player player = event.getPlayer();
Bukkit.getScheduler().runTask(plugin, () -> player.kickPlayer("Wrong code!"));
}
else
{
event.getPlayer().sendMessage("Logged in");
}
}
}

View File

@ -0,0 +1,100 @@
package eu.oskar3123.spigot2fa.listener;
import eu.oskar3123.spigot2fa.Main;
import org.bukkit.Material;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.*;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerItemHeldEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.inventory.ItemStack;
public class MapListener implements Listener
{
private Main plugin;
public MapListener(Main plugin)
{
this.plugin = plugin;
}
@EventHandler
public void quit(PlayerQuitEvent event)
{
if (!plugin.tfaHandler.isInProcess(event.getPlayer().getUniqueId()))
{
return;
}
plugin.tfaHandler.stopCreating(event.getPlayer());
event.getPlayer().getInventory().setItemInMainHand(new ItemStack(Material.AIR));
}
@EventHandler
public void drop(PlayerDropItemEvent event)
{
if (!plugin.tfaHandler.isInProcess(event.getPlayer().getUniqueId()))
{
return;
}
if (event.getItemDrop().getItemStack().getType() != Material.FILLED_MAP)
{
return;
}
plugin.tfaHandler.stopCreating(event.getPlayer());
event.getPlayer().sendMessage("Aborted adding two-factor authentication.");
event.getItemDrop().remove();
}
@EventHandler
public void slot(PlayerItemHeldEvent event)
{
if (!plugin.tfaHandler.isInProcess(event.getPlayer().getUniqueId()))
{
return;
}
event.setCancelled(true);
}
@EventHandler
public void open(InventoryOpenEvent event)
{
if (!plugin.tfaHandler.isInProcess(event.getPlayer().getUniqueId()))
{
return;
}
event.setCancelled(true);
}
@EventHandler
public void open(InventoryClickEvent event)
{
if (!plugin.tfaHandler.isInProcess(event.getWhoClicked().getUniqueId()))
{
return;
}
event.setCancelled(true);
}
@EventHandler
public void open(InventoryCreativeEvent event)
{
if (!plugin.tfaHandler.isInProcess(event.getWhoClicked().getUniqueId()))
{
return;
}
event.setCancelled(true);
}
@EventHandler
public void open(InventoryDragEvent event)
{
if (!plugin.tfaHandler.isInProcess(event.getWhoClicked().getUniqueId()))
{
return;
}
event.setCancelled(true);
}
}

View File

@ -1,47 +0,0 @@
package eu.oskar3123.spigot2fa.map;
import eu.oskar3123.spigot2fa.tfa.TFA;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.MapMeta;
import org.bukkit.map.MapView;
import java.io.IOException;
public class CreateMapRunnable implements Runnable
{
private Player player;
public CreateMapRunnable(Player player)
{
this.player = player;
}
@Override
public void run()
{
try
{
ItemStack map = new ItemStack(Material.FILLED_MAP);
MapView view = Bukkit.createMap(player.getWorld());
view.getRenderers().clear();
String secret = TFA.getRandomSecretKey();
view.addRenderer(new QRMapRenderer(secret, player));
MapMeta mapMeta = (MapMeta) map.getItemMeta();
mapMeta.setMapId(view.getId());
map.setItemMeta(mapMeta);
player.getInventory().setItemInMainHand(map);
player.sendMap(view);
player.sendMessage("Secret Key: " + secret);
}
catch (IOException e)
{
e.printStackTrace();
player.sendMessage("Failed to generate qr code");
}
}
}

View File

@ -25,10 +25,14 @@ public class QRMapRenderer extends MapRenderer
{
this.plugin = plugin;
this.uuid = player.getUniqueId();
String[] formattedSecretKey = secretKey .replaceAll("((?:.{4} ){4})", "$1;").split(";");
String[] formattedSecretKey = secretKey.replaceAll("((?:.{4} ){4})", "$1;").split(";");
this.line1 = formattedSecretKey[0].trim();
this.line2 = formattedSecretKey[1].trim();
initImage(TFA.getGoogleAuthenticatorBarCode(secretKey, "Mineworlds", player.getName() + " (" + player.getUniqueId().toString() + ")"));
String issuer = plugin.configHandler.getConfig(plugin.config).getString("2fa.issuer");
String account = plugin.configHandler.getConfig(plugin.config).getString("2fa.account")
.replace("{NAME}", player.getName())
.replace("{UUID}", player.getUniqueId().toString());
initImage(TFA.getGoogleAuthenticatorBarCode(secretKey, issuer, account));
}
private void initImage(String qrValue) throws IOException

View File

@ -22,7 +22,7 @@ public class TFA
return secretKey.toUpperCase();
}
public static String getTOTPCode(String secretKey)
public static String[] getTOTPCodes(String secretKey)
{
String normalizedBase32Key = secretKey.replace(" ", "").toUpperCase();
Base32 base32 = new Base32();
@ -30,7 +30,11 @@ public class TFA
String hexKey = Hex.encodeHexString(bytes);
long time = (System.currentTimeMillis() / 1000) / 30;
String hexTime = Long.toHexString(time);
return TOTP.generateTOTP(hexKey, hexTime, "6");
String code1 = TOTP.generateTOTP(hexKey, hexTime, "6");
time = time - 1;
hexTime = Long.toHexString(time);
String code2 = TOTP.generateTOTP(hexKey, hexTime, "6");
return new String[] {code1, code2};
}
public static String getGoogleAuthenticatorBarCode(String secretKey, String issuer, String account)

View File

@ -0,0 +1,25 @@
package eu.oskar3123.spigot2fa.util;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
public class PlayerUtils
{
public static OfflinePlayer getOfflinePlayer(String name)
{
for (OfflinePlayer ply : Bukkit.getOfflinePlayers())
{
if (ply == null || ply.getName() == null || ply.getName().isEmpty())
{
continue;
}
if (ply.getName().toLowerCase().startsWith(name.toLowerCase()))
{
return ply;
}
}
return null;
}
}

View File

@ -1,3 +1,5 @@
2fa:
issuer: 'Mineworlds'
account: '{NAME}'
# session time in minutes
sessiontime: 1440

View File

@ -5,3 +5,5 @@ authors: [oskar3123]
main: eu.oskar3123.spigot2fa.Main
commands:
2fa:
usage: '/<command> add/remove/<code>'
description: 'Two-factor authorization command'