From eaa48b56b20fd7b09d7470920aa085768557651b Mon Sep 17 00:00:00 2001 From: comendantmc <63936053+comendantmc@users.noreply.github.com> Date: Fri, 9 Feb 2024 05:34:25 -0500 Subject: [PATCH] Fix 2fa issues --- .../net/elytrium/limboauth/LimboAuth.java | 22 +++---- .../java/net/elytrium/limboauth/Settings.java | 9 ++- .../limboauth/command/TotpCommand.java | 65 ++++++++++++++----- .../limboauth/handler/AuthSessionHandler.java | 26 ++++++-- 4 files changed, 90 insertions(+), 32 deletions(-) diff --git a/src/main/java/net/elytrium/limboauth/LimboAuth.java b/src/main/java/net/elytrium/limboauth/LimboAuth.java index 792bd0bc..5447bc4e 100644 --- a/src/main/java/net/elytrium/limboauth/LimboAuth.java +++ b/src/main/java/net/elytrium/limboauth/LimboAuth.java @@ -148,7 +148,7 @@ public class LimboAuth { private final Map cachedAuthChecks = new ConcurrentHashMap<>(); private final Map premiumCache = new ConcurrentHashMap<>(); - private final Map bruteforceCache = new ConcurrentHashMap<>(); + private final Map bruteforceCache = new ConcurrentHashMap<>(); private final Map postLoginTasks = new ConcurrentHashMap<>(); private final Set unsafePasswords = new HashSet<>(); private final Set forcedPreviously = Collections.synchronizedSet(new HashSet<>()); @@ -543,7 +543,7 @@ public void authPlayer(Player player) { return; } - if (this.getBruteforceAttempts(player.getRemoteAddress().getAddress()) >= Settings.IMP.MAIN.BRUTEFORCE_MAX_ATTEMPTS) { + if (this.getBruteforceAttempts(player.getRemoteAddress().getAddress().toString()) >= Settings.IMP.MAIN.BRUTEFORCE_MAX_ATTEMPTS) { player.disconnect(this.bruteforceAttemptKick); return; } @@ -859,26 +859,26 @@ public boolean isPremium(String nickname) { } } - public void incrementBruteforceAttempts(InetAddress address) { - this.getBruteforceUser(address).incrementAttempts(); + public void incrementBruteforceAttempts(String key) { + this.getBruteforceUser(key).incrementAttempts(); } - public int getBruteforceAttempts(InetAddress address) { - return this.getBruteforceUser(address).getAttempts(); + public int getBruteforceAttempts(String key) { + return this.getBruteforceUser(key).getAttempts(); } - private CachedBruteforceUser getBruteforceUser(InetAddress address) { - CachedBruteforceUser user = this.bruteforceCache.get(address); + private CachedBruteforceUser getBruteforceUser(String key) { + CachedBruteforceUser user = this.bruteforceCache.get(key); if (user == null) { user = new CachedBruteforceUser(System.currentTimeMillis()); - this.bruteforceCache.put(address, user); + this.bruteforceCache.put(key, user); } return user; } - public void clearBruteforceAttempts(InetAddress address) { - this.bruteforceCache.remove(address); + public void clearBruteforceAttempts(String key) { + this.bruteforceCache.remove(key); } public void saveForceOfflineMode(String nickname) { diff --git a/src/main/java/net/elytrium/limboauth/Settings.java b/src/main/java/net/elytrium/limboauth/Settings.java index 84c5471c..0a1970b8 100644 --- a/src/main/java/net/elytrium/limboauth/Settings.java +++ b/src/main/java/net/elytrium/limboauth/Settings.java @@ -136,6 +136,8 @@ public static class MAIN { public long PURGE_BRUTEFORCE_CACHE_MILLIS = 28800000; @Comment("Used to ban IPs when a possible attacker incorrectly enters the password") public int BRUTEFORCE_MAX_ATTEMPTS = 10; + @Comment("Used to ban IPs when a possible attacker incorrectly enters OTP") + public int BRUTEFORCE_MAX_OTP_ATTEMPTS = 10; @Comment("QR Generator URL, set {data} placeholder") public String QR_GENERATOR_URL = "https://api.qrserver.com/v1/create-qr-code/?data={data}&size=200x200&ecc=M&margin=30"; public String TOTP_ISSUER = "LimboAuth by Elytrium"; @@ -386,6 +388,8 @@ public static class STRINGS { public String LOGIN = "{PRFX} &aPlease, login using &6/login &a, you have &6{0} &aattempts."; public String LOGIN_WRONG_PASSWORD = "{PRFX} &cYou''ve entered the wrong password, you have &6{0} &cattempts left."; public String LOGIN_WRONG_PASSWORD_KICK = "{PRFX}{NL}&cYou've entered the wrong password numerous times!"; + public String LOGIN_WRONG_OTP_IMMEDIATE_KICK = "{PRFX}{NL}&cYou've entered wrong 2FA code!"; + public String LOGIN_WRONG_OTP_KICK = "{PRFX}{NL}&cYou've entered wrong 2FA code numerous times!"; public String LOGIN_SUCCESSFUL = "{PRFX} &aSuccessfully logged in!"; @Comment(value = "Can be empty.", at = Comment.At.SAME_LINE) public String LOGIN_TITLE = "&fPlease, login using &6/login &a."; @@ -453,10 +457,11 @@ public static class STRINGS { public String TOTP_SUCCESSFUL = "{PRFX} &aSuccessfully enabled 2FA!"; public String TOTP_DISABLED = "{PRFX} &aSuccessfully disabled 2FA!"; @Comment("Or if totp-need-pass set to false remove the \"\" part.") - public String TOTP_USAGE = "{PRFX} Usage: &6/2fa enable &f or &6/2fa disable &f."; + public String TOTP_USAGE = "{PRFX} Usage: &6/2fa enable &f, &6/2fa verify &f or &6/2fa disable &f."; public String TOTP_WRONG = "{PRFX} &cWrong 2FA key!"; + public String TOTP_VERIFY = "{PRFX} Type &6/2fa verify &f to enable 2FA."; public String TOTP_ALREADY_ENABLED = "{PRFX} &c2FA is already enabled. Disable it using &6/2fa disable &c."; - public String TOTP_QR = "{PRFX} Click here to open 2FA QR code in browser."; + public String TOTP_QR = "{PRFX} &9&nClick here to open 2FA QR code in browser."; public String TOTP_TOKEN = "{PRFX} &aYour 2FA token &7(Click to copy)&a: &6{0}"; public String TOTP_RECOVERY = "{PRFX} &aYour recovery codes &7(Click to copy)&a: &6{0}"; diff --git a/src/main/java/net/elytrium/limboauth/command/TotpCommand.java b/src/main/java/net/elytrium/limboauth/command/TotpCommand.java index 447b8461..33487c93 100644 --- a/src/main/java/net/elytrium/limboauth/command/TotpCommand.java +++ b/src/main/java/net/elytrium/limboauth/command/TotpCommand.java @@ -31,6 +31,8 @@ import java.sql.SQLException; import java.text.MessageFormat; import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import net.elytrium.commons.kyori.serialization.Serializer; import net.elytrium.limboauth.LimboAuth; import net.elytrium.limboauth.Settings; @@ -54,6 +56,7 @@ public class TotpCommand extends RatelimitedCommand { private final Component alreadyEnabled; private final Component errorOccurred; private final Component successful; + private final Component verify; private final String issuer; private final String qrGeneratorUrl; private final Component qr; @@ -64,6 +67,8 @@ public class TotpCommand extends RatelimitedCommand { private final Component wrong; private final Component crackedCommand; + private final Map totpSecretTmpMap = new ConcurrentHashMap<>(); + public TotpCommand(Dao playerDao) { this.playerDao = playerDao; @@ -75,6 +80,7 @@ public TotpCommand(Dao playerDao) { this.wrongPassword = serializer.deserialize(Settings.IMP.MAIN.STRINGS.WRONG_PASSWORD); this.alreadyEnabled = serializer.deserialize(Settings.IMP.MAIN.STRINGS.TOTP_ALREADY_ENABLED); this.errorOccurred = serializer.deserialize(Settings.IMP.MAIN.STRINGS.ERROR_OCCURRED); + this.verify = serializer.deserialize(Settings.IMP.MAIN.STRINGS.TOTP_VERIFY); this.successful = serializer.deserialize(Settings.IMP.MAIN.STRINGS.TOTP_SUCCESSFUL); this.issuer = Settings.IMP.MAIN.TOTP_ISSUER; this.qrGeneratorUrl = Settings.IMP.MAIN.QR_GENERATOR_URL; @@ -119,16 +125,7 @@ public void execute(CommandSource source, String[] args) { } String secret = this.secretGenerator.generate(); - try { - updateBuilder = this.playerDao.updateBuilder(); - updateBuilder.where().eq(RegisteredPlayer.LOWERCASE_NICKNAME_FIELD, usernameLowercase); - updateBuilder.updateColumnValue(RegisteredPlayer.TOTP_TOKEN_FIELD, secret); - updateBuilder.update(); - } catch (SQLException e) { - source.sendMessage(this.errorOccurred); - throw new SQLRuntimeException(e); - } - source.sendMessage(this.successful); + this.totpSecretTmpMap.put(playerInfo.getUuid(), secret); QrData data = new QrData.Builder() .label(username) @@ -136,14 +133,52 @@ public void execute(CommandSource source, String[] args) { .issuer(this.issuer) .build(); String qrUrl = this.qrGeneratorUrl.replace("{data}", URLEncoder.encode(data.getUri(), StandardCharsets.UTF_8)); - source.sendMessage(this.qr.clickEvent(ClickEvent.openUrl(qrUrl))); + Component clickableQR = Component.empty().append(this.qr).clickEvent(ClickEvent.openUrl(qrUrl)).compact(); Serializer serializer = LimboAuth.getSerializer(); - source.sendMessage(serializer.deserialize(MessageFormat.format(this.token, secret)) - .clickEvent(ClickEvent.copyToClipboard(secret))); + + Component totpToken = serializer.deserialize(MessageFormat.format(this.token, secret)) + .clickEvent(ClickEvent.copyToClipboard(secret)); + String codes = String.join(", ", this.codesGenerator.generateCodes(this.recoveryCodesAmount)); - source.sendMessage(serializer.deserialize(MessageFormat.format(this.recovery, codes)) - .clickEvent(ClickEvent.copyToClipboard(codes))); + Component restoreCodes = serializer.deserialize(MessageFormat.format(this.recovery, codes)) + .clickEvent(ClickEvent.copyToClipboard(codes)); + + Component combined = Component.empty() + .append(clickableQR).appendNewline() + .append(totpToken).appendNewline() + .append(restoreCodes).appendNewline() + .append(this.verify) + .compact(); + source.sendMessage(combined); + } else { + source.sendMessage(this.usage); + } + } else if (args[0].equalsIgnoreCase("verify")) { + if (args.length == 2) { + playerInfo = AuthSessionHandler.fetchInfoLowercased(this.playerDao, usernameLowercase); + + if (playerInfo == null) { + source.sendMessage(this.notRegistered); + return; + } + + String secret = this.totpSecretTmpMap.get(playerInfo.getUuid()); + + if (AuthSessionHandler.TOTP_CODE_VERIFIER.isValidCode(secret, args[1])) { + try { + updateBuilder = this.playerDao.updateBuilder(); + updateBuilder.where().eq(RegisteredPlayer.LOWERCASE_NICKNAME_FIELD, usernameLowercase); + updateBuilder.updateColumnValue(RegisteredPlayer.TOTP_TOKEN_FIELD, secret); + updateBuilder.update(); + } catch (SQLException e) { + source.sendMessage(this.errorOccurred); + throw new SQLRuntimeException(e); + } + source.sendMessage(this.successful); + } else { + source.sendMessage(this.wrong); + } } else { source.sendMessage(this.usage); } diff --git a/src/main/java/net/elytrium/limboauth/handler/AuthSessionHandler.java b/src/main/java/net/elytrium/limboauth/handler/AuthSessionHandler.java index a8a5f608..f37418a5 100644 --- a/src/main/java/net/elytrium/limboauth/handler/AuthSessionHandler.java +++ b/src/main/java/net/elytrium/limboauth/handler/AuthSessionHandler.java @@ -72,6 +72,8 @@ public class AuthSessionHandler implements LimboSessionHandler { private static Title registerSuccessfulTitle; private static Component[] loginWrongPassword; private static Component loginWrongPasswordKick; + private static Component loginWrongOTPKick; + private static Component loginWrongOTPImmediateKick; private static Component totp; @Nullable private static Title totpTitle; @@ -261,7 +263,10 @@ public void onChat(String message) { this.finishLogin(); return; } else { - this.checkBruteforceAttempts(); + this.checkBruteforceOTPAttempts(); + if (this.proxyPlayer.isActive()) { + this.proxyPlayer.disconnect(loginWrongOTPImmediateKick); + } } } } @@ -328,12 +333,22 @@ public void onGeneric(Object packet) { } private void checkBruteforceAttempts() { - this.plugin.incrementBruteforceAttempts(this.proxyPlayer.getRemoteAddress().getAddress()); - if (this.plugin.getBruteforceAttempts(this.proxyPlayer.getRemoteAddress().getAddress()) >= Settings.IMP.MAIN.BRUTEFORCE_MAX_ATTEMPTS) { + this.plugin.incrementBruteforceAttempts(this.proxyPlayer.getRemoteAddress().getAddress().toString()); + if (this.plugin.getBruteforceAttempts(this.proxyPlayer.getRemoteAddress().getAddress().toString()) >= Settings.IMP.MAIN.BRUTEFORCE_MAX_ATTEMPTS) { this.proxyPlayer.disconnect(loginWrongPasswordKick); } } + private void checkBruteforceOTPAttempts() { + this.plugin.incrementBruteforceAttempts("otp:" + this.proxyPlayer.getRemoteAddress().getAddress().toString()); + if ( + this.plugin.getBruteforceAttempts("otp:" + this.proxyPlayer.getRemoteAddress().getAddress().toString()) + >= Settings.IMP.MAIN.BRUTEFORCE_MAX_OTP_ATTEMPTS + ) { + this.proxyPlayer.disconnect(loginWrongOTPKick); + } + } + private void saveTempPassword(String password) { this.tempPassword = password; } @@ -411,7 +426,8 @@ private void finishLogin() { this.proxyPlayer.showTitle(loginSuccessfulTitle); } - this.plugin.clearBruteforceAttempts(this.proxyPlayer.getRemoteAddress().getAddress()); + this.plugin.clearBruteforceAttempts(this.proxyPlayer.getRemoteAddress().getAddress().toString()); + this.plugin.clearBruteforceAttempts("otp:" + this.proxyPlayer.getRemoteAddress().getAddress().toString()); this.plugin.getServer().getEventManager() .fire(new PostAuthorizationEvent(this::finishAuth, this.player, this.playerInfo, this.tempPassword)) @@ -471,6 +487,8 @@ public static void reload() { loginWrongPassword[i] = serializer.deserialize(MessageFormat.format(Settings.IMP.MAIN.STRINGS.LOGIN_WRONG_PASSWORD, i + 1)); } loginWrongPasswordKick = serializer.deserialize(Settings.IMP.MAIN.STRINGS.LOGIN_WRONG_PASSWORD_KICK); + loginWrongOTPKick = serializer.deserialize(Settings.IMP.MAIN.STRINGS.LOGIN_WRONG_OTP_KICK); + loginWrongOTPImmediateKick = serializer.deserialize(Settings.IMP.MAIN.STRINGS.LOGIN_WRONG_OTP_IMMEDIATE_KICK); totp = serializer.deserialize(Settings.IMP.MAIN.STRINGS.TOTP); if (Settings.IMP.MAIN.STRINGS.TOTP_TITLE.isEmpty() && Settings.IMP.MAIN.STRINGS.TOTP_SUBTITLE.isEmpty()) { totpTitle = null;