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