Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix known 2fa issues #146

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions src/main/java/net/elytrium/limboauth/LimboAuth.java
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ public class LimboAuth {

private final Map<String, CachedSessionUser> cachedAuthChecks = new ConcurrentHashMap<>();
private final Map<String, CachedPremiumUser> premiumCache = new ConcurrentHashMap<>();
private final Map<InetAddress, CachedBruteforceUser> bruteforceCache = new ConcurrentHashMap<>();
private final Map<String, CachedBruteforceUser> bruteforceCache = new ConcurrentHashMap<>();
private final Map<UUID, Runnable> postLoginTasks = new ConcurrentHashMap<>();
private final Set<String> unsafePasswords = new HashSet<>();
private final Set<String> forcedPreviously = Collections.synchronizedSet(new HashSet<>());
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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) {
Expand Down
9 changes: 7 additions & 2 deletions src/main/java/net/elytrium/limboauth/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -386,6 +388,8 @@ public static class STRINGS {
public String LOGIN = "{PRFX} &aPlease, login using &6/login <password>&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 <password>&a.";
Expand Down Expand Up @@ -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 \"<current password>\" part.")
public String TOTP_USAGE = "{PRFX} Usage: &6/2fa enable <current password>&f or &6/2fa disable <totp key>&f.";
public String TOTP_USAGE = "{PRFX} Usage: &6/2fa enable <current password>&f, &6/2fa verify <totp key>&f or &6/2fa disable <totp key>&f.";
public String TOTP_WRONG = "{PRFX} &cWrong 2FA key!";
public String TOTP_VERIFY = "{PRFX} Type &6/2fa verify <totp key>&f to enable 2FA.";
public String TOTP_ALREADY_ENABLED = "{PRFX} &c2FA is already enabled. Disable it using &6/2fa disable <key>&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}";

Expand Down
65 changes: 50 additions & 15 deletions src/main/java/net/elytrium/limboauth/command/TotpCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -64,6 +67,8 @@ public class TotpCommand extends RatelimitedCommand {
private final Component wrong;
private final Component crackedCommand;

private final Map<String, String> totpSecretTmpMap = new ConcurrentHashMap<>();

public TotpCommand(Dao<RegisteredPlayer, String> playerDao) {
this.playerDao = playerDao;

Expand All @@ -75,6 +80,7 @@ public TotpCommand(Dao<RegisteredPlayer, String> 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;
Expand Down Expand Up @@ -119,31 +125,60 @@ 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)
.secret(secret)
.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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
}
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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;
Expand Down