├── .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 | 
39 | 
40 | 
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 |
--------------------------------------------------------------------------------