WIP
This commit is contained in:
commit
e0406a8f65
|
@ -0,0 +1,101 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<name>Spigot2FA</name>
|
||||||
|
<groupId>eu.oskar3123</groupId>
|
||||||
|
<artifactId>spigot2fa</artifactId>
|
||||||
|
<version>1.0.0-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>spigot-repo</id>
|
||||||
|
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>jitpack.io</id>
|
||||||
|
<url>https://jitpack.io</url>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>placeholderapi</id>
|
||||||
|
<url>http://repo.extendedclip.com/content/repositories/placeholderapi/</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.spigotmc</groupId>
|
||||||
|
<artifactId>spigot-api</artifactId>
|
||||||
|
<version>1.13.2-R0.1-SNAPSHOT</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bukkit</groupId>
|
||||||
|
<artifactId>bukkit</artifactId>
|
||||||
|
<version>1.13.2-R0.1-SNAPSHOT</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.kenglxn.qrgen</groupId>
|
||||||
|
<artifactId>javase</artifactId>
|
||||||
|
<version>2.5.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>commons-codec</groupId>
|
||||||
|
<artifactId>commons-codec</artifactId>
|
||||||
|
<version>1.10</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>me.clip</groupId>
|
||||||
|
<artifactId>placeholderapi</artifactId>
|
||||||
|
<version>2.9.2</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.1</version>
|
||||||
|
<configuration>
|
||||||
|
<source>1.8</source>
|
||||||
|
<target>1.8</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<artifactSet>
|
||||||
|
<includes>
|
||||||
|
<include>com.github.kenglxn.qrgen:javase</include>
|
||||||
|
<include>com.github.kenglxn.qrgen:core</include>
|
||||||
|
<include>com.google.zxing:javase</include>
|
||||||
|
<include>com.google.zxing:core</include>
|
||||||
|
<include>com.beust:jcommander</include>
|
||||||
|
<include>com.github.jai-imageio:jai-imageio-core</include>
|
||||||
|
<include>commons-codec:commons-codec</include>
|
||||||
|
</includes>
|
||||||
|
</artifactSet>
|
||||||
|
<finalName>${project.name}</finalName>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
|
@ -0,0 +1,47 @@
|
||||||
|
package eu.oskar3123.spigot2fa;
|
||||||
|
|
||||||
|
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 org.bukkit.plugin.PluginManager;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
public class Main extends JavaPlugin
|
||||||
|
{
|
||||||
|
|
||||||
|
public TFAHandler tfaHandler;
|
||||||
|
public ConfigHandler configHandler;
|
||||||
|
public Config config;
|
||||||
|
public Config players;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable()
|
||||||
|
{
|
||||||
|
this.configHandler = new ConfigHandler(this);
|
||||||
|
this.config = this.configHandler.addConfig(new Config("config"));
|
||||||
|
this.players = this.configHandler.addConfig(new Config("players"));
|
||||||
|
this.tfaHandler = new TFAHandler(this);
|
||||||
|
registerCommands();
|
||||||
|
registerListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerCommands()
|
||||||
|
{
|
||||||
|
getCommand("2fa").setExecutor(new TFACommand(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerListeners()
|
||||||
|
{
|
||||||
|
PluginManager pm = this.getServer().getPluginManager();
|
||||||
|
pm.registerEvents(new JoinQuitListener(this), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package eu.oskar3123.spigot2fa.command;
|
||||||
|
|
||||||
|
import eu.oskar3123.spigot2fa.Main;
|
||||||
|
import eu.oskar3123.spigot2fa.handler.TFAHandler;
|
||||||
|
import eu.oskar3123.spigot2fa.tfa.TFA;
|
||||||
|
import org.apache.commons.lang.StringUtils;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
public class TFACommand implements CommandExecutor
|
||||||
|
{
|
||||||
|
|
||||||
|
private Main plugin;
|
||||||
|
private TFAHandler th;
|
||||||
|
|
||||||
|
public TFACommand(Main plugin)
|
||||||
|
{
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.th = plugin.tfaHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] args)
|
||||||
|
{
|
||||||
|
Player player = (Player) sender;
|
||||||
|
if (!th.isInProcess(player.getUniqueId()))
|
||||||
|
{
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
th.creatingSuccess(player);
|
||||||
|
player.sendMessage("Successfully activated 2FA");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
th.creatingFailed(player);
|
||||||
|
player.sendMessage("That code is incorrect, aborting");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package eu.oskar3123.spigot2fa.config;
|
||||||
|
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
public class Config
|
||||||
|
{
|
||||||
|
|
||||||
|
String name;
|
||||||
|
File file;
|
||||||
|
FileConfiguration fileConfig;
|
||||||
|
|
||||||
|
public Config(String name)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
package eu.oskar3123.spigot2fa.config;
|
||||||
|
|
||||||
|
import com.google.common.base.Charsets;
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
public class ConfigHandler
|
||||||
|
{
|
||||||
|
|
||||||
|
private JavaPlugin plugin;
|
||||||
|
|
||||||
|
public ConfigHandler(JavaPlugin instance)
|
||||||
|
{
|
||||||
|
plugin = instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Config addConfig(Config config)
|
||||||
|
{
|
||||||
|
saveDefaultConfig(config);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileConfiguration getConfig(Config config)
|
||||||
|
{
|
||||||
|
if (config.fileConfig == null)
|
||||||
|
{
|
||||||
|
reloadConfig(config);
|
||||||
|
}
|
||||||
|
return config.fileConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reloadConfig(Config config)
|
||||||
|
{
|
||||||
|
if (config.fileConfig == null)
|
||||||
|
{
|
||||||
|
config.file = new File(plugin.getDataFolder(), config.name + ".yml");
|
||||||
|
}
|
||||||
|
config.fileConfig = YamlConfiguration.loadConfiguration(config.file);
|
||||||
|
config.fileConfig.options().copyDefaults(true);
|
||||||
|
|
||||||
|
InputStream defConfigStream = plugin.getResource(config.name + ".yml");
|
||||||
|
if (defConfigStream != null)
|
||||||
|
{
|
||||||
|
YamlConfiguration defConfig = YamlConfiguration.loadConfiguration(new InputStreamReader(defConfigStream, Charsets.UTF_8));
|
||||||
|
config.fileConfig.setDefaults(defConfig);
|
||||||
|
saveConfig(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveConfig(Config config)
|
||||||
|
{
|
||||||
|
if (config.fileConfig == null || config.file == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
getConfig(config).save(config.file);
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
plugin.getLogger().log(Level.SEVERE, "Could not save config to " + config.file, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveDefaultConfig(Config config)
|
||||||
|
{
|
||||||
|
if (config.file == null)
|
||||||
|
{
|
||||||
|
config.file = new File(plugin.getDataFolder(), config.name + ".yml");
|
||||||
|
}
|
||||||
|
if (!config.file.exists())
|
||||||
|
{
|
||||||
|
plugin.saveResource(config.name + ".yml", false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
package eu.oskar3123.spigot2fa.handler;
|
||||||
|
|
||||||
|
import eu.oskar3123.spigot2fa.Main;
|
||||||
|
import eu.oskar3123.spigot2fa.map.QRMapRenderer;
|
||||||
|
import eu.oskar3123.spigot2fa.tfa.TFA;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.configuration.Configuration;
|
||||||
|
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;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class TFAHandler
|
||||||
|
{
|
||||||
|
|
||||||
|
private Main plugin;
|
||||||
|
private Map<UUID, String> isInProcess = new HashMap<>();
|
||||||
|
|
||||||
|
public TFAHandler(Main plugin)
|
||||||
|
{
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String formatSecret(String secret)
|
||||||
|
{
|
||||||
|
return secret.toLowerCase().replaceAll("(.{4})(?=.{4})", "$1 ");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasEnabled2FA(UUID uuid)
|
||||||
|
{
|
||||||
|
return plugin.configHandler.getConfig(plugin.players).isString(uuid.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void creatingFailed(Player player)
|
||||||
|
{
|
||||||
|
remove(player.getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean matchCode(String code1, String code2)
|
||||||
|
{
|
||||||
|
return code1.equalsIgnoreCase(code2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean matchCode(Player player, String code)
|
||||||
|
{
|
||||||
|
String actualCode = TFA.getTOTPCode(getKey(player.getUniqueId()));
|
||||||
|
return matchCode(actualCode, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String remove(UUID uuid)
|
||||||
|
{
|
||||||
|
return isInProcess.remove(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String startCreating(Player player)
|
||||||
|
{
|
||||||
|
String key = TFA.getRandomSecretKey();
|
||||||
|
isInProcess.put(player.getUniqueId(), key);
|
||||||
|
showQRCode(player, key);
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showQRCode(Player player, String secret)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ItemStack map = new ItemStack(Material.FILLED_MAP);
|
||||||
|
MapView view = Bukkit.createMap(player.getWorld());
|
||||||
|
view.getRenderers().clear();
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInProcess(UUID uuid)
|
||||||
|
{
|
||||||
|
return isInProcess.containsKey(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKey(UUID uuid)
|
||||||
|
{
|
||||||
|
if (isInProcess.containsKey(uuid))
|
||||||
|
{
|
||||||
|
return isInProcess.get(uuid);
|
||||||
|
}
|
||||||
|
Configuration players = plugin.configHandler.getConfig(plugin.players);
|
||||||
|
return players.getString(uuid.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package eu.oskar3123.spigot2fa.map;
|
||||||
|
|
||||||
|
import eu.oskar3123.spigot2fa.Main;
|
||||||
|
import eu.oskar3123.spigot2fa.tfa.TFA;
|
||||||
|
import net.glxn.qrgen.javase.QRCode;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.map.*;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class QRMapRenderer extends MapRenderer
|
||||||
|
{
|
||||||
|
|
||||||
|
private Main plugin;
|
||||||
|
private String line1;
|
||||||
|
private String line2;
|
||||||
|
private Image image;
|
||||||
|
private UUID uuid;
|
||||||
|
|
||||||
|
public QRMapRenderer(Main plugin, String secretKey, Player player) throws IOException
|
||||||
|
{
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.uuid = player.getUniqueId();
|
||||||
|
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() + ")"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initImage(String qrValue) throws IOException
|
||||||
|
{
|
||||||
|
byte[] imageData = QRCode.from(qrValue).withSize(110, 110).stream().toByteArray();
|
||||||
|
image = ImageIO.read(new ByteArrayInputStream(imageData));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(MapView map, MapCanvas canvas, Player player)
|
||||||
|
{
|
||||||
|
map.setUnlimitedTracking(false);
|
||||||
|
map.setCenterX(player.getLocation().getBlockX() + 256);
|
||||||
|
byte color = MapPalette.matchColor(255, 255, 255);
|
||||||
|
for (int x = 0; x < 128; x++)
|
||||||
|
{
|
||||||
|
for (int y = 0; y < 128; y++)
|
||||||
|
{
|
||||||
|
canvas.setPixel(x, y, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!player.getUniqueId().equals(uuid))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
canvas.drawImage(9, 18, image);
|
||||||
|
canvas.drawText(2, 2, MinecraftFont.Font, line1);
|
||||||
|
canvas.drawText(2, 11, MinecraftFont.Font, line2);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package eu.oskar3123.spigot2fa.tfa;
|
||||||
|
|
||||||
|
import org.apache.commons.codec.binary.Base32;
|
||||||
|
import org.apache.commons.codec.binary.Hex;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
public class TFA
|
||||||
|
{
|
||||||
|
|
||||||
|
public static String getRandomSecretKey()
|
||||||
|
{
|
||||||
|
SecureRandom random = new SecureRandom();
|
||||||
|
byte[] bytes = new byte[20];
|
||||||
|
random.nextBytes(bytes);
|
||||||
|
Base32 base32 = new Base32();
|
||||||
|
String secretKey = base32.encodeToString(bytes);
|
||||||
|
// make the secret key more human-readable by lower-casing and
|
||||||
|
// inserting spaces between each group of 4 characters
|
||||||
|
return secretKey.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getTOTPCode(String secretKey)
|
||||||
|
{
|
||||||
|
String normalizedBase32Key = secretKey.replace(" ", "").toUpperCase();
|
||||||
|
Base32 base32 = new Base32();
|
||||||
|
byte[] bytes = base32.decode(normalizedBase32Key);
|
||||||
|
String hexKey = Hex.encodeHexString(bytes);
|
||||||
|
long time = (System.currentTimeMillis() / 1000) / 30;
|
||||||
|
String hexTime = Long.toHexString(time);
|
||||||
|
return TOTP.generateTOTP(hexKey, hexTime, "6");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getGoogleAuthenticatorBarCode(String secretKey, String issuer, String account)
|
||||||
|
{
|
||||||
|
String normalizedBase32Key = secretKey.replace(" ", "").toUpperCase();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return "otpauth://totp/"
|
||||||
|
+ URLEncoder.encode(issuer + ":" + account, "UTF-8").replace("+", "%20")
|
||||||
|
+ "?secret=" + URLEncoder.encode(normalizedBase32Key, "UTF-8").replace("+", "%20")
|
||||||
|
+ "&issuer=" + URLEncoder.encode(issuer, "UTF-8").replace("+", "%20");
|
||||||
|
}
|
||||||
|
catch (UnsupportedEncodingException e)
|
||||||
|
{
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,253 @@
|
||||||
|
/**
|
||||||
|
Copyright (c) 2011 IETF Trust and the persons identified as
|
||||||
|
authors of the code. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, is permitted pursuant to, and subject to the license
|
||||||
|
terms contained in, the Simplified BSD License set forth in Section
|
||||||
|
4.c of the IETF Trust's Legal Provisions Relating to IETF Documents
|
||||||
|
(http://trustee.ietf.org/license-info).
|
||||||
|
*/
|
||||||
|
package eu.oskar3123.spigot2fa.tfa;
|
||||||
|
|
||||||
|
import java.lang.reflect.UndeclaredThrowableException;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is an example implementation of the OATH
|
||||||
|
* TOTP algorithm.
|
||||||
|
* Visit www.openauthentication.org for more information.
|
||||||
|
*
|
||||||
|
* @author Johan Rydell, PortWise, Inc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class TOTP {
|
||||||
|
|
||||||
|
private TOTP() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method uses the JCE to provide the crypto algorithm.
|
||||||
|
* HMAC computes a Hashed Message Authentication Code with the
|
||||||
|
* crypto hash algorithm as a parameter.
|
||||||
|
*
|
||||||
|
* @param crypto: the crypto algorithm (HmacSHA1, HmacSHA256,
|
||||||
|
* HmacSHA512)
|
||||||
|
* @param keyBytes: the bytes to use for the HMAC key
|
||||||
|
* @param text: the message or text to be authenticated
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
private static byte[] hmac_sha(String crypto, byte[] keyBytes,
|
||||||
|
byte[] text){
|
||||||
|
try {
|
||||||
|
Mac hmac;
|
||||||
|
hmac = Mac.getInstance(crypto);
|
||||||
|
SecretKeySpec macKey =
|
||||||
|
new SecretKeySpec(keyBytes, "RAW");
|
||||||
|
hmac.init(macKey);
|
||||||
|
return hmac.doFinal(text);
|
||||||
|
} catch (GeneralSecurityException gse) {
|
||||||
|
throw new UndeclaredThrowableException(gse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method converts a HEX string to Byte[]
|
||||||
|
*
|
||||||
|
* @param hex: the HEX string
|
||||||
|
*
|
||||||
|
* @return: a byte array
|
||||||
|
*/
|
||||||
|
|
||||||
|
private static byte[] hexStr2Bytes(String hex){
|
||||||
|
// Adding one byte to get the right conversion
|
||||||
|
// Values starting with "0" can be converted
|
||||||
|
byte[] bArray = new BigInteger("10" + hex,16).toByteArray();
|
||||||
|
|
||||||
|
// Copy all the REAL bytes, not the "first"
|
||||||
|
byte[] ret = new byte[bArray.length - 1];
|
||||||
|
for (int i = 0; i < ret.length; i++)
|
||||||
|
ret[i] = bArray[i+1];
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int[] DIGITS_POWER
|
||||||
|
// 0 1 2 3 4 5 6 7 8
|
||||||
|
= {1,10,100,1000,10000,100000,1000000,10000000,100000000 };
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method generates a TOTP value for the given
|
||||||
|
* set of parameters.
|
||||||
|
*
|
||||||
|
* @param key: the shared secret, HEX encoded
|
||||||
|
* @param time: a value that reflects a time
|
||||||
|
* @param returnDigits: number of digits to return
|
||||||
|
*
|
||||||
|
* @return: a numeric String in base 10 that includes
|
||||||
|
* {@link truncationDigits} digits
|
||||||
|
*/
|
||||||
|
|
||||||
|
public static String generateTOTP(String key,
|
||||||
|
String time,
|
||||||
|
String returnDigits){
|
||||||
|
return generateTOTP(key, time, returnDigits, "HmacSHA1");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method generates a TOTP value for the given
|
||||||
|
* set of parameters.
|
||||||
|
*
|
||||||
|
* @param key: the shared secret, HEX encoded
|
||||||
|
* @param time: a value that reflects a time
|
||||||
|
* @param returnDigits: number of digits to return
|
||||||
|
*
|
||||||
|
* @return: a numeric String in base 10 that includes
|
||||||
|
* {@link truncationDigits} digits
|
||||||
|
*/
|
||||||
|
|
||||||
|
public static String generateTOTP256(String key,
|
||||||
|
String time,
|
||||||
|
String returnDigits){
|
||||||
|
return generateTOTP(key, time, returnDigits, "HmacSHA256");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method generates a TOTP value for the given
|
||||||
|
* set of parameters.
|
||||||
|
*
|
||||||
|
* @param key: the shared secret, HEX encoded
|
||||||
|
* @param time: a value that reflects a time
|
||||||
|
* @param returnDigits: number of digits to return
|
||||||
|
*
|
||||||
|
* @return: a numeric String in base 10 that includes
|
||||||
|
* {@link truncationDigits} digits
|
||||||
|
*/
|
||||||
|
|
||||||
|
public static String generateTOTP512(String key,
|
||||||
|
String time,
|
||||||
|
String returnDigits){
|
||||||
|
return generateTOTP(key, time, returnDigits, "HmacSHA512");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method generates a TOTP value for the given
|
||||||
|
* set of parameters.
|
||||||
|
*
|
||||||
|
* @param key: the shared secret, HEX encoded
|
||||||
|
* @param time: a value that reflects a time
|
||||||
|
* @param returnDigits: number of digits to return
|
||||||
|
* @param crypto: the crypto function to use
|
||||||
|
*
|
||||||
|
* @return: a numeric String in base 10 that includes
|
||||||
|
* {@link truncationDigits} digits
|
||||||
|
*/
|
||||||
|
|
||||||
|
public static String generateTOTP(String key,
|
||||||
|
String time,
|
||||||
|
String returnDigits,
|
||||||
|
String crypto){
|
||||||
|
int codeDigits = Integer.decode(returnDigits).intValue();
|
||||||
|
String result = null;
|
||||||
|
|
||||||
|
// Using the counter
|
||||||
|
// First 8 bytes are for the movingFactor
|
||||||
|
// Compliant with base RFC 4226 (HOTP)
|
||||||
|
while (time.length() < 16 )
|
||||||
|
time = "0" + time;
|
||||||
|
|
||||||
|
// Get the HEX in a Byte[]
|
||||||
|
byte[] msg = hexStr2Bytes(time);
|
||||||
|
byte[] k = hexStr2Bytes(key);
|
||||||
|
|
||||||
|
byte[] hash = hmac_sha(crypto, k, msg);
|
||||||
|
|
||||||
|
// put selected bytes into result int
|
||||||
|
int offset = hash[hash.length - 1] & 0xf;
|
||||||
|
|
||||||
|
int binary =
|
||||||
|
((hash[offset] & 0x7f) << 24) |
|
||||||
|
((hash[offset + 1] & 0xff) << 16) |
|
||||||
|
((hash[offset + 2] & 0xff) << 8) |
|
||||||
|
(hash[offset + 3] & 0xff);
|
||||||
|
|
||||||
|
int otp = binary % DIGITS_POWER[codeDigits];
|
||||||
|
|
||||||
|
result = Integer.toString(otp);
|
||||||
|
while (result.length() < codeDigits) {
|
||||||
|
result = "0" + result;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
// Seed for HMAC-SHA1 - 20 bytes
|
||||||
|
String seed = "3132333435363738393031323334353637383930";
|
||||||
|
// Seed for HMAC-SHA256 - 32 bytes
|
||||||
|
String seed32 = "3132333435363738393031323334353637383930" +
|
||||||
|
"313233343536373839303132";
|
||||||
|
// Seed for HMAC-SHA512 - 64 bytes
|
||||||
|
String seed64 = "3132333435363738393031323334353637383930" +
|
||||||
|
"3132333435363738393031323334353637383930" +
|
||||||
|
"3132333435363738393031323334353637383930" +
|
||||||
|
"31323334";
|
||||||
|
long T0 = 0;
|
||||||
|
long X = 30;
|
||||||
|
long testTime[] = {59L, 1111111109L, 1111111111L,
|
||||||
|
1234567890L, 2000000000L, 20000000000L};
|
||||||
|
|
||||||
|
String steps = "0";
|
||||||
|
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||||
|
df.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
System.out.println(
|
||||||
|
"+---------------+-----------------------+" +
|
||||||
|
"------------------+--------+--------+");
|
||||||
|
System.out.println(
|
||||||
|
"| Time(sec) | Time (UTC format) " +
|
||||||
|
"| Value of T(Hex) | TOTP | Mode |");
|
||||||
|
System.out.println(
|
||||||
|
"+---------------+-----------------------+" +
|
||||||
|
"------------------+--------+--------+");
|
||||||
|
|
||||||
|
for (int i=0; i<testTime.length; i++) {
|
||||||
|
long T = (testTime[i] - T0)/X;
|
||||||
|
steps = Long.toHexString(T).toUpperCase();
|
||||||
|
while (steps.length() < 16) steps = "0" + steps;
|
||||||
|
String fmtTime = String.format("%1$-11s", testTime[i]);
|
||||||
|
String utcTime = df.format(new Date(testTime[i]*1000));
|
||||||
|
System.out.print("| " + fmtTime + " | " + utcTime +
|
||||||
|
" | " + steps + " |");
|
||||||
|
System.out.println(generateTOTP(seed, steps, "8",
|
||||||
|
"HmacSHA1") + "| SHA1 |");
|
||||||
|
System.out.print("| " + fmtTime + " | " + utcTime +
|
||||||
|
" | " + steps + " |");
|
||||||
|
System.out.println(generateTOTP(seed32, steps, "8",
|
||||||
|
"HmacSHA256") + "| SHA256 |");
|
||||||
|
System.out.print("| " + fmtTime + " | " + utcTime +
|
||||||
|
" | " + steps + " |");
|
||||||
|
System.out.println(generateTOTP(seed64, steps, "8",
|
||||||
|
"HmacSHA512") + "| SHA512 |");
|
||||||
|
|
||||||
|
System.out.println(
|
||||||
|
"+---------------+-----------------------+" +
|
||||||
|
"------------------+--------+--------+");
|
||||||
|
}
|
||||||
|
}catch (final Exception e){
|
||||||
|
System.out.println("Error : " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
2fa:
|
||||||
|
issuer: 'Mineworlds'
|
||||||
|
account: '{NAME}'
|
|
@ -0,0 +1,7 @@
|
||||||
|
name: Spigot2FA
|
||||||
|
version: 1.0.0
|
||||||
|
api-version: 1.13
|
||||||
|
authors: [oskar3123]
|
||||||
|
main: eu.oskar3123.spigot2fa.Main
|
||||||
|
commands:
|
||||||
|
2fa:
|
Loading…
Reference in New Issue