This commit is contained in:
Oskar Nordling 2018-11-22 20:18:51 +01:00
commit e0406a8f65
14 changed files with 901 additions and 0 deletions

101
pom.xml Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
2fa:
issuer: 'Mineworlds'
account: '{NAME}'

View File

View File

@ -0,0 +1,7 @@
name: Spigot2FA
version: 1.0.0
api-version: 1.13
authors: [oskar3123]
main: eu.oskar3123.spigot2fa.Main
commands:
2fa: