├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── pom.xml └── src └── main ├── java └── com │ └── github │ └── games647 │ └── securemyaccount │ ├── Account.java │ ├── ImageGenerator.java │ ├── ImageRenderer.java │ ├── MapGiver.java │ ├── SecureMyAccount.java │ ├── TOTP.java │ ├── command │ ├── EnableCommand.java │ └── LoginCommand.java │ └── listener │ ├── InventoryPinListener.java │ ├── PreventListener.java │ └── SessionListener.java └── resources ├── config.yml └── plugin.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse 2 | .classpath 3 | .project 4 | .settings/ 5 | 6 | # NetBeans 7 | nbproject/ 8 | nb-configuration.xml 9 | 10 | # IntelliJ 11 | *.iml 12 | *.ipr 13 | *.iws 14 | .idea/ 15 | 16 | # Maven 17 | target/ 18 | pom.xml.versionsBackup 19 | 20 | # Gradle 21 | .gradle 22 | 23 | # Ignore Gradle GUI config 24 | gradle-app.setting 25 | 26 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 27 | !gradle-wrapper.jar 28 | 29 | # various other potential build files 30 | build/ 31 | bin/ 32 | dist/ 33 | manifest.mf 34 | *.log 35 | 36 | # Vim 37 | .*.sw[a-p] 38 | 39 | # virtual machine crash logs, see https://www.java.com/en/download/help/error_hotspot.xml 40 | hs_err_pid* 41 | 42 | # Mac filesystem dust 43 | .DS_Store 44 | 45 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Use https://travis-ci.org/ for automatic testing 2 | 3 | # speed up testing https://blog.travis-ci.com/2014-12-17-faster-builds-with-container-based-infrastructure/ 4 | sudo: false 5 | 6 | # This is a java project 7 | language: java 8 | 9 | script: mvn test -B 10 | 11 | jdk: 12 | - oraclejdk8 13 | - oraclejdk9 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.3 4 | 5 | * Upgrade to Java 8 6 | * Migrate to Java NIO files 7 | * Added pin inventory - easier login 8 | * Fixed Java 7 support 9 | * Added already login check 10 | * Added force same ip property 11 | * Fixed correct auth check for player who doesn't enabled authentication 12 | 13 | ## 0.2 14 | 15 | * Added config 16 | * Added server ip config property 17 | * Added ip auto login 18 | * Added protected commands property 19 | * Added protection for other events besides commands 20 | * Added configurable commandOnly protection 21 | * Added /register alias for a new account 22 | * Added /createkey alias for a new account 23 | 24 | ## 0.1 25 | 26 | * First release 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SecureMyAccount 2 | 3 | Visit the plugin page [here](https://dev.bukkit.org/bukkit-plugins/securemyaccount/). 4 | 5 | Inspired by GitHub and Google, here is a Bukkit plugin to use two factor authentication. Your Mojang/Minecraft 6 | account is protected by a single password or login token. What happens if your account gets stolen? Therefore this 7 | plugin exists. 8 | 9 | The password is time based and generated by your smartphone completely independent from your Mojang account and your 10 | computer. This process is also known as 2FA. 11 | 12 | ## Features 13 | 14 | * Many other plugins like this sends a clickable link to Google for the QR code. This plugin doesn't do that. 15 | The code is generated rendered only on the server for increased privacy and security 16 | * Protect selected commands 17 | * Provides a better security to your server 18 | * Displays the QR code on a minecraft map 19 | * User friendly. Just scan the QR code with your smartphone! 20 | * A session will be valid for certain time period so you don't have to enter your password again. 21 | * No other plugins required 22 | 23 | ## Commands 24 | Command | Description 25 | ----------------|-------------- 26 | /register | Requests a new secret key 27 | /login < code > | Starts a new session to prove your identity 28 | 29 | ## Permissions 30 | Permission Node | Description 31 | ----------------|-------------- 32 | securemyaccount.command.enable | Permission to invoke the request command 33 | securemyaccount.command.start | Permission to start a new session 34 | securemyaccount.command.protect | Protected player accounts 35 | 36 | ## Images 37 | 38 | ![Ingame map QR code](https://i.imgur.com/9YuekuKl.png) 39 | ![App code generation](https://i.imgur.com/HWNR8SK.png) 40 | ![inventory pin](https://i.imgur.com/JCmmMPOm.png) 41 | 42 | ## Requirements 43 | 44 | 2 Factor App on your phone, desktop or offline (specialized hardware tokens). 45 | 46 | ### Apps (Open-Source only) 47 | 48 | IOS 49 | * Authenticator [AppStore](https://itunes.apple.com/us/app/authenticator/id766157276) 50 | * FreeOTP [AppStore](https://itunes.apple.com/us/app/freeotp-authenticator/id872559395) 51 | 52 | Android 53 | * andOTP [F-Droid](https://f-droid.org/en/packages/org.shadowice.flocke.andotp/) 54 | [PlayStore](https://play.google.com/store/apps/details?id=org.shadowice.flocke.andotp) 55 | * Yubico Authenticator [F-Droid](https://play.google.com/store/apps/details?id=com.yubico.yubioath) 56 | [PlayStore](https://play.google.com/store/apps/details?id=com.yubico.yubioath) 57 | * Requires YubiKey hardware token 58 | * OnlyKey U2F [PlayStore](https://play.google.com/store/apps/details?id=to.crp.android.onlykeyu2f) 59 | * Requires OnlyKey hardware token 60 | 61 | Desktop (Linux, Mac, Windows): 62 | * YubiKey Authenticator [Download](https://www.yubico.com/products/services-software/download/yubico-authenticator/) 63 | * Requires YubiKey hardware token 64 | * NitroKey App [Download](https://www.nitrokey.com/download) 65 | * Requires Nitrokey hardware token 66 | * OnlyKey App 67 | * Requires OnlyKey hardware token 68 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | com.github.games647 6 | 7 | securemyaccount 8 | jar 9 | 10 | SecureMyAccount 11 | 0.3 12 | 13 | https://dev.bukkit.org/bukkit-plugins/securemyaccount/ 14 | 15 | Protect your user account with two factor authentication 16 | 17 | 18 | 19 | UTF-8 20 | 21 | 1.8 22 | 1.8 23 | 24 | 25 | 26 | install 27 | 28 | ${project.name} 29 | 30 | 31 | 32 | org.apache.maven.plugins 33 | maven-shade-plugin 34 | 3.1.0 35 | 36 | false 37 | true 38 | 39 | 40 | 41 | package 42 | 43 | shade 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | src/main/resources 53 | 54 | true 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | spigot-repo 63 | https://hub.spigotmc.org/nexus/content/repositories/snapshots/ 64 | 65 | 66 | 67 | 68 | jitpack.io 69 | https://jitpack.io 70 | 71 | 72 | 73 | 74 | 75 | 76 | org.spigotmc 77 | spigot-api 78 | 1.12.2-R0.1-SNAPSHOT 79 | provided 80 | 81 | 82 | 83 | com.github.games647 84 | googleauth 85 | 1.2.0-SMALL 86 | 87 | 88 | org.apache.httpcomponents 89 | httpclient 90 | 91 | 92 | commons-codec 93 | commons-codec 94 | 95 | 96 | 97 | 98 | 99 | com.google.zxing 100 | javase 101 | 3.3.2 102 | 103 | 104 | com.beust 105 | jcommander 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/securemyaccount/Account.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.securemyaccount; 2 | 3 | import java.net.InetAddress; 4 | import java.util.UUID; 5 | 6 | public class Account { 7 | 8 | private final UUID uuid; 9 | private String secretCode; 10 | private InetAddress ip; 11 | 12 | public Account(UUID uuid) { 13 | this.uuid = uuid; 14 | } 15 | 16 | public Account(UUID uuid, String secretCode, InetAddress ip) { 17 | this.uuid = uuid; 18 | this.secretCode = secretCode; 19 | this.ip = ip; 20 | } 21 | 22 | public UUID getUUID() { 23 | return uuid; 24 | } 25 | 26 | public String getSecretCode() { 27 | return secretCode; 28 | } 29 | 30 | public boolean isRegistered() { 31 | return secretCode != null; 32 | } 33 | 34 | public void setSecretCode(String secretCode) { 35 | this.secretCode = secretCode; 36 | } 37 | 38 | public InetAddress getIP() { 39 | return ip; 40 | } 41 | 42 | public void setIP(InetAddress ip) { 43 | this.ip = ip; 44 | } 45 | 46 | @Override 47 | public String toString() { 48 | return this.getClass().getSimpleName() + '{' + 49 | "uuid=" + uuid + 50 | ", ip='" + ip + '\'' + 51 | '}'; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/securemyaccount/ImageGenerator.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.securemyaccount; 2 | 3 | import com.google.zxing.BarcodeFormat; 4 | import com.google.zxing.Writer; 5 | import com.google.zxing.WriterException; 6 | import com.google.zxing.client.j2se.MatrixToImageWriter; 7 | import com.google.zxing.common.BitMatrix; 8 | import com.google.zxing.qrcode.QRCodeWriter; 9 | 10 | import java.awt.image.BufferedImage; 11 | import java.util.logging.Level; 12 | 13 | import org.bukkit.Bukkit; 14 | import org.bukkit.entity.Player; 15 | 16 | public class ImageGenerator implements Runnable { 17 | 18 | private static final String PREFIX = "otpauth://totp/"; 19 | private static final int MINECRAFT_MAP_SIZE = 128; 20 | 21 | private final SecureMyAccount plugin; 22 | private final Player player; 23 | 24 | private final String secret; 25 | private final String serverHost; 26 | 27 | public ImageGenerator(SecureMyAccount plugin, Player player, String serverHost, String secret) { 28 | this.plugin = plugin; 29 | this.player = player; 30 | this.secret = secret; 31 | this.serverHost = serverHost; 32 | } 33 | 34 | @Override 35 | public void run() { 36 | Writer qrWriter = new QRCodeWriter(); 37 | try { 38 | //generate 39 | String contents = PREFIX + player.getName() + '@' + serverHost + "?secret=" + secret; 40 | BitMatrix encode = qrWriter.encode(contents, BarcodeFormat.QR_CODE, MINECRAFT_MAP_SIZE, MINECRAFT_MAP_SIZE); 41 | BufferedImage resultImage = MatrixToImageWriter.toBufferedImage(encode); 42 | 43 | //reschedule to the main thread to run non thread-safe methods 44 | Bukkit.getScheduler().runTask(plugin, new MapGiver(player, resultImage)); 45 | } catch (WriterException writeEx) { 46 | plugin.getLogger().log(Level.SEVERE, "Tried downloading image", writeEx); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/securemyaccount/ImageRenderer.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.securemyaccount; 2 | 3 | import java.awt.image.BufferedImage; 4 | import java.util.UUID; 5 | 6 | import org.bukkit.entity.Player; 7 | import org.bukkit.map.MapCanvas; 8 | import org.bukkit.map.MapRenderer; 9 | import org.bukkit.map.MapView; 10 | 11 | public class ImageRenderer extends MapRenderer { 12 | 13 | private final UUID forPlayer; 14 | private BufferedImage image; 15 | 16 | public ImageRenderer(Player player, BufferedImage image) { 17 | super(true); 18 | 19 | //just save the uuid in order to prevent memory leaks by keeping the player reference 20 | this.forPlayer = player.getUniqueId(); 21 | this.image = image; 22 | } 23 | 24 | @Override 25 | public void render(MapView map, MapCanvas canvas, Player player) { 26 | //the image is just for the player who requested a new key 27 | if (image != null && player.getUniqueId().equals(forPlayer)) { 28 | canvas.drawImage(0, 0, image); 29 | //release resources in order to prevent memory leaks 30 | image = null; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/securemyaccount/MapGiver.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.securemyaccount; 2 | 3 | import java.awt.image.BufferedImage; 4 | 5 | import org.bukkit.Bukkit; 6 | import org.bukkit.ChatColor; 7 | import org.bukkit.Material; 8 | import org.bukkit.entity.Player; 9 | import org.bukkit.inventory.ItemStack; 10 | import org.bukkit.map.MapView; 11 | 12 | class MapGiver implements Runnable { 13 | 14 | private final Player player; 15 | private final BufferedImage resultImage; 16 | 17 | public MapGiver(Player player, BufferedImage resultImage) { 18 | this.player = player; 19 | this.resultImage = resultImage; 20 | } 21 | 22 | @Override 23 | public void run() { 24 | MapView createdView = installRenderer(player, resultImage); 25 | //stack count 0 prevents the item from being dropped 26 | ItemStack mapItem = new ItemStack(Material.MAP, 1, createdView.getId()); 27 | player.getInventory().addItem(mapItem); 28 | player.sendMessage(ChatColor.DARK_GREEN + "Here is your secret code. Just scan it with your phone"); 29 | player.sendMessage(ChatColor.DARK_GREEN + "Then drop it in order to remove it"); 30 | } 31 | 32 | private MapView installRenderer(Player player, BufferedImage image) { 33 | MapView mapView = Bukkit.createMap(player.getWorld()); 34 | mapView.getRenderers().forEach(mapView::removeRenderer); 35 | 36 | mapView.addRenderer(new ImageRenderer(player, image)); 37 | return mapView; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/securemyaccount/SecureMyAccount.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.securemyaccount; 2 | 3 | import com.github.games647.securemyaccount.command.EnableCommand; 4 | import com.github.games647.securemyaccount.command.LoginCommand; 5 | import com.github.games647.securemyaccount.listener.InventoryPinListener; 6 | import com.github.games647.securemyaccount.listener.PreventListener; 7 | import com.github.games647.securemyaccount.listener.SessionListener; 8 | import com.google.common.collect.Lists; 9 | import com.google.common.collect.Sets; 10 | 11 | import java.io.IOException; 12 | import java.net.InetAddress; 13 | import java.net.UnknownHostException; 14 | import java.nio.file.Files; 15 | import java.nio.file.Path; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.Set; 19 | import java.util.UUID; 20 | import java.util.concurrent.ConcurrentHashMap; 21 | import java.util.logging.Level; 22 | 23 | import org.bukkit.entity.Player; 24 | import org.bukkit.plugin.java.JavaPlugin; 25 | 26 | public class SecureMyAccount extends JavaPlugin { 27 | 28 | private final Set sessions = Sets.newConcurrentHashSet(); 29 | private final Map cache = new ConcurrentHashMap<>(); 30 | 31 | private final TOTP totp = new TOTP(); 32 | 33 | @Override 34 | public void onEnable() { 35 | saveDefaultConfig(); 36 | 37 | //register commands 38 | getCommand(getName().toLowerCase()).setExecutor(new EnableCommand(this)); 39 | getCommand("startsession").setExecutor(new LoginCommand(this)); 40 | getCommand("unregister").setExecutor(new LoginCommand(this)); 41 | 42 | //register listeners 43 | getServer().getPluginManager().registerEvents(new PreventListener(this), this); 44 | getServer().getPluginManager().registerEvents(new InventoryPinListener(this), this); 45 | getServer().getPluginManager().registerEvents(new SessionListener(this), this); 46 | } 47 | 48 | @Override 49 | public void onDisable() { 50 | sessions.clear(); 51 | cache.clear(); 52 | } 53 | 54 | public TOTP getTotp() { 55 | return totp; 56 | } 57 | 58 | //thread-safe 59 | public void startSession(Player player) { 60 | sessions.add(player.getUniqueId()); 61 | } 62 | 63 | //thread-safe 64 | public boolean isInSession(Player player) { 65 | return sessions.contains(player.getUniqueId()); 66 | } 67 | 68 | //thread-safe 69 | public void endSession(Player player) { 70 | sessions.remove(player.getUniqueId()); 71 | cache.remove(player.getUniqueId()); 72 | } 73 | 74 | //todo make async 75 | public Account getOrLoadAccount(Player player) { 76 | return cache.computeIfAbsent(player.getUniqueId(), uuid -> { 77 | Path file = getDataFolder().toPath().resolve(uuid.toString()); 78 | if (Files.exists(file)) { 79 | try { 80 | List lines = Files.readAllLines(file); 81 | String secretCode = lines.get(0); 82 | 83 | InetAddress ip = null; 84 | try { 85 | ip = InetAddress.getByName(lines.get(1)); 86 | } catch (UnknownHostException unknownHostEx) { 87 | getLogger().log(Level.WARNING, "Cannot parse account IP address", unknownHostEx); 88 | } 89 | 90 | return new Account(uuid, secretCode, ip); 91 | } catch (IOException ex) { 92 | getLogger().log(Level.SEVERE, "Error loading account", ex); 93 | } 94 | } else { 95 | return new Account(uuid); 96 | } 97 | 98 | return null; 99 | }); 100 | } 101 | 102 | //todo make async 103 | public boolean saveAccount(Player player) { 104 | UUID uniqueId = player.getUniqueId(); 105 | Account account = cache.get(uniqueId); 106 | if (account != null) { 107 | Path file = getDataFolder().toPath().resolve(uniqueId.toString()); 108 | try { 109 | Files.write(file, Lists.newArrayList(account.getSecretCode(), account.getIP().getHostAddress())); 110 | return true; 111 | } catch (IOException ex) { 112 | getLogger().log(Level.SEVERE, "Error saving account", ex); 113 | } 114 | } 115 | 116 | return false; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/securemyaccount/TOTP.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.securemyaccount; 2 | 3 | import com.google.common.primitives.Ints; 4 | import com.warrenstrange.googleauth.GoogleAuthenticator; 5 | import com.warrenstrange.googleauth.GoogleAuthenticatorConfig.GoogleAuthenticatorConfigBuilder; 6 | import com.warrenstrange.googleauth.HmacHashFunction; 7 | import com.warrenstrange.googleauth.IGoogleAuthenticator; 8 | import com.warrenstrange.googleauth.KeyRepresentation; 9 | 10 | public class TOTP { 11 | 12 | private final IGoogleAuthenticator gAuth = new GoogleAuthenticator(new GoogleAuthenticatorConfigBuilder() 13 | .setHmacHashFunction(HmacHashFunction.HmacSHA512) 14 | .setKeyRepresentation(KeyRepresentation.BASE64) 15 | .build()); 16 | 17 | public String generateSecretKey() { 18 | return gAuth.createCredentials().getKey(); 19 | } 20 | 21 | public boolean checkPassword(String secretKey, String userInput) { 22 | Integer code = Ints.tryParse(userInput); 23 | return code != null && gAuth.authorize(secretKey, code); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/securemyaccount/command/EnableCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.securemyaccount.command; 2 | 3 | import com.github.games647.securemyaccount.Account; 4 | import com.github.games647.securemyaccount.ImageGenerator; 5 | import com.github.games647.securemyaccount.SecureMyAccount; 6 | 7 | import org.bukkit.Bukkit; 8 | import org.bukkit.ChatColor; 9 | import org.bukkit.command.Command; 10 | import org.bukkit.command.CommandExecutor; 11 | import org.bukkit.command.CommandSender; 12 | import org.bukkit.entity.Player; 13 | 14 | public class EnableCommand implements CommandExecutor { 15 | 16 | private final SecureMyAccount plugin; 17 | 18 | public EnableCommand(SecureMyAccount plugin) { 19 | this.plugin = plugin; 20 | } 21 | 22 | @Override 23 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 24 | if (sender instanceof Player) { 25 | Player player = (Player) sender; 26 | 27 | generateKey(player); 28 | } else { 29 | sender.sendMessage(ChatColor.DARK_RED + "You have to be a player in order to receive a map item"); 30 | } 31 | 32 | return true; 33 | } 34 | 35 | private void generateKey(Player player) { 36 | Account account = plugin.getOrLoadAccount(player); 37 | if (account.isRegistered()) { 38 | player.sendMessage(ChatColor.DARK_RED + "You have already a secret key"); 39 | return; 40 | } 41 | 42 | String secretKey = plugin.getTotp().generateSecretKey(); 43 | account.setSecretCode(secretKey); 44 | account.setIP(player.getAddress().getAddress()); 45 | if (!plugin.saveAccount(player)) { 46 | player.sendMessage(ChatColor.DARK_RED + "Error while saving your secret key"); 47 | return; 48 | } 49 | 50 | String serverIp = plugin.getConfig().getString("serverIp"); 51 | if (serverIp.isEmpty()) { 52 | serverIp = Bukkit.getIp(); 53 | } 54 | 55 | Runnable imageDownloader = new ImageGenerator(plugin, player, serverIp, secretKey); 56 | plugin.getServer().getScheduler().runTaskAsynchronously(plugin, imageDownloader); 57 | player.sendMessage(ChatColor.DARK_GREEN + "Queued generation of your secret key"); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/securemyaccount/command/LoginCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.securemyaccount.command; 2 | 3 | import com.github.games647.securemyaccount.Account; 4 | import com.github.games647.securemyaccount.SecureMyAccount; 5 | 6 | import java.util.logging.Level; 7 | 8 | import org.bukkit.Bukkit; 9 | import org.bukkit.ChatColor; 10 | import org.bukkit.Material; 11 | import org.bukkit.command.Command; 12 | import org.bukkit.command.CommandExecutor; 13 | import org.bukkit.command.CommandSender; 14 | import org.bukkit.entity.Player; 15 | import org.bukkit.event.inventory.InventoryType; 16 | import org.bukkit.inventory.Inventory; 17 | import org.bukkit.inventory.InventoryView; 18 | import org.bukkit.inventory.ItemStack; 19 | import org.bukkit.inventory.meta.ItemMeta; 20 | 21 | public class LoginCommand implements CommandExecutor { 22 | 23 | private final SecureMyAccount plugin; 24 | 25 | public LoginCommand(SecureMyAccount plugin) { 26 | this.plugin = plugin; 27 | } 28 | 29 | @Override 30 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 31 | if (sender instanceof Player) { 32 | if (plugin.isInSession((Player) sender)) { 33 | sender.sendMessage(ChatColor.DARK_RED + "You're aready logged in"); 34 | } else if (args.length > 0) { 35 | checkCode((Player) sender, args[0]); 36 | } else { 37 | if (plugin.getConfig().getBoolean("inventoryPin")) { 38 | openCodeInventory((Player) sender); 39 | } else { 40 | sender.sendMessage(ChatColor.DARK_RED + "Your time based password code is missing"); 41 | } 42 | } 43 | } else { 44 | sender.sendMessage(ChatColor.DARK_RED + "You don't have to start a session, you're not a player"); 45 | } 46 | 47 | return true; 48 | } 49 | 50 | private void checkCode(Player player, String code) { 51 | if (plugin.isInSession(player)) { 52 | player.sendMessage(ChatColor.DARK_GREEN + "You're already loggedin"); 53 | return; 54 | } 55 | 56 | Account account = plugin.getOrLoadAccount(player); 57 | if (!account.isRegistered()) { 58 | player.sendMessage(ChatColor.DARK_RED + "You don't have key generated yet"); 59 | return; 60 | } 61 | 62 | String newIp = player.getAddress().getHostString(); 63 | if (plugin.getConfig().getBoolean("forceSampleIp") && !account.getIP().equals(newIp)) { 64 | player.sendMessage(ChatColor.DARK_RED + "You don't have the same IP as last time"); 65 | return; 66 | } 67 | 68 | try { 69 | if (plugin.getTotp().checkPassword(account.getSecretCode(), code)) { 70 | InventoryView openInventory = player.getOpenInventory(); 71 | if (openInventory.getType() == InventoryType.PLAYER) { 72 | openInventory.close(); 73 | } 74 | 75 | player.sendMessage(ChatColor.DARK_GREEN + "Accepted. You can continue"); 76 | 77 | account.setIP(player.getAddress().getAddress()); 78 | plugin.saveAccount(player); 79 | plugin.startSession(player); 80 | } else { 81 | player.sendMessage(ChatColor.DARK_RED + "Incorrect password"); 82 | } 83 | } catch (Exception ex) { 84 | plugin.getLogger().log(Level.SEVERE, null, ex); 85 | } 86 | } 87 | 88 | private void openCodeInventory(Player player) { 89 | Inventory inventory = Bukkit.createInventory(player, InventoryType.PLAYER, "Pin code"); 90 | 91 | inventory.setItem(2, new ItemStack(Material.STONE, 0)); 92 | 93 | inventory.setItem(3, new ItemStack(Material.STONE, 1)); 94 | inventory.setItem(4, new ItemStack(Material.STONE, 2)); 95 | inventory.setItem(5, new ItemStack(Material.STONE, 3)); 96 | 97 | inventory.setItem(12, new ItemStack(Material.STONE, 4)); 98 | inventory.setItem(13, new ItemStack(Material.STONE, 5)); 99 | inventory.setItem(14, new ItemStack(Material.STONE, 6)); 100 | 101 | inventory.setItem(21, new ItemStack(Material.STONE, 7)); 102 | inventory.setItem(22, new ItemStack(Material.STONE, 8)); 103 | inventory.setItem(23, new ItemStack(Material.STONE, 9)); 104 | 105 | ItemStack cancelBlock = new ItemStack(Material.BARRIER, 1); 106 | setDisplayName(cancelBlock, ChatColor.DARK_RED + "Cancel"); 107 | 108 | ItemStack removeBlock = new ItemStack(Material.REDSTONE_BLOCK, 1); 109 | setDisplayName(removeBlock, ChatColor.DARK_RED + "Remove"); 110 | 111 | ItemStack enterBlock = new ItemStack(Material.SLIME_BALL, 1); 112 | setDisplayName(enterBlock, ChatColor.DARK_GREEN + "Enter"); 113 | 114 | inventory.setItem(27, cancelBlock); 115 | inventory.setItem(28, removeBlock); 116 | inventory.setItem(35, enterBlock); 117 | 118 | player.openInventory(inventory); 119 | } 120 | 121 | private void setDisplayName(ItemStack item, String display) { 122 | ItemMeta itemMeta = item.getItemMeta(); 123 | itemMeta.setDisplayName(display); 124 | item.setItemMeta(itemMeta); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/securemyaccount/listener/InventoryPinListener.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.securemyaccount.listener; 2 | 3 | import com.github.games647.securemyaccount.SecureMyAccount; 4 | 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.EventHandler; 7 | import org.bukkit.event.EventPriority; 8 | import org.bukkit.event.Listener; 9 | import org.bukkit.event.inventory.InventoryClickEvent; 10 | import org.bukkit.inventory.Inventory; 11 | import org.bukkit.inventory.InventoryView; 12 | import org.bukkit.inventory.ItemStack; 13 | 14 | public class InventoryPinListener implements Listener { 15 | 16 | private static final int PIN_START = 29; 17 | private static final int PIN_END = 36 - 1; 18 | 19 | private final SecureMyAccount plugin; 20 | 21 | public InventoryPinListener(SecureMyAccount plugin) { 22 | this.plugin = plugin; 23 | } 24 | 25 | @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) 26 | public void onItemClick(InventoryClickEvent inventoryClickEvent) { 27 | Player player = (Player) inventoryClickEvent.getWhoClicked(); 28 | 29 | InventoryView inventory = player.getOpenInventory(); 30 | if (plugin.getConfig().getBoolean("inventoryPin") && inventory != null 31 | && "Pin code".equals(inventory.getTitle())) { 32 | if (inventoryClickEvent.getRawSlot() < 36) { 33 | Inventory clickedInventory = inventoryClickEvent.getClickedInventory(); 34 | ItemStack currentItem = inventoryClickEvent.getCurrentItem(); 35 | if (currentItem != null) { 36 | switch (currentItem.getType()) { 37 | case STONE: 38 | int nextPos = nextPinPos(clickedInventory); 39 | if (nextPos != -1) { 40 | inventory.setItem(nextPos, currentItem); 41 | } 42 | break; 43 | case BARRIER: 44 | player.closeInventory(); 45 | break; 46 | case REDSTONE_BLOCK: 47 | deleteLast(inventory); 48 | break; 49 | case SLIME_BALL: 50 | submitPin((Player) inventoryClickEvent.getWhoClicked(), clickedInventory); 51 | break; 52 | default: 53 | break; 54 | } 55 | } 56 | } 57 | 58 | inventoryClickEvent.setCancelled(true); 59 | } 60 | } 61 | 62 | private int nextPinPos(Inventory inventory) { 63 | for (int i = PIN_START; i < PIN_END; i++) { 64 | ItemStack item = inventory.getItem(i); 65 | if (item == null) { 66 | return i; 67 | } 68 | } 69 | 70 | return -1; 71 | } 72 | 73 | private void deleteLast(InventoryView inventory) { 74 | for (int i = PIN_END - 1; i > PIN_START - 1; i--) { 75 | ItemStack item = inventory.getItem(i); 76 | if (item != null) { 77 | inventory.setItem(i, null); 78 | break; 79 | } 80 | } 81 | } 82 | 83 | private void submitPin(Player player, Inventory inventory) { 84 | StringBuilder builder = new StringBuilder(6); 85 | for (int i = PIN_START; i < PIN_END; i++) { 86 | ItemStack item = inventory.getItem(i); 87 | if (item != null) { 88 | builder.append(item.getAmount()); 89 | } 90 | } 91 | 92 | if (builder.length() == 6) { 93 | builder.insert(0, "login "); 94 | player.performCommand(builder.toString()); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/securemyaccount/listener/PreventListener.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.securemyaccount.listener; 2 | 3 | import com.github.games647.securemyaccount.SecureMyAccount; 4 | 5 | import java.util.List; 6 | import java.util.regex.Pattern; 7 | 8 | import org.bukkit.ChatColor; 9 | import org.bukkit.Location; 10 | import org.bukkit.entity.LivingEntity; 11 | import org.bukkit.entity.Player; 12 | import org.bukkit.event.Cancellable; 13 | import org.bukkit.event.EventHandler; 14 | import org.bukkit.event.EventPriority; 15 | import org.bukkit.event.Listener; 16 | import org.bukkit.event.block.BlockBreakEvent; 17 | import org.bukkit.event.block.BlockPlaceEvent; 18 | import org.bukkit.event.entity.EntityPickupItemEvent; 19 | import org.bukkit.event.inventory.InventoryClickEvent; 20 | import org.bukkit.event.player.AsyncPlayerChatEvent; 21 | import org.bukkit.event.player.PlayerCommandPreprocessEvent; 22 | import org.bukkit.event.player.PlayerDropItemEvent; 23 | import org.bukkit.event.player.PlayerInteractEvent; 24 | import org.bukkit.event.player.PlayerMoveEvent; 25 | 26 | public class PreventListener implements Listener { 27 | 28 | private final Pattern slashRemover = Pattern.compile("/"); 29 | 30 | private final SecureMyAccount plugin; 31 | 32 | public PreventListener(SecureMyAccount plugin) { 33 | this.plugin = plugin; 34 | } 35 | 36 | //prevent events before other plugins will notice them (call order is from the lowest to the highest) 37 | @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) 38 | public void onCommand(PlayerCommandPreprocessEvent commandEvent) { 39 | Player invoker = commandEvent.getPlayer(); 40 | 41 | //remove the command identifier and further command arguments 42 | String command = slashRemover.matcher(commandEvent.getMessage()).replaceFirst("").split(" ")[0]; 43 | if ("login".equalsIgnoreCase(command) || "register".equalsIgnoreCase(command)) { 44 | //ignore our own commands 45 | return; 46 | } 47 | 48 | if (plugin.getConfig().getBoolean("commandOnlyProtection")) { 49 | List protectedCommands = plugin.getConfig().getStringList("protectedCommands"); 50 | if (protectedCommands.isEmpty() || protectedCommands.contains(command)) { 51 | if (!plugin.isInSession(invoker)) { 52 | invoker.sendMessage(ChatColor.DARK_RED + "This action is protected for extra security"); 53 | invoker.sendMessage(ChatColor.DARK_RED + "Please type /session "); 54 | commandEvent.setCancelled(true); 55 | } 56 | } 57 | } else { 58 | checkLoginStatus(invoker, commandEvent); 59 | } 60 | } 61 | 62 | @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) 63 | public void onAsyncPlayerChat(AsyncPlayerChatEvent asyncPlayerChatEvent) { 64 | //keep mind that this have to be thread-safe 65 | checkLoginStatus(asyncPlayerChatEvent.getPlayer(), asyncPlayerChatEvent); 66 | } 67 | 68 | @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) 69 | public void onPlayerMove(PlayerMoveEvent playerMoveEvent) { 70 | Location from = playerMoveEvent.getFrom(); 71 | Location to = playerMoveEvent.getTo(); 72 | 73 | if (from.getBlockX() != to.getBlockX() || from.getBlockZ() != to.getBlockZ()) { 74 | checkLoginStatus(playerMoveEvent.getPlayer(), playerMoveEvent); 75 | } 76 | } 77 | 78 | @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) 79 | public void onBlockPlace(BlockPlaceEvent blockPlaceEvent) { 80 | checkLoginStatus(blockPlaceEvent.getPlayer(), blockPlaceEvent); 81 | } 82 | 83 | @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) 84 | public void onBlockBreak(BlockBreakEvent blockBreakEvent) { 85 | checkLoginStatus(blockBreakEvent.getPlayer(), blockBreakEvent); 86 | } 87 | 88 | @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) 89 | public void onPlayerInteract(PlayerInteractEvent playerInteractEvent) { 90 | checkLoginStatus(playerInteractEvent.getPlayer(), playerInteractEvent); 91 | } 92 | 93 | @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) 94 | public void onItemPickup(EntityPickupItemEvent pickupItemEvent) { 95 | LivingEntity entity = pickupItemEvent.getEntity(); 96 | if (entity instanceof Player) { 97 | checkLoginStatus((Player) entity, pickupItemEvent); 98 | } 99 | } 100 | 101 | @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) 102 | public void onItemDrop(PlayerDropItemEvent dropItemEvent) { 103 | checkLoginStatus(dropItemEvent.getPlayer(), dropItemEvent); 104 | } 105 | 106 | @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) 107 | public void onInventoryClick(InventoryClickEvent clickEvent) { 108 | checkLoginStatus((Player) clickEvent.getWhoClicked(), clickEvent); 109 | } 110 | 111 | //this lookup have to be highly optimized, because events like the move event will call this very often 112 | private void checkLoginStatus(Player player, Cancellable cancelEvent) { 113 | //thread-safe 114 | if (plugin.isInSession(player) || plugin.getConfig().getBoolean("commandOnlyProtection")) { 115 | return; 116 | } 117 | 118 | if (!plugin.getConfig().getBoolean("protectAll") 119 | && !player.hasPermission(plugin.getName().toLowerCase() + ".protect") ) { 120 | //we don't need to protect this player 121 | return; 122 | } 123 | 124 | cancelEvent.setCancelled(true); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/securemyaccount/listener/SessionListener.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.securemyaccount.listener; 2 | 3 | import com.github.games647.securemyaccount.Account; 4 | import com.github.games647.securemyaccount.ImageRenderer; 5 | import com.github.games647.securemyaccount.SecureMyAccount; 6 | 7 | import java.net.InetAddress; 8 | 9 | import org.bukkit.Bukkit; 10 | import org.bukkit.ChatColor; 11 | import org.bukkit.Material; 12 | import org.bukkit.entity.Item; 13 | import org.bukkit.entity.Player; 14 | import org.bukkit.event.EventHandler; 15 | import org.bukkit.event.EventPriority; 16 | import org.bukkit.event.Listener; 17 | import org.bukkit.event.player.PlayerDropItemEvent; 18 | import org.bukkit.event.player.PlayerJoinEvent; 19 | import org.bukkit.event.player.PlayerQuitEvent; 20 | import org.bukkit.inventory.ItemStack; 21 | import org.bukkit.map.MapView; 22 | 23 | public class SessionListener implements Listener { 24 | 25 | private final SecureMyAccount plugin; 26 | 27 | public SessionListener(SecureMyAccount plugin) { 28 | this.plugin = plugin; 29 | } 30 | 31 | //listen to high priority in order to ignore for example player kicks 32 | @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) 33 | public void onPlayerJoin(PlayerJoinEvent joinEvent) { 34 | Player player = joinEvent.getPlayer(); 35 | if (plugin.getConfig().getBoolean("protectAll") 36 | || player.hasPermission(plugin.getName().toLowerCase() + ".protect")) { 37 | Account account = plugin.getOrLoadAccount(player); 38 | if (account.isRegistered()) { 39 | InetAddress newIp = player.getAddress().getAddress(); 40 | if (newIp.equals(account.getIP())) { 41 | player.sendMessage(ChatColor.DARK_GREEN + "IP auto login"); 42 | plugin.startSession(player); 43 | } else if (!plugin.getConfig().getBoolean("commandOnlyProtection")) { 44 | player.sendMessage(ChatColor.DARK_GREEN + "Your account is protected. Please login /login "); 45 | } 46 | } else { 47 | player.sendMessage(ChatColor.DARK_GREEN + "This account should be protected. Generating key..."); 48 | player.performCommand("register"); 49 | } 50 | } 51 | } 52 | 53 | @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH) 54 | public void onDrop(PlayerDropItemEvent dropItemEvent) { 55 | Item itemDrop = dropItemEvent.getItemDrop(); 56 | ItemStack mapItem = itemDrop.getItemStack(); 57 | if (isOurGraph(mapItem)) { 58 | itemDrop.setItemStack(new ItemStack(Material.AIR)); 59 | } 60 | } 61 | 62 | private boolean isOurGraph(ItemStack mapItem) { 63 | if (mapItem == null || mapItem.getType() != Material.MAP) { 64 | return false; 65 | } 66 | 67 | short mapId = mapItem.getDurability(); 68 | MapView map = Bukkit.getMap(mapId); 69 | return map != null && map.getRenderers().stream() 70 | .anyMatch(ImageRenderer.class::isInstance); 71 | } 72 | 73 | @EventHandler 74 | public void onPlayerQuit(PlayerQuitEvent quitEvent) { 75 | plugin.endSession(quitEvent.getPlayer()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/resources/config.yml: -------------------------------------------------------------------------------- 1 | # ${project.name} config 2 | 3 | # What text should be displayed next to the player like in the image the server ip: 127.0.0.1 4 | # https://i.imgur.com/HWNR8SK.png 5 | # If this is empty the default server ip from the server.properties will be used 6 | serverIp: '' 7 | 8 | # If the player has the same ip as the previous successful login 9 | # the player will be automatically logged in 10 | ipAutoLogin: false 11 | 12 | # If players can only login into their account if it's the same ip as the last time 13 | forceSampleIp: false 14 | 15 | # By default only players who have the ${project.artifactId}.protect permission have to register and login 16 | # If this value is true every player has to do it. Otherwise players can optionally enable it. 17 | protectAll: false 18 | 19 | # Should a inventory be opened where the players could enter their pin 20 | inventoryPin: false 21 | 22 | # Should only the specified commands be protected from unauthorized access 23 | # Other actions like move, chat, block break, ... will be allowed if this is true 24 | commandOnlyProtection: true 25 | 26 | # If command only protection is enabled, these commands are protected. If the list is empty all commands are protected 27 | protectedCommands: 28 | - op 29 | - pex 30 | - stop 31 | - reload 32 | - ban 33 | -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | # project information for Bukkit in order to register our plugin with all it components 2 | # ${-} are variables from Maven (pom.xml) which will be replaced after the build 3 | name: ${project.name} 4 | version: ${project.version} 5 | main: ${project.groupId}.${project.artifactId}.${project.name} 6 | 7 | # meta information for plugin managers 8 | authors: [games647, 'https://github.com/games647/SecureMyAccount/graphs/contributors'] 9 | description: | 10 | ${project.description} 11 | website: ${project.url} 12 | dev-url: ${project.url} 13 | 14 | # This plugin don't have to be transformed for compatibility with Minecraft >= 1.13 15 | api-version: 1.13 16 | 17 | commands: 18 | ${project.artifactId}: 19 | description: 'All relevant 2 factor auth commands' 20 | aliases: [2fa, secureme, secure, sec, createkey, register] 21 | permission: ${project.artifactId}.command.enable 22 | 23 | startsession: 24 | description: 'Starts a two factor auth session to prove your identity' 25 | aliases: [login, session, sess] 26 | usage: / [code] 27 | permission: ${project.artifactId}.command.start 28 | 29 | unregister: 30 | description: 'Reset the account of yourself or others' 31 | aliases: [resetpin] 32 | usage: / [player] 33 | permission: ${project.artifactId}.command.reset 34 | 35 | permissions: 36 | ${project.artifactId}.protect: 37 | description: 'Player who have this permission have to register' 38 | 39 | ${project.artifactId}.command.start: 40 | description: 'Command to login' 41 | 42 | ${project.artifactId}.command.enable: 43 | description: 'Command to enable 2fa authentification' 44 | 45 | ${project.artifactId}.command.reset: 46 | description: 'Command to reset 2fa authentification' 47 | 48 | ${project.artifactId}.command.reset.others: 49 | description: 'Command to reset 2fa authentification of other players' 50 | children: 51 | ${project.artifactId}.command.reset: true 52 | --------------------------------------------------------------------------------