From e0406a8f6535616a7a9d233f582c2a1075ee7652 Mon Sep 17 00:00:00 2001 From: oskar3123 Date: Thu, 22 Nov 2018 20:18:51 +0100 Subject: [PATCH] WIP --- pom.xml | 101 +++++++ .../java/eu/oskar3123/spigot2fa/Main.java | 47 ++++ .../spigot2fa/command/TFACommand.java | 53 ++++ .../eu/oskar3123/spigot2fa/config/Config.java | 19 ++ .../spigot2fa/config/ConfigHandler.java | 85 ++++++ .../spigot2fa/handler/TFAHandler.java | 113 ++++++++ .../spigot2fa/listener/JoinQuitListener.java | 59 ++++ .../spigot2fa/map/CreateMapRunnable.java | 47 ++++ .../spigot2fa/map/QRMapRenderer.java | 62 +++++ .../java/eu/oskar3123/spigot2fa/tfa/TFA.java | 52 ++++ .../java/eu/oskar3123/spigot2fa/tfa/TOTP.java | 253 ++++++++++++++++++ src/main/resources/config.yml | 3 + src/main/resources/players.yml | 0 src/main/resources/plugin.yml | 7 + 14 files changed, 901 insertions(+) create mode 100644 pom.xml create mode 100644 src/main/java/eu/oskar3123/spigot2fa/Main.java create mode 100644 src/main/java/eu/oskar3123/spigot2fa/command/TFACommand.java create mode 100644 src/main/java/eu/oskar3123/spigot2fa/config/Config.java create mode 100644 src/main/java/eu/oskar3123/spigot2fa/config/ConfigHandler.java create mode 100644 src/main/java/eu/oskar3123/spigot2fa/handler/TFAHandler.java create mode 100644 src/main/java/eu/oskar3123/spigot2fa/listener/JoinQuitListener.java create mode 100644 src/main/java/eu/oskar3123/spigot2fa/map/CreateMapRunnable.java create mode 100644 src/main/java/eu/oskar3123/spigot2fa/map/QRMapRenderer.java create mode 100644 src/main/java/eu/oskar3123/spigot2fa/tfa/TFA.java create mode 100644 src/main/java/eu/oskar3123/spigot2fa/tfa/TOTP.java create mode 100644 src/main/resources/config.yml create mode 100644 src/main/resources/players.yml create mode 100644 src/main/resources/plugin.yml diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f9def84 --- /dev/null +++ b/pom.xml @@ -0,0 +1,101 @@ + + + + 4.0.0 + + Spigot2FA + eu.oskar3123 + spigot2fa + 1.0.0-SNAPSHOT + + + UTF-8 + + + + + spigot-repo + https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + + + jitpack.io + https://jitpack.io + + + placeholderapi + http://repo.extendedclip.com/content/repositories/placeholderapi/ + + + + + + org.spigotmc + spigot-api + 1.13.2-R0.1-SNAPSHOT + + + org.bukkit + bukkit + 1.13.2-R0.1-SNAPSHOT + + + com.github.kenglxn.qrgen + javase + 2.5.0 + + + commons-codec + commons-codec + 1.10 + + + me.clip + placeholderapi + 2.9.2 + provided + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-shade-plugin + + + package + + shade + + + + + + + com.github.kenglxn.qrgen:javase + com.github.kenglxn.qrgen:core + com.google.zxing:javase + com.google.zxing:core + com.beust:jcommander + com.github.jai-imageio:jai-imageio-core + commons-codec:commons-codec + + + ${project.name} + + + + + + \ No newline at end of file diff --git a/src/main/java/eu/oskar3123/spigot2fa/Main.java b/src/main/java/eu/oskar3123/spigot2fa/Main.java new file mode 100644 index 0000000..9a41d6d --- /dev/null +++ b/src/main/java/eu/oskar3123/spigot2fa/Main.java @@ -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); + } + +} diff --git a/src/main/java/eu/oskar3123/spigot2fa/command/TFACommand.java b/src/main/java/eu/oskar3123/spigot2fa/command/TFACommand.java new file mode 100644 index 0000000..d957441 --- /dev/null +++ b/src/main/java/eu/oskar3123/spigot2fa/command/TFACommand.java @@ -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; + } + } + } + +} diff --git a/src/main/java/eu/oskar3123/spigot2fa/config/Config.java b/src/main/java/eu/oskar3123/spigot2fa/config/Config.java new file mode 100644 index 0000000..63d55a3 --- /dev/null +++ b/src/main/java/eu/oskar3123/spigot2fa/config/Config.java @@ -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; + } + +} diff --git a/src/main/java/eu/oskar3123/spigot2fa/config/ConfigHandler.java b/src/main/java/eu/oskar3123/spigot2fa/config/ConfigHandler.java new file mode 100644 index 0000000..1458389 --- /dev/null +++ b/src/main/java/eu/oskar3123/spigot2fa/config/ConfigHandler.java @@ -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); + } + } + +} diff --git a/src/main/java/eu/oskar3123/spigot2fa/handler/TFAHandler.java b/src/main/java/eu/oskar3123/spigot2fa/handler/TFAHandler.java new file mode 100644 index 0000000..2aee8aa --- /dev/null +++ b/src/main/java/eu/oskar3123/spigot2fa/handler/TFAHandler.java @@ -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 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()); + } + +} diff --git a/src/main/java/eu/oskar3123/spigot2fa/listener/JoinQuitListener.java b/src/main/java/eu/oskar3123/spigot2fa/listener/JoinQuitListener.java new file mode 100644 index 0000000..71d97fb --- /dev/null +++ b/src/main/java/eu/oskar3123/spigot2fa/listener/JoinQuitListener.java @@ -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 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"); + } + } + +} diff --git a/src/main/java/eu/oskar3123/spigot2fa/map/CreateMapRunnable.java b/src/main/java/eu/oskar3123/spigot2fa/map/CreateMapRunnable.java new file mode 100644 index 0000000..f7e74e7 --- /dev/null +++ b/src/main/java/eu/oskar3123/spigot2fa/map/CreateMapRunnable.java @@ -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"); + } + } + +} diff --git a/src/main/java/eu/oskar3123/spigot2fa/map/QRMapRenderer.java b/src/main/java/eu/oskar3123/spigot2fa/map/QRMapRenderer.java new file mode 100644 index 0000000..80ae6ce --- /dev/null +++ b/src/main/java/eu/oskar3123/spigot2fa/map/QRMapRenderer.java @@ -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); + } + +} diff --git a/src/main/java/eu/oskar3123/spigot2fa/tfa/TFA.java b/src/main/java/eu/oskar3123/spigot2fa/tfa/TFA.java new file mode 100644 index 0000000..3d57211 --- /dev/null +++ b/src/main/java/eu/oskar3123/spigot2fa/tfa/TFA.java @@ -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); + } + } + +} diff --git a/src/main/java/eu/oskar3123/spigot2fa/tfa/TOTP.java b/src/main/java/eu/oskar3123/spigot2fa/tfa/TOTP.java new file mode 100644 index 0000000..44e88b8 --- /dev/null +++ b/src/main/java/eu/oskar3123/spigot2fa/tfa/TOTP.java @@ -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