diff --git a/core/build.gradle.kts b/core/build.gradle.kts index af44bdd..7f40490 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -4,4 +4,5 @@ dependencies { compileOnly("com.destroystokyo:paperspigot:1.12.2") implementation("com.github.cryptomorin:XSeries:7.8.0") implementation("com.warrenstrange:googleauth:1.4.0") + implementation("com.google.zxing:javase:3.4.1") } \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/account/MiniAccount.java b/core/src/main/java/zone/themcgamer/core/account/MiniAccount.java index 16bbdd4..514e182 100644 --- a/core/src/main/java/zone/themcgamer/core/account/MiniAccount.java +++ b/core/src/main/java/zone/themcgamer/core/account/MiniAccount.java @@ -2,7 +2,6 @@ package zone.themcgamer.core.account; import lombok.Getter; import org.bukkit.plugin.java.JavaPlugin; -import zone.themcgamer.core.account.Account; import zone.themcgamer.core.module.Module; import java.sql.ResultSet; diff --git a/core/src/main/java/zone/themcgamer/core/twoFactor/TwoFactorAuthentication.java b/core/src/main/java/zone/themcgamer/core/twoFactor/TwoFactorAuthentication.java index fd517f4..14019ce 100644 --- a/core/src/main/java/zone/themcgamer/core/twoFactor/TwoFactorAuthentication.java +++ b/core/src/main/java/zone/themcgamer/core/twoFactor/TwoFactorAuthentication.java @@ -1,7 +1,11 @@ package zone.themcgamer.core.twoFactor; +import com.cryptomorin.xseries.XMaterial; import com.cryptomorin.xseries.XSound; import com.warrenstrange.googleauth.GoogleAuthenticator; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.chat.HoverEvent; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.entity.Entity; @@ -13,13 +17,18 @@ import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.event.entity.FoodLevelChangeEvent; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.player.*; +import org.bukkit.inventory.ItemStack; +import org.bukkit.map.MapView; import org.bukkit.plugin.java.JavaPlugin; import zone.themcgamer.common.EnumUtils; import zone.themcgamer.core.account.Account; import zone.themcgamer.core.account.AccountManager; import zone.themcgamer.core.account.MiniAccount; +import zone.themcgamer.core.common.ItemBuilder; import zone.themcgamer.core.common.Style; import zone.themcgamer.core.module.ModuleInfo; +import zone.themcgamer.core.twoFactor.image.QRImageGenerator; +import zone.themcgamer.core.twoFactor.image.QRMapRenderer; import zone.themcgamer.data.Rank; import zone.themcgamer.data.mysql.MySQLController; @@ -109,11 +118,25 @@ public class TwoFactorAuthentication extends MiniAccount { String secretKey = googleAuthenticator.createCredentials().getKey(); twoFactorClient.setSecretKey(secretKey); - // TODO: 2/20/21 generate a qr map and give it to the player so they can scan it on their twoFactor app + // QR Map + ItemStack firstSlotItem = player.getInventory().getItem(0); + if (firstSlotItem != null) + twoFactorClient.setFirstSlotItem(firstSlotItem); + + // Creating the map + Bukkit.getScheduler().runTaskAsynchronously(getPlugin(), new QRImageGenerator(player, twoFactorClient.getSecretKey(), image -> { + MapView mapView = Bukkit.createMap(player.getWorld()); + mapView.getRenderers().removeIf(mapView::removeRenderer); + mapView.addRenderer(new QRMapRenderer(player, image)); + player.getInventory().setItem(0, new ItemBuilder(XMaterial.FILLED_MAP, 1, (byte) mapView.getId()).toItemStack()); + player.getInventory().setHeldItemSlot(0); + })); + player.sendMessage(Style.main(getName(), "Hey §b" + player.getName() + "§7, you have not setup your two factor authentication yet!")); - player.sendMessage(Style.main(getName(), "To begin, open your authentication app of choice and insert the secret key")); - player.sendMessage(Style.main(getName(), "§f" + secretKey)); - player.sendMessage(Style.main(getName(), "Once done, type the 6 digit code provided by your authentication app into the chat")); + player.sendMessage(new ComponentBuilder(Style.main(getName(), "To begin, open your authentication app of choice and scan the QR code on the map or enter " + + "the code §6" + secretKey + " §7manually. Once done, type the 6 digit code provided by your authentication app into the chat")) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("§aClick to copy").create())) + .event(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, secretKey)).create()); return; } player.sendMessage(Style.main(getName(), "§cYou need to re-authenticate!")); @@ -143,13 +166,16 @@ public class TwoFactorAuthentication extends MiniAccount { } TwoFactorClient twoFactorClient = optionalTwoFactorClient.get(); String secretKey = twoFactorClient.getSecretKey(); - if (!googleAuthenticator.authorize(secretKey, code)) + if (!googleAuthenticator.authorize(secretKey, code)) // If the provided code is incorrect, show an error to the player player.sendMessage(Style.main(getName(), "§cInvalid authentication code!")); else { + // If the code is correct, we wanna authenticate the player twoFactorClient.setLastAuthentication(System.currentTimeMillis()); repository.authenticate(optionalAccount.get().getId(), secretKey, twoFactorClient.getLastAuthentication()); authenticating.remove(player.getUniqueId()); player.playSound(player.getEyeLocation(), XSound.ENTITY_PLAYER_LEVELUP.parseSound(), 0.9f, 1f); + if (twoFactorClient.getFirstSlotItem() != null) + player.getInventory().setItem(0, twoFactorClient.getFirstSlotItem()); player.sendMessage(Style.main(getName(), "§aAuthenticated!")); } } @@ -194,6 +220,16 @@ public class TwoFactorAuthentication extends MiniAccount { authenticating.remove(event.getPlayer().getUniqueId()); } + /** + * Check whetherr or not the provided {@link Player} is authenticating + * + * @param player the player to check + * @return whether or not they're authenticating + */ + public boolean isAuthenticating(Player player) { + return authenticating.contains(player.getUniqueId()); + } + private void cancelEvent(Cancellable cancellable) { if (!(cancellable instanceof PlayerEvent)) return; diff --git a/core/src/main/java/zone/themcgamer/core/twoFactor/TwoFactorClient.java b/core/src/main/java/zone/themcgamer/core/twoFactor/TwoFactorClient.java index 777a0f0..02beb1f 100644 --- a/core/src/main/java/zone/themcgamer/core/twoFactor/TwoFactorClient.java +++ b/core/src/main/java/zone/themcgamer/core/twoFactor/TwoFactorClient.java @@ -3,6 +3,7 @@ package zone.themcgamer.core.twoFactor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.bukkit.inventory.ItemStack; import java.util.concurrent.TimeUnit; @@ -14,6 +15,11 @@ public class TwoFactorClient { private String secretKey; private long lastAuthentication; + // This is not stored in MySQL, this is just a copy of the item in the first slot of the player's inventory + // when they join. We save this item as we place the QR map in that slot and we give them the item when they + // authenticate + private ItemStack firstSlotItem; + public boolean requiresAuthentication() { return secretKey == null || (System.currentTimeMillis() - lastAuthentication) >= TimeUnit.DAYS.toMillis(1L); } diff --git a/core/src/main/java/zone/themcgamer/core/twoFactor/image/QRImageGenerator.java b/core/src/main/java/zone/themcgamer/core/twoFactor/image/QRImageGenerator.java new file mode 100644 index 0000000..4d4991a --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/twoFactor/image/QRImageGenerator.java @@ -0,0 +1,35 @@ +package zone.themcgamer.core.twoFactor.image; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.WriterException; +import com.google.zxing.client.j2se.MatrixToImageWriter; +import com.google.zxing.qrcode.QRCodeWriter; +import lombok.AllArgsConstructor; +import org.bukkit.entity.Player; + +import java.awt.image.BufferedImage; +import java.util.function.Consumer; + +/** + * @author Braydon + */ +@AllArgsConstructor +public class QRImageGenerator implements Runnable { + private static final int MAP_SIZE = 132; + + private final Player player; + private final String secretKey; + private final Consumer imageConsumer; + + @Override + public void run() { + QRCodeWriter qrWriter = new QRCodeWriter(); + try { + imageConsumer.accept(MatrixToImageWriter.toBufferedImage(qrWriter.encode( + "otpauth://totp/" + player.getName() + "@McGamerZone?secret=" + secretKey, + BarcodeFormat.QR_CODE, MAP_SIZE, MAP_SIZE))); + } catch (WriterException ex) { + ex.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/twoFactor/image/QRMapRenderer.java b/core/src/main/java/zone/themcgamer/core/twoFactor/image/QRMapRenderer.java new file mode 100644 index 0000000..5c9479a --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/twoFactor/image/QRMapRenderer.java @@ -0,0 +1,32 @@ +package zone.themcgamer.core.twoFactor.image; + +import org.bukkit.entity.Player; +import org.bukkit.map.MapCanvas; +import org.bukkit.map.MapRenderer; +import org.bukkit.map.MapView; + +import java.awt.image.BufferedImage; + +/** + * @author Braydon + */ +public class QRMapRenderer extends MapRenderer { + private final Player player; + private BufferedImage bufferedImage; + + public QRMapRenderer(Player player, BufferedImage bufferedImage) { + super(true); + this.player = player; + this.bufferedImage = bufferedImage; + } + + @Override + public void render(MapView mapView, MapCanvas mapCanvas, Player player) { + if (bufferedImage == null) + return; + if (this.player.equals(player)) { + mapCanvas.drawImage(0, 0, bufferedImage); + bufferedImage = null; + } + } +} \ No newline at end of file